From c7abe99c138f9bdcf417808b12361d1da2e18e58 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sun, 24 May 2026 14:08:46 +0000 Subject: fix(locale): deep-merge fallback into partial namespace returns svelte-i18n's getJSON returns the active locale's sub-tree verbatim; nested keys missing in that locale do not auto-fall-back to the fallbackLocale. The locale store now deep-merges the English value into the resolved object so JP users see English for any sub-key the JA dictionary omits (instead of blanks). --- src/stores/locale.ts | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/stores/locale.ts b/src/stores/locale.ts index 826d717c..1b94ea96 100644 --- a/src/stores/locale.ts +++ b/src/stores/locale.ts @@ -25,6 +25,22 @@ interface Options { values?: InterpolationValues; } +const FALLBACK_LOCALE = "en"; + +const isPlainObject = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const deepMerge = (fallback: unknown, current: unknown): unknown => { + if (current === undefined || current === null) return fallback; + if (!isPlainObject(current) || !isPlainObject(fallback)) return current; + + const merged: Record = { ...fallback }; + for (const [key, value] of Object.entries(current)) { + merged[key] = deepMerge(fallback[key], value); + } + return merged; +}; + const createLocale = () => { return derived(json, ($json) => { return (options: Options = {}) => @@ -32,9 +48,24 @@ const createLocale = () => { {}, { get(_target, key) { - const localisation = $json(key.toString(), options.locale); + const keyStr = key.toString(); + const current = $json(keyStr, options.locale); + const currentMissing = current === keyStr || current == null; + + let resolved: unknown; + if (options.locale === FALLBACK_LOCALE) { + if (currentMissing) return undefined; + resolved = current; + } else { + const fallback = $json(keyStr, FALLBACK_LOCALE); + const fallbackMissing = + fallback === keyStr || fallback == null; - if (localisation === key.toString()) return undefined; + if (currentMissing && fallbackMissing) return undefined; + if (currentMissing) resolved = fallback; + else if (fallbackMissing) resolved = current; + else resolved = deepMerge(fallback, current); + } const replaceValues = ( localisation: InterpolationValues, @@ -45,14 +76,14 @@ const createLocale = () => { const updatedLocalisation: InterpolationValues = {}; - for (const [key, value] of Object.entries(localisation)) { + for (const [k, value] of Object.entries(localisation)) { if (typeof value === "string") { - updatedLocalisation[key] = value.replace( + updatedLocalisation[k] = value.replace( /\{(\w+)\}/g, (match, name) => (values ? values[name] : match) as string, ); } else { - updatedLocalisation[key] = replaceValues( + updatedLocalisation[k] = replaceValues( value as InterpolationValues, values, ) as InterpolationValue; @@ -64,11 +95,11 @@ const createLocale = () => { if (options.values) return replaceValues( - localisation as unknown as InterpolationValues, + resolved as unknown as InterpolationValues, options.values, ); - return localisation; + return resolved; }, }, ); -- cgit v1.2.3