Commit 8f31b305 by CaIon

fix(i18n): standardize locale formatting for Intl APIs

parent 5cbb7b0b
...@@ -39,7 +39,6 @@ export function LanguageSwitcher() { ...@@ -39,7 +39,6 @@ export function LanguageSwitcher() {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const user = useAuthStore((s) => s.auth.user) const user = useAuthStore((s) => s.auth.user)
const currentLanguage = normalizeInterfaceLanguage(i18n.language) const currentLanguage = normalizeInterfaceLanguage(i18n.language)
console.log('Current interface language: %s', currentLanguage)
const handleChangeLanguage = useCallback( const handleChangeLanguage = useCallback(
async (code: string) => { async (code: string) => {
await i18n.changeLanguage(code) await i18n.changeLanguage(code)
......
...@@ -51,6 +51,7 @@ import { ...@@ -51,6 +51,7 @@ import {
formatQuotaWithCurrency, formatQuotaWithCurrency,
getCurrencyLabel, getCurrencyLabel,
} from '@/lib/currency' } from '@/lib/currency'
import { toIntlLocale } from '@/i18n/languages'
import { formatTimestampToDate } from '@/lib/format' import { formatTimestampToDate } from '@/lib/format'
import { truncateText } from '@/lib/utils' import { truncateText } from '@/lib/utils'
...@@ -310,7 +311,7 @@ function BalanceCell({ channel }: { channel: Channel }) { ...@@ -310,7 +311,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
const withSuffix = (value: string) => const withSuffix = (value: string) =>
tokenSuffix && value !== '-' ? `${value}${tokenSuffix}` : value tokenSuffix && value !== '-' ? `${value}${tokenSuffix}` : value
const locale = i18n.resolvedLanguage || i18n.language const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language)
const balanceFormatOptions = { const balanceFormatOptions = {
digitsLarge: 2, digitsLarge: 2,
digitsSmall: 4, digitsSmall: 4,
...@@ -519,7 +520,7 @@ export function useChannelsColumns( ...@@ -519,7 +520,7 @@ export function useChannelsColumns(
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { sensitiveVisible } = useChannels() const { sensitiveVisible } = useChannels()
const enableSelection = options.enableSelection ?? true 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 // The column definitions only depend on the translation function, the active
// locale, and sensitive-data visibility. Memoizing keeps the array (and every // locale, and sensitive-data visibility. Memoizing keeps the array (and every
// cell renderer reference) stable across unrelated re-renders, so react-table // cell renderer reference) stable across unrelated re-renders, so react-table
......
...@@ -31,6 +31,7 @@ import type { ...@@ -31,6 +31,7 @@ import type {
QuotaDataItem, QuotaDataItem,
DashboardFilters, DashboardFilters,
} from '@/features/dashboard/types' } from '@/features/dashboard/types'
import { toIntlLocale } from '@/i18n/languages'
import { formatCompactNumber, formatNumber, formatQuota } from '@/lib/format' import { formatCompactNumber, formatNumber, formatQuota } from '@/lib/format'
import { computeTimeRange } from '@/lib/time' import { computeTimeRange } from '@/lib/time'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
...@@ -121,7 +122,7 @@ export function LogStatCards(props: LogStatCardsProps) { ...@@ -121,7 +122,7 @@ export function LogStatCards(props: LogStatCardsProps) {
const items = statCardsConfig.map((config) => { const items = statCardsConfig.map((config) => {
const rawValue = config.getValue(adaptedStats, timeRangeMinutes) const rawValue = config.getValue(adaptedStats, timeRangeMinutes)
const locale = i18n.resolvedLanguage || i18n.language const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language)
const formatted = const formatted =
config.key === 'quota' config.key === 'quota'
? { ? {
......
...@@ -47,6 +47,7 @@ import { ...@@ -47,6 +47,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { toIntlLocale } from '@/i18n/languages'
import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
...@@ -411,7 +412,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { ...@@ -411,7 +412,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
{formatTimestampRelative( {formatTimestampRelative(
instance.last_seen_at, instance.last_seen_at,
'seconds', 'seconds',
i18n.language toIntlLocale(i18n.language)
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
......
...@@ -38,6 +38,7 @@ import type { ...@@ -38,6 +38,7 @@ import type {
SystemTask, SystemTask,
SystemTaskStatus, SystemTaskStatus,
} from '@/features/system-settings/types' } from '@/features/system-settings/types'
import { toIntlLocale } from '@/i18n/languages'
import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
...@@ -185,7 +186,7 @@ function SystemTasksTable(props: SystemTasksTableProps) { ...@@ -185,7 +186,7 @@ function SystemTasksTable(props: SystemTasksTableProps) {
{formatTimestampRelative( {formatTimestampRelative(
task.updated_at, task.updated_at,
'seconds', 'seconds',
i18n.language toIntlLocale(i18n.language)
)} )}
</TableCell> </TableCell>
<TableCell <TableCell
......
...@@ -41,8 +41,33 @@ export function normalizeInterfaceLanguage(value?: string | null): string { ...@@ -41,8 +41,33 @@ export function normalizeInterfaceLanguage(value?: string | null): string {
normalized = 'zhCN' normalized = 'zhCN'
} }
console.log('Normalized interface language: %s -> %s', value, normalized)
return INTERFACE_LANGUAGE_OPTIONS.some((lang) => lang.code === normalized) return INTERFACE_LANGUAGE_OPTIONS.some((lang) => lang.code === normalized)
? normalized ? normalized
: 'en' : '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
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment