Add I18n interface for mustache templates
This commit adds an interface that allows mustache templates to get I18n translation strings, transform any arguments that may be present in them, and then render them on the frontend. This is our first real step to getting internationalizing the Gollum frontend.
This commit is contained in:
committed by
benjamin wil
parent
f3e17bb6a6
commit
a46852504c
@@ -15,6 +15,7 @@ require 'pathname'
|
||||
require 'gollum'
|
||||
require 'gollum/assets'
|
||||
require 'gollum/views/helpers'
|
||||
require 'gollum/views/helpers/locale_helpers'
|
||||
require 'gollum/views/layout'
|
||||
require 'gollum/views/editable'
|
||||
require 'gollum/views/has_page'
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
module Precious
|
||||
module Views
|
||||
module LocaleHelpers
|
||||
NO_METHOD_MESSAGE = 'Argument must be a view method'
|
||||
YAML_VARIABLE_REGEXP = /\%\{[\w]+\}/
|
||||
|
||||
# Returns all I18n translation strings for the current view class.
|
||||
# This method support YAML arguments. For example:
|
||||
#
|
||||
# last_edited: This content was last edited at %{date}.
|
||||
#
|
||||
# Where the `date` argument must be a method available on the current
|
||||
# class.
|
||||
#
|
||||
# Use this interface within Mustache templates to render any user
|
||||
# interface strings in the current locale. For example:
|
||||
#
|
||||
# {{ t.last_edited }}
|
||||
#
|
||||
def t
|
||||
autofill I18n.t(locale_klass_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Recursively looks up I18n translation values and autofills any YAML
|
||||
# arguments with the return value of the current class's matching method.
|
||||
#
|
||||
# When a translation value with an argument has no matching method, we
|
||||
# then return that value transformed to include the `no_method_message`
|
||||
#
|
||||
def autofill(yaml)
|
||||
yaml.map { |i18n_key, i18n_value|
|
||||
if i18n_value.is_a? Hash
|
||||
[i18n_key, autofill(i18n_value)]
|
||||
elsif has_arguments?(i18n_value)
|
||||
fill_argument_content(i18n_key, i18n_value)
|
||||
else
|
||||
[i18n_key, i18n_value]
|
||||
end
|
||||
}.to_h
|
||||
end
|
||||
|
||||
def fill_argument_content(i18n_key, i18n_value)
|
||||
i18n_value.gsub!(YAML_VARIABLE_REGEXP) do |argument|
|
||||
method_name = argument.gsub(/[^\w]/, '')
|
||||
|
||||
next if method_name.nil?
|
||||
|
||||
begin
|
||||
self.public_send(method_name)
|
||||
rescue NoMethodError => error
|
||||
no_method_message(method_name)
|
||||
end
|
||||
end
|
||||
|
||||
[i18n_key, i18n_value]
|
||||
end
|
||||
|
||||
def has_arguments?(i18n_value)
|
||||
i18n_value.match?(YAML_VARIABLE_REGEXP)
|
||||
end
|
||||
|
||||
# Returns the current class name in a format that is acceptable in YAML.
|
||||
# To summarize its function:
|
||||
#
|
||||
# NameOfConstant => name_of_constant
|
||||
#
|
||||
def locale_klass_name
|
||||
@locale_klass_name ||= self.class.name.gsub(/::/, '/').
|
||||
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
||||
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
||||
tr('-', '_').
|
||||
downcase
|
||||
end
|
||||
|
||||
def no_method_message(method_name, message = NO_METHOD_MESSAGE)
|
||||
"[#{message}: #{method_name}]"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,10 +6,11 @@ module Precious
|
||||
include Rack::Utils
|
||||
include Sprockets::Helpers
|
||||
include Precious::Views::AppHelpers
|
||||
include Precious::Views::LocaleHelpers
|
||||
include Precious::Views::SprocketsHelpers
|
||||
include Precious::Views::RouteHelpers
|
||||
include Precious::Views::OcticonHelpers
|
||||
|
||||
|
||||
alias_method :h, :escape_html
|
||||
|
||||
attr_reader :name, :path
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
require_relative "../../helper"
|
||||
require_relative "../../../lib/gollum/views/helpers"
|
||||
|
||||
describe Precious::Views::LocaleHelpers do
|
||||
class TestClass < Mustache
|
||||
include Precious::Views::LocaleHelpers
|
||||
|
||||
def author
|
||||
"J.R.R."
|
||||
end
|
||||
|
||||
def location
|
||||
"Bloemfontein"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
::I18n.available_locales = [:en, :de]
|
||||
::I18n.load_path = Dir[File.expand_path("test/support/locales" + "/*.yml")]
|
||||
end
|
||||
|
||||
def teardown
|
||||
I18n.locale = :en
|
||||
end
|
||||
|
||||
let(:dummy_instance) { TestClass.new }
|
||||
|
||||
describe "#t" do
|
||||
describe "mustache usage" do
|
||||
let(:subject) { dummy_instance.render(mustache_template) }
|
||||
|
||||
let(:mustache_template) { "{{ t.hello_world }}" }
|
||||
|
||||
describe "in the default locale" do
|
||||
it "returns the translation string" do
|
||||
_(subject).must_equal "Hello world"
|
||||
end
|
||||
end
|
||||
|
||||
describe "in the configured locale" do
|
||||
it "returns the translation string" do
|
||||
I18n.locale = :de
|
||||
|
||||
_(subject).must_equal "Hallo Welt"
|
||||
end
|
||||
end
|
||||
|
||||
describe "translations with YAML arguments" do
|
||||
let(:mustache_template) { "{{ t.author_info.full }}" }
|
||||
|
||||
describe "in the default locale" do
|
||||
it "autofills YAML arguments" do
|
||||
_(subject).must_equal "Author J.R.R. is from Bloemfontein"
|
||||
end
|
||||
end
|
||||
|
||||
describe "in the configured locale" do
|
||||
it "autofills YAML arguments" do
|
||||
I18n.locale = :de
|
||||
|
||||
_(subject).must_equal "Autor J.R.R. ist vom Bloemfontein"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "translations with invalid arguments" do
|
||||
let(:mustache_template) { "{{ t.has_invalid_argument }}" }
|
||||
|
||||
it "fails gracefully with embedded error message" do
|
||||
expected_string = "Welcome to " \
|
||||
"[#{TestClass::NO_METHOD_MESSAGE}: no_matching_method]"
|
||||
|
||||
_(subject).must_equal expected_string
|
||||
end
|
||||
end
|
||||
|
||||
describe "out of scope translations" do
|
||||
let(:mustache_template) { "{{ t.never_called }}" }
|
||||
|
||||
it "does not include translation keys from other classes" do
|
||||
_(subject).must_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "missing translations" do
|
||||
let(:mustache_template) { "{{ t.nested.nonexistent_key }}" }
|
||||
|
||||
it "outputs an empty string" do
|
||||
_(subject).must_be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "usage" do
|
||||
let(:subject) { dummy_instance.t }
|
||||
|
||||
it "returns a hash" do
|
||||
_(subject).must_be_kind_of Hash
|
||||
end
|
||||
|
||||
it "returns translation keys under 'test_class'" do
|
||||
i18n_keys = I18n.t("test_class").keys
|
||||
|
||||
_(subject.keys).must_equal i18n_keys
|
||||
end
|
||||
|
||||
it "does not return translation keys under other classes" do
|
||||
other_i18n_keys = I18n.t("nonexistant_test_class").keys
|
||||
|
||||
_(subject.keys).wont_include other_i18n_keys
|
||||
end
|
||||
|
||||
it "returns nested keys" do
|
||||
nested_keys = subject[:author_info].keys
|
||||
|
||||
_(nested_keys).must_equal [:full]
|
||||
end
|
||||
|
||||
describe "auto-filled YAML arguments" do
|
||||
let(:subject) { dummy_instance.t[:author_info][:full] }
|
||||
|
||||
it "auto-fills in the default locale" do
|
||||
_(subject).must_equal "Author J.R.R. is from Bloemfontein"
|
||||
end
|
||||
|
||||
it "auto-fills in a configured locale" do
|
||||
I18n.locale = :de
|
||||
|
||||
_(subject).must_equal "Autor J.R.R. ist vom Bloemfontein"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -5,6 +5,7 @@ require 'shoulda'
|
||||
require 'mocha/setup'
|
||||
require 'fileutils'
|
||||
require 'minitest/reporters'
|
||||
require 'minitest/spec'
|
||||
require 'twitter_cldr'
|
||||
require 'tmpdir'
|
||||
|
||||
@@ -93,4 +94,4 @@ def context(*args, &block)
|
||||
klass.class_eval &block
|
||||
end
|
||||
|
||||
$contexts = []
|
||||
$contexts = []
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
de:
|
||||
test_class:
|
||||
author_info:
|
||||
full: Autor %{author} ist vom %{location}
|
||||
has_invalid_argument: Willkommen in %{no_matching_method}
|
||||
hello_world: Hallo Welt
|
||||
nonexistant_test_class:
|
||||
never_called: Nie angerufen
|
||||
@@ -0,0 +1,8 @@
|
||||
en:
|
||||
test_class:
|
||||
author_info:
|
||||
full: Author %{author} is from %{location}
|
||||
has_invalid_argument: Welcome to %{no_matching_method}
|
||||
hello_world: Hello world
|
||||
nonexistant_test_class:
|
||||
never_called: Never called
|
||||
Reference in New Issue
Block a user