关于 i18n
面向 Rails 开发者的 I18n 实践技能,用于为应用添加国际化和本地化。内容涵盖翻译文件组织、按请求切换语言,以及日期、数字、模型和邮件的本地化。
主要功能
- 01使用 locale 文件管理翻译
- 02配置语言列表与回退规则
- 03采用惰性查找和插值
- 04本地化日期、数字及复数
- 05翻译模型、验证错误和邮件
- 06按请求安全切换语言
适用场景
- 01为现有 Rails 应用添加多语言支持
- 02整理和拆分 locale 文件
- 03按用户偏好或 URL 切换语言
- 04本地化表单错误、日期与邮件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill i18n安装后在 Claude Code 中输入/i18n确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/locale-files.mdlocale-switching.mdlookups.mdmodel-translations.mdpluralization.mdtesting.md详细介绍
Rails I18n 专家
使用 I18n 框架为 Rails 应用添加国际化和本地化。所有面向用户的字符串都应放在 locale 文件中——绝不硬编码。
理念
核心原则:
- 所有字符串都放在 locale 文件中 — 不要在视图、控制器、邮件程序或模型中硬编码面向用户的文本
- 处处使用惰性查找 — 在视图和控制器中使用
.title,而不是books.index.title - 按功能而非语言组织 — 按领域拆分 locale 文件(模型、视图、默认值),不要使用一个巨大的文件
- YAML 为王 — 除非需要用于日期格式的 Ruby lambda,否则使用
.yml文件 - 在开发和测试环境中及时报错 — 设置
raise_on_missing_translations = true,以便尽早发现缺失的键
何时使用此技能
- 为现有 Rails 应用添加 I18n 支持
- 创建或编辑 YAML locale 文件
- 使用
t()/I18n.t()和l()/I18n.l()辅助方法 - 设置 locale 切换(URL、子域名、请求头、用户偏好)
- 翻译 Active Record 模型名称、属性和错误消息
- 本地化日期、时间、数字和货币
- 为非英语 locale 设置复数规则
- 在大型应用中组织 locale 文件
- 配置回退和可用 locale
说明
第 1 步:检查现有 I18n 设置
首先检查项目当前的 I18n 配置——不一致的约定会导致键查找失败:
# Check existing locale files
find config/locales -name "*.yml" -o -name "*.rb" | sort
# Check I18n config
grep -r "i18n" config/application.rb config/initializers/ config/environments/
# Check available locales
grep -r "available_locales" config/
# Check for existing translation usage
rg "I18n\.t\b|\ t[\(\ ][\'\"\.]" --type ruby --type erb -l
# Check for hardcoded strings in views (potential I18n candidates)
rg -l ">[A-Z][a-z]+" app/views/ --type erb
遵循现有约定。如果项目使用扁平键,就不要引入嵌套键。如果项目按功能组织,就遵循这种方式。
第 2 步:正确配置 I18n
在 config/application.rb 中进行最低限度的可用配置:
# config/application.rb
config.i18n.available_locales = [:en, :es, :fr]
config.i18n.default_locale = :en
config.i18n.fallbacks = true # Falls back to default_locale
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]
在测试和开发环境中捕获缺失的翻译:
# config/environments/test.rb
config.i18n.raise_on_missing_translations = true
# config/environments/development.rb
config.i18n.raise_on_missing_translations = true
设置 available_locales — 如果不设置,任何 locale 字符串都会被接受,可能导致文件系统遍历攻击和意外的回退行为。
第 3 步:正确使用翻译辅助方法
基本查找
# In views (translate helper auto-available)
t("hello") # Simple key
t("messages.welcome") # Nested key
t(:welcome, scope: :messages) # Same thing, symbol + scope
# In controllers/models/services
I18n.t("messages.welcome")
# With default fallback
t("missing.key", default: "Fallback text")
t("missing.key", default: [:other_key, "Final fallback"])
惰性查找(优先使用)
惰性查找会根据视图路径或控制器操作自动确定作用域:
# config/locales/en.yml
en:
books:
index:
title: "All Books"
empty: "No books found"
show:
title: "Book Details"
create:
success: "Book created!"
failure: "Could not create book."
<%# app/views/books/index.html.erb %>
<h1><%= t(".title") %></h1> <%# Resolves to books.index.title %>
<p><%= t(".empty") %></p> <%# Resolves to books.index.empty %>
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def create
if @book.save
redirect_to @book, notice: t(".success") # books.create.success
else
flash.now[:alert] = t(".failure") # books.create.failure
render :new, status: :unprocessable_entity
end
end
end
在视图和控制器中优先使用惰性查找(.key) — 它们能让翻译键保持 DRY,并与文件结构关联。仅在引用共享或全局键时使用完整路径。
插值
en:
greeting: "Hello, %{name}!"
item_count: "You have %{count} items in %{location}"
t("greeting", name: current_user.name)
t("item_count", count: 5, location: "your cart")
不要将 scope 或 default 用作插值变量名——它们是 I18n 的保留名称,会引发 I18n::ReservedInterpolationKey。
复数形式
en:
notifications:
zero: "No notifications" # optional for English
one: "1 notification"
other: "%{count} notifications"
t("notifications", count: 0) # => "No notifications"
t("notifications", count: 1) # => "1 notification"
t("notifications", count: 42) # => "42 notifications"
:count 变量具有特殊作用 — 它既会选择复数形式,也会插入字符串中。
英语只需要 one 和 other。其他语言需要不同的形式:
- 阿拉伯语:
zero、one、two、few、many、other - 俄语:
one、few、many、other - 日语: 仅
other
使用 rails-i18n gem 获取特定 locale 的复数规则。
HTML 安全翻译
以 _html 结尾或命名为 html 的键,在视图中会自动标记为 HTML 安全:
en:
welcome_html: "<strong>Welcome</strong> to %{app_name}"
help:
html: "Need <em>help</em>? <a href='%{url}'>Contact us</a>"
<%= t("welcome_html", app_name: "MyApp") %> <%# HTML not escaped %>
插值的值仍会进行转义(可防范 XSS)。仅当翻译本身包含标记时才使用 _html 键。
第 4 步:翻译 Active Record 模型
模型名称和属性
en:
activerecord:
models:
user:
one: "User"
other: "Users"
admin/post: "Admin Post" # Namespaced model
attributes:
user:
email: "Email address"
first_name: "First name"
user/role: # Nested attribute
admin: "Administrator"
User.model_name.human # => "User"
User.model_name.human(count: 2) # => "Users"
User.human_attribute_name(:email) # => "Email address"
验证错误消息
错误消息会按以下顺序查找(匹配到的第一个优先):
activerecord.errors.models.MODEL.attributes.ATTRIBUTE.ERROR
activerecord.errors.models.MODEL.ERROR
activerecord.errors.messages.ERROR
errors.attributes.ATTRIBUTE.ERROR
errors.messages.ERROR
en:
activerecord:
errors:
models:
user:
attributes:
email:
blank: "is required — we need this to contact you"
taken: "is already registered"
name:
too_short: "must be at least %{count} characters"
# Applies to all attributes on User:
invalid: "has a problem"
# Applies to all models:
messages:
blank: "can't be empty"
# Global fallback for all models:
errors:
format: "%{attribute}: %{message}" # Customize full_message format
messages:
blank: "is required"
错误消息中可用的插值变量:model、attribute、value、count。
第 5 步:本地化日期、时间和数字
日期/时间格式
en:
date:
formats:
default: "%Y-%m-%d"
short: "%b %d"
long: "%B %d, %Y"
time:
formats:
default: "%a, %d %b %Y %H:%M:%S %z"
short: "%d %b %H:%M"
long: "%B %d, %Y %H:%M"
l(Date.today) # Default format
l(Date.today, format: :short) # Short format
l(Time.current, format: :long) # Long format
日期和时间请使用 l() — strftime 会忽略当前 locale,因此非英语用户看到的日期格式将不正确。
数字格式
数字辅助方法(number_to_currency、number_with_delimiter 等)会从 locale 文件中读取设置:
en:
number:
format:
separator: "."
delimiter: ","
precision: 3
currency:
format:
unit: "$"
format: "%u%n" # $1,000.00
separator: "."
delimiter: ","
precision: 2
es:
number:
currency:
format:
unit: "€"
format: "%n %u" # 1.000,00 €
separator: ","
delimiter: "."
第 6 步:按请求设置 locale
使用带有 I18n.with_locale 的 around_action — 直接设置 I18n.locale = 会在多线程服务器(Puma)中跨请求泄漏,导致用户看到其他用户的 locale。
从 URL 路径读取(推荐)
# config/routes.rb
scope "(:locale)", locale: /en|es|fr/ do
resources :books
# ... all routes
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
def default_url_options
{ locale: I18n.locale }
end
end
组合优先级链
def resolve_locale
params[:locale].presence ||
current_user&.locale.presence ||
request.env["HTTP_ACCEPT_LANGUAGE"]&.scan(/^[a-z]{2}/)&.first&.then { |l|
l.to_sym if I18n.available_locales.include?(l.to_sym)
} ||
I18n.default_locale
end
步骤 7:组织语言环境文件
小型应用: 每种语言环境使用一个文件(config/locales/en.yml、es.yml)。
中型/大型应用 — 按关注点拆分:
config/locales/
defaults/en.yml # Date, time, number formats
models/en.yml # AR model names, attributes, errors
views/en.yml # View translations (lazy lookup keys)
mailers/en.yml # Mailer subjects and content
加载嵌套目录: config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]
YAML 规则: 顶层键 = 语言环境。键使用 snake_case。将 'true'/'false'/'yes'/'no'/'on'/'off' 引起来作为键(否则 YAML 会将它们解析为布尔值)。嵌套层级保持在 4 层以内。
步骤 8:Action Mailer 翻译
邮件程序主题会自动从 mailer_scope.action_name.subject 解析:
en:
user_mailer:
welcome:
subject: "Welcome to %{app_name}!"
class UserMailer < ApplicationMailer
def welcome(user)
mail(to: user.email) # Subject auto-resolved
# Or with interpolation: mail(to: user.email, subject: default_i18n_subject(app_name: "MyApp"))
end
end
步骤 9:回退
# config/application.rb
config.i18n.fallbacks = true # Falls back to default_locale
config.i18n.fallbacks = { es: :en, fr: :en } # Or specific chains
在开发/测试环境中,关闭回退功能,并设置 raise_on_missing_translations = true。
Agent 常见错误
- 硬编码字符串 — 使用
"Record saved",而不是t(".success") - YAML 嵌套错误 — 忘记在顶层添加语言环境键,或缩进错误
- 未使用惰性查找 — 在视图/控制器中使用
t("users.show.title"),而不是t(".title") - 忘记设置
available_locales— 未设置时会接受任意语言环境字符串(存在安全风险) - 使用
I18n.locale =— 在使用线程的服务端会导致语言环境在请求间泄漏;请使用I18n.with_locale - 复数形式未传入
count— 返回原始哈希,而不是字符串 - 缺少
_html后缀 — 翻译中的 HTML 会被转义 - 未给 YAML 布尔值加引号 —
true、false、yes、no会被解析为布尔值;将它们用作键时请加引号 - 忘记重启服务端 — 加载新的语言环境文件需要重启
快速参考
翻译查找方法
| 上下文 | 方法 | 惰性查找 |
|---|---|---|
| 视图 | t(".key") 或 t("full.key") | ✅ 是 |
| 控制器 | t(".key") 或 I18n.t("full.key") | ✅ 是 |
| 模型 | I18n.t("full.key") | ❌ 否 |
| 邮件程序 | I18n.t("full.key") | ❌ 否 |
| 服务/作业 | I18n.t("full.key") | ❌ 否 |
基本语言环境文件结构
en:
# View translations (lazy lookups)
controller_name:
action_name:
key: "value"
# Shared/global
shared:
save: "Save"
cancel: "Cancel"
# Active Record
activerecord:
models:
model_name: { one: "Singular", other: "Plural" }
attributes:
model_name:
attribute: "Label"
errors:
models:
model_name:
attributes:
attribute:
error_type: "message"
# Formats (or use rails-i18n gem)
date:
formats: { default: "%Y-%m-%d", short: "%b %d", long: "%B %d, %Y" }
time:
formats: { default: "%Y-%m-%d %H:%M" }
number:
currency:
format: { unit: "$" }
# Mailers
mailer_name:
action_name:
subject: "Subject line"
新语言环境检查清单
- 添加到
available_locales - 创建与现有结构对应的语言环境文件
- 添加
rails-i18ngem,以提供日期/时间/数字默认值和复数形式规则 - 使用
raise_on_missing_translations = true进行测试 - 添加语言环境切换器 UI;如果基于 URL,还要更新
default_url_options
如需了解详细模式、示例和边缘情况,请参阅 references/ 目录:
references/lookups.md— 翻译查找方法、插值、惰性查找references/locale-files.md— YAML 模式、文件组织、日期/时间/数字本地化、自定义后端references/pluralization.md— 各语言的复数形式规则references/model-translations.md— Active Record 模型/属性/错误翻译references/locale-switching.md— 语言环境切换策略和回退配置references/testing.md— I18n 测试、常用 gem(rails-i18n、i18n-tasks、mobility)