fix(i18n): форматировать даты и числа локалью выбранного языка, а не браузера

This commit is contained in:
kewldan
2026-07-14 00:04:45 +03:00
parent 9f909b0f2c
commit 81fd96c2a7
19 changed files with 108 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ export function formatUptime(seconds: number): string {
import i18next from 'i18next';
import { currencyApi, type ExchangeRates } from '../api/currency';
import { uiLocale } from './uiLocale';
const LANG_CURRENCY_MAP: Record<
string,
@@ -60,18 +61,10 @@ export function formatPrice(kopeks: number, lang?: string): string {
}
}
const SHORT_DATE_LOCALE_MAP: Record<string, string> = {
ru: 'ru-RU',
en: 'en-US',
zh: 'zh-CN',
fa: 'fa-IR',
};
/** Date-only (dd.mm.yyyy) in the active UI locale; '-' for a null date. */
export function formatShortDate(date: string | null): string {
if (!date) return '-';
const locale = SHORT_DATE_LOCALE_MAP[i18next.language] || 'ru-RU';
return new Date(date).toLocaleDateString(locale, {
return new Date(date).toLocaleDateString(uiLocale(), {
day: '2-digit',
month: '2-digit',
year: 'numeric',

View File

@@ -0,0 +1,36 @@
import i18next from 'i18next';
import { beforeAll, describe, expect, it } from 'vitest';
import { uiLocale } from './uiLocale';
describe('uiLocale', () => {
beforeAll(async () => {
await i18next.init({ lng: 'ru', resources: {} });
});
it('маппит язык интерфейса в BCP-47 тег', async () => {
await i18next.changeLanguage('en');
expect(uiLocale()).toBe('en-US');
await i18next.changeLanguage('zh');
expect(uiLocale()).toBe('zh-CN');
await i18next.changeLanguage('fa');
expect(uiLocale()).toBe('fa-IR');
});
it('режет региональный суффикс языка', async () => {
await i18next.changeLanguage('en-GB');
expect(uiLocale()).toBe('en-US');
});
it('падает в ru-RU для неизвестного языка', async () => {
await i18next.changeLanguage('xx');
expect(uiLocale()).toBe('ru-RU');
});
it('дата форматируется по языку интерфейса, а не браузера', async () => {
await i18next.changeLanguage('en');
const date = new Date('2026-07-13T12:00:00Z');
expect(date.toLocaleDateString(uiLocale(), { timeZone: 'UTC' })).toBe('7/13/2026');
await i18next.changeLanguage('ru');
expect(date.toLocaleDateString(uiLocale(), { timeZone: 'UTC' })).toBe('13.07.2026');
});
});

18
src/utils/uiLocale.ts Normal file
View File

@@ -0,0 +1,18 @@
import i18next from 'i18next';
const LOCALE_MAP: Record<string, string> = {
ru: 'ru-RU',
en: 'en-US',
zh: 'zh-CN',
fa: 'fa-IR',
};
/**
* BCP-47 тег активного языка интерфейса для toLocale*-методов и Intl-API.
* Без него даты/числа форматируются локалью браузера (или жёстким 'ru-RU')
* и не реагируют на смену языка в приложении.
*/
export function uiLocale(): string {
const lang = (i18next.language || '').split('-')[0];
return LOCALE_MAP[lang] || 'ru-RU';
}