Skip to content

FEATURE: Localization fallbacks (server-side)#9

Open
everettbu wants to merge 1 commit into
localization-system-prefrom
localization-system-post
Open

FEATURE: Localization fallbacks (server-side)#9
everettbu wants to merge 1 commit into
localization-system-prefrom
localization-system-post

Conversation

@everettbu

Copy link
Copy Markdown
Contributor

Test 9

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*
@github-actions

Copy link
Copy Markdown

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 maloyan4good left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) }
end

And 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 mfeuerstein left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants