From 8f31b30598ddf7eb03c4407ff8d18e196bbc6c78 Mon Sep 17 00:00:00 2001 From: CaIon Date: Tue, 7 Jul 2026 13:40:06 +0800 Subject: [PATCH] fix(i18n): standardize locale formatting for Intl APIs --- .../src/components/language-switcher.tsx | 1 - .../channels/components/channels-columns.tsx | 5 ++-- .../components/models/log-stat-cards.tsx | 3 ++- .../components/system-instances-panel.tsx | 3 ++- .../components/system-tasks-panel.tsx | 3 ++- web/default/src/i18n/languages.ts | 27 ++++++++++++++++++- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/web/default/src/components/language-switcher.tsx b/web/default/src/components/language-switcher.tsx index fcc6c1a4..a73c2cb3 100644 --- a/web/default/src/components/language-switcher.tsx +++ b/web/default/src/components/language-switcher.tsx @@ -39,7 +39,6 @@ export function LanguageSwitcher() { const { i18n, t } = useTranslation() const user = useAuthStore((s) => s.auth.user) const currentLanguage = normalizeInterfaceLanguage(i18n.language) - console.log('Current interface language: %s', currentLanguage) const handleChangeLanguage = useCallback( async (code: string) => { await i18n.changeLanguage(code) diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 807e35c6..8cb2f340 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -51,6 +51,7 @@ import { formatQuotaWithCurrency, getCurrencyLabel, } from '@/lib/currency' +import { toIntlLocale } from '@/i18n/languages' import { formatTimestampToDate } from '@/lib/format' import { truncateText } from '@/lib/utils' @@ -310,7 +311,7 @@ function BalanceCell({ channel }: { channel: Channel }) { const withSuffix = (value: string) => tokenSuffix && value !== '-' ? `${value}${tokenSuffix}` : value - const locale = i18n.resolvedLanguage || i18n.language + const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language) const balanceFormatOptions = { digitsLarge: 2, digitsSmall: 4, @@ -519,7 +520,7 @@ export function useChannelsColumns( const { t, i18n } = useTranslation() const { sensitiveVisible } = useChannels() const enableSelection = options.enableSelection ?? true - const locale = i18n.resolvedLanguage || i18n.language + const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language) // The column definitions only depend on the translation function, the active // locale, and sensitive-data visibility. Memoizing keeps the array (and every // cell renderer reference) stable across unrelated re-renders, so react-table diff --git a/web/default/src/features/dashboard/components/models/log-stat-cards.tsx b/web/default/src/features/dashboard/components/models/log-stat-cards.tsx index d6800cf6..61099246 100644 --- a/web/default/src/features/dashboard/components/models/log-stat-cards.tsx +++ b/web/default/src/features/dashboard/components/models/log-stat-cards.tsx @@ -31,6 +31,7 @@ import type { QuotaDataItem, DashboardFilters, } from '@/features/dashboard/types' +import { toIntlLocale } from '@/i18n/languages' import { formatCompactNumber, formatNumber, formatQuota } from '@/lib/format' import { computeTimeRange } from '@/lib/time' import { cn } from '@/lib/utils' @@ -121,7 +122,7 @@ export function LogStatCards(props: LogStatCardsProps) { const items = statCardsConfig.map((config) => { const rawValue = config.getValue(adaptedStats, timeRangeMinutes) - const locale = i18n.resolvedLanguage || i18n.language + const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language) const formatted = config.key === 'quota' ? { diff --git a/web/default/src/features/system-info/components/system-instances-panel.tsx b/web/default/src/features/system-info/components/system-instances-panel.tsx index 5348614d..496ed757 100644 --- a/web/default/src/features/system-info/components/system-instances-panel.tsx +++ b/web/default/src/features/system-info/components/system-instances-panel.tsx @@ -47,6 +47,7 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' +import { toIntlLocale } from '@/i18n/languages' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' @@ -411,7 +412,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { {formatTimestampRelative( instance.last_seen_at, 'seconds', - i18n.language + toIntlLocale(i18n.language) )} diff --git a/web/default/src/features/system-info/components/system-tasks-panel.tsx b/web/default/src/features/system-info/components/system-tasks-panel.tsx index 0cd6f499..3a49a3fd 100644 --- a/web/default/src/features/system-info/components/system-tasks-panel.tsx +++ b/web/default/src/features/system-info/components/system-tasks-panel.tsx @@ -38,6 +38,7 @@ import type { SystemTask, SystemTaskStatus, } from '@/features/system-settings/types' +import { toIntlLocale } from '@/i18n/languages' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' @@ -185,7 +186,7 @@ function SystemTasksTable(props: SystemTasksTableProps) { {formatTimestampRelative( task.updated_at, 'seconds', - i18n.language + toIntlLocale(i18n.language) )} %s', value, normalized) return INTERFACE_LANGUAGE_OPTIONS.some((lang) => lang.code === normalized) ? normalized : 'en' } + +/** + * Convert an interface language code (the values i18next uses, such as `zhCN` / + * `zhTW`) into a valid BCP-47 locale tag that the `Intl.*` APIs accept. + * + * `new Intl.NumberFormat('zhCN')` throws `RangeError: Invalid language tag`, so + * any locale derived from `i18n.language` / `i18n.resolvedLanguage` MUST be run + * through this before it reaches an `Intl` constructor. Unknown values fall back + * to `undefined`, which makes `Intl` use the runtime default locale. + */ +export function toIntlLocale(value?: string | null): string | undefined { + if (!value) return undefined + switch (value) { + case 'zhCN': + return 'zh-CN' + case 'zhTW': + return 'zh-TW' + default: + break + } + try { + return Intl.getCanonicalLocales(value)[0] + } catch { + return undefined + } +}