From 228655a73ac39c532c2f1fd5ccd82712eea64116 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 19 Jul 2026 00:03:39 -0400 Subject: [PATCH] =?UTF-8?q?refactor(desktop):=20share=20one=20active?= =?UTF-8?q?=E2=86=92default=E2=86=92key=20i18n=20resolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the fallback walk out of `translateNow` into a single `translateFrom` that takes a per-locale message source, so any translator (core catalog or a plugin's bundles) reuses the exact dot-path walk, interpolation, and active-locale/English/raw-key chain. Adds `getRuntimeI18nLocale`. --- apps/desktop/src/i18n/runtime.ts | 49 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/i18n/runtime.ts b/apps/desktop/src/i18n/runtime.ts index b9276aaf9651..71d2162c15fc 100644 --- a/apps/desktop/src/i18n/runtime.ts +++ b/apps/desktop/src/i18n/runtime.ts @@ -1,6 +1,6 @@ import { TRANSLATIONS } from './catalog' import { DEFAULT_LOCALE } from './languages' -import type { Locale, Translations } from './types' +import type { Locale } from './types' let runtimeLocale: Locale = DEFAULT_LOCALE @@ -8,17 +8,13 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } -function resolvePath(catalog: Translations, key: string): unknown { - return key.split('.').reduce((current, part) => { - if (!isRecord(current)) { - return undefined - } - - return current[part] - }, catalog) +/** Walk a dot-path (`a.b.c`) into a nested message tree. */ +function resolvePath(source: unknown, key: string): unknown { + return key.split('.').reduce((current, part) => (isRecord(current) ? current[part] : undefined), source) } -function renderTranslation(value: unknown, args: unknown[]): string | null { +/** A string is returned as-is, a function is called with `args`, else `null`. */ +function render(value: unknown, args: unknown[]): null | string { if (typeof value === 'string') { return value } @@ -30,19 +26,22 @@ function renderTranslation(value: unknown, args: unknown[]): string | null { return null } -export function setRuntimeI18nLocale(locale: Locale) { - runtimeLocale = locale -} - -export function translateNow(key: string, ...args: unknown[]): string { - const active = renderTranslation(resolvePath(TRANSLATIONS[runtimeLocale], key), args) +/** The active → DEFAULT → key resolution every translator shares. `source` + * yields a message tree per locale — the app catalog, or a plugin's bundles. */ +export function translateFrom( + source: (locale: Locale) => unknown, + locale: Locale, + key: string, + args: unknown[] +): string { + const active = render(resolvePath(source(locale), key), args) if (active !== null) { return active } - if (runtimeLocale !== DEFAULT_LOCALE) { - const fallback = renderTranslation(resolvePath(TRANSLATIONS[DEFAULT_LOCALE], key), args) + if (locale !== DEFAULT_LOCALE) { + const fallback = render(resolvePath(source(DEFAULT_LOCALE), key), args) if (fallback !== null) { return fallback @@ -51,3 +50,17 @@ export function translateNow(key: string, ...args: unknown[]): string { return key } + +export function setRuntimeI18nLocale(locale: Locale) { + runtimeLocale = locale +} + +/** The locale module-level translators resolve against (the app's active + * `display.language`). Plugin `ctx.i18n.t` reads this too. */ +export function getRuntimeI18nLocale(): Locale { + return runtimeLocale +} + +export function translateNow(key: string, ...args: unknown[]): string { + return translateFrom(locale => TRANSLATIONS[locale], runtimeLocale, key, args) +}