-
Notifications
You must be signed in to change notification settings - Fork 6k
feat(i18n): add Simplified Chinese (zh-CN) translation #8765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
15ad556
72ca185
71c6883
3daba48
dd9e887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,22 +3,37 @@ | |
| * | ||
| * Locale resolution order: | ||
| * 1. GOOSE_LOCALE config value (set via environment variable, passed through appConfig) | ||
| * 2. navigator.language (browser/OS locale) | ||
| * 2. navigator.languages (full accept-language list from OS/browser) | ||
| * 3. "en" (fallback) | ||
| * | ||
| * For Chinese: any Simplified Chinese tag (zh, zh-CN, zh-Hans, zh-Hans-CN, zh-SG, zh-MY) | ||
| * maps to the "zh-CN" catalog. Traditional variants (zh-TW, zh-HK, zh-Hant) are not yet | ||
| * translated and fall through to English. | ||
| */ | ||
|
|
||
| // Re-export react-intl utilities that components use directly | ||
| export { defineMessages, useIntl } from 'react-intl'; | ||
|
|
||
| /** The set of locales that have translation catalogs. */ | ||
| const SUPPORTED_LOCALES = new Set(['en']); | ||
| const SUPPORTED_LOCALES = new Set(['en', 'zh-CN']); | ||
|
|
||
| /** | ||
| * Map Simplified Chinese aliases (zh, zh-Hans*, zh-SG, zh-MY) to "zh-CN". | ||
| * Traditional variants (zh-Hant*, zh-TW, zh-HK, zh-MO) and non-Chinese tags pass through unchanged. | ||
| */ | ||
| function resolveChineseAlias(tag: string): string { | ||
| const lower = tag.toLowerCase(); | ||
| if (/^zh-(hant|tw|hk|mo)(-|$)/.test(lower)) return tag; | ||
| if (lower === 'zh' || lower.startsWith('zh-')) return 'zh-CN'; | ||
| return tag; | ||
| } | ||
|
|
||
| /** | ||
| * Detect the user's preferred locale. | ||
| * | ||
| * Returns two values: | ||
| * - `locale`: the full BCP 47 tag (e.g. "en-GB") for formatting (dates, numbers). | ||
| * - `messageLocale`: the base language that has a translation catalog (e.g. "en"). | ||
| * - `messageLocale`: the locale key that has a translation catalog (e.g. "en", "zh-CN"). | ||
| */ | ||
| export function getLocale(): { locale: string; messageLocale: string } { | ||
| const explicit = | ||
|
|
@@ -32,13 +47,22 @@ export function getLocale(): { locale: string; messageLocale: string } { | |
| candidates.push(explicit); | ||
| } | ||
|
|
||
| if (typeof navigator !== 'undefined' && navigator.language) { | ||
| candidates.push(navigator.language); | ||
| // Walk navigator.languages (full preference list) so a user whose primary UI | ||
| // language isn't supported still gets a supported language from later in their list. | ||
| if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) { | ||
| for (const tag of navigator.languages) { | ||
| if (tag) candidates.push(tag); | ||
| } | ||
| } | ||
|
|
||
| for (const tag of candidates) { | ||
| for (const rawTag of candidates) { | ||
| // Normalize underscores to hyphens so POSIX-style tags like "zh_CN" work. | ||
| const normalized = rawTag.replace(/_/g, '-'); | ||
| const tag = resolveChineseAlias(normalized); | ||
|
|
||
| // Exact match first | ||
| if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag }; | ||
|
Comment on lines
+61
to
64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| // Try base language (e.g. "pt-BR" → "pt") for the catalog, but keep the | ||
| // full regional tag for formatting so date/number output respects the region. | ||
| const base = tag.split('-')[0]; | ||
|
|
@@ -48,7 +72,7 @@ export function getLocale(): { locale: string; messageLocale: string } { | |
| // Intl APIs, so fall back to the base language in that case. | ||
| let locale = base; | ||
| try { | ||
| [locale] = Intl.getCanonicalLocales(tag); | ||
| [locale] = Intl.getCanonicalLocales(normalized); | ||
| } catch { | ||
| // tag is not valid BCP 47 — use the base language instead | ||
| } | ||
|
|
@@ -70,9 +94,7 @@ export const currentMessageLocale = resolvedLocale.messageLocale; | |
| * Load compiled messages for a given locale. | ||
| * Returns an empty object for English (react-intl uses defaultMessage as fallback). | ||
| */ | ||
| export async function loadMessages( | ||
| locale: string | ||
| ): Promise<Record<string, string>> { | ||
| export async function loadMessages(locale: string): Promise<Record<string, string>> { | ||
| if (locale === 'en') { | ||
| // English strings live in source code as defaultMessage — no catalog needed. | ||
| return {}; | ||
|
|
@@ -83,7 +105,9 @@ export async function loadMessages( | |
| const mod = await import(`./compiled/${locale}.json`); | ||
| return mod.default ?? mod; | ||
| } catch { | ||
| console.warn(`[i18n] No message catalog found for locale "${locale}", falling back to English.`); | ||
| console.warn( | ||
| `[i18n] No message catalog found for locale "${locale}", falling back to English.` | ||
| ); | ||
| return {}; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
getLocalepath only readsnavigator.languages, so when that array is unavailable or empty (common in jsdom tests and some embedded browser contexts), locale resolution skips the browser locale entirely and falls back to English unlessGOOSE_LOCALEis set. This is a regression from the previous behavior that usednavigator.language, and it can silently disable both regional formatting and message catalog selection for users whose locale is otherwise detectable.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Electron ships Chromium, which has had navigator.languages since 2014. There's no Electron runtime where it's unavailable — the only place that case can occur is in jsdom tests, which we control directly.