Resume-Matcher 国际化(i18n)指南:UI 与内容双语言体系、翻译接入与多语言新增实践
【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher
本篇技术指南以 docs/agent/features/i18n.md 为核心骨架,结合 docs/agent/features/i18n-preparation.md 的落地计划,深入解析 Resume-Matcher 的多语言体系。你将掌握:项目支持哪些语言、UI 语言与内容语言两套设置如何独立工作、翻译消息如何在前端加载与替换、
{output_language}如何贯穿到后端 LLM 提示词,以及如何在本地新增一种语言并完成前后端配置。文中所有结论均可回到仓库源码逐一验证,可直接作为开发者的上手与扩展手册。
一、项目概况与 i18n 目标
Resume-Matcher 是一个本地运行的 AI 简历工具,支持简历(Resume)、PDF、求职信(Cover Letter)等内容生成,并可对接 100+ 本地或云端 LLM。多语言能力是其面向全球用户的基础设施,具体表现为两层目标:
- UI 国际化:界面文本(按钮、标签、导航)随语言切换;
- 内容国际化:由 LLM 生成的简历、求职信、外联消息、面试准备材料等,按指定语言输出。
仓库在 docs/agent/features/i18n.md 中明确了两套语言设置可在设置页独立配置,互不干扰;docs/agent/features/i18n-preparation.md 则记录了从单语言演进到多语言的具体准备步骤,包括消息文件布局、翻译键结构与后端未来扩展方向。本文按"语言支持 → 双语言架构 → 前端实现 → 后端实现 → 存储 → 新增语言 → 质量保障"的顺序展开。
二、支持的语言与权威配置源
2.1 支持语言一览
| Code | Language | Native Name | Flag | Message File |
|---|---|---|---|---|
en | English | English | 🇺🇸 | messages/en.json |
es | Spanish | Español | 🇪🇸 | messages/es.json |
zh | Chinese (Simplified) | 中文 | 🇨🇳 | messages/zh.json |
ja | Japanese | 日本語 | 🇯🇵 | messages/ja.json |
pt | Portuguese (Brazilian) | Português | 🇧🇷 | messages/pt-BR.json |
fr | French | Français | 🇫🇷 | messages/fr.json |
对应消息文件均位于 apps/frontend/messages/ 目录下。注意两点细节:
- 语言码与文件名不一致:葡萄牙语的语言码是
pt,但消息文件名为pt-BR.json(巴西葡萄牙语); - 默认语言为
en,任何未匹配的语言都会回退到英文。
2.2 权威配置源:i18n/config.ts
语言清单的单一事实来源是 apps/frontend/i18n/config.ts,其中定义了:
export const locales = ['en', 'es', 'zh', 'ja', 'pt', 'fr'] as const; export type Locale = (typeof locales)[number]; export const defaultLocale: Locale = 'en'; export const localeNames: Record<Locale, string> = { en: 'English', es: 'Español', zh: '中文', ja: '日本語', pt: 'Português', fr: 'Français', }; export const localeFlags: Record<Locale, string> = { en: '🇺🇸', es: '🇪🇸', zh: '🇨🇳', ja: '🇯🇵', pt: '🇧🇷', fr: '🇫🇷', };从源码结构看,locales数组同时驱动了Locale类型推导与localeNames/localeFlags两个映射表的完整性约束:新增语言时,三处必须同步修改,否则 TypeScript 类型检查会直接报错——这正是该文件被称为"source of truth"的原因。
三、双语言体系:UI Language 与 Content Language
3.1 两者的区别
| 维度 | UI Language | Content Language |
|---|---|---|
| 作用对象 | 界面文本(按钮、标签、导航) | LLM 生成内容(简历、求职信等) |
| 前端控制 | useTranslations钩子 | LanguageProvider上下文 |
| 后端参与 | 否(纯前端) | 是(写入后端配置并注入提示词) |
| 存储位置 | localStorage | localStorage + 后端配置 |
二者在 apps/frontend/app/(default)/settings/page.tsx/settings/page.tsx) 的设置页中以两个独立的分段选择器(segmented control)呈现,分别调用setUiLanguage(lang)与setContentLanguage(lang)(见 设置页 L1275-L1322/settings/page.tsx#L1275-L1322))。
3.2 LanguageProvider:双语言状态的核心上下文
apps/frontend/lib/context/language-context.tsx 中的LanguageProvider是前端语言状态的统一入口,其加载流程如下:
- 从 localStorage 读取
resume_matcher_ui_language与resume_matcher_content_language(若值在locales内则直接采用); - 调用
fetchLanguageConfig()从后端拉取已保存的content_language并覆盖本地缓存,确保前后端同步; - 加载完成后将
isLoading置为false。
setContentLanguage采用乐观更新 + 失败回滚策略:先更新本地 state 与 localStorage,再调用updateLanguageConfig({ content_language: lang })持久化到后端;若请求失败,则回滚到上一个语言并打印错误日志(language-context.tsx L64-L87)。setUiLanguage则只写 localStorage,不涉及后端。
任何消费该上下文的组件都必须位于LanguageProvider之内,否则useLanguage()会抛出useLanguage must be used within a LanguageProvider错误。
四、前端翻译实现:无依赖的 JSON 加载方案
4.1 设计选型
与许多项目引入next-intl不同,Resume-Matcher 当前的 UI 翻译采用了最简方案:消息以 JSON 静态导入,按当前 UI 语言选择对应文件,无任何外部依赖。这一设计决策写在该项目的 i18n 准备文档中,也在 apps/frontend/lib/i18n/index.ts 的文件头注释里明确说明。
4.2 消息加载:messages.ts
apps/frontend/lib/i18n/messages.ts 负责把语言码映射到具体的 JSON 文件:
import en from '@/messages/en.json'; import es from '@/messages/es.json'; import zh from '@/messages/zh.json'; import ja from '@/messages/ja.json'; import pt from '@/messages/pt-BR.json'; // 注意:pt 加载 pt-BR.json import fr from '@/messages/fr.json'; export type Messages = typeof en; // 以 en 为类型基准 const allMessages: Record<Locale, Messages> = { en, es, zh, ja, pt, fr }; export function getMessages(locale: Locale): Messages { return allMessages[locale] || allMessages.en; // 未匹配时回退 en }type Messages = typeof en是关键约束:所有语言文件的 JSON 结构必须与en.json完全一致,否则tsc/next build会编译失败(这一约束同时被独立的校验脚本利用,见第七节)。
4.3 翻译键结构
翻译消息以嵌套 JSON 组织,顶层按功能模块分区。以 apps/frontend/messages/en.json 为参考,典型结构如下:
{ "dashboard": { "title": "Dashboard", "masterResume": "Master Resume" }, "builder": { "save": "Save", "download": "Download PDF" }, "common": { "save": "Save" } }键通过点号(.)定位嵌套层级,例如t('common.save')。
4.4 翻译查询与参数替换:translations.ts + utils.ts
apps/frontend/lib/i18n/translations.ts 提供useTranslations客户端钩子:
import { useTranslations } from '@/lib/i18n'; const { t } = useTranslations(); <button>{t('common.save')}</button>其底层依赖两个纯函数(apps/frontend/lib/i18n/utils.ts):
getNestedValue(obj, path):按点号路径遍历嵌套对象;任何一步缺失都返回原始 path 字符串(便于发现问题),而不是抛异常;applyParams(value, params):以{param}形式替换占位符,例如t('greeting', { name: 'Ada' })会把"Hello, {name}"渲染为"Hello, Ada"。
此外,该文件还导出了面向服务端组件的getMessages与translate(locale, key, params?),便于在 Server Component 中直接按指定 locale 取翻译。
五、后端内容生成:{output_language}贯穿 LLM 提示词
5.1 语言码 → 全称的映射
LLM 提示词中使用的不是语言码,而是语言全称。映射表定义在 apps/backend/app/prompts/templates.py 顶部:
LANGUAGE_NAMES = { "en": "English", "es": "Spanish", "zh": "Chinese (Simplified)", "ja": "Japanese", "pt": "Brazilian Portuguese", "fr": "French", } def get_language_name(code: str) -> str: """Get full language name from code.""" return LANGUAGE_NAMES.get(code, "English")注意get_language_name的兜底逻辑:未识别的语言码一律返回"English",因此后端在接收入参时务必先做语言码校验(见 5.3)。
5.2 提示词中的注入方式
所有生成类提示词模板都通过{output_language}占位符接收目标语言,例如:
- 简历改进三档提示词(
IMPROVE_RESUME_PROMPT_NUDGE/_KEYWORDS/_FULL):IMPORTANT: Generate ALL text content (summary, descriptions, skills) in {output_language}. - 求职信提示词(
COVER_LETTER_PROMPT):IMPORTANT: Write in {output_language}. - 外联消息提示词(
OUTREACH_MESSAGE_PROMPT):IMPORTANT: Write in {output_language}. - 面试准备提示词(
INTERVIEW_PREP_PROMPT):IMPORTANT: Write in {output_language}.且额外要求"不翻译 JSON 属性名,只翻译字符串值" - 标题生成提示词(
GENERATE_TITLE_PROMPT):IMPORTANT: Write in {output_language}. - 技能目标计划提示词(
SKILL_TARGET_PLAN_PROMPT):6. Generate reasons in {output_language}. - 差异式改进提示词(
DIFF_IMPROVE_PROMPT):7. Generate all new text in {output_language}
注意:{output_language}是自定义提示词的三个必需占位符之一。后端在 apps/backend/app/routers/config.py 的update_feature_prompts端点中通过validate_prompt_placeholders校验{job_description}、{resume_data}、{output_language}三者是否齐全,缺失任一占位符都会返回 422 及结构化错误详情(config.py L377-L429)。这意味着即使你完全自定义求职信/外联消息提示词,也必须保留{output_language}以支持多语言内容生成。
5.3 服务层的语言解析调用链
内容语言从"配置"到"提示词"的传递路径,可从源码清晰还原:
- 前端
setContentLanguage→PUT /config/language(apps/frontend/lib/api/config.ts 的updateLanguageConfig); - 后端路由 apps/backend/app/routers/config.py 的
update_language_config校验语言码是否在SUPPORTED_LANGUAGES内,随后写入配置文件(支持从旧的单一language字段自动迁移到ui_language+content_language双字段); - 各服务调用时先取语言码,再经
get_language_name(code)转为全称注入提示词。例如:- apps/backend/app/services/cover_letter.py L51/L105/L152 均先
output_language = get_language_name(language)再格式化提示词; - apps/backend/app/services/improver.py L533/L846/L936 在简历改进(nudge/keywords/full)时做同样转换;
- apps/backend/app/services/interview_prep.py L113 使用
get_language_name(language); - apps/backend/app/services/resume_wizard.py L355 通过
get_content_language()从配置缓存读取内容语言再转换; - apps/backend/app/routers/enrichment.py 的多个技能/内容增强端点同样遵循该模式(L110/L216/L289/L503)。
- apps/backend/app/services/cover_letter.py L51/L105/L152 均先
重要事实:后端SUPPORTED_LANGUAGES = ["en", "es", "zh", "ja", "pt", "fr"](config.py L256)与前端locales完全一致,这是新增语言时前后端必须同步维护的两处清单。
5.4 已存内容不翻译
i18n 文档明确约定:数据库中的既有内容保持原始语言,不进行自动翻译。内容语言只影响新生成的文本。这一约束避免了对用户既有简历/求职信数据的破坏性改写,也意味着切换语言后,历史记录仍以当时生成的语言展示。
六、存储键位一览
| Key | 用途 | 存储位置 |
|---|---|---|
resume_matcher_ui_language | UI 语言 | 仅 localStorage |
resume_matcher_content_language | 内容语言 | localStorage + 后端配置 |
两个键名常量定义于 apps/frontend/lib/context/language-context.tsx L11-L12(UI_STORAGE_KEY/CONTENT_STORAGE_KEY)。后端侧,语言配置持久化在应用配置文件中(ui_language/content_language字段),并通过 apps/backend/app/config_cache.py 的缓存机制加速读取;resume_wizard.py正是通过get_content_language()读取该缓存。
七、新增一种语言的完整步骤
综合 docs/agent/features/i18n.md 与 docs/agent/features/i18n-preparation.md 两份文档,并对照当前仓库实际实现(语言码de德语仅作为示例),完整步骤如下:
步骤 1:创建翻译消息文件
在apps/frontend/messages/下新建{code}.json,结构与en.json完全一致。若语言码与文件名不一致(如pt→pt-BR.json),按实际文件命名,并在第 3 步的 import 中指定正确路径。
步骤 2:注册到前端语言清单
编辑 apps/frontend/i18n/config.ts,把新语言码加入locales数组,并同步补全localeNames与localeFlags:
export const locales = ['en', 'es', 'zh', 'ja', 'pt', 'fr', 'de'] as const; // localeNames 增加: de: 'Deutsch' // localeFlags 增加: de: '🇩🇪'步骤 3:注册消息导入
编辑 apps/frontend/lib/i18n/messages.ts,新增import de from '@/messages/de.json';并加入allMessages映射。注意:由于type Messages = typeof en,此处若 JSON 键缺失或类型形状不一致,TypeScript 编译将直接失败——这是"结构一致"约束在类型层面的体现。
步骤 4:注册后端语言全称
编辑 apps/backend/app/prompts/templates.py 的LANGUAGE_NAMES字典,为语言码补充全称(如"de": "German"),否则get_language_name会回退到"English",LLM 将以英文生成内容。
步骤 5:同步后端受支持语言清单
编辑 apps/backend/app/routers/config.py L256 处的SUPPORTED_LANGUAGES,加入新语言码。若不更新,PUT /config/language会因校验失败返回 400Unsupported content language: de. Supported: [...]。
步骤 6(可选):运行校验脚本
仓库提供了 scripts/check_locale_parity.py,用纯 Python 标准库(不依赖 Node/npm)校验每个语言文件与en.json的结构一致性:缺失键、叶子节点与对象形状不一致、非法 JSON 都会导致退出码 1;多余键仅作为警告输出。该脚本面向本地 pre-push 钩子场景,可在提交前快速发现问题。
八、质量保障:locale 一致性校验与测试
前端测试目录中与 i18n 相关的用例包括:
- apps/frontend/tests/i18n-locale-parity.test.ts:验证各语言文件与
en.json的键结构一致性; - apps/frontend/tests/i18n-utils.test.ts:覆盖
getNestedValue与applyParams的边界行为; - apps/frontend/tests/i18n-server.test.ts:验证服务端
translate能力。
后端侧,apps/backend/tests/unit/test_check_locale_parity.py 与 apps/backend/tests/unit/test_prompt_guardrails.py 分别守护校验脚本逻辑与提示词占位符约束。若你修改了消息文件,务必运行npm test(前端 vitest)与对应 Python 测试,确保typeof en类型约束与占位符完整性不被破坏。
九、实践要点与 FAQ
Q1:UI 语言切换后界面不变?UI 语言完全由前端控制,检查localStorage.getItem('resume_matcher_ui_language')是否更新,并确认组件位于LanguageProvider内、使用了useTranslations()。
Q2:内容语言切换后,生成的简历仍是英文?优先检查三处:后端SUPPORTED_LANGUAGES是否包含该语言码(config.py L256);LANGUAGE_NAMES是否映射了全称(templates.py L4-L11);请求是否真的把content_language传到了生成端点(get_language_name的兜底值是"English")。
Q3:自定义求职信提示词报 422?自定义提示词必须包含{job_description}、{resume_data}、{output_language}三个占位符(config.py L384),后端会精确返回缺失项。
Q4:既有数据会随语言切换被翻译吗?不会。内容语言只影响后续新生成的文本,数据库中的存量内容保持原语言。
Q5:新增语言最容易漏掉哪一步?最容易漏的是后端SUPPORTED_LANGUAGES与LANGUAGE_NAMES两处——它们不参与前端 TypeScript 类型检查,漏掉不会编译报错,但会静默导致内容生成回退到英文。
十、小结
Resume-Matcher 的 i18n 设计遵循"前端 UI 与后端内容解耦"的原则:前端以locales配置为权威、用无依赖的 JSON 静态导入渲染界面;后端以SUPPORTED_LANGUAGES为边界、用{output_language}占位符驱动所有 LLM 提示词;两端通过resume_matcher_content_language与/config/language接口保持同步。新增语言时,五处注册点(消息文件、config.ts、messages.ts、LANGUAGE_NAMES、SUPPORTED_LANGUAGES)缺一不可,而check_locale_parity.py与相关测试则为此提供了自动化保障。后续若需要更深度的本地化(如日期格式、复数规则),可参考 docs/agent/features/i18n-preparation.md 中关于后端 i18n 的扩展规划,在现有{output_language}机制上继续演进。
【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考