001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.util.Locale; 025 026import static java.util.Objects.requireNonNull; 027 028/** 029 * Immutable server-wide MCP localization behavior and policy. 030 * <p> 031 * The configured fallback locale is the language of every canonical 032 * annotation and builder string hosted by the server and the locale of 033 * canonical default text. It is independent of a provider or localization 034 * library's terminal fallback locale. The application-owned context provider 035 * performs request-specific locale selection and fallback resolution and 036 * captures one immutable translation snapshot. 037 * 038 * @author <a href="https://www.revetkn.com">Mark Allen</a> 039 */ 040@ThreadSafe 041public final class McpLocalizer { 042 private static final int DEFAULT_MAXIMUM_LOCALIZABLE_TEXT_COUNT_PER_RESPONSE = 043 32_768; 044 private static final int MAXIMUM_SUPPORTED_LOCALIZABLE_TEXT_COUNT_PER_RESPONSE = 045 100_000; 046 @NonNull 047 private final Locale fallbackLocale; 048 @NonNull 049 private final McpLocalizationContextProvider contextProvider; 050 @NonNull 051 private final McpLocalizationFailurePolicy failurePolicy; 052 private final int maximumLocalizableTextCountPerResponse; 053 054 /** 055 * Vends a builder primed with the canonical non-root fallback locale and 056 * application-owned context provider. 057 * 058 * @param fallbackLocale canonical BCP 47 fallback locale 059 * @param localizationContextProvider request context provider 060 * @return localizer builder 061 * @throws NullPointerException if an argument is null 062 * @throws IllegalArgumentException if {@code fallbackLocale} is not a 063 * canonical, non-root BCP 47 locale of at most 255 ASCII bytes 064 */ 065 @NonNull 066 public static Builder withFallbackLocale(@NonNull Locale fallbackLocale, 067 @NonNull McpLocalizationContextProvider 068 localizationContextProvider) { 069 Locale requiredFallbackLocale = 070 McpLocaleSupport.requireCanonicalCatalogLocale( 071 fallbackLocale, "fallbackLocale"); 072 return new Builder(requiredFallbackLocale, localizationContextProvider); 073 } 074 075 private McpLocalizer(@NonNull Builder builder) { 076 this.fallbackLocale = builder.fallbackLocale; 077 this.contextProvider = builder.contextProvider; 078 this.failurePolicy = builder.failurePolicy; 079 this.maximumLocalizableTextCountPerResponse = 080 builder.maximumLocalizableTextCountPerResponse; 081 } 082 083 /** @return configured canonical non-root fallback locale */ 084 @NonNull 085 public Locale getFallbackLocale() { 086 return this.fallbackLocale; 087 } 088 089 /** @return application-owned request context provider */ 090 @NonNull 091 public McpLocalizationContextProvider getContextProvider() { 092 return this.contextProvider; 093 } 094 095 /** 096 * Returns the whole-response framework-catalog failure policy. It does not 097 * authorize a synthetic context when application context creation fails. 098 * 099 * @return framework-catalog localization failure policy 100 */ 101 @NonNull 102 public McpLocalizationFailurePolicy getFailurePolicy() { 103 return this.failurePolicy; 104 } 105 106 /** 107 * Returns the maximum number of provider text lookups permitted while 108 * rendering one framework response. 109 * 110 * @return positive bounded response-local lookup limit 111 */ 112 @NonNull 113 public Integer getMaximumLocalizableTextCountPerResponse() { 114 return this.maximumLocalizableTextCountPerResponse; 115 } 116 117 /** 118 * Single-threaded builder for an immutable localizer. 119 * 120 * @author <a href="https://www.revetkn.com">Mark Allen</a> 121 */ 122 @NotThreadSafe 123 public static final class Builder { 124 @NonNull 125 private final Locale fallbackLocale; 126 @NonNull 127 private final McpLocalizationContextProvider contextProvider; 128 @NonNull 129 private McpLocalizationFailurePolicy failurePolicy; 130 private int maximumLocalizableTextCountPerResponse; 131 132 private Builder(@NonNull Locale fallbackLocale, 133 @NonNull McpLocalizationContextProvider 134 localizationContextProvider) { 135 this.fallbackLocale = fallbackLocale; 136 this.contextProvider = requireNonNull(localizationContextProvider); 137 this.failurePolicy = 138 McpLocalizationFailurePolicy.USE_DEFAULT_TEXT; 139 this.maximumLocalizableTextCountPerResponse = 140 DEFAULT_MAXIMUM_LOCALIZABLE_TEXT_COUNT_PER_RESPONSE; 141 } 142 143 /** 144 * Sets the whole-response behavior for unexpected framework-catalog 145 * provider or replacement validation failure. The default uses canonical 146 * fallback text. Application context-creation failure remains a sanitized 147 * failure before handler/interceptor entry under either policy. 148 * 149 * @param failurePolicy failure policy, or null to restore the default 150 * @return this builder 151 */ 152 @NonNull 153 public Builder failurePolicy( 154 @Nullable McpLocalizationFailurePolicy failurePolicy) { 155 this.failurePolicy = failurePolicy == null 156 ? McpLocalizationFailurePolicy.USE_DEFAULT_TEXT 157 : failurePolicy; 158 return this; 159 } 160 161 /** 162 * Sets the maximum provider lookup count for one framework response. 163 * The default is 32,768 and the supported range is 1 through 100,000. 164 * 165 * @param maximumLocalizableTextCountPerResponse positive bounded limit, or 166 * null to restore the default 167 * @return this builder 168 * @throws IllegalArgumentException if the value is outside the supported 169 * range 170 */ 171 @NonNull 172 public Builder maximumLocalizableTextCountPerResponse( 173 @Nullable Integer maximumLocalizableTextCountPerResponse) { 174 if (maximumLocalizableTextCountPerResponse == null) { 175 this.maximumLocalizableTextCountPerResponse = 176 DEFAULT_MAXIMUM_LOCALIZABLE_TEXT_COUNT_PER_RESPONSE; 177 return this; 178 } 179 int requiredMaximum = maximumLocalizableTextCountPerResponse; 180 if (requiredMaximum < 1 181 || requiredMaximum 182 > MAXIMUM_SUPPORTED_LOCALIZABLE_TEXT_COUNT_PER_RESPONSE) 183 throw new IllegalArgumentException( 184 "Maximum localizable text count per response must be " 185 + "between 1 and 100000."); 186 this.maximumLocalizableTextCountPerResponse = requiredMaximum; 187 return this; 188 } 189 190 /** @return immutable server localizer */ 191 @NonNull 192 public McpLocalizer build() { 193 return new McpLocalizer(this); 194 } 195 } 196}