mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
feat(i18n): add Japanese dashboard locale
This commit is contained in:
1 parent
447033cb03
commit
8b5158bec5
3 files changed
+1248
-34
No files matched your search
+1200
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
|||||||
export const LOCALE_STORAGE_KEY = 'ccs-ui-locale';
|
export const LOCALE_STORAGE_KEY = 'ccs-ui-locale';
|
||||||
|
|
||||||
export const SUPPORTED_LOCALES = ['en', 'zh-CN', 'vi'] as const;
|
export const SUPPORTED_LOCALES = ['en', 'zh-CN', 'vi', 'ja'] as const;
|
||||||
|
|
||||||
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
|
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ export function isSupportedLocale(locale: string): locale is AppLocale {
|
|||||||
|
|
||||||
export function normalizeLocale(locale: string | null | undefined): AppLocale {
|
export function normalizeLocale(locale: string | null | undefined): AppLocale {
|
||||||
if (!locale) return 'en';
|
if (!locale) return 'en';
|
||||||
|
if (locale.toLowerCase().startsWith('ja')) return 'ja';
|
||||||
if (locale.toLowerCase().startsWith('zh')) return 'zh-CN';
|
if (locale.toLowerCase().startsWith('zh')) return 'zh-CN';
|
||||||
if (locale.toLowerCase().startsWith('vi')) return 'vi';
|
if (locale.toLowerCase().startsWith('vi')) return 'vi';
|
||||||
return 'en';
|
return 'en';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
LOCALE_STORAGE_KEY,
|
LOCALE_STORAGE_KEY,
|
||||||
normalizeLocale,
|
normalizeLocale,
|
||||||
persistLocale,
|
persistLocale,
|
||||||
|
SUPPORTED_LOCALES,
|
||||||
} from '@/lib/locales';
|
} from '@/lib/locales';
|
||||||
|
|
||||||
function flattenKeys(node: unknown, prefix = '', out = new Set<string>()): Set<string> {
|
function flattenKeys(node: unknown, prefix = '', out = new Set<string>()): Set<string> {
|
||||||
@@ -91,22 +92,28 @@ describe('Dashboard i18n', () => {
|
|||||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(LOCALE_STORAGE_KEY, 'zh-CN');
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(LOCALE_STORAGE_KEY, 'zh-CN');
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
it('supports Vietnamese locale in switcher and persistence', async () => {
|
it.each([
|
||||||
render(<LanguageSwitcher />);
|
{ locale: 'vi', label: 'Vietnamese', browserLocale: 'vi-VN' },
|
||||||
|
{ locale: 'ja', label: 'Japanese', browserLocale: 'ja-JP' },
|
||||||
|
])(
|
||||||
|
'supports $locale locale in switcher and persistence',
|
||||||
|
async ({ locale, label, browserLocale }) => {
|
||||||
|
render(<LanguageSwitcher />);
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('combobox'));
|
await userEvent.click(screen.getByRole('combobox'));
|
||||||
await userEvent.click(await screen.findByText('Vietnamese'));
|
await userEvent.click(await screen.findByText(label));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(i18n.language).toBe('vi');
|
expect(i18n.language).toBe(locale);
|
||||||
});
|
});
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(LOCALE_STORAGE_KEY, 'vi');
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(LOCALE_STORAGE_KEY, locale);
|
||||||
expect(normalizeLocale('vi-VN')).toBe('vi');
|
expect(normalizeLocale(browserLocale)).toBe(locale);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('restores locale from persisted storage', () => {
|
it('restores locale from persisted storage', () => {
|
||||||
persistLocale('vi');
|
persistLocale('ja');
|
||||||
expect(getInitialLocale()).toBe('vi');
|
expect(getInitialLocale()).toBe('ja');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows Chinese labels on translated settings tabs', async () => {
|
it('shows Chinese labels on translated settings tabs', async () => {
|
||||||
@@ -119,32 +126,38 @@ describe('Dashboard i18n', () => {
|
|||||||
expect(screen.getByText('认证')).toBeInTheDocument();
|
expect(screen.getByText('认证')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps vi translation keys in parity with en and preserves placeholders', () => {
|
it.each(SUPPORTED_LOCALES.filter((locale) => locale !== 'en'))(
|
||||||
const resources = i18n.options.resources as
|
'keeps %s translation keys in parity with en and preserves placeholders',
|
||||||
| Record<string, { translation: Record<string, unknown> }>
|
(locale) => {
|
||||||
| undefined;
|
const resources = i18n.options.resources as
|
||||||
|
| Record<string, { translation: Record<string, unknown> }>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
const enTranslation = resources?.en?.translation;
|
const enTranslation = resources?.en?.translation;
|
||||||
const viTranslation = resources?.vi?.translation;
|
const localeTranslation = resources?.[locale]?.translation;
|
||||||
|
|
||||||
expect(enTranslation).toBeDefined();
|
expect(enTranslation).toBeDefined();
|
||||||
expect(viTranslation).toBeDefined();
|
expect(localeTranslation).toBeDefined();
|
||||||
|
|
||||||
const enKeys = flattenKeys(enTranslation);
|
const enKeys = flattenKeys(enTranslation);
|
||||||
const viKeys = flattenKeys(viTranslation);
|
const localeKeys = flattenKeys(localeTranslation);
|
||||||
|
|
||||||
expect([...viKeys].sort()).toEqual([...enKeys].sort());
|
expect([...localeKeys].sort()).toEqual([...enKeys].sort());
|
||||||
|
|
||||||
for (const key of enKeys) {
|
for (const key of enKeys) {
|
||||||
const enValue = key
|
const enValue = key
|
||||||
.split('.')
|
.split('.')
|
||||||
.reduce<unknown>((acc, part) => (acc as Record<string, unknown>)?.[part], enTranslation);
|
.reduce<unknown>((acc, part) => (acc as Record<string, unknown>)?.[part], enTranslation);
|
||||||
const viValue = key
|
const localeValue = key
|
||||||
.split('.')
|
.split('.')
|
||||||
.reduce<unknown>((acc, part) => (acc as Record<string, unknown>)?.[part], viTranslation);
|
.reduce<unknown>(
|
||||||
|
(acc, part) => (acc as Record<string, unknown>)?.[part],
|
||||||
|
localeTranslation
|
||||||
|
);
|
||||||
|
|
||||||
if (typeof enValue !== 'string' || typeof viValue !== 'string') continue;
|
if (typeof enValue !== 'string' || typeof localeValue !== 'string') continue;
|
||||||
expect(collectPlaceholders(viValue)).toEqual(collectPlaceholders(enValue));
|
expect(collectPlaceholders(localeValue)).toEqual(collectPlaceholders(enValue));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user