lib/dry/schema/messages/yaml.rb in dry-schema-1.4.3 vs lib/dry/schema/messages/yaml.rb in dry-schema-1.5.0
- old
+ new
@@ -1,21 +1,27 @@
# frozen_string_literal: true
-require 'yaml'
-require 'pathname'
+require "yaml"
+require "pathname"
-require 'dry/equalizer'
-require 'dry/schema/constants'
-require 'dry/schema/messages/abstract'
+require "dry/equalizer"
+require "dry/schema/constants"
+require "dry/schema/messages/abstract"
module Dry
module Schema
# Plain YAML message backend
#
# @api public
class Messages::YAML < Messages::Abstract
- LOCALE_TOKEN = '%<locale>s'
+ LOCALE_TOKEN = "%<locale>s"
+ TOKEN_REGEXP = /%{(\w*)}/.freeze
+ EMPTY_CONTEXT = Object.new.tap { |ctx|
+ def ctx.context
+ binding
+ end
+ }.freeze.context
include Dry::Equalizer(:data)
# Loaded localized message templates
#
@@ -43,26 +49,31 @@
# @api private
def self.flat_hash(hash, path = [], keys = {})
hash.each do |key, value|
flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
- if value.is_a?(String) && hash['text'] != value
+ if value.is_a?(String) && hash["text"] != value
keys[[*path, key].join(DOT)] = {
text: value,
meta: EMPTY_HASH
}
- elsif value.is_a?(Hash) && value['text'].is_a?(String)
+ elsif value.is_a?(Hash) && value["text"].is_a?(String)
keys[[*path, key].join(DOT)] = {
- text: value['text'],
- meta: value.dup.delete_if { |k| k == 'text' }.map { |k, v| [k.to_sym, v] }.to_h
+ text: value["text"],
+ meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h
}
end
end
keys
end
# @api private
+ def self.cache
+ @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
+ end
+
+ # @api private
def initialize(data: EMPTY_HASH, config: nil)
super()
@data = data
@config = config if config
@t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
@@ -75,11 +86,11 @@
#
# @return [String]
#
# @api public
def looked_up_paths(predicate, options)
- super.map { |path| path % { locale: options[:locale] || default_locale } }
+ super.map { |path| path % {locale: options[:locale] || default_locale} }
end
# Get a message for the given key and its options
#
# @param [Symbol] key
@@ -122,17 +133,53 @@
end
end
# @api private
def prepare
- @data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge)
+ @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
self
end
+ # @api private
+ def interpolatable_data(key, options, **data)
+ tokens = evaluation_context(key, options).fetch(:tokens)
+ data.select { |k,| tokens.include?(k) }
+ end
+
+ # @api private
+ def interpolate(key, options, **data)
+ evaluator = evaluation_context(key, options).fetch(:evaluator)
+ data.empty? ? evaluator.() : evaluator.(**data)
+ end
+
private
# @api private
+ def evaluation_context(key, options)
+ cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
+ tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
+ text = input.gsub("%", "#")
+
+ # rubocop:disable Security/Eval
+ evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
+ -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
+ RUBY
+ # rubocop:enable Security/Eval
+
+ {
+ tokens: tokens,
+ evaluator: evaluator
+ }
+ end
+ end
+
+ # @api private
+ def cache
+ @cache ||= self.class.cache[self]
+ end
+
+ # @api private
def load_translations(path)
data = self.class.flat_hash(YAML.load_file(path))
return data unless custom_top_namespace?(path)
@@ -141,10 +188,10 @@
# @api private
def evaluated_key(key, options)
return key unless key.include?(LOCALE_TOKEN)
- key % { locale: options[:locale] || default_locale }
+ key % {locale: options[:locale] || default_locale}
end
end
end
end