Files
lzt404 830fc2c1cc fix(api): reflect disabled keys and platform counts
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.
2026-10-06 01:22:41 +08:00

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);
}
});
});