Use the native disabled flag for individual keys, dim disabled rows, and mark partially disabled groups. Name the actual add action in the empty state, count easy-mode platforms by provider group, and use the Meta icon.
150 lines
7.4 KiB
TypeScript
150 lines
7.4 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { languageOptions, normalizeLocale, supportedLocales, translate } from '../src/i18n';
|
|
import { en, ja, zhCN, zhTW, type MessageKey } from '../src/i18n/resources';
|
|
import { jaOverrides } from '../src/i18n/ja';
|
|
|
|
describe('i18n', () => {
|
|
it('localizes system appearance controls in every language', () => {
|
|
expect(translate('zh-CN', 'app.theme.system')).toBe('自动');
|
|
expect(translate('zh-TW', 'app.theme.system')).toBe('自動');
|
|
expect(translate('zh-TW', 'app.theme.label')).toBe('外觀模式');
|
|
expect(translate('zh-TW', 'app.theme.switchToSystem')).toBe('隨系統自動切換亮色和暗色');
|
|
expect(translate('en', 'app.theme.system')).toBe('Auto');
|
|
expect(translate('ja', 'app.theme.system')).toBe('自動');
|
|
});
|
|
|
|
it('uses credential-file terminology throughout both Chinese locales', () => {
|
|
for (const key of ['app.nav.authFiles', 'authFiles.title'] as const) {
|
|
expect(translate('zh-CN', key)).toBe('凭证文件');
|
|
expect(translate('zh-TW', key)).toBe('憑證檔案');
|
|
}
|
|
expect(translate('zh-CN', 'authFiles.uploaded', { count: 2 })).toBe('已上传 2 个凭证文件');
|
|
expect(translate('zh-TW', 'authFiles.uploaded', { count: 2 })).toBe('已上傳 2 個憑證檔案');
|
|
expect(translate('zh-CN', 'quota.fileDisabled')).toBe('凭证文件已停用');
|
|
expect(Object.values(zhCN).filter((message) => message.includes('认证文件'))).toEqual([]);
|
|
expect(Object.values(zhTW).filter((message) => /認證(?:檔案|文件)/.test(message))).toEqual([]);
|
|
});
|
|
|
|
it('makes the OAuth-only model exclusion scope explicit in every locale', () => {
|
|
for (const locale of supportedLocales) {
|
|
const description = translate(locale, 'authFiles.models.globalDescription', { provider: 'Codex' });
|
|
expect(description).toContain('Codex');
|
|
expect(description).toContain('OAuth');
|
|
expect(description).toContain('API');
|
|
expect(translate(locale, 'authFiles.fileOnly')).toContain('API');
|
|
}
|
|
expect(translate('zh-CN', 'authFiles.models.globalDescription', { provider: 'Codex' })).toContain('不影响 API 接入');
|
|
});
|
|
|
|
it('normalizes supported variants and keeps English as the fallback', () => {
|
|
expect(normalizeLocale('en-US')).toBe('en');
|
|
expect(normalizeLocale('en')).toBe('en');
|
|
expect(normalizeLocale('zh-CN')).toBe('zh-CN');
|
|
expect(normalizeLocale('zh-TW')).toBe('zh-TW');
|
|
expect(normalizeLocale('zh-Hant-HK')).toBe('zh-TW');
|
|
expect(normalizeLocale('ja-JP')).toBe('ja');
|
|
expect(normalizeLocale('unsupported')).toBe('en');
|
|
});
|
|
|
|
it('translates messages and interpolates variables', () => {
|
|
expect(translate('zh-CN', 'kernel.install.installingVersion', { version: '1.2.3' }))
|
|
.toBe('正在安装 1.2.3');
|
|
expect(translate('en', 'kernel.install.installingVersion', { version: '1.2.3' }))
|
|
.toBe('Installing 1.2.3');
|
|
expect(translate('zh-TW', 'kernel.install.installingVersion', { version: '1.2.3' }))
|
|
.toBe('正在安裝 1.2.3');
|
|
expect(translate('ja', 'kernel.install.installingVersion', { version: '1.2.3' }))
|
|
.toBe('1.2.3 をインストールしています');
|
|
});
|
|
|
|
it('translates client API compatibility formats consistently', () => {
|
|
expect(translate('zh-CN', 'kernel.access.openaiDescription')).toBe('OpenAI兼容格式');
|
|
expect(translate('zh-TW', 'kernel.access.claudeDescription')).toBe('Anthropic相容格式');
|
|
expect(translate('ja', 'kernel.access.geminiDescription')).toBe('Gemini互換形式');
|
|
expect(translate('en', 'kernel.access.openaiDescription')).toBe('OpenAI-compatible format');
|
|
});
|
|
|
|
it('uses each language native name independently of the active locale', () => {
|
|
expect(languageOptions).toEqual([
|
|
{ value: 'zh-CN', nativeLabel: '简体中文' },
|
|
{ value: 'zh-TW', nativeLabel: '繁體中文' },
|
|
{ value: 'ja', nativeLabel: '日本語' },
|
|
{ value: 'en', nativeLabel: 'English' },
|
|
]);
|
|
});
|
|
|
|
it('requires complete dictionaries without a Japanese-to-English fallback', () => {
|
|
expect(Object.keys(en).sort()).toEqual(Object.keys(zhCN).sort());
|
|
expect(Object.keys(zhTW).sort()).toEqual(Object.keys(zhCN).sort());
|
|
expect(Object.keys(ja).sort()).toEqual(Object.keys(zhCN).sort());
|
|
expect(Object.keys(jaOverrides).sort()).toEqual(Object.keys(zhCN).sort());
|
|
expect(ja).toBe(jaOverrides);
|
|
});
|
|
|
|
it('preserves interpolation variables and nonempty messages in every locale', () => {
|
|
const variables = (message: string) => [...new Set(
|
|
[...message.matchAll(/\{(\w+)\}/g)].map((match) => match[1]),
|
|
)].sort();
|
|
for (const [locale, messages] of Object.entries({ en, ja, zhTW })) {
|
|
const mismatches = (Object.keys(zhCN) as MessageKey[]).filter((key) =>
|
|
!messages[key].trim()
|
|
|| JSON.stringify(variables(messages[key])) !== JSON.stringify(variables(zhCN[key])),
|
|
);
|
|
expect({ locale, mismatches }).toEqual({ locale, mismatches: [] });
|
|
}
|
|
});
|
|
|
|
it('does not include untranslated Chinese prose in English messages', () => {
|
|
expect(Object.entries(en).filter(([, message]) => /\p{Script=Han}/u.test(message))).toEqual([]);
|
|
});
|
|
|
|
it('names the actual add action in the empty API access prompt', () => {
|
|
expect(translate('zh-CN', 'apiAccess.empty.addFirst', { action: '添加 API Key' }))
|
|
.toBe('点击右上角“添加 API Key”添加第一个接入');
|
|
expect(translate('en', 'apiAccess.empty.addFirst', { action: 'Add Service' }))
|
|
.toBe('Click “Add Service” in the upper-right to create your first connection');
|
|
expect(translate('ja', 'apiAccess.empty.addFirst', { action: 'API キーを追加' }))
|
|
.toBe('右上の「API キーを追加」から最初の接続を追加してください');
|
|
expect(translate('zh-CN', 'easyMode.api.supportedPlatforms')).toContain('Vertex');
|
|
});
|
|
|
|
it('localizes API controls and usage labels in Chinese, English, and Japanese', () => {
|
|
for (const [key, chinese, english, japanese] of [
|
|
['apiAccess.cloak.auto', '自动', 'Automatic', '自動'],
|
|
['apiAccess.cloak.always', '始终启用', 'Always enabled', '常に有効'],
|
|
['apiAccess.cloak.never', '从不启用', 'Never enabled', '常に無効'],
|
|
['usage.column.provider', '提供商', 'Provider', 'プロバイダー'],
|
|
['usage.unit.requests', '次请求', 'requests', 'リクエスト'],
|
|
] as const) {
|
|
expect(translate('zh-CN', key)).toBe(chinese);
|
|
expect(translate('en', key)).toBe(english);
|
|
expect(translate('ja', key)).toBe(japanese);
|
|
}
|
|
});
|
|
|
|
it('preserves technical field names and units in every interface language', () => {
|
|
for (const locale of supportedLocales) {
|
|
for (const [key, term] of [
|
|
['easyMode.api.baseUrl', 'API Base URL'],
|
|
['easyMode.api.apiKey', 'API Key'],
|
|
['apiAccess.field.baseUrl', 'Base URL'],
|
|
['apiAccess.field.key', 'API Key'],
|
|
['kernel.access.apiUrl', 'API URL'],
|
|
['usage.unit.tokens', 'Token'],
|
|
] as const) {
|
|
expect(translate(locale, key)).toBe(term);
|
|
}
|
|
for (const key of [
|
|
'easyMode.api.description',
|
|
'easyMode.guide.cardStep1ApiDesc',
|
|
'easyMode.guide.cardStep2ApiFillTip',
|
|
'apiAccess.error.baseRequired',
|
|
'model.error.invalidBaseUrl',
|
|
] as const) {
|
|
expect(translate(locale, key)).toContain('Base URL');
|
|
}
|
|
expect(translate(locale, 'usage.stat.tokens')).toMatch(/tokens?/i);
|
|
}
|
|
});
|
|
});
|