Module: RailsErrorDashboard::I18nStore

Defined in:
lib/rails_error_dashboard/i18n_store.rb

Overview

RED's own translation store, deliberately isolated from the host app's I18n.

WHY A PRIVATE BACKEND INSTEAD OF THE USUAL ENGINE LOAD PATH

The conventional way to translate a Rails engine is to append config/locales to I18n.load_path and namespace the keys. That shares the host's backend, and sharing it hands the host three ways to break the dashboard:

1. config.i18n.raise_on_missing_translations = true turns any key we
 forgot into a 500 — on the error dashboard, the one page that has to
 work when everything else is broken.
2. enforce_available_locales with a short available_locales list raises
 I18n::InvalidLocale as soon as RED asks for its own locale.
3. A custom exception_handler can raise on anything it likes.

RED's locale is also deliberately independent of the host's — that is the whole point of issue #148. Sharing a backend would re-create the coupling that bug was about.

The trade-off: hosts cannot override RED's strings with their own locale files. That is the right default for a self-hosted ops tool, and it can be relaxed later without breaking anything.

NOTHING IN HERE MAY RAISE. Every public method is total: it returns a String for any input, including garbage. See #translate.

Constant Summary collapse

DEFAULT_LOCALE =
"en".freeze
ENDONYMS =

The language's own name for itself. See .locale_options for why these are constants rather than translation keys.

{
  "en" => "English",
  "de" => "Deutsch",
  "fr" => "Français",
  "es" => "Español",
  "pt-BR" => "Português (Brasil)",
  "ja" => "日本語",
  "ru" => "Русский",
  "uk" => "Українська",
  "pl" => "Polski",
  "zh-CN" => "简体中文",
  "it" => "Italiano"
}.freeze

Class Method Summary collapse

Class Method Details

.available?(value) ⇒ Boolean

Compares case-insensitively rather than against a downcased copy — "pt-BR" is a real locale filename and downcasing it would report a locale we ship as unavailable.

Returns:

  • (Boolean)

    whether RED ships this locale



157
158
159
160
161
162
163
164
# File 'lib/rails_error_dashboard/i18n_store.rb', line 157

def available?(value)
  candidate = value.to_s.strip
  return false if candidate.empty?

  available_locales.any? { |locale| locale.to_s.casecmp?(candidate) }
rescue StandardError
  false
end

.available_localesArray<Symbol>

Locales RED ships, derived from the files actually present.

Returns:

  • (Array<Symbol>)


113
114
115
# File 'lib/rails_error_dashboard/i18n_store.rb', line 113

def available_locales
  @available_locales ||= locale_files.map { |path| File.basename(path, ".yml").to_sym }.sort
end

.backendObject

Double-checked locking. The fast path reads a fully-built backend; the slow path builds it under the mutex. build_backend assigns only after load_translations returns, so no thread can observe a half-loaded backend through @backend.



170
171
172
173
174
175
176
177
# File 'lib/rails_error_dashboard/i18n_store.rb', line 170

def backend
  cached = @backend
  return cached if cached

  load_mutex.synchronize do
    @backend ||= build_backend
  end
end

.locale_optionsArray<Array(String, String)>

Each shipped locale paired with its ENDONYM — the language's own name for itself (Deutsch, not German).

Deliberately NOT translation keys. An endonym is a property of the language, not of the locale you are viewing from: a picker that renders "German" when viewed from English and "Deutsch" when viewed from German is unusable precisely when you need it, which is when you are stuck in a language you cannot read. Every entry reads the same in every locale.

A locale with a file but no entry here falls back to its own tag ("xh"), which is honest and still selectable, rather than being hidden.

Returns:

  • (Array<Array(String, String)>)

    [locale, endonym] pairs, sorted by locale so the picker's order is stable.



131
132
133
134
135
# File 'lib/rails_error_dashboard/i18n_store.rb', line 131

def locale_options
  available_locales.map { |locale| [ locale.to_s, ENDONYMS.fetch(locale.to_s, locale.to_s) ] }
rescue StandardError
  [ [ DEFAULT_LOCALE, ENDONYMS.fetch(DEFAULT_LOCALE) ] ]
end

.reset!Object

Test seam. Clears memoized state so specs can reload from disk.



180
181
182
183
184
185
# File 'lib/rails_error_dashboard/i18n_store.rb', line 180

def reset!
  load_mutex.synchronize do
    @backend = nil
    @available_locales = nil
  end
end

.resolve(value) ⇒ String

Resolve an arbitrary value to a locale RED can actually serve. Matches case-insensitively ("EN" -> :en) because a wrong-cased tag that passes a format check but misses the dictionary is a mid-render failure.

Returns:

  • (String)

    a locale RED ships, or "en"



142
143
144
145
146
147
148
149
150
# File 'lib/rails_error_dashboard/i18n_store.rb', line 142

def resolve(value)
  candidate = value.to_s.strip
  return DEFAULT_LOCALE if candidate.empty?

  match = available_locales.find { |locale| locale.to_s.casecmp?(candidate) }
  match ? match.to_s : DEFAULT_LOCALE
rescue StandardError
  DEFAULT_LOCALE
end

.subtree(key, locale: DEFAULT_LOCALE) ⇒ Hash

Fetch a whole branch of the dictionary as a Hash, for callers that need the tree rather than one leaf — the JS payload is the only one today.

#translate deliberately treats a Hash result as a miss: a key resolving to a subtree instead of a leaf is a caller bug when you asked for text. Here it is the point, so this is a separate method rather than a flag on #translate.

Falls back to English as a whole branch, not key by key. A partially translated locale returning a half-English tree would be harder to debug than one that is cleanly English until it is finished.

Parameters:

  • key (String, Symbol)

    dot-separated key, e.g. "red.js"

Returns:

  • (Hash)

    deep-frozen dup, or {} for a miss. Never nil, never raises.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/rails_error_dashboard/i18n_store.rb', line 95

def subtree(key, locale: DEFAULT_LOCALE)
  return {} if key.nil? || key.to_s.empty?

  resolved = lookup_subtree(key, locale)
  return resolved unless resolved.equal?(MISSING)

  unless locale.to_s == DEFAULT_LOCALE
    fallback = lookup_subtree(key, DEFAULT_LOCALE)
    return fallback unless fallback.equal?(MISSING)
  end

  {}
rescue StandardError
  {}
end

.translate(key, locale: DEFAULT_LOCALE, **options) ⇒ String Also known as: t

Translate key in locale, falling back to English, then to a readable last resort derived from the key itself.

Parameters:

  • key (String, Symbol)

    dot-separated key, e.g. "red.nav.errors"

  • locale (String, Symbol) (defaults to: DEFAULT_LOCALE)

    target locale

Returns:

  • (String)

    always a String — never nil, never a raise



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/rails_error_dashboard/i18n_store.rb', line 62

def translate(key, locale: DEFAULT_LOCALE, **options)
  return "" if key.nil? || key.to_s.empty?

  resolved = lookup(key, locale, options)
  return resolved unless resolved.equal?(MISSING)

  unless locale.to_s == DEFAULT_LOCALE
    fallback = lookup(key, DEFAULT_LOCALE, options)
    return fallback unless fallback.equal?(MISSING)
  end

  humanized_key(key)
rescue StandardError
  # Truly last resort. A translation lookup must never be the reason a
  # dashboard page fails to render.
  humanized_key(key)
end