@@ -159,13 +159,61 @@ open class Tolgee(
159159 return changeListeners.remove(listener)
160160 }
161161
162+ /* *
163+ * Generates progressive fallback variations for a locale by removing components
164+ * from right to left following BCP 47 structure (language-script-region-variant).
165+ *
166+ * Examples:
167+ * - "zh-Hans-CN" → ["zh-Hans-CN", "zh-Hans", "zh"]
168+ * - "en-US" → ["en-US", "en"]
169+ * - "sr-Cyrl" → ["sr-Cyrl", "sr"]
170+ * - "en" → ["en"]
171+ *
172+ * @param locale The locale to generate fallbacks for
173+ * @return List of locales in fallback order (most specific to least specific)
174+ */
175+ private fun generateLocaleFallbacks (locale : Locale ): List <Locale > {
176+ val localeTag = locale.toTag(" -" )
177+ val components = localeTag.split(" -" )
178+
179+ val fallbacks = mutableListOf<Locale >()
180+
181+ // Start with the full locale
182+ fallbacks.add(locale)
183+
184+ // Generate intermediate variations by removing components from right to left
185+ for (i in components.size - 1 downTo 2 ) {
186+ val fallbackTag = components.subList(0 , i).joinToString(" -" )
187+ fallbacks.add(forLocaleTag(fallbackTag))
188+ }
189+
190+ // Add base language if not already included (when components.size > 1)
191+ if (components.size > 1 ) {
192+ fallbacks.add(forLocaleTag(components[0 ]))
193+ }
194+
195+ return fallbacks
196+ }
197+
162198 /* *
163199 * Resolves the most appropriate available locale from the given locale.
164200 *
165201 * Resolution strategy:
166- * 1. If the exact locale is available, return it (e.g., "en-US" → "en-US")
167- * 2. Otherwise, try to find an exact match for the base language (e.g., "en-US" → "en")
168- * 3. If still not found, use the default language if configured
202+ * 1. Try exact locale match (e.g., "zh-Hans-CN" → "zh-Hans-CN")
203+ * 2. Try intermediate variations by progressively removing components:
204+ * - "zh-Hans-CN" → "zh-Hans"
205+ * - "zh-Hans" → "zh"
206+ * 3. Use the default language if configured
207+ *
208+ * The fallback process follows BCP 47 locale tag structure, removing
209+ * rightmost components (variant, region, script) one at a time until
210+ * a match is found.
211+ *
212+ * Examples:
213+ * - "zh-Hans-CN" → "zh-Hans-CN" → "zh-Hans" → "zh" → default
214+ * - "en-Latn-US" → "en-Latn-US" → "en-Latn" → "en" → default
215+ * - "sr-Cyrl" → "sr-Cyrl" → "sr" → default
216+ * - "en-US" → "en-US" → "en" → default
169217 *
170218 * Available locales are determined from:
171219 * 1. `config.availableLocales` (if manually specified)
@@ -188,27 +236,17 @@ open class Tolgee(
188236 return locale
189237 }
190238
191- // First, check if exact locale is available
192- if (locale in availableLocales) {
193- return locale
194- }
195-
196- // Fallback: Look for exact base language match only
197- // Find exact match for base language (e.g., "en" without region/script)
198- val baseLanguageMatch = availableLocales.firstOrNull { availableLocale ->
199- // Match only if:
200- // 1. The language matches
201- // 2. It's a base language (no region/script), checked by comparing tag to language
202- availableLocale.language == locale.language &&
203- availableLocale.toTag(" -" ) == availableLocale.language
204- }
239+ // Generate progressive fallback variations
240+ val fallbackCandidates = generateLocaleFallbacks(locale)
205241
206- if (baseLanguageMatch != null ) {
207- return baseLanguageMatch
242+ // Try each fallback candidate in order
243+ for (candidate in fallbackCandidates) {
244+ if (candidate in availableLocales) {
245+ return candidate
246+ }
208247 }
209248
210- // Final fallback: Use default language if configured, otherwise null
211- // We assume the default language is available
249+ // Final fallback: Use default language if configured
212250 return config.defaultLanguage
213251 }
214252
@@ -496,10 +534,13 @@ open class Tolgee(
496534 * Instead, it will use the provided list of locales. Can be used to save on network requests.
497535 *
498536 * This list is used when determining the fallback language for translations.
499- * For example, if the requested language doesn't exist, but it's non-regional variant
500- * exists (`en-US` doesn't exist, but `en` does),we can use that instead. If we don't
501- * have a list of available locales and manifest fetching fails, the fallback mechanism
502- * will be disabled and only exactly matching locale will be used.
537+ * The SDK performs progressive fallback through intermediate locale variations:
538+ * - If "zh-Hans-CN" doesn't exist, tries "zh-Hans"
539+ * - If "zh-Hans" doesn't exist, tries "zh"
540+ * - Finally uses the default language if configured
541+ *
542+ * If we don't have a list of available locales and manifest fetching fails, the fallback
543+ * mechanism will be disabled and only exactly matching locale will be used.
503544 */
504545 var availableLocales: List <Locale >? = null
505546
0 commit comments