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

@@ -3,6 +3,7 @@
* Shows prominent success messages for balance top-ups and subscription purchases. * Shows prominent success messages for balance top-ups and subscription purchases.
*/ */
import { uiLocale } from '@/utils/uiLocale';
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -96,7 +97,7 @@ export default function SuccessNotificationModal() {
// Format expiry date // Format expiry date
const formattedExpiry = data.expiresAt const formattedExpiry = data.expiresAt
? new Date(data.expiresAt).toLocaleDateString(undefined, { ? new Date(data.expiresAt).toLocaleDateString(uiLocale(), {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',

View File

@@ -1,7 +1,8 @@
import { uiLocale } from '@/utils/uiLocale';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { UseMutationResult } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query';
import TrafficProgressBar from './TrafficProgressBar'; import TrafficProgressBar from './TrafficProgressBar';
import Sparkline from './Sparkline'; import Sparkline from './Sparkline';
import { useAnimatedNumber } from '../../hooks/useAnimatedNumber'; import { useAnimatedNumber } from '../../hooks/useAnimatedNumber';
@@ -48,7 +49,7 @@ export default function SubscriptionCardActive({
const isAtDeviceLimit = const isAtDeviceLimit =
subscription.device_limit > 0 && connectedDevices >= subscription.device_limit; subscription.device_limit > 0 && connectedDevices >= subscription.device_limit;
const formattedDate = new Date(subscription.end_date).toLocaleDateString(); const formattedDate = new Date(subscription.end_date).toLocaleDateString(uiLocale());
const daysLeft = subscription.days_left; const daysLeft = subscription.days_left;
// Sparkline placeholder data (hidden until API provides daily usage) // Sparkline placeholder data (hidden until API provides daily usage)

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useNavigate, useLocation } from 'react-router'; import { Link, useNavigate, useLocation } from 'react-router';
@@ -37,7 +38,7 @@ export default function SubscriptionCardExpired({
const [isRenewing, setIsRenewing] = useState(false); const [isRenewing, setIsRenewing] = useState(false);
const [renewError, setRenewError] = useState<string | null>(null); const [renewError, setRenewError] = useState<string | null>(null);
const formattedDate = new Date(subscription.end_date).toLocaleDateString(); const formattedDate = new Date(subscription.end_date).toLocaleDateString(uiLocale());
// Detect limited (traffic exhausted) state // Detect limited (traffic exhausted) state
const isLimited = subscription.is_limited; const isLimited = subscription.is_limited;

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -413,7 +414,7 @@ export default function Balance() {
{getTypeLabel(tx.type)} {getTypeLabel(tx.type)}
</span> </span>
<span className="text-xs text-dark-500"> <span className="text-xs text-dark-500">
{new Date(tx.created_at).toLocaleDateString()} {new Date(tx.created_at).toLocaleDateString(uiLocale())}
</span> </span>
</div> </div>
{tx.description && ( {tx.description && (

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useMemo, useEffect, useCallback } from 'react'; import { useState, useMemo, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useNavigate, useSearchParams, Link } from 'react-router'; import { useNavigate, useSearchParams, Link } from 'react-router';
@@ -74,7 +75,7 @@ function isGiftActivated(gift: SentGift): boolean {
function formatGiftDate(dateStr: string | null): string { function formatGiftDate(dateStr: string | null): string {
if (!dateStr) return ''; if (!dateStr) return '';
const date = new Date(dateStr); const date = new Date(dateStr);
return date.toLocaleDateString(navigator.language || 'ru-RU', { return date.toLocaleDateString(uiLocale(), {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',

View File

@@ -1,12 +1,13 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCaretDown } from 'react-icons/pi'; import { PiCaretDown } from 'react-icons/pi';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { infoApi, FaqPage, InfoVisibility } from '../api/info'; import { infoApi, type FaqPage, type InfoVisibility } from '../api/info';
import { formatContent } from '../utils/legalContent'; import { formatContent } from '../utils/legalContent';
import { infoPagesApi } from '../api/infoPages'; import { infoPagesApi } from '../api/infoPages';
import { promoApi, LoyaltyTierInfo } from '../api/promo'; import { promoApi, type LoyaltyTierInfo } from '../api/promo';
import type { FaqItem, ReplacesTab } from '../api/infoPages'; import type { FaqItem, ReplacesTab } from '../api/infoPages';
import { DocumentIcon, InfoIcon, QuestionIcon, ShieldIcon, StarIcon } from '@/components/icons'; import { DocumentIcon, InfoIcon, QuestionIcon, ShieldIcon, StarIcon } from '@/components/icons';
@@ -489,7 +490,7 @@ export default function Info() {
/> />
{rules.updated_at && ( {rules.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(rules.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(rules.updated_at).toLocaleDateString(uiLocale())}
</p> </p>
)} )}
</div> </div>
@@ -517,7 +518,7 @@ export default function Info() {
/> />
{privacy.updated_at && ( {privacy.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(privacy.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(privacy.updated_at).toLocaleDateString(uiLocale())}
</p> </p>
)} )}
</div> </div>
@@ -545,7 +546,7 @@ export default function Info() {
/> />
{offer.updated_at && ( {offer.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(offer.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(offer.updated_at).toLocaleDateString(uiLocale())}
</p> </p>
)} )}
</div> </div>
@@ -566,7 +567,7 @@ export default function Info() {
} }
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', { return new Intl.NumberFormat(uiLocale(), {
style: 'currency', style: 'currency',
currency: 'RUB', currency: 'RUB',
minimumFractionDigits: 0, minimumFractionDigits: 0,

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router'; import { useParams, useNavigate, Link } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -88,7 +89,7 @@ function formatCountdown(seconds: number): string {
function formatDate(dateStr: string | null): string { function formatDate(dateStr: string | null): string {
if (!dateStr) return '-'; if (!dateStr) return '-';
try { try {
return new Date(dateStr).toLocaleDateString(undefined, { return new Date(dateStr).toLocaleDateString(uiLocale(), {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
@@ -99,7 +100,7 @@ function formatDate(dateStr: string | null): string {
} }
function formatBalance(kopeks: number): string { function formatBalance(kopeks: number): string {
return Math.floor(kopeks / 100).toLocaleString(); return Math.floor(kopeks / 100).toLocaleString(uiLocale());
} }
// -- Radio Indicator -- // -- Radio Indicator --

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate } from 'react-router'; import { Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -303,7 +304,7 @@ export default function Profile() {
<div className="flex items-center justify-between py-3"> <div className="flex items-center justify-between py-3">
<span className="text-dark-400">{t('profile.registeredAt')}</span> <span className="text-dark-400">{t('profile.registeredAt')}</span>
<span className="font-medium text-dark-100"> <span className="font-medium text-dark-100">
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : '-'} {user?.created_at ? new Date(user.created_at).toLocaleDateString(uiLocale()) : '-'}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router'; import { Link } from 'react-router';
@@ -77,7 +78,8 @@ export default function PublicLegal({ doc }: PublicLegalProps) {
<div dangerouslySetInnerHTML={{ __html: formatContent(data.content) }} /> <div dangerouslySetInnerHTML={{ __html: formatContent(data.content) }} />
{data.updated_at && ( {data.updated_at && (
<p className="mt-4 text-xs text-dark-500"> <p className="mt-4 text-xs text-dark-500">
{t('info.updatedAt', 'Обновлено')}: {new Date(data.updated_at).toLocaleDateString()} {t('info.updatedAt', 'Обновлено')}:{' '}
{new Date(data.updated_at).toLocaleDateString(uiLocale())}
</p> </p>
)} )}
</div> </div>

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StatCard } from '@/components/stats'; import { StatCard } from '@/components/stats';
import { import {
@@ -25,19 +26,19 @@ export function NetworkStats({ data, className }: NetworkStatsProps) {
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:gap-x-6 sm:gap-y-2"> <div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:gap-x-6 sm:gap-y-2">
<StatCard <StatCard
label={t('admin.referralNetwork.stats.totalUsers')} label={t('admin.referralNetwork.stats.totalUsers')}
value={data.total_users.toLocaleString()} value={data.total_users.toLocaleString(uiLocale())}
icon={<UsersIcon className="h-5 w-5" />} icon={<UsersIcon className="h-5 w-5" />}
tone="neutral" tone="neutral"
/> />
<StatCard <StatCard
label={t('admin.referralNetwork.stats.totalReferrers')} label={t('admin.referralNetwork.stats.totalReferrers')}
value={data.total_referrers.toLocaleString()} value={data.total_referrers.toLocaleString(uiLocale())}
icon={<PartnerIcon className="h-5 w-5" />} icon={<PartnerIcon className="h-5 w-5" />}
tone="neutral" tone="neutral"
/> />
<StatCard <StatCard
label={t('admin.referralNetwork.stats.totalCampaigns')} label={t('admin.referralNetwork.stats.totalCampaigns')}
value={data.total_campaigns.toLocaleString()} value={data.total_campaigns.toLocaleString(uiLocale())}
icon={<CampaignIcon className="h-5 w-5" />} icon={<CampaignIcon className="h-5 w-5" />}
tone="neutral" tone="neutral"
/> />

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { referralNetworkApi } from '@/api/referralNetwork'; import { referralNetworkApi } from '@/api/referralNetwork';
@@ -115,7 +116,7 @@ export function UserDetailPanel({ userId, className }: UserDetailPanelProps) {
{user.subscription_end && ( {user.subscription_end && (
<p className="mt-0.5 text-xs text-dark-400"> <p className="mt-0.5 text-xs text-dark-400">
{t('admin.referralNetwork.user.validUntil', { {t('admin.referralNetwork.user.validUntil', {
date: new Date(user.subscription_end).toLocaleDateString(), date: new Date(user.subscription_end).toLocaleDateString(uiLocale()),
})} })}
</p> </p>
)} )}

View File

@@ -1,10 +1,11 @@
import { uiLocale } from '@/utils/uiLocale';
import type { SubscriptionStatus } from '@/types/referralNetwork'; import type { SubscriptionStatus } from '@/types/referralNetwork';
/** /**
* Format kopeks to a human-readable ruble string. * Format kopeks to a human-readable ruble string.
*/ */
export function formatKopeksToRubles(kopeks: number): string { export function formatKopeksToRubles(kopeks: number): string {
return `${(kopeks / 100).toLocaleString('ru-RU')}`; return `${(kopeks / 100).toLocaleString(uiLocale())}`;
} }
/** /**

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -17,7 +18,7 @@ function formatCardDate(dateStr: string): string {
try { try {
const date = new Date(dateStr); const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr; if (isNaN(date.getTime())) return dateStr;
return date.toLocaleDateString(); return date.toLocaleDateString(uiLocale());
} catch { } catch {
return dateStr; return dateStr;
} }

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react'; import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -73,7 +74,7 @@ const CountdownTimer = memo(function CountdownTimer({
const isExpired = !isActive; const isExpired = !isActive;
const isUrgent = countdown.days <= 3; const isUrgent = countdown.days <= 3;
const formattedDate = new Date(endDate).toLocaleDateString(undefined, { const formattedDate = new Date(endDate).toLocaleDateString(uiLocale(), {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
@@ -997,7 +998,7 @@ export default function Subscription() {
</div> </div>
<div className="mt-0.5 font-mono text-[9px] text-dark-50/20"> <div className="mt-0.5 font-mono text-[9px] text-dark-50/20">
{t('subscription.trafficResetAt')}:{' '} {t('subscription.trafficResetAt')}:{' '}
{new Date(purchase.expires_at).toLocaleDateString(undefined, { {new Date(purchase.expires_at).toLocaleDateString(uiLocale(), {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
@@ -1017,8 +1018,12 @@ export default function Subscription() {
/> />
</div> </div>
<div className="mt-1 flex justify-between font-mono text-[9px] text-dark-50/20"> <div className="mt-1 flex justify-between font-mono text-[9px] text-dark-50/20">
<span>{new Date(purchase.created_at).toLocaleDateString()}</span> <span>
<span>{new Date(purchase.expires_at).toLocaleDateString()}</span> {new Date(purchase.created_at).toLocaleDateString(uiLocale())}
</span>
<span>
{new Date(purchase.expires_at).toLocaleDateString(uiLocale())}
</span>
</div> </div>
</div> </div>
))} ))}
@@ -1203,7 +1208,7 @@ export default function Subscription() {
</div> </div>
<div className="mt-1 text-[12px] text-dark-50/35"> <div className="mt-1 text-[12px] text-dark-50/35">
{t('subscription.pause.pausedDescription')}{' '} {t('subscription.pause.pausedDescription')}{' '}
{new Date(subscription.end_date).toLocaleDateString()} ( {new Date(subscription.end_date).toLocaleDateString(uiLocale())} (
{t('subscription.pause.days', { count: subscription.days_left })}) {t('subscription.pause.days', { count: subscription.days_left })})
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -411,7 +412,7 @@ export default function Support() {
</span> </span>
</div> </div>
<div className="text-xs text-dark-500"> <div className="text-xs text-dark-500">
{new Date(ticket.updated_at).toLocaleDateString()} {new Date(ticket.updated_at).toLocaleDateString(uiLocale())}
</div> </div>
</button> </button>
))} ))}
@@ -558,7 +559,7 @@ export default function Support() {
</span> </span>
<span className="text-xs text-dark-500"> <span className="text-xs text-dark-500">
{t('support.created')}{' '} {t('support.created')}{' '}
{new Date(selectedTicket.created_at).toLocaleDateString()} {new Date(selectedTicket.created_at).toLocaleDateString(uiLocale())}
</span> </span>
</div> </div>
</div> </div>
@@ -587,7 +588,7 @@ export default function Support() {
{msg.is_from_admin ? t('support.supportTeam') : t('support.you')} {msg.is_from_admin ? t('support.supportTeam') : t('support.you')}
</span> </span>
<span className="text-xs text-dark-500"> <span className="text-xs text-dark-500">
{new Date(msg.created_at).toLocaleString()} {new Date(msg.created_at).toLocaleString(uiLocale())}
</span> </span>
</div> </div>
{msg.message_text && ( {msg.message_text && (

View File

@@ -1,3 +1,4 @@
import { uiLocale } from '@/utils/uiLocale';
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -798,7 +799,7 @@ export default function Wheel() {
{item.prize_display_name} {item.prize_display_name}
</div> </div>
<div className="text-xs text-dark-500"> <div className="text-xs text-dark-500">
{new Date(item.created_at).toLocaleDateString()} {new Date(item.created_at).toLocaleDateString(uiLocale())}
</div> </div>
</div> </div>
</div> </div>

View File

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