diff --git a/lib/liquid.rb b/lib/liquid.rb index 62b1c80d..22a6ddef 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -48,6 +48,7 @@ end require "liquid/version" require 'liquid/lexer' require 'liquid/parser' +require 'liquid/i18n' require 'liquid/drop' require 'liquid/extensions' require 'liquid/errors' diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb index a5c3b1e4..ed7f7ace 100644 --- a/lib/liquid/block.rb +++ b/lib/liquid/block.rb @@ -41,7 +41,7 @@ module Liquid unknown_tag($1, $2, tokens) end else - raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} " + raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect)) end when IsVariable new_var = create_variable(token) @@ -80,11 +80,14 @@ module Liquid def unknown_tag(tag, params, tokens) case tag when 'else' - raise SyntaxError, "#{block_name} tag does not expect else tag" + raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else", + :block_name => block_name)) when 'end' - raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}" + raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter", + :block_name => block_name, + :block_delimiter => block_delimiter)) else - raise SyntaxError, "Unknown tag '#{tag}'" + raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag)) end end @@ -100,7 +103,7 @@ module Liquid token.scan(ContentOfVariable) do |content| return Variable.new(content.first, @options) end - raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ") + raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect)) end def render(context) @@ -110,7 +113,7 @@ module Liquid protected def assert_missing_delimitation! - raise SyntaxError.new("#{block_name} tag was never closed") + raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name)) end def render_all(list, context) diff --git a/lib/liquid/htmltags.rb b/lib/liquid/htmltags.rb index 62a53895..05b04190 100644 --- a/lib/liquid/htmltags.rb +++ b/lib/liquid/htmltags.rb @@ -11,7 +11,7 @@ module Liquid @attributes[key] = value end else - raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3") + raise SyntaxError.new(options[:locale].t("errors.syntax.table_row")) end super diff --git a/lib/liquid/i18n.rb b/lib/liquid/i18n.rb new file mode 100644 index 00000000..3ae54452 --- /dev/null +++ b/lib/liquid/i18n.rb @@ -0,0 +1,39 @@ +require 'yaml' + +module Liquid + class I18n + DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml") + + class TranslationError < StandardError + end + + attr_reader :path + + def initialize(path = DEFAULT_LOCALE) + @path = path + end + + def translate(name, vars = {}) + interpolate(deep_fetch_translation(name), vars) + end + alias_method :t, :translate + + def locale + @locale ||= YAML.load_file(@path) + end + + private + def interpolate(name, vars) + name.gsub(/%{(\w+)}/) { + raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] + "#{vars[$1.to_sym]}" + } + end + + def deep_fetch_translation(name) + name.split('.').reduce(locale) do |level, cur| + level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" + end + end + end +end diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml new file mode 100644 index 00000000..a6ce9355 --- /dev/null +++ b/lib/liquid/locales/en.yml @@ -0,0 +1,21 @@ +--- + errors: + syntax: + assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" + capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" + case: "Syntax Error in 'case' - Valid syntax: case [condition]" + case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" + case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " + cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" + for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]" + for_invalid_in: "For loops require an 'in' clause" + for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" + if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" + include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" + unknown_tag: "Unknown tag '%{tag}'" + invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" + unexpected_else: "%{block_name} tag does not expect else tag" + tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" + tag_never_closed: "'%{block_name}' tag was never closed" + meta_syntax_error: "Liquid syntax error: #{e.message}" + table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index 34fdf196..45c6bed6 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -16,7 +16,7 @@ module Liquid @to = $1 @from = Variable.new($2) else - raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]") + raise SyntaxError.new options[:locale].t("errors.syntax.assign") end super diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 4f5b34c9..92b99508 100644 --- a/lib/liquid/tags/capture.rb +++ b/lib/liquid/tags/capture.rb @@ -18,7 +18,7 @@ module Liquid if markup =~ Syntax @to = $1 else - raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]") + raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) end super diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb index a9b2316b..ce2ad886 100644 --- a/lib/liquid/tags/case.rb +++ b/lib/liquid/tags/case.rb @@ -9,7 +9,7 @@ module Liquid if markup =~ Syntax @left = $1 else - raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]") + raise SyntaxError.new(options[:locale].t("errors.syntax.case")) end super @@ -50,7 +50,7 @@ module Liquid while markup # Create a new nodelist and assign it to the new block if not markup =~ WhenSyntax - raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ") + raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when")) end markup = $2 @@ -63,7 +63,7 @@ module Liquid def record_else_condition(markup) if not markup.strip.empty? - raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ") + raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else")) end block = ElseCondition.new diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb index e970dfbe..de811422 100644 --- a/lib/liquid/tags/cycle.rb +++ b/lib/liquid/tags/cycle.rb @@ -24,7 +24,7 @@ module Liquid @variables = variables_from_string(markup) @name = "'#{@variables.to_s}'" else - raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]") + raise SyntaxError.new(options[:locale].t("errors.syntax.cycle")) end super end diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 69fc9d33..5902704c 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -128,14 +128,14 @@ module Liquid @attributes[key] = value end else - raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]") + raise SyntaxError.new(options[:locale].t("errors.syntax.for")) end end def strict_parse(markup) p = Parser.new(markup) @variable_name = p.consume(:id) - raise SyntaxError, "For loops require an 'in' clause" unless p.id?('in') + raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in")) unless p.id?('in') @collection_name = p.expression @name = "#{@variable_name}-#{@collection_name}" @reversed = p.id?('reversed') @@ -143,7 +143,7 @@ module Liquid @attributes = {} while p.look(:id) && p.look(:colon, 1) unless attribute = p.id?('limit') || p.id?('offset') - raise SyntaxError, "Invalid attribute in for loop. Valid attributes are limit and offset" + raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute")) end p.consume val = p.expression diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index ac1767d3..c376f6c3 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -10,7 +10,6 @@ module Liquid # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # class If < Block - SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]" Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o @@ -54,14 +53,14 @@ module Liquid def lax_parse(markup) expressions = markup.scan(ExpressionsAndOperators).reverse - raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax + raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift =~ Syntax condition = Condition.new($1, $2, $3) while not expressions.empty? operator = (expressions.shift).to_s.strip - raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax + raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift.to_s =~ Syntax new_condition = Condition.new($1, $2, $3) new_condition.send(operator.to_sym, condition) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index 42eff176..76448f82 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -29,7 +29,7 @@ module Liquid end else - raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]") + raise SyntaxError.new(options[:locale].t("errors.syntax.include")) end super diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index 87f1491c..1f2bfd14 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -14,6 +14,10 @@ module Liquid # template.render('user_name' => 'bob') # class Template + DEFAULT_OPTIONS = { + :locale => I18n.new + } + attr_accessor :root, :resource_limits @@file_system = BlankFileSystem.new @@ -68,7 +72,7 @@ module Liquid # Parse source code. # Returns self for easy chaining def parse(source, options = {}) - @root = Document.new(tokenize(source), options) + @root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options)) @warnings = nil self end @@ -119,7 +123,7 @@ module Liquid when nil Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) else - raise ArgumentError, "Expect Hash or Liquid::Context as parameter" + raise ArgumentError, "Expected Hash or Liquid::Context as parameter" end case args.last diff --git a/test/fixtures/en_locale.yml b/test/fixtures/en_locale.yml new file mode 100644 index 00000000..0b113c67 --- /dev/null +++ b/test/fixtures/en_locale.yml @@ -0,0 +1,9 @@ +--- + simple: "less is more" + whatever: "something %{something}" + errors: + i18n: + undefined_interpolation: "undefined key %{key}" + unknown_translation: "translation '%{name}' wasn't found" + syntax: + oops: "something wasn't right" diff --git a/test/liquid/assign_test.rb b/test/liquid/assign_test.rb index d1eb6151..f4edb9aa 100644 --- a/test/liquid/assign_test.rb +++ b/test/liquid/assign_test.rb @@ -18,4 +18,10 @@ class AssignTest < Test::Unit::TestCase '{% assign foo = values | split: "," %}.{{ foo[1] }}.', 'values' => "foo,bar,baz") end + + def test_assign_syntax_error + assert_match_syntax_error(/assign/, + '{% assign foo not values %}.', + 'values' => "foo,bar,baz") + end end # AssignTest diff --git a/test/liquid/i18n_test.rb b/test/liquid/i18n_test.rb new file mode 100644 index 00000000..1042ac00 --- /dev/null +++ b/test/liquid/i18n_test.rb @@ -0,0 +1,37 @@ +require 'test_helper' + +class I18nTest < Test::Unit::TestCase + include Liquid + + def setup + @i18n = I18n.new(fixture("en_locale.yml")) + end + + def test_simple_translate_string + assert_equal "less is more", @i18n.translate("simple") + end + + def test_nested_translate_string + assert_equal "something wasn't right", @i18n.translate("errors.syntax.oops") + end + + def test_single_string_interpolation + assert_equal "something different", @i18n.translate("whatever", :something => "different") + end + + def test_raises_translation_error_on_undefined_interpolation_key + assert_raise I18n::TranslationError do + @i18n.translate("whatever", :oopstypos => "yes") + end + end + + def test_raises_unknown_translation + assert_raise I18n::TranslationError do + @i18n.translate("doesnt_exist") + end + end + + def test_sets_default_path_to_en + assert_equal I18n::DEFAULT_LOCALE, I18n.new.path + end +end diff --git a/test/liquid/template_test.rb b/test/liquid/template_test.rb index 04f1e508..5fd4d1e7 100644 --- a/test/liquid/template_test.rb +++ b/test/liquid/template_test.rb @@ -143,4 +143,18 @@ class TemplateTest < Test::Unit::TestCase assert_equal 'bar', t.parse('{{bar}}').render(drop) assert_equal 'haha', t.parse("{{baz}}").render(drop) end -end # TemplateTest + + def test_sets_default_localization_in_document + t = Template.new + t.parse('') + assert_instance_of I18n, t.root.options[:locale] + end + + def test_sets_default_localization_in_context_with_quick_initialization + t = Template.new + t.parse('{{foo}}', :locale => I18n.new(fixture("en_locale.yml"))) + + assert_instance_of I18n, t.root.options[:locale] + assert_equal fixture("en_locale.yml"), t.root.options[:locale].path + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index aaabf15e..e1f12f0a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,7 +7,9 @@ begin rescue LoadError puts "Couldn't load ruby-debug. gem install ruby-debug if you need it." end -require File.join(File.dirname(__FILE__), '..', 'lib', 'liquid') + +$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')) +require 'liquid.rb' mode = :strict if env_mode = ENV['LIQUID_PARSER_MODE'] @@ -19,6 +21,12 @@ Liquid::Template.error_mode = mode module Test module Unit + class TestCase + def fixture(name) + File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name) + end + end + module Assertions include Liquid @@ -32,6 +40,13 @@ module Test assert_match expected, Template.parse(template).render(assigns) end + def assert_match_syntax_error(match, template, registers = {}) + exception = assert_raise(Liquid::SyntaxError) { + Template.parse(template).render(assigns) + } + assert_match match, exception.message + end + def with_error_mode(mode) old_mode = Liquid::Template.error_mode Liquid::Template.error_mode = mode