fix(i18n): align pluralization with locale rules (#14266)
Loads Rails locale-specific pluralization rules so languages with an `other`-only plural model can safely use Crowdin exports without maintaining duplicate `one` keys. ## Closes None ## Why Crowdin exports Rails YAML pluralized strings using each target language's plural categories. These categories come from Unicode CLDR and represent grammatical forms, not a literal "number is 1" bucket. Some languages need separate forms such as `one` and `other`, but languages like Japanese, Korean, Indonesian, Thai, Vietnamese, and Chinese use the same form for `1`, `2`, `5`, and larger counts in these strings. For those locales, CLDR correctly models the plural category as `other` only. Before this change, Chatwoot still relied on Rails' default English-style plural behavior for these locales. That meant a valid Crowdin export containing only `other` could fail at runtime when Rails received `count: 1` and looked for a missing `one` branch. Keeping duplicate `one` keys would only fight Crowdin on every translation sync. The runtime should instead follow the locale's plural rules. ## What changed - Added `rails-i18n` and enabled only its pluralization module. - Added explicit `other`-only plural rules for Chatwoot's underscore Chinese locale aliases, `zh_CN` and `zh_TW`. - Removed redundant `one` keys from the affected Devise and `time_units` translations. ## Validation - Ran a Rails runner check across `id`, `ja`, `ko`, `ms`, `th`, `vi`, `zh_CN`, and `zh_TW` to verify `errors.messages.not_saved` and `time_units.days` resolve with only `other` for `count: 1`. - Ran YAML parse validation for all edited locale files. - Ran `bundle exec rubocop Gemfile config/application.rb config/initializers/i18n_pluralization.rb`.
This commit is contained in:
@@ -84,6 +84,7 @@ gem 'barnes'
|
||||
gem 'devise', '>= 4.9.4'
|
||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||
gem 'devise_token_auth', '>= 1.2.3'
|
||||
gem 'rails-i18n', '~> 7.0'
|
||||
# two-factor authentication
|
||||
gem 'devise-two-factor', '>= 5.0.0'
|
||||
# authorization
|
||||
|
||||
@@ -727,6 +727,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (7.0.10)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
@@ -1125,6 +1128,7 @@ DEPENDENCIES
|
||||
rack-mini-profiler (>= 3.2.0)
|
||||
rack-timeout
|
||||
rails (~> 7.1)
|
||||
rails-i18n (~> 7.0)
|
||||
redis
|
||||
redis-namespace
|
||||
responders (>= 3.1.1)
|
||||
|
||||
@@ -37,6 +37,7 @@ module Chatwoot
|
||||
class Application < Rails::Application
|
||||
# Initialize configuration defaults for originally generated Rails version.
|
||||
config.load_defaults 7.0
|
||||
config.rails_i18n.enabled_modules = [:pluralization]
|
||||
|
||||
config.eager_load_paths << Rails.root.join('lib')
|
||||
config.eager_load_paths << Rails.root.join('enterprise/lib')
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
other_plural_rule = ->(_count) { :other }
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
I18n.backend.store_translations(:zh_CN, i18n: { plural: { rule: other_plural_rule } })
|
||||
I18n.backend.store_translations(:zh_TW, i18n: { plural: { rule: other_plural_rule } })
|
||||
end
|
||||
@@ -57,5 +57,4 @@ id:
|
||||
not_found: "tidak ditemukan"
|
||||
not_locked: "tidak terkunci"
|
||||
not_saved:
|
||||
one: "%{count} kesalahan mengakibatkan %{resource} ini tidak dapat disimpan:"
|
||||
other: "%{count} kesalahan mengakibatkan %{resource} ini tidak dapat disimpan:"
|
||||
|
||||
@@ -57,5 +57,4 @@ ja:
|
||||
not_found: "見つかりませんでした"
|
||||
not_locked: "はロックされていません"
|
||||
not_saved:
|
||||
one: "%{count} 個のエラーが発生し、 %{resource} を保存できませんでした:"
|
||||
other: "%{count} 個のエラーが発生し、 %{resource} を保存できませんでした:"
|
||||
|
||||
@@ -57,5 +57,4 @@ ko:
|
||||
not_found: "찾을 수 없습니다"
|
||||
not_locked: "잠겨 있지 않습니다"
|
||||
not_saved:
|
||||
one: "%{count}개의 오류로 인해 이 %{resource}을(를) 저장할 수 없습니다:"
|
||||
other: "%{count}개의 오류로 인해 이 %{resource}을(를) 저장할 수 없습니다:"
|
||||
|
||||
@@ -57,5 +57,4 @@ ms:
|
||||
not_found: "not found"
|
||||
not_locked: "was not locked"
|
||||
not_saved:
|
||||
one: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
other: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
|
||||
@@ -57,5 +57,4 @@ th:
|
||||
not_found: "not found"
|
||||
not_locked: "was not locked"
|
||||
not_saved:
|
||||
one: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
other: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
|
||||
@@ -57,5 +57,4 @@ vi:
|
||||
not_found: "không tìm thấy"
|
||||
not_locked: "không được khoá"
|
||||
not_saved:
|
||||
one: "Có %{count} lỗi được tìm thấy từ %{resource}:"
|
||||
other: "Có %{count} lỗi được tìm thấy từ %{resource}:"
|
||||
|
||||
@@ -57,5 +57,4 @@ zh_CN:
|
||||
not_found: "找不到"
|
||||
not_locked: "未锁定"
|
||||
not_saved:
|
||||
one: "%{count} 个错误禁止保存 %{resource}:"
|
||||
other: "%{count} 个错误禁止保存 %{resource}:"
|
||||
|
||||
@@ -57,5 +57,4 @@ zh_TW:
|
||||
not_found: "找不到。"
|
||||
not_locked: "並未被鎖定。"
|
||||
not_saved:
|
||||
one: "有 %{count} 個錯誤導致 %{resource} 不能被儲存:"
|
||||
other: "有 %{count} 個錯誤導致 %{resource} 不能被儲存:"
|
||||
|
||||
@@ -435,16 +435,12 @@ id:
|
||||
button: Buka percakapan
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} days'
|
||||
other: '%{count} days'
|
||||
hours:
|
||||
one: '%{count} hours'
|
||||
other: '%{count} hours'
|
||||
minutes:
|
||||
one: '%{count} minutes'
|
||||
other: '%{count} minutes'
|
||||
seconds:
|
||||
one: '%{count} seconds'
|
||||
other: '%{count} seconds'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ ja:
|
||||
button: 会話を開く
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} 日'
|
||||
other: '%{count} 日'
|
||||
hours:
|
||||
one: '%{count} 時間'
|
||||
other: '%{count} 時間'
|
||||
minutes:
|
||||
one: '%{count} 分'
|
||||
other: '%{count} 分'
|
||||
seconds:
|
||||
one: '%{count} 秒'
|
||||
other: '%{count} 秒'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ ko:
|
||||
button: 대화 열기
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count}일'
|
||||
other: '%{count}일'
|
||||
hours:
|
||||
one: '%{count}시간'
|
||||
other: '%{count}시간'
|
||||
minutes:
|
||||
one: '%{count}분'
|
||||
other: '%{count}분'
|
||||
seconds:
|
||||
one: '%{count}초'
|
||||
other: '%{count}초'
|
||||
auto_assignment:
|
||||
default_policy_name: '기본 정책'
|
||||
|
||||
@@ -435,16 +435,12 @@ ms:
|
||||
button: Open conversation
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} days'
|
||||
other: '%{count} days'
|
||||
hours:
|
||||
one: '%{count} hours'
|
||||
other: '%{count} hours'
|
||||
minutes:
|
||||
one: '%{count} minutes'
|
||||
other: '%{count} minutes'
|
||||
seconds:
|
||||
one: '%{count} seconds'
|
||||
other: '%{count} seconds'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ th:
|
||||
button: เปิดดูการสนทนา
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} days'
|
||||
other: '%{count} days'
|
||||
hours:
|
||||
one: '%{count} hours'
|
||||
other: '%{count} hours'
|
||||
minutes:
|
||||
one: '%{count} minutes'
|
||||
other: '%{count} minutes'
|
||||
seconds:
|
||||
one: '%{count} seconds'
|
||||
other: '%{count} seconds'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ vi:
|
||||
button: Mở cuộc trò chuyện
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} days'
|
||||
other: '%{count} days'
|
||||
hours:
|
||||
one: '%{count} hours'
|
||||
other: '%{count} hours'
|
||||
minutes:
|
||||
one: '%{count} minutes'
|
||||
other: '%{count} minutes'
|
||||
seconds:
|
||||
one: '%{count} seconds'
|
||||
other: '%{count} seconds'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ zh_CN:
|
||||
button: 重新打开会话
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} 天'
|
||||
other: '%{count} 天'
|
||||
hours:
|
||||
one: '%{count} 小时'
|
||||
other: '%{count} 小时'
|
||||
minutes:
|
||||
one: '%{count} 分钟'
|
||||
other: '%{count} 分钟'
|
||||
seconds:
|
||||
one: '%{count} 秒'
|
||||
other: '%{count} 秒'
|
||||
auto_assignment:
|
||||
default_policy_name: 'Default Policy'
|
||||
|
||||
@@ -435,16 +435,12 @@ zh_TW:
|
||||
button: '開啟對話'
|
||||
time_units:
|
||||
days:
|
||||
one: '%{count} 天'
|
||||
other: '%{count} 天'
|
||||
hours:
|
||||
one: '%{count} 小時'
|
||||
other: '%{count} 小時'
|
||||
minutes:
|
||||
one: '%{count} 分鐘'
|
||||
other: '%{count} 分鐘'
|
||||
seconds:
|
||||
one: '%{count} 秒'
|
||||
other: '%{count} 秒'
|
||||
auto_assignment:
|
||||
default_policy_name: '預設策略'
|
||||
|
||||
Reference in New Issue
Block a user