mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-03 00:55:11 -07:00
Merge pull request #241 from Shopify/i18n-error
Add I18n syntax error translation
This commit is contained in:
@@ -48,6 +48,7 @@ end
|
|||||||
require "liquid/version"
|
require "liquid/version"
|
||||||
require 'liquid/lexer'
|
require 'liquid/lexer'
|
||||||
require 'liquid/parser'
|
require 'liquid/parser'
|
||||||
|
require 'liquid/i18n'
|
||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
|
|||||||
+9
-6
@@ -41,7 +41,7 @@ module Liquid
|
|||||||
unknown_tag($1, $2, tokens)
|
unknown_tag($1, $2, tokens)
|
||||||
end
|
end
|
||||||
else
|
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
|
end
|
||||||
when IsVariable
|
when IsVariable
|
||||||
new_var = create_variable(token)
|
new_var = create_variable(token)
|
||||||
@@ -80,11 +80,14 @@ module Liquid
|
|||||||
def unknown_tag(tag, params, tokens)
|
def unknown_tag(tag, params, tokens)
|
||||||
case tag
|
case tag
|
||||||
when 'else'
|
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'
|
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
|
else
|
||||||
raise SyntaxError, "Unknown tag '#{tag}'"
|
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ module Liquid
|
|||||||
token.scan(ContentOfVariable) do |content|
|
token.scan(ContentOfVariable) do |content|
|
||||||
return Variable.new(content.first, @options)
|
return Variable.new(content.first, @options)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def render(context)
|
def render(context)
|
||||||
@@ -110,7 +113,7 @@ module Liquid
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def assert_missing_delimitation!
|
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
|
end
|
||||||
|
|
||||||
def render_all(list, context)
|
def render_all(list, context)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module Liquid
|
|||||||
@attributes[key] = value
|
@attributes[key] = value
|
||||||
end
|
end
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -16,7 +16,7 @@ module Liquid
|
|||||||
@to = $1
|
@to = $1
|
||||||
@from = Variable.new($2)
|
@from = Variable.new($2)
|
||||||
else
|
else
|
||||||
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
|
raise SyntaxError.new options[:locale].t("errors.syntax.assign")
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module Liquid
|
|||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = $1
|
@to = $1
|
||||||
else
|
else
|
||||||
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
|
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Liquid
|
|||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@left = $1
|
@left = $1
|
||||||
else
|
else
|
||||||
raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
|
raise SyntaxError.new(options[:locale].t("errors.syntax.case"))
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
@@ -50,7 +50,7 @@ module Liquid
|
|||||||
while markup
|
while markup
|
||||||
# Create a new nodelist and assign it to the new block
|
# Create a new nodelist and assign it to the new block
|
||||||
if not markup =~ WhenSyntax
|
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
|
end
|
||||||
|
|
||||||
markup = $2
|
markup = $2
|
||||||
@@ -63,7 +63,7 @@ module Liquid
|
|||||||
|
|
||||||
def record_else_condition(markup)
|
def record_else_condition(markup)
|
||||||
if not markup.strip.empty?
|
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
|
end
|
||||||
|
|
||||||
block = ElseCondition.new
|
block = ElseCondition.new
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ module Liquid
|
|||||||
@variables = variables_from_string(markup)
|
@variables = variables_from_string(markup)
|
||||||
@name = "'#{@variables.to_s}'"
|
@name = "'#{@variables.to_s}'"
|
||||||
else
|
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
|
end
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -128,14 +128,14 @@ module Liquid
|
|||||||
@attributes[key] = value
|
@attributes[key] = value
|
||||||
end
|
end
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
p = Parser.new(markup)
|
p = Parser.new(markup)
|
||||||
@variable_name = p.consume(:id)
|
@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
|
@collection_name = p.expression
|
||||||
@name = "#{@variable_name}-#{@collection_name}"
|
@name = "#{@variable_name}-#{@collection_name}"
|
||||||
@reversed = p.id?('reversed')
|
@reversed = p.id?('reversed')
|
||||||
@@ -143,7 +143,7 @@ module Liquid
|
|||||||
@attributes = {}
|
@attributes = {}
|
||||||
while p.look(:id) && p.look(:colon, 1)
|
while p.look(:id) && p.look(:colon, 1)
|
||||||
unless attribute = p.id?('limit') || p.id?('offset')
|
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
|
end
|
||||||
p.consume
|
p.consume
|
||||||
val = p.expression
|
val = p.expression
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module Liquid
|
|||||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||||
#
|
#
|
||||||
class If < Block
|
class If < Block
|
||||||
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
|
||||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
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
|
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)
|
def lax_parse(markup)
|
||||||
expressions = markup.scan(ExpressionsAndOperators).reverse
|
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)
|
condition = Condition.new($1, $2, $3)
|
||||||
|
|
||||||
while not expressions.empty?
|
while not expressions.empty?
|
||||||
operator = (expressions.shift).to_s.strip
|
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 = Condition.new($1, $2, $3)
|
||||||
new_condition.send(operator.to_sym, condition)
|
new_condition.send(operator.to_sym, condition)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ module Liquid
|
|||||||
# template.render('user_name' => 'bob')
|
# template.render('user_name' => 'bob')
|
||||||
#
|
#
|
||||||
class Template
|
class Template
|
||||||
|
DEFAULT_OPTIONS = {
|
||||||
|
:locale => I18n.new
|
||||||
|
}
|
||||||
|
|
||||||
attr_accessor :root, :resource_limits
|
attr_accessor :root, :resource_limits
|
||||||
@@file_system = BlankFileSystem.new
|
@@file_system = BlankFileSystem.new
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ module Liquid
|
|||||||
# Parse source code.
|
# Parse source code.
|
||||||
# Returns self for easy chaining
|
# Returns self for easy chaining
|
||||||
def parse(source, options = {})
|
def parse(source, options = {})
|
||||||
@root = Document.new(tokenize(source), options)
|
@root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
||||||
@warnings = nil
|
@warnings = nil
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
@@ -119,7 +123,7 @@ module Liquid
|
|||||||
when nil
|
when nil
|
||||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||||
else
|
else
|
||||||
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
|
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||||
end
|
end
|
||||||
|
|
||||||
case args.last
|
case args.last
|
||||||
|
|||||||
Vendored
+9
@@ -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"
|
||||||
@@ -18,4 +18,10 @@ class AssignTest < Test::Unit::TestCase
|
|||||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||||
'values' => "foo,bar,baz")
|
'values' => "foo,bar,baz")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_assign_syntax_error
|
||||||
|
assert_match_syntax_error(/assign/,
|
||||||
|
'{% assign foo not values %}.',
|
||||||
|
'values' => "foo,bar,baz")
|
||||||
|
end
|
||||||
end # AssignTest
|
end # AssignTest
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -143,4 +143,18 @@ class TemplateTest < Test::Unit::TestCase
|
|||||||
assert_equal 'bar', t.parse('{{bar}}').render(drop)
|
assert_equal 'bar', t.parse('{{bar}}').render(drop)
|
||||||
assert_equal 'haha', t.parse("{{baz}}").render(drop)
|
assert_equal 'haha', t.parse("{{baz}}").render(drop)
|
||||||
end
|
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
|
||||||
|
|||||||
+16
-1
@@ -7,7 +7,9 @@ begin
|
|||||||
rescue LoadError
|
rescue LoadError
|
||||||
puts "Couldn't load ruby-debug. gem install ruby-debug if you need it."
|
puts "Couldn't load ruby-debug. gem install ruby-debug if you need it."
|
||||||
end
|
end
|
||||||
require File.join(File.dirname(__FILE__), '..', 'lib', 'liquid')
|
|
||||||
|
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
|
||||||
|
require 'liquid.rb'
|
||||||
|
|
||||||
mode = :strict
|
mode = :strict
|
||||||
if env_mode = ENV['LIQUID_PARSER_MODE']
|
if env_mode = ENV['LIQUID_PARSER_MODE']
|
||||||
@@ -19,6 +21,12 @@ Liquid::Template.error_mode = mode
|
|||||||
|
|
||||||
module Test
|
module Test
|
||||||
module Unit
|
module Unit
|
||||||
|
class TestCase
|
||||||
|
def fixture(name)
|
||||||
|
File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module Assertions
|
module Assertions
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
@@ -32,6 +40,13 @@ module Test
|
|||||||
assert_match expected, Template.parse(template).render(assigns)
|
assert_match expected, Template.parse(template).render(assigns)
|
||||||
end
|
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)
|
def with_error_mode(mode)
|
||||||
old_mode = Liquid::Template.error_mode
|
old_mode = Liquid::Template.error_mode
|
||||||
Liquid::Template.error_mode = mode
|
Liquid::Template.error_mode = mode
|
||||||
|
|||||||
Reference in New Issue
Block a user