Skip to content

Commit 95ba77c

Browse files
LyaQanYiclaude
andauthored
feat(i18n): Add support for Spanish and Portuguese (Project-N-E-K-O#924)
* feat(i18n): Add support for Spanish and Portuguese * fix(i18n): address PR Project-N-E-K-O#924 review feedback for es/pt support - Add Element Plus locale imports for es/pt and register them in plugin-manager elLocaleMap so EP components don't fall back to zh-CN - Tighten language code matching with new _matches_lang_code helper: startswith('es')/('pt') was over-matching estonian/esperanto. Now uses exact-code, code-with-dash/underscore, or explicit alias set ({spanish, latam}/{portuguese, brazilian}) - Stop forcing src='en' for ambiguous Latin text in translate_text: when detect_language falls back to 'en' due to missing diacritics (e.g. "Hola como estas") and target is en/es/pt, pass 'auto' to Google and 'unknown' to translatepy so the translator does its own source detection Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a2ff6be commit 95ba77c

13 files changed

Lines changed: 7673 additions & 57 deletions

File tree

config/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,18 @@ def get_default_mmd_settings() -> dict:
472472
'女': 'Женский',
473473
'T酱, 小T': 'Тян-тян, малышка Т',
474474
},
475+
'es': {
476+
'哥哥': 'Hermano',
477+
'男': 'Masculino',
478+
'女': 'Femenino',
479+
'T酱, 小T': 'T-chan, Pequeña T',
480+
},
481+
'pt': {
482+
'哥哥': 'Irmão',
483+
'男': 'Masculino',
484+
'女': 'Feminino',
485+
'T酱, 小T': 'T-chan, Pequena T',
486+
},
475487
# zh 和 zh-CN 使用原始中文值(不需要翻译)
476488
}
477489

@@ -517,6 +529,10 @@ def get_localized_default_characters(language: str | None = None) -> dict:
517529
value_trans = _VALUE_TRANSLATIONS.get('en')
518530
elif lang_lower.startswith('ru'):
519531
value_trans = _VALUE_TRANSLATIONS.get('ru')
532+
elif lang_lower.startswith('es'):
533+
value_trans = _VALUE_TRANSLATIONS.get('es')
534+
elif lang_lower.startswith('pt'):
535+
value_trans = _VALUE_TRANSLATIONS.get('pt')
520536

521537
# 如果不需要翻译显示字段(简体中文/韩语等),仍需本地化 system_prompt
522538
if value_trans is None:

config/prompts_proactive.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,10 @@ def _normalize_prompt_language(lang: str) -> str:
12461246
return 'ko'
12471247
if lang_lower.startswith('ru'):
12481248
return 'ru'
1249+
# es/pt 当前未提供原生 proactive prompt 翻译,回退到 en(LLM 能理解英文系统 prompt,
1250+
# 输出语言由全局语言变量驱动的其他机制控制)
1251+
if lang_lower.startswith('es') or lang_lower.startswith('pt'):
1252+
return 'en'
12491253
return 'en'
12501254

12511255

config/prompts_sys.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@
2020
# =====================================================================
2121

2222
def _loc(d: dict, lang: str) -> str:
23-
"""从多语言 dict 按 lang 取值,缺失则回退 'zh'。"""
24-
if lang not in d:
23+
"""从多语言 dict 按 lang 取值,缺失则回退 'en'。
24+
25+
对于已在 SUPPORTED_LANGUAGES 但未在某个 dict 里显式翻译的语言
26+
(当前是 'es'、'pt')静默回退到英文,不打 WARNING。
27+
非本地化层的内部 LLM 系统 prompt 保持英文即可,LLM 能正确处理。
28+
"""
29+
_SILENT_FALLBACK = {'es', 'pt'}
30+
if lang not in d and lang not in _SILENT_FALLBACK:
2531
print(f"WARNING: Unexpected lang code {lang}")
2632
return d.get(lang, d['en'])
2733

@@ -334,6 +340,8 @@ def _loc(d: dict, lang: str) -> str:
334340
'ja': '以下の要件に従い、ユーザーのテキストを{source_name}から{target_name}に翻訳してください。',
335341
'ko': '요구사항에 따라 사용자의 텍스트를 {source_name}에서 {target_name}(으)로 번역하세요.',
336342
'ru': 'Переведите текст пользователя с {source_name} на {target_name} согласно требованиям.',
343+
'es': 'Traduce el texto del usuario de {source_name} a {target_name} según los requisitos.',
344+
'pt': 'Traduza o texto do usuário de {source_name} para {target_name} conforme os requisitos.',
337345
}
338346

339347
# 翻译要求(水印包裹部分)
@@ -343,15 +351,19 @@ def _loc(d: dict, lang: str) -> str:
343351
'ja': '1. 原文の語調とスタイルを維持する\n2. 原文の意味を正確に伝える\n3. 翻訳結果のみを出力し、説明や注釈は一切加えない\n4. テキストに含まれる絵文字や特殊記号はそのまま残す',
344352
'ko': '1. 원문의 어조와 스타일을 유지할 것\n2. 원문의 의미를 정확히 전달할 것\n3. 번역 결과만 출력하고 설명이나 부연을 추가하지 말 것\n4. 텍스트에 포함된 이모지나 특수 기호는 그대로 유지할 것',
345353
'ru': '1. Сохраняйте тон и стиль оригинала\n2. Точно передавайте смысл исходного текста\n3. Выводите только перевод, без пояснений и примечаний\n4. Сохраняйте эмодзи и специальные символы из текста',
354+
'es': '1. Mantén el tono y el estilo del texto original\n2. Transmite el significado con precisión\n3. Devuelve solo la traducción, sin explicaciones ni notas\n4. Conserva los emojis y símbolos especiales del texto',
355+
'pt': '1. Mantenha o tom e o estilo do texto original\n2. Transmita o significado com precisão\n3. Retorne apenas a tradução, sem explicações ou notas\n4. Preserve emojis e símbolos especiais do texto',
346356
}
347357

348358
# 语言名称(外层 key=UI 语言,内层 key=语言代码)
349359
TRANSLATION_LANG_NAMES = {
350-
'zh': {'zh': '中文', 'en': '英文', 'ja': '日语', 'ko': '韩语', 'ru': '俄语'},
351-
'en': {'zh': 'Chinese', 'en': 'English', 'ja': 'Japanese', 'ko': 'Korean', 'ru': 'Russian'},
352-
'ja': {'zh': '中国語', 'en': '英語', 'ja': '日本語', 'ko': '韓国語', 'ru': 'ロシア語'},
353-
'ko': {'zh': '중국어', 'en': '영어', 'ja': '일본어', 'ko': '한국어', 'ru': '러시아어'},
354-
'ru': {'zh': 'китайский', 'en': 'английский', 'ja': 'японский', 'ko': 'корейский', 'ru': 'русский'},
360+
'zh': {'zh': '中文', 'en': '英文', 'ja': '日语', 'ko': '韩语', 'ru': '俄语', 'es': '西班牙语', 'pt': '葡萄牙语'},
361+
'en': {'zh': 'Chinese', 'en': 'English', 'ja': 'Japanese', 'ko': 'Korean', 'ru': 'Russian', 'es': 'Spanish', 'pt': 'Portuguese'},
362+
'ja': {'zh': '中国語', 'en': '英語', 'ja': '日本語', 'ko': '韓国語', 'ru': 'ロシア語', 'es': 'スペイン語', 'pt': 'ポルトガル語'},
363+
'ko': {'zh': '중국어', 'en': '영어', 'ja': '일본어', 'ko': '한국어', 'ru': '러시아어', 'es': '스페인어', 'pt': '포르투갈어'},
364+
'ru': {'zh': 'китайский', 'en': 'английский', 'ja': 'японский', 'ko': 'корейский', 'ru': 'русский', 'es': 'испанский', 'pt': 'португальский'},
365+
'es': {'zh': 'chino', 'en': 'inglés', 'ja': 'japonés', 'ko': 'coreano', 'ru': 'ruso', 'es': 'español', 'pt': 'portugués'},
366+
'pt': {'zh': 'chinês', 'en': 'inglês', 'ja': 'japonês', 'ko': 'coreano', 'ru': 'russo', 'es': 'espanhol', 'pt': 'português'},
355367
}
356368

357369
# ---------- 对话备忘录注入 LLM 上下文 ----------
@@ -361,6 +373,8 @@ def _loc(d: dict, lang: str) -> str:
361373
'ja': '以前の会話のメモ: {summary}',
362374
'ko': '이전 대화의 메모: {summary}',
363375
'ru': 'Заметки из предыдущих разговоров: {summary}',
376+
'es': 'Notas de conversaciones previas: {summary}',
377+
'pt': 'Notas de conversas anteriores: {summary}',
364378
}
365379

366380
MEMORY_MEMO_EMPTY = {
@@ -369,6 +383,8 @@ def _loc(d: dict, lang: str) -> str:
369383
'ja': '以前の会話のメモ: なし。',
370384
'ko': '이전 대화의 메모: 없음.',
371385
'ru': 'Заметки из предыдущих разговоров: нет.',
386+
'es': 'Notas de conversaciones previas: ninguna.',
387+
'pt': 'Notas de conversas anteriores: nenhuma.',
372388
}
373389

374390
# ---------- 搜索关键词生成 prompt ----------
@@ -383,6 +399,8 @@ def _loc(d: dict, lang: str) -> str:
383399
'ja': 'ウィンドウタイトルから検索キーワードを生成してください。\n\n要件:\n1. 異なる角度から検索用のキーワードを 3 つ生成\n2. 各キーワードは簡潔に、2〜6 語程度\n3. キーワードは多様性を持たせる\n4. 3 行のみ出力し、番号・句読点・説明等は一切不要',
384400
'ko': '창 제목에서 검색 키워드를 생성하세요.\n\n요구사항:\n1. 서로 다른 관점에서 검색 키워드 3개 생성\n2. 각 키워드는 간결하게, 2~6 단어 정도\n3. 키워드는 다양하게\n4. 정확히 3줄만 출력하고 번호, 구두점, 설명 등은 추가하지 마세요',
385401
'ru': 'Сгенерируйте ключевые слова для поиска на основе заголовка окна.\n\nТребования:\n1. Сгенерируйте 3 разнообразных ключевых слова для поиска с разных сторон\n2. Каждое ключевое слово — кратко, около 2-6 слов\n3. Ключевые слова должны быть разнообразными\n4. Выведите ровно 3 строки, по одному ключевому слову, без номеров, пунктуации и пояснений',
402+
'es': 'Generas palabras clave de búsqueda a partir del título de una ventana.\n\nRequisitos:\n1. Genera 3 palabras clave diversas desde distintos ángulos\n2. Cada palabra clave debe ser concisa, de 2 a 6 palabras\n3. Mantén las palabras clave variadas\n4. Devuelve exactamente 3 líneas, una palabra clave por línea, sin números, puntuación, explicaciones ni texto adicional',
403+
'pt': 'Você gera palavras-chave de busca a partir do título de uma janela.\n\nRequisitos:\n1. Gere 3 palavras-chave diversas de ângulos distintos\n2. Cada palavra-chave deve ser concisa, com 2 a 6 palavras\n3. Mantenha as palavras-chave variadas\n4. Retorne exatamente 3 linhas, uma palavra-chave por linha, sem números, pontuação, explicações ou texto adicional',
386404
}
387405

388406
SEARCH_KEYWORD_USER = {
@@ -391,6 +409,8 @@ def _loc(d: dict, lang: str) -> str:
391409
'ja': '======以下为窗口标题======\n{window_title}\n======以上为窗口标题======\n\n検索キーワードを 3 つ出力してください。',
392410
'ko': '======以下为窗口标题======\n{window_title}\n======以上为窗口标题======\n\n검색 키워드 3개를 출력하세요.',
393411
'ru': '======以下为窗口标题======\n{window_title}\n======以上为窗口标题======\n\nВыведите 3 ключевых слова для поиска.',
412+
'es': '======以下为窗口标题======\n{window_title}\n======以上为窗口标题======\n\nDevuelve 3 palabras clave de búsqueda.',
413+
'pt': '======以下为窗口标题======\n{window_title}\n======以上为窗口标题======\n\nRetorne 3 palavras-chave de busca.',
394414
}
395415

396416
# =====================================================================

frontend/plugin-manager/src/components/common/LanguageSwitcher.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
<el-dropdown-item command="ru" :disabled="currentSetting === 'ru'">
2727
<span>🇷🇺 Русский</span>
2828
</el-dropdown-item>
29+
<el-dropdown-item command="es" :disabled="currentSetting === 'es'">
30+
<span>🇪🇸 Español</span>
31+
</el-dropdown-item>
32+
<el-dropdown-item command="pt" :disabled="currentSetting === 'pt'">
33+
<span>🇵🇹 Português</span>
34+
</el-dropdown-item>
2935
</el-dropdown-menu>
3036
</template>
3137
</el-dropdown>
@@ -46,7 +52,9 @@ const LOCALE_SHORT_LABELS: Record<AppLocale, string> = {
4652
'en-US': 'EN',
4753
'ja': 'JP',
4854
'ko': 'KR',
49-
'ru': 'RU'
55+
'ru': 'RU',
56+
'es': 'ES',
57+
'pt': 'PT'
5058
}
5159
5260
const displayLabel = computed(() => LOCALE_SHORT_LABELS[getLocale()])

frontend/plugin-manager/src/i18n/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import enUS from './locales/en-US'
88
import ja from './locales/ja'
99
import ko from './locales/ko'
1010
import ru from './locales/ru'
11+
import es from './locales/es'
12+
import pt from './locales/pt'
1113

12-
export const SUPPORTED_LOCALES = ['zh-CN', 'zh-TW', 'en-US', 'ja', 'ko', 'ru'] as const
14+
export const SUPPORTED_LOCALES = ['zh-CN', 'zh-TW', 'en-US', 'ja', 'ko', 'ru', 'es', 'pt'] as const
1315
export type AppLocale = (typeof SUPPORTED_LOCALES)[number]
1416
export type LocaleSetting = AppLocale | 'auto'
1517
const DEFAULT_LOCALE: AppLocale = 'zh-CN'
@@ -34,6 +36,8 @@ function resolveLocaleFromBrowser(): AppLocale {
3436
if (langCode === 'ja') return 'ja'
3537
if (langCode === 'ko') return 'ko'
3638
if (langCode === 'ru') return 'ru'
39+
if (langCode === 'es') return 'es'
40+
if (langCode === 'pt') return 'pt'
3741
if (langCode === 'zh') {
3842
const upper = lang.toUpperCase()
3943
if (upper.includes('HANS')) return 'zh-CN'
@@ -72,7 +76,9 @@ export const i18n = createI18n({
7276
'en-US': enUS,
7377
'ja': ja,
7478
'ko': ko,
75-
'ru': ru
79+
'ru': ru,
80+
'es': es,
81+
'pt': pt
7682
}
7783
})
7884

0 commit comments

Comments
 (0)