feat(i18n): add Prism and I18n
This commit is contained in:
1 parent
f309fb35ed
commit
de19b8fabf
2 files changed
+152
No files matched your search
@@ -0,0 +1,68 @@
|
|||||||
|
module Prism
|
||||||
|
module I18n
|
||||||
|
# Holds translations for every locale as a nested Hash, keyed by symbol
|
||||||
|
# at every level ("posts.title" -> { posts: { title: "..." } }).
|
||||||
|
# Loaded from plain JSON files (no YAML/psych dependency - this build
|
||||||
|
# of Ruby has neither) or stored programmatically.
|
||||||
|
class Backend
|
||||||
|
def initialize
|
||||||
|
@translations = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_translations(locale, data)
|
||||||
|
locale = locale.to_sym
|
||||||
|
@translations[locale] ||= {}
|
||||||
|
deep_merge!(@translations[locale], symbolize(data))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_file(path)
|
||||||
|
locale = File.basename(path, '.*').to_sym
|
||||||
|
data = JSON.parse(File.read(path))
|
||||||
|
store_translations(locale, data)
|
||||||
|
locale
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup(locale, key)
|
||||||
|
segments = key.to_s.split('.').map(&:to_sym)
|
||||||
|
root = @translations[locale.to_sym]
|
||||||
|
return nil unless root
|
||||||
|
segments.reduce(root) do |node, segment|
|
||||||
|
return nil unless node.is_a?(Hash)
|
||||||
|
node[segment]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def locales
|
||||||
|
@translations.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear!
|
||||||
|
@translations = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def symbolize(value)
|
||||||
|
case value
|
||||||
|
when Hash
|
||||||
|
value.each_with_object({}) { |(key, nested), acc| acc[key.to_sym] = symbolize(nested) }
|
||||||
|
when Array
|
||||||
|
value.map { |entry| symbolize(entry) }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def deep_merge!(target, source)
|
||||||
|
source.each do |key, value|
|
||||||
|
if value.is_a?(Hash) && target[key].is_a?(Hash)
|
||||||
|
deep_merge!(target[key], value)
|
||||||
|
else
|
||||||
|
target[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
module Prism
|
||||||
|
module I18n
|
||||||
|
# Resolves a translate/localize call against a Backend: locale fallback
|
||||||
|
# chains, %{name} interpolation, and English-style count pluralization
|
||||||
|
# (:zero/:one/:other keys, falling back to :other).
|
||||||
|
class Translator
|
||||||
|
MissingTranslationError = Class.new(StandardError)
|
||||||
|
MissingInterpolationError = Class.new(StandardError)
|
||||||
|
|
||||||
|
DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
DEFAULT_DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
def initialize(backend, default_locale: :en, fallbacks: {})
|
||||||
|
@backend = backend
|
||||||
|
@default_locale = default_locale.to_sym
|
||||||
|
@fallbacks = fallbacks.transform_keys(&:to_sym).transform_values { |chain| Array(chain).map(&:to_sym) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def translate(key, locale: @default_locale, count: nil, default: nil, **interpolations)
|
||||||
|
entry = resolve(key, locale)
|
||||||
|
if entry.nil?
|
||||||
|
return interpolate(default, count: count, **interpolations) unless default.nil?
|
||||||
|
raise MissingTranslationError, "translation missing: #{locale}.#{key}"
|
||||||
|
end
|
||||||
|
entry = pluralize(entry, count) if entry.is_a?(Hash) && !count.nil?
|
||||||
|
interpolate(entry, count: count, **interpolations)
|
||||||
|
end
|
||||||
|
alias_method :t, :translate
|
||||||
|
|
||||||
|
def localize(value, locale: @default_locale, format: :default)
|
||||||
|
pattern = resolve("#{scope_for(value)}.formats.#{format}", locale) ||
|
||||||
|
default_format_for(value)
|
||||||
|
value.strftime(pattern)
|
||||||
|
end
|
||||||
|
alias_method :l, :localize
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resolve(key, locale)
|
||||||
|
chain_for(locale).each do |candidate|
|
||||||
|
value = @backend.lookup(candidate, key)
|
||||||
|
return value unless value.nil?
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def chain_for(locale)
|
||||||
|
locale = locale.to_sym
|
||||||
|
chain = [locale] + @fallbacks.fetch(locale, [])
|
||||||
|
chain << @default_locale unless chain.include?(@default_locale)
|
||||||
|
chain.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def pluralize(rules, count)
|
||||||
|
key = plural_key(count, rules)
|
||||||
|
rules[key] || rules[:other] || rules.values.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def plural_key(count, rules)
|
||||||
|
return :zero if count == 0 && rules.key?(:zero)
|
||||||
|
return :one if count == 1
|
||||||
|
:other
|
||||||
|
end
|
||||||
|
|
||||||
|
def interpolate(text, count: nil, **interpolations)
|
||||||
|
return text unless text.is_a?(String)
|
||||||
|
values = interpolations.dup
|
||||||
|
values[:count] = count unless count.nil?
|
||||||
|
text.gsub(/%\{(\w+)\}/) do
|
||||||
|
key = Regexp.last_match(1).to_sym
|
||||||
|
values.fetch(key) { raise MissingInterpolationError, "missing interpolation argument #{key.inspect} for #{text.inspect}" }.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_for(value)
|
||||||
|
value.is_a?(Time) ? 'time' : 'date'
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_format_for(value)
|
||||||
|
value.is_a?(Time) ? DEFAULT_TIME_FORMAT : DEFAULT_DATE_FORMAT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user