From 069b3483024b8ad1556586306b5a7800b6619202 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 20:36:15 +0200 Subject: [PATCH] feat: add Prism and I18n --- lib/prism/i18n.rb | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 lib/prism/i18n.rb diff --git a/lib/prism/i18n.rb b/lib/prism/i18n.rb new file mode 100644 index 0000000..3d25913 --- /dev/null +++ b/lib/prism/i18n.rb @@ -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