Module: HeadMusic::Style::Template
- Defined in:
- lib/head_music/style/template.rb
Overview
Renders the customer-facing strings a guide and its items produce.
I18n fails quietly in four ways, each of which ships a raw template to a student on a green suite:
I18n.t(key, raise: true) # no values -> “%minimum” intact, no raise I18n.t(key, count: “one”) # a word where a number belongs -> silently plural I18n.t(key, default: “x”) # a reserved key as a value -> hijacks the lookup I18n.t(“missing.key”) # -> “Translation missing: …” rather than an error
Defined Under Namespace
Classes: MissingTemplate
Constant Summary collapse
- SCOPE =
"head_music.style"- RUDIMENT_SCOPE =
Note values are rudiment vocabulary that the sentences borrow. Reachable through the same seam so they get the plural fallback and the unfilled- interpolation guard the style strings get.
"head_music.rudiments"- NUMBER_WORDS_SCOPE =
"head_music.number_words"- PLURAL_RULES =
I18n’s Simple backend implements English’s rule and no other, so a locale whose forms differ raises rather than choosing. Kept here as a seam rather than by including I18n::Backend::Pluralization, which would switch pluralization on for the host application’s locales too.
{ ru: ->(count) { tens = count % 100 units = count % 10 if units == 1 && tens != 11 then :one elsif (2..4).cover?(units) && !(12..14).cover?(tens) then :few else :many end } }.freeze
- INTERPOLATION =
/%\{/- FORBIDDEN_VALUE_KEYS =
count selects a plural form, so a value by that name never reaches the template.
(I18n::RESERVED_KEYS + [:count]).freeze
Class Method Summary collapse
-
.exists?(key, scope: SCOPE) ⇒ Boolean
-
.fell_back_to_ruby ⇒ Object
-
.guard_value_keys!(values) ⇒ Object
-
.humanize_knows?(locale) ⇒ Boolean
-
.humanize_locale(locale) ⇒ Object
-
.in_locale_of(key, &block) ⇒ Object
Values are built in the locale the sentence renders in.
-
.known_plural_form(entry, count) ⇒ Object
Applying a rule the gem knows is not a fallback, so it is not recorded as one – otherwise every Russian sentence would report a plural gap and the guard that watches for real ones would have to be loosened.
-
.number_word(number, locale: I18n.locale) ⇒ Object
A locale that spells its own numerals wins.
-
.pluralize(key, count:, scope: SCOPE, **values) ⇒ Object
A locale with no plural data should read a little wrong rather than raise at a student.
-
.render(key, scope: SCOPE, **values) ⇒ Object
scope is safe as a keyword rather than a value: I18n reserves the name, so no template could ever have interpolated %scope anyway.
-
.resolved_locale(key, locale: I18n.locale, scope: SCOPE) ⇒ Object
fallback: false because I18n.exists? consults the chain and so answers true for every locale, which is no answer at all.
-
.ruby_plural_form(entry, count) ⇒ Object
English’s rule first, then whatever the entry carries.
-
.spelled_number(number, locale) ⇒ Object
fallback: false so a locale without its own numerals falls through to humanize, rather than reading English’s word as though it were its own.
-
.verify!(entries) ⇒ Object
Renders every template the registry can produce – including the violation branches an analysis chooses between – at load, so a missing or unfillable one fails on require rather than in front of a student.
Instance Method Summary collapse
-
#exists?(key, scope: SCOPE) ⇒ Object
private
-
#fell_back_to_ruby ⇒ Object
private
-
#guard_value_keys!(values) ⇒ Object
private
-
#humanize_knows?(locale) ⇒ Object
private
-
#humanize_locale(locale) ⇒ Object
private
-
#in_locale_of(key, &block) ⇒ Object
private
Values are built in the locale the sentence renders in.
-
#known_plural_form(entry, count) ⇒ Object
private
Applying a rule the gem knows is not a fallback, so it is not recorded as one – otherwise every Russian sentence would report a plural gap and the guard that watches for real ones would have to be loosened.
-
#number_word(number, locale: I18n.locale) ⇒ Object
private
A locale that spells its own numerals wins.
-
#pluralize(key, count:, scope: SCOPE, **values) ⇒ Object
private
A locale with no plural data should read a little wrong rather than raise at a student.
-
#render(key, scope: SCOPE, **values) ⇒ Object
private
scope is safe as a keyword rather than a value: I18n reserves the name, so no template could ever have interpolated %scope anyway.
-
#resolved_locale(key, locale: I18n.locale, scope: SCOPE) ⇒ Object
private
fallback: false because I18n.exists? consults the chain and so answers true for every locale, which is no answer at all.
-
#ruby_plural_form(entry, count) ⇒ Object
private
English’s rule first, then whatever the entry carries.
-
#spelled_number(number, locale) ⇒ Object
private
fallback: false so a locale without its own numerals falls through to humanize, rather than reading English’s word as though it were its own.
-
#verify!(entries) ⇒ Object
private
Renders every template the registry can produce – including the violation branches an analysis chooses between – at load, so a missing or unfillable one fails on require rather than in front of a student.
Class Method Details
.exists?(key, scope: SCOPE) ⇒ Boolean
57 58 59 |
# File 'lib/head_music/style/template.rb', line 57 def exists?(key, scope: SCOPE) I18n.exists?("#{scope}.#{key}") end |
.fell_back_to_ruby ⇒ Object
131 132 133 |
# File 'lib/head_music/style/template.rb', line 131 def fell_back_to_ruby @fell_back_to_ruby ||= [] end |
.guard_value_keys!(values) ⇒ Object
155 156 157 158 159 160 |
# File 'lib/head_music/style/template.rb', line 155 def guard_value_keys!(values) clashing = values.keys & FORBIDDEN_VALUE_KEYS return if clashing.empty? || clashing == [:count] raise ArgumentError, "#{clashing.join(", ")} would be read by I18n rather than interpolated" end |
.humanize_knows?(locale) ⇒ Boolean
93 94 95 96 97 98 |
# File 'lib/head_music/style/template.rb', line 93 def humanize_knows?(locale) 1.humanize(locale: locale) true rescue false end |
.humanize_locale(locale) ⇒ Object
75 76 77 |
# File 'lib/head_music/style/template.rb', line 75 def humanize_locale(locale) I18n.fallbacks[locale].detect { |candidate| humanize_knows?(candidate) } || :en end |
.in_locale_of(key, &block) ⇒ Object
Values are built in the locale the sentence renders in. Otherwise a German reader gets “Write at least Acht notes.”: the sentence falls back to English while the number humanizes into German.
89 90 91 |
# File 'lib/head_music/style/template.rb', line 89 def in_locale_of(key, &block) I18n.with_locale(resolved_locale(key), &block) end |
.known_plural_form(entry, count) ⇒ Object
Applying a rule the gem knows is not a fallback, so it is not recorded as one – otherwise every Russian sentence would report a plural gap and the guard that watches for real ones would have to be loosened.
119 120 121 122 |
# File 'lib/head_music/style/template.rb', line 119 def known_plural_form(entry, count) form = PLURAL_RULES[I18n.locale]&.call(count) form if form && entry.key?(form) end |
.number_word(number, locale: I18n.locale) ⇒ Object
A locale that spells its own numerals wins. humanize ships no Italian at all, and where it does ship a locale the form is wrong for this sentence: capitalized mid-sentence for German, masculine where the note values being counted are feminine.
65 66 67 |
# File 'lib/head_music/style/template.rb', line 65 def number_word(number, locale: I18n.locale) spelled_number(number, locale) || number.humanize(locale: humanize_locale(locale)) end |
.pluralize(key, count:, scope: SCOPE, **values) ⇒ Object
A locale with no plural data should read a little wrong rather than raise at a student. Narrow deliberately: rescuing MissingTemplate too would swallow an unfilled interpolation, which is the failure verify! exists to catch.
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/head_music/style/template.rb', line 103 def pluralize(key, count:, scope: SCOPE, **values) render(key, count: count, scope: scope, **values) rescue I18n::InvalidPluralizationData => error known = known_plural_form(error.entry, count) return render("#{key}.#{known}", count: count, scope: scope, **values) if known form = ruby_plural_form(error.entry, count) raise MissingTemplate, "#{key} has no plural form to fall back to" if form.nil? fell_back_to_ruby << key unless fell_back_to_ruby.include?(key) render("#{key}.#{form}", count: count, scope: scope, **values) end |
.render(key, scope: SCOPE, **values) ⇒ Object
scope is safe as a keyword rather than a value: I18n reserves the name, so no template could ever have interpolated %scope anyway. Declaring it binds a caller’s scope: to the parameter, which is why guard_value_keys! never sees it.
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/head_music/style/template.rb', line 46 def render(key, scope: SCOPE, **values) guard_value_keys!(values) rendered = I18n.t(key, scope: scope, raise: true, **values) raise MissingTemplate, "#{key} did not render to a string" unless rendered.is_a?(String) raise MissingTemplate, "#{key} left an interpolation unfilled: #{rendered}" if INTERPOLATION.match?(rendered) rendered rescue I18n::MissingTranslationData, I18n::MissingInterpolationArgument => e raise MissingTemplate, "#{key}: #{e.}" end |
.resolved_locale(key, locale: I18n.locale, scope: SCOPE) ⇒ Object
fallback: false because I18n.exists? consults the chain and so answers true for every locale, which is no answer at all.
81 82 83 84 |
# File 'lib/head_music/style/template.rb', line 81 def resolved_locale(key, locale: I18n.locale, scope: SCOPE) chain = I18n.fallbacks[locale] chain.detect { |candidate| I18n.exists?("#{scope}.#{key}", candidate, fallback: false) } || locale end |
.ruby_plural_form(entry, count) ⇒ Object
English’s rule first, then whatever the entry carries. Reading the singular for a plural count keeps the sentence, which a noun phrase would not.
126 127 128 129 |
# File 'lib/head_music/style/template.rb', line 126 def ruby_plural_form(entry, count) preferred = (count == 1) ? :one : :other ([preferred, :other, :one] & entry.keys).first || entry.keys.first end |
.spelled_number(number, locale) ⇒ Object
fallback: false so a locale without its own numerals falls through to humanize, rather than reading English’s word as though it were its own.
71 72 73 |
# File 'lib/head_music/style/template.rb', line 71 def spelled_number(number, locale) I18n.t(number.to_s, scope: NUMBER_WORDS_SCOPE, locale: locale, fallback: false, default: nil) end |
.verify!(entries) ⇒ Object
Renders every template the registry can produce – including the violation branches an analysis chooses between – at load, so a missing or unfillable one fails on require rather than in front of a student. In English deliberately: a host application’s locale must not decide whether the gem loads. Returns the keys that needed the Ruby plural fallback.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/head_music/style/template.rb', line 140 def verify!(entries) fell_back_to_ruby.clear I18n.with_locale(:en) do entries.each do |guide| guide.instruction guide.guide_items.each do |item| item.name item.instruction item.violation_previews end end end fell_back_to_ruby.dup end |
Instance Method Details
#exists?(key, scope: SCOPE) ⇒ Object (private)
57 58 59 |
# File 'lib/head_music/style/template.rb', line 57 def exists?(key, scope: SCOPE) I18n.exists?("#{scope}.#{key}") end |
#fell_back_to_ruby ⇒ Object (private)
131 132 133 |
# File 'lib/head_music/style/template.rb', line 131 def fell_back_to_ruby @fell_back_to_ruby ||= [] end |
#guard_value_keys!(values) ⇒ Object (private)
155 156 157 158 159 160 |
# File 'lib/head_music/style/template.rb', line 155 def guard_value_keys!(values) clashing = values.keys & FORBIDDEN_VALUE_KEYS return if clashing.empty? || clashing == [:count] raise ArgumentError, "#{clashing.join(", ")} would be read by I18n rather than interpolated" end |
#humanize_knows?(locale) ⇒ Object (private)
93 94 95 96 97 98 |
# File 'lib/head_music/style/template.rb', line 93 def humanize_knows?(locale) 1.humanize(locale: locale) true rescue false end |
#humanize_locale(locale) ⇒ Object (private)
75 76 77 |
# File 'lib/head_music/style/template.rb', line 75 def humanize_locale(locale) I18n.fallbacks[locale].detect { |candidate| humanize_knows?(candidate) } || :en end |
#in_locale_of(key, &block) ⇒ Object (private)
Values are built in the locale the sentence renders in. Otherwise a German reader gets “Write at least Acht notes.”: the sentence falls back to English while the number humanizes into German.
89 90 91 |
# File 'lib/head_music/style/template.rb', line 89 def in_locale_of(key, &block) I18n.with_locale(resolved_locale(key), &block) end |
#known_plural_form(entry, count) ⇒ Object (private)
Applying a rule the gem knows is not a fallback, so it is not recorded as one – otherwise every Russian sentence would report a plural gap and the guard that watches for real ones would have to be loosened.
119 120 121 122 |
# File 'lib/head_music/style/template.rb', line 119 def known_plural_form(entry, count) form = PLURAL_RULES[I18n.locale]&.call(count) form if form && entry.key?(form) end |
#number_word(number, locale: I18n.locale) ⇒ Object (private)
A locale that spells its own numerals wins. humanize ships no Italian at all, and where it does ship a locale the form is wrong for this sentence: capitalized mid-sentence for German, masculine where the note values being counted are feminine.
65 66 67 |
# File 'lib/head_music/style/template.rb', line 65 def number_word(number, locale: I18n.locale) spelled_number(number, locale) || number.humanize(locale: humanize_locale(locale)) end |
#pluralize(key, count:, scope: SCOPE, **values) ⇒ Object (private)
A locale with no plural data should read a little wrong rather than raise at a student. Narrow deliberately: rescuing MissingTemplate too would swallow an unfilled interpolation, which is the failure verify! exists to catch.
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/head_music/style/template.rb', line 103 def pluralize(key, count:, scope: SCOPE, **values) render(key, count: count, scope: scope, **values) rescue I18n::InvalidPluralizationData => error known = known_plural_form(error.entry, count) return render("#{key}.#{known}", count: count, scope: scope, **values) if known form = ruby_plural_form(error.entry, count) raise MissingTemplate, "#{key} has no plural form to fall back to" if form.nil? fell_back_to_ruby << key unless fell_back_to_ruby.include?(key) render("#{key}.#{form}", count: count, scope: scope, **values) end |
#render(key, scope: SCOPE, **values) ⇒ Object (private)
scope is safe as a keyword rather than a value: I18n reserves the name, so no template could ever have interpolated %scope anyway. Declaring it binds a caller’s scope: to the parameter, which is why guard_value_keys! never sees it.
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/head_music/style/template.rb', line 46 def render(key, scope: SCOPE, **values) guard_value_keys!(values) rendered = I18n.t(key, scope: scope, raise: true, **values) raise MissingTemplate, "#{key} did not render to a string" unless rendered.is_a?(String) raise MissingTemplate, "#{key} left an interpolation unfilled: #{rendered}" if INTERPOLATION.match?(rendered) rendered rescue I18n::MissingTranslationData, I18n::MissingInterpolationArgument => e raise MissingTemplate, "#{key}: #{e.}" end |
#resolved_locale(key, locale: I18n.locale, scope: SCOPE) ⇒ Object (private)
fallback: false because I18n.exists? consults the chain and so answers true for every locale, which is no answer at all.
81 82 83 84 |
# File 'lib/head_music/style/template.rb', line 81 def resolved_locale(key, locale: I18n.locale, scope: SCOPE) chain = I18n.fallbacks[locale] chain.detect { |candidate| I18n.exists?("#{scope}.#{key}", candidate, fallback: false) } || locale end |
#ruby_plural_form(entry, count) ⇒ Object (private)
English’s rule first, then whatever the entry carries. Reading the singular for a plural count keeps the sentence, which a noun phrase would not.
126 127 128 129 |
# File 'lib/head_music/style/template.rb', line 126 def ruby_plural_form(entry, count) preferred = (count == 1) ? :one : :other ([preferred, :other, :one] & entry.keys).first || entry.keys.first end |
#spelled_number(number, locale) ⇒ Object (private)
fallback: false so a locale without its own numerals falls through to humanize, rather than reading English’s word as though it were its own.
71 72 73 |
# File 'lib/head_music/style/template.rb', line 71 def spelled_number(number, locale) I18n.t(number.to_s, scope: NUMBER_WORDS_SCOPE, locale: locale, fallback: false, default: nil) end |
#verify!(entries) ⇒ Object (private)
Renders every template the registry can produce – including the violation branches an analysis chooses between – at load, so a missing or unfillable one fails on require rather than in front of a student. In English deliberately: a host application’s locale must not decide whether the gem loads. Returns the keys that needed the Ruby plural fallback.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/head_music/style/template.rb', line 140 def verify!(entries) fell_back_to_ruby.clear I18n.with_locale(:en) do entries.each do |guide| guide.instruction guide.guide_items.each do |item| item.name item.instruction item.violation_previews end end end fell_back_to_ruby.dup end |