FEATURE: Localization fallbacks (server-side)#9
Conversation
The FallbackLocaleList object tells I18n::Backend::Fallbacks what order the languages should be attempted in. Because of the translate_accelerator patch, the SiteSetting.default_locale is *not* guaranteed to be fully loaded after the server starts, so a call to ensure_loaded! is added after the locale is set for the current user. The declarations of config.i18n.fallbacks = true in the environment files were actually garbage, because the I18n.default_locale was SiteSetting.default_locale, so there was nothing to fall back to. *derp*
|
This pull request has been automatically marked as stale because it has been open for 60 days with no activity. To keep it open, remove the stale tag, push code, or add a comment. Otherwise, it will be closed in 14 days. |
maloyan4good
left a comment
There was a problem hiding this comment.
Code Review
Verdict: NEEDS DISCUSSION
Confidence: HIGH
Summary
This PR introduces server-side localization fallbacks by replacing the Rails-level config.i18n.fallbacks = true setting with a custom FallbackLocaleList class that enforces a [user_locale, site_locale, :en] fallback chain. It also consolidates the pluralization initializer into the new i18n.rb initializer and adds an ensure_loaded! helper to the translate accelerator.
Findings
| Priority | Issue | Location |
|---|---|---|
| P1 | ensure_loaded! in FallbackLocaleList calls I18n.ensure_loaded! but the translate accelerator's ensure_loaded! is not thread-safe — it initializes @loaded_locales outside the mutex |
lib/freedom_patches/translate_accelerator.rb:62-65 |
| P1 | FallbackLocaleList#ensure_loaded! reads I18n.locale at call time, but set_locale calls it after setting I18n.locale — if locale is not yet set (e.g. during boot/reload), I18n.locale defaults to :en, silently loading only English and skipping the site locale |
config/initializers/i18n.rb:20-22 |
| P2 | FallbackLocaleList inherits from Hash but only overrides [] — other Hash methods (fetch, values_at, merge, etc.) will return unexpected results if called by the i18n backend internals |
config/initializers/i18n.rb:12 |
| P2 | I18n.backend.class.send(:include, I18n::Backend::Fallbacks) uses .class which will include the module into the concrete backend class globally and permanently — if the backend is ever swapped or wrapped (e.g. in tests), this silently breaks |
config/initializers/i18n.rb:8 |
| P2 | The translate accelerator's translate method caches by "\#{key}\#{config.locale}\#{config.backend.object_id}" but does not account for fallbacks — a cache hit for a missing key in the user locale will return the cached miss rather than falling through to the fallback |
lib/freedom_patches/translate_accelerator.rb:68-76 |
| P3 | ensure_loaded! in the translate accelerator duplicates the guard already present in load_locale (which checks @loaded_locales.include? inside the mutex) — the outer check in ensure_loaded! is a TOCTOU race |
lib/freedom_patches/translate_accelerator.rb:62-65 |
Details
[P1] has a TOCTOU race on
File: lib/freedom_patches/translate_accelerator.rb:62-65
The new method initializes @loaded_locales outside the LOAD_MUTEX and checks include? without holding the lock. load_locale itself is mutex-protected, but the guard check in ensure_loaded! is not, creating a race window in multi-threaded Rails servers (Puma).
# Current (racy)
def ensure_loaded!(locale)
@loaded_locales ||= []
load_locale locale unless @loaded_locales.include?(locale)
end
# Safer — delegate entirely to load_locale which already holds the mutex
def ensure_loaded!(locale)
load_locale(locale)
end
# load_locale already returns early if locale is already loaded (inside the mutex)[P1] in may silently use wrong locale during boot
File: config/initializers/i18n.rb:20-22
FallbackLocaleList#ensure_loaded! reads I18n.locale at call time. During a reload! cycle (triggered by execute_reload in the accelerator), ensure_all_loaded! is called which also calls I18n.fallbacks[locale] — but this path is fine. The risk is that set_locale calls I18n.fallbacks.ensure_loaded! after setting I18n.locale, which is correct for the request path. However, the method name ensure_loaded! on the fallback list is confusing because it conflates two responsibilities: computing the fallback list and triggering locale loading. Consider passing the locale explicitly:
def ensure_loaded!(locale = I18n.locale)
self[locale].each { |l| I18n.ensure_loaded!(l) }
endAnd in set_locale:
I18n.fallbacks.ensure_loaded!(I18n.locale)This makes the dependency explicit and avoids implicit global state reads.
[P2] Translation cache does not account for fallback misses
File: lib/freedom_patches/translate_accelerator.rb:68-76
The LRU cache key is "\#{key}\#{config.locale}". If a key is missing in the user locale and falls back to the site locale or :en, the result is cached under the user locale key — which is correct. But if the key is genuinely missing in all fallback locales, translate_no_cache raises/returns a missing-translation string, and that miss is cached. This was true before this PR too, but the new fallback chain makes it more likely to hit this path. Not a blocker, but worth noting.
Recommendation
Address the thread-safety issue in ensure_loaded! (P1) by delegating directly to load_locale which already handles the mutex guard. Clarify the FallbackLocaleList#ensure_loaded! API by passing the locale explicitly rather than reading I18n.locale implicitly. The Hash inheritance for FallbackLocaleList is fragile — consider a plain Struct or BasicObject subclass, or at minimum document which Hash interface the i18n backend depends on.
mfeuerstein
left a comment
There was a problem hiding this comment.
PR Review — approved
Reviewed 7 files. 0 high-severity issues found. Verdict: approved.
app/controllers/application_controller.rb (low)
- Reviewed app/controllers/application_controller.rb — looks good
lib/freedom_patches/translate_accelerator.rb (low)
- Reviewed lib/freedom_patches/translate_accelerator.rb — looks good
config/cloud/cloud66/files/production.rb (low)
- Reviewed config/cloud/cloud66/files/production.rb — looks good
config/initializers/pluralization.rb (low)
- Reviewed config/initializers/pluralization.rb — looks good
config/environments/profile.rb (low)
- Reviewed config/environments/profile.rb — looks good
config/initializers/i18n.rb (medium)
- Reviewed config/initializers/i18n.rb — looks good
config/environments/production.rb (low)
- Reviewed config/environments/production.rb — looks good
Test 9