feat: add Prism and I18n

This commit is contained in:
dualfroz committed 2026-09-06 20:36:15 +02:00
1 parent bb80e7d3c4
commit 069b348302
1 file changed
+124
+124
View File
@@ -0,0 +1,124 @@
require_relative 'i18n/backend'
require_relative 'i18n/translator'
module Prism
# Rails-i18n-like translation API: JSON translation files (this Ruby
# build has no psych/libyaml, so YAML is not an option), a per-thread
# current locale, fallback chains, and Accept-Language negotiation.
module I18n
class << self
def backend
@backend ||= Backend.new
end
def default_locale
@default_locale ||= :en
end
def default_locale=(locale)
@translator = nil
@default_locale = locale.to_sym
end
def fallbacks
@fallbacks ||= {}
end
def fallbacks=(hash)
@translator = nil
@fallbacks = hash
end
def locale
Thread.current[:prism_i18n_locale] || default_locale
end
def locale=(new_locale)
Thread.current[:prism_i18n_locale] = new_locale&.to_sym
end
def with_locale(new_locale)
previous = locale
self.locale = new_locale
yield
ensure
self.locale = previous
end
def load_path
@load_path ||= []
end
def load_translations!
load_path.each do |path|
Dir.glob(File.join(path, '**', '*.json')).sort.each { |file| backend.load_file(file) }
end
@translator = nil
end
def store_translations(locale, data)
@translator = nil
backend.store_translations(locale, data)
end
def available_locales
backend.locales
end
def translate(key, **options)
options[:locale] ||= locale
translator.translate(key, **options)
end
alias_method :t, :translate
def localize(value, **options)
options[:locale] ||= locale
translator.localize(value, **options)
end
alias_method :l, :localize
# Picks the first locale in the client's Accept-Language header that
# this app has translations for, honoring q-values (like
# Mime.best_from_accept but for BCP-47 language tags, matching on
# the primary subtag: "en-US" negotiates against an :en backend).
def best_locale_from_accept(header, available: available_locales)
return nil if header.nil? || header.strip.empty?
wanted = available.map(&:to_s)
parse_accept_language(header).each do |tag|
exact = wanted.find { |candidate| candidate.casecmp?(tag) }
return exact.to_sym if exact
primary = tag.split('-').first
match = wanted.find { |candidate| candidate.casecmp?(primary) }
return match.to_sym if match
end
nil
end
def reset!
@backend = nil
@translator = nil
@default_locale = nil
@fallbacks = nil
@load_path = nil
Thread.current[:prism_i18n_locale] = nil
end
private
def translator
@translator ||= Translator.new(backend, default_locale: default_locale, fallbacks: fallbacks)
end
def parse_accept_language(header)
entries = header.split(',').filter_map do |part|
tag, *params = part.strip.split(';').map(&:strip)
next if tag.nil? || tag.empty?
quality = 1.0
params.each { |param| quality = param.sub('q=', '').to_f if param.start_with?('q=') }
[tag, quality]
end
entries.sort_by.with_index { |(_, quality), index| [-quality, index] }.map(&:first)
end
end
end
end