feat: added i18n support; added Simplified Chinese support
This commit is contained in:
@@ -59,6 +59,7 @@ describe('processUserMessage slash commands', () => {
|
||||
configState.set('general.claude_openrouter.api_key', '');
|
||||
configState.set('general.openai_compatible.base_url', '');
|
||||
configState.set('general.openai_compatible.model', '');
|
||||
configState.set('general.language', 'en_us');
|
||||
|
||||
configManagerMock.getIsInitialized.mockReturnValue(true);
|
||||
configManagerMock.initialize.mockClear();
|
||||
@@ -99,6 +100,17 @@ describe('processUserMessage slash commands', () => {
|
||||
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm.md');
|
||||
});
|
||||
|
||||
it('uses the localized welcome asset when zh-CN is selected', async () => {
|
||||
configState.set('general.language', 'zh_cn');
|
||||
configState.set('general.llm_provider', 'local_browser');
|
||||
|
||||
const result = await processUserMessage('/welcome');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm-zh_cn.md'));
|
||||
expect(result.metadata).toMatchObject({ command: 'welcome', variant: 'local' });
|
||||
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm-zh_cn.md');
|
||||
});
|
||||
|
||||
it('uses the new-user welcome for non-local providers without required config', async () => {
|
||||
configState.set('general.llm_provider', 'openai');
|
||||
configState.set('general.openai.api_key', '');
|
||||
@@ -132,6 +144,59 @@ describe('processUserMessage slash commands', () => {
|
||||
expect(message?.content).toContain('welcome_local_llm.md');
|
||||
});
|
||||
|
||||
it('uses localized welcome assets for addWelcomeMessage under auto + Chinese locale', async () => {
|
||||
configState.set('general.language', 'auto');
|
||||
vi.stubGlobal('navigator', {
|
||||
languages: ['zh-CN', 'en-US'],
|
||||
language: 'zh-CN',
|
||||
});
|
||||
|
||||
const message = await addWelcomeMessage();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm-zh_cn.md'));
|
||||
expect(message?.content).toContain('welcome_local_llm-zh_cn.md');
|
||||
});
|
||||
|
||||
it('fetches the localized help guide for /help under zh-CN', async () => {
|
||||
configState.set('general.language', 'zh_cn');
|
||||
|
||||
const result = await processUserMessage('/help');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/help-zh_cn.md'));
|
||||
expect(result).toMatchObject({
|
||||
displayUserMessage: false,
|
||||
sendToLLM: false,
|
||||
finalMessageForLLM: null,
|
||||
metadata: { command: 'help' },
|
||||
});
|
||||
expect(result.pseudoAssistantResponse).toContain('chat/help-zh_cn.md');
|
||||
});
|
||||
|
||||
it('falls back to the English help guide when the localized file is missing', async () => {
|
||||
configState.set('general.language', 'zh_cn');
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.includes('chat/help-zh_cn.md')) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => `content:${url}`,
|
||||
};
|
||||
}));
|
||||
|
||||
const result = await processUserMessage('/help');
|
||||
|
||||
expect(fetch).toHaveBeenNthCalledWith(1, expect.stringContaining('chat/help-zh_cn.md'));
|
||||
expect(fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('chat/help.md'));
|
||||
expect(result.pseudoAssistantResponse).toContain('chat/help.md');
|
||||
});
|
||||
|
||||
it('fetches the hotkeys guide for /hotkeys', async () => {
|
||||
const result = await processUserMessage('/hotkeys');
|
||||
|
||||
@@ -145,6 +210,15 @@ describe('processUserMessage slash commands', () => {
|
||||
expect(result.pseudoAssistantResponse).toContain('chat/hotkeys.md');
|
||||
});
|
||||
|
||||
it('fetches the localized hotkeys guide for /hotkeys under zh-CN', async () => {
|
||||
configState.set('general.language', 'zh_cn');
|
||||
|
||||
const result = await processUserMessage('/hotkeys');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/hotkeys-zh_cn.md'));
|
||||
expect(result.pseudoAssistantResponse).toContain('chat/hotkeys-zh_cn.md');
|
||||
});
|
||||
|
||||
it('supports /hotkey as an alias of /hotkeys', async () => {
|
||||
const result = await processUserMessage('/hotkey');
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useProjectStore } from '../../stores/projectStore';
|
||||
import { ConfigManager } from '../../core/config/ConfigManager';
|
||||
import { SystemPrompts } from '../../agent/core/SystemPrompts';
|
||||
import { detectLocalLLMRuntimeSupport, LOCAL_LLM_PROVIDER_KEY } from '../localLLMConfig';
|
||||
import { normalizeLanguageSetting, resolveLanguageSetting } from '../../i18n/locale';
|
||||
import type { ResolvedLocaleCode } from '../../i18n/types';
|
||||
|
||||
export interface UserMessageFilterResult {
|
||||
// Whether to render the user message bubble (div.message-user)
|
||||
@@ -59,6 +61,48 @@ function getWelcomeUrl(variant: 'local' | 'new' | 'again'): string {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCurrentCommandLocale(configManager: ConfigManager): ResolvedLocaleCode {
|
||||
const languageSetting = normalizeLanguageSetting(configManager.get('general.language'));
|
||||
return resolveLanguageSetting(languageSetting);
|
||||
}
|
||||
|
||||
function resolveLocalizedChatMarkdownUrl(baseFileName: string, locale: ResolvedLocaleCode): string {
|
||||
if (locale === 'en_us') {
|
||||
return `${import.meta.env.BASE_URL}chat/${baseFileName}`;
|
||||
}
|
||||
|
||||
const extensionIndex = baseFileName.lastIndexOf('.');
|
||||
const localizedFileName = extensionIndex >= 0
|
||||
? `${baseFileName.slice(0, extensionIndex)}-${locale}${baseFileName.slice(extensionIndex)}`
|
||||
: `${baseFileName}-${locale}`;
|
||||
return `${import.meta.env.BASE_URL}chat/${localizedFileName}`;
|
||||
}
|
||||
|
||||
async function fetchLocalizedChatMarkdown(
|
||||
configManager: ConfigManager,
|
||||
baseFileName: string,
|
||||
): Promise<string> {
|
||||
const locale = resolveCurrentCommandLocale(configManager);
|
||||
const localizedUrl = resolveLocalizedChatMarkdownUrl(baseFileName, locale);
|
||||
const fallbackUrl = `${import.meta.env.BASE_URL}chat/${baseFileName}`;
|
||||
|
||||
const urlsToTry = locale === 'en_us' || localizedUrl === fallbackUrl
|
||||
? [fallbackUrl]
|
||||
: [localizedUrl, fallbackUrl];
|
||||
|
||||
let lastStatus = 'unknown';
|
||||
|
||||
for (const url of urlsToTry) {
|
||||
const resp = await fetch(url);
|
||||
if (resp.ok) {
|
||||
return await resp.text();
|
||||
}
|
||||
lastStatus = String(resp.status);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch chat markdown ${baseFileName}: ${lastStatus}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a user message before it is displayed or sent to the LLM.
|
||||
* Handles slash-commands and returns a structured decision.
|
||||
@@ -96,12 +140,8 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
||||
}
|
||||
|
||||
const variant = getWelcomeVariant(configManager);
|
||||
const url = getWelcomeUrl(variant);
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
|
||||
}
|
||||
const md = await resp.text();
|
||||
const baseFileName = getWelcomeUrl(variant).split('/').pop() ?? 'welcome_new.md';
|
||||
const md = await fetchLocalizedChatMarkdown(configManager, baseFileName);
|
||||
return {
|
||||
displayUserMessage: false,
|
||||
sendToLLM: false,
|
||||
@@ -123,12 +163,12 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
||||
|
||||
case '/help': {
|
||||
try {
|
||||
const url = `${import.meta.env.BASE_URL}chat/help.md`;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
|
||||
const configManager = ConfigManager.instance();
|
||||
if (!configManager.getIsInitialized()) {
|
||||
await configManager.initialize();
|
||||
}
|
||||
const md = await resp.text();
|
||||
|
||||
const md = await fetchLocalizedChatMarkdown(configManager, 'help.md');
|
||||
return {
|
||||
displayUserMessage: false,
|
||||
sendToLLM: false,
|
||||
@@ -151,12 +191,12 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
||||
case '/hotkeys':
|
||||
case '/hotkey': {
|
||||
try {
|
||||
const url = `${import.meta.env.BASE_URL}chat/hotkeys.md`;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
|
||||
const configManager = ConfigManager.instance();
|
||||
if (!configManager.getIsInitialized()) {
|
||||
await configManager.initialize();
|
||||
}
|
||||
const md = await resp.text();
|
||||
|
||||
const md = await fetchLocalizedChatMarkdown(configManager, 'hotkeys.md');
|
||||
return {
|
||||
displayUserMessage: false,
|
||||
sendToLLM: false,
|
||||
|
||||
Reference in New Issue
Block a user