Lingo.dev CLI 可通过已配置的 config/locales本地化引擎 翻译 Rails 的 YAML 文件。Rails 原生内置 i18n API——应用中所有可翻译文本都存放在按 locale 划分的 YAML 文件里。Lingo.dev 能无缝接入你现有的流程,无需额外引入运行时依赖。
这篇指南会带你从头到尾完成 Rails 应用本地化:配置 CLI、按 locale 组织 YAML 文件、在请求时切换 locale,以及在 CI 中自动化翻译。
演示仓库
克隆或 fork lingodotdev/ruby-on-rails-localization-example,边看边上手。这是一个可直接运行的 Rails 应用,包含 config/locales YAML 文件、已提交的 .lingo/config.json,并且翻译结果也已就绪,因此你可以一边看配置,一边对照它生成的输出。
Rails 本地化如何运作#
Rails 会从 config/locales/ 下的 YAML 文件中读取翻译。每个文件都以 locale code 作为根键,内部则包含嵌套键,对应你在代码里通过 I18n.t 使用的查找路径。
| 层级 | 存放内容 | 示例文件 |
|---|---|---|
| UI 文案 | 按钮、标签、flash 消息 | config/locales/en.yml |
| 邮件文案 | ActionMailer 的主题和正文 | config/locales/mailers.en.yml |
| 模型错误 | 校验消息和属性名 | config/locales/activerecord.en.yml |
每个 Rails YAML 文件的第一个键,都是 locale 代码本身——en:、es:、fr:。Rails 是根据这个根键而不是文件名来索引翻译的:它会加载 config/locales/ 下的所有文件,并将每个文件的内容存到它所声明的根键之下。所以,一个 es.yml 就算根仍然是 en:,也不会被忽略——它会被合并进 en 命名空间。结果就是西班牙语完全没有翻译,而英文翻译则会被悄悄覆盖。
因此,翻译这种格式时,改写的是这个键本身,而不只是值。yaml-root-key 格式正是为此而生:它会遍历根键下的整棵树,只翻译字符串值,并把目标文件写成以目标 locale 为根。嵌套键、%{name} 插值 token,以及 CLDR 复数类别(zero/one/two/few/many/other)都属于结构内容,因此会原样保留——注释和 YAML anchors 也一样。
前提条件#
创建本地化引擎
确认 Ruby 和 Rails 版本
本指南适用于 Rails 7.2 及以上版本,而它要求 Ruby 3.1 及以上。先检查你的版本:
ruby -v
rails -v确认 Node.js 版本
CLI 需要 Node.js 22 或更高版本:
node -v设置 Rails i18n
本指南默认你的应用已经将翻译存放在 config/locales/*.yml 中。如果你的视图或控制器里还有硬编码字符串,请先将它们提取为 t() 调用。比如,把下面这段替换成:
<h1>Welcome</h1>替换为:
<h1><%= t(".welcome") %></h1>然后把对应的键添加到 config/locales/en.yml 中。完整迁移步骤可参考 Rails 的 internationalization guide。
组织翻译文件#
Rails 会自动加载 *.yml 下所有匹配的 config/locales/ 文件。建议将源 locale 和各个目标语言文件放在同一目录中,让这个目录成为唯一可信来源:
config/locales/
en.yml # Source locale
es.yml # Generated by Lingo.dev
fr.yml
de.yml一个典型的 en.yml 会同时包含普通字符串、嵌套命名空间、%{name} 插值和复数形式:
en:
hello: "Hello"
home:
welcome: "Welcome, %{name}!"
cta: "Get started"
notifications:
unread:
zero: "No unread notifications"
one: "1 unread notification"
other: "%{count} unread notifications"
errors:
messages:
blank: "can't be blank"配置 CLI#
安装 CLI、完成认证,并将项目关联到你的引擎:
npm install -g @lingo.dev/cli
lingo login
lingo init
lingo linklingo init 和 lingo link 会创建 .lingo/config.json,其中包含你的 locales,以及引擎的 orgId 和 engineId。请将它与 .lingo/lock.json 一并提交,这样每台机器和每次 CI 运行都能共享同一份状态。
将一个 files[] 条目指向源 locale 文件,并显式指定格式:
{
"orgId": "org_...",
"engineId": "eng_...",
"sourceLocale": "en",
"targetLocales": ["es", "fr", "de"],
"files": [{ "pattern": "config/locales/en.yml", "format": "yaml-root-key" }]
}这里的 "format": "yaml-root-key" 是必填项,不是可选项。仅凭 .yml 路径,无法判断它的根键是 locale 还是普通配置,所以 CLI 不会自行猜测:如果省略 format,文件就会按通用 yaml 处理,只翻译值,并把根保留为 en:——也就是上面提到的那种静默失败。
目标文件会根据源路径自动推导,因此 config/locales/en.yml 会生成 config/locales/es.yml、config/locales/fr.yml 和 config/locales/de.yml。
除了 en.yml 之外,Rails 还会加载按功能拆分的文件——devise.en.yml、mailers.en.yml、activerecord.en.yml。再添加一个带 glob 的第二条目,把它们也覆盖进去:
{
"files": [
{ "pattern": "config/locales/en.yml", "format": "yaml-root-key" },
{ "pattern": "config/locales/*.en.yml", "format": "yaml-root-key" }
]
}这两个模式不会重叠:第一个只匹配 en.yml,第二个只匹配以 .en.yml 结尾的文件,因此像 es.yml 和 devise.es.yml 这样已翻译的文件,绝不会被当作源文件再次拾取。devise.en.yml 会生成 devise.es.yml。
为多个 locale 配置 Rails#
告诉 Rails 哪些 locale 可用,以及默认使用哪个。在 config/application.rb 中:
module YourApp
class Application < Rails::Application
config.i18n.available_locales = [:en, :es, :fr, :de]
config.i18n.default_locale = :en
config.i18n.fallbacks = [:en]
end
end在 ApplicationController 中根据 URL 参数或 Accept-Language 请求头选择当前请求的 locale:
class ApplicationController < ActionController::Base
around_action :switch_locale
private
def switch_locale(&action)
locale = params[:locale] || http_accept_locale || I18n.default_locale
I18n.with_locale(locale, &action)
end
def http_accept_locale
header = request.headers["Accept-Language"].to_s
header.scan(/[a-z]{2}/).find { |l| I18n.available_locales.map(&:to_s).include?(l) }
end
def default_url_options
{ locale: I18n.locale }
end
end在视图中渲染翻译#
在 ERB 模板中使用 t 和 l 辅助方法。键名前面的点会相对于当前视图路径解析,让翻译键与使用它们的模板就近放置:
<h1><%= t(".welcome", name: @user_name) %></h1>
<p><%= t("notifications.unread", count: @unread_count) %></p>
<%= link_to t(".cta"), signup_path, class: "btn-primary" %>在布局中添加一个语言切换器:
<nav>
<% I18n.available_locales.each do |locale| %>
<%= link_to locale.upcase, url_for(locale: locale) %>
<% end %>
</nav>在本地执行翻译#
lingo push --waitpush 会上传源文件,通过你的本地化引擎处理后,再把翻译后的文件写回。--wait 会让命令保持阻塞,直到输出生成完成——这一点值得显式传入,因为一个即将发布的变更会调整默认行为:push 将只提交任务并立即返回,之后需要你用 lingo pull 来拉取结果。
后续运行只会翻译增量内容:push 会对源文件计算哈希,并与 .lingo/lock.json 对比,因此未变更的条目不会产生任何成本。项目首次运行时——或新增 locale 后——则需要处理整份语料:
lingo push --backfill-missing传入一个 glob,就可以把本次运行限定在部分文件上:
lingo push "config/locales/**"如果要获取在其他地方生成的输出——比如另一台机器或 CI——请运行 lingo pull。
首次运行翻译后,请重启 Rails 服务器,以便加载新的 YAML 文件:
bin/rails server访问 /es 查看西班牙语效果。
复数形式#
Rails 使用 CLDR 复数类别——zero、one、two、few、many、other。给 count: 传入 I18n.t 参数后,Rails 就会自动选择匹配的键:
t("notifications.unread", count: 0) # => "No unread notifications"
t("notifications.unread", count: 1) # => "1 unread notification"
t("notifications.unread", count: 12) # => "12 unread notifications"CLI 会在原位翻译每一种复数变体。如果目标 locale 需要比英语的 one/other 更多的类别,请在源 en.yml 中先定义好。
在 CI 中实现自动化#
Lingo.dev GitHub App 会在每次 push 和 pull request 时于服务端完成翻译——无需 runner,也不用在仓库中存放 API key。它会根据已提交的 .lingo/config.json 解析所用引擎,因此你提交的文件里必须包含 orgId 和 engineId。
如果你更希望在自己的流水线里运行 CLI——GitHub Actions、GitLab CI、Bitbucket Pipelines——请参阅 CI/CD Workflows。将 LINGO_API_KEY 作为 secret 提供,并像调用其他构建步骤一样调用 lingo push --wait。
部署前验证#
把 lingo check 作为部署前的闸门,确保未翻译内容不会进入生产环境。如果仍有任何条目需要翻译,它会以非零状态码退出,且不会写入任何内容。
lingo check将它作为独立的 CI 步骤,放在资源预编译或容器构建之前:
- name: Verify translations
run: lingo check
- name: Precompile assets
run: bundle exec rails assets:precompile