i18nliner 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.txt +20 -0
- data/README.md +320 -0
- data/Rakefile +9 -0
- data/lib/i18nliner/call_helpers.rb +69 -0
- data/lib/i18nliner/commands/basic_formatter.rb +13 -0
- data/lib/i18nliner/commands/check.rb +59 -0
- data/lib/i18nliner/commands/color_formatter.rb +13 -0
- data/lib/i18nliner/commands/dump.rb +8 -0
- data/lib/i18nliner/commands/generic_command.rb +17 -0
- data/lib/i18nliner/errors.rb +29 -0
- data/lib/i18nliner/extractors/abstract_extractor.rb +33 -0
- data/lib/i18nliner/extractors/ruby_extractor.rb +102 -0
- data/lib/i18nliner/extractors/translate_call.rb +111 -0
- data/lib/i18nliner/extractors/translation_hash.rb +45 -0
- data/lib/i18nliner/processors/abstract_processor.rb +33 -0
- data/lib/i18nliner/processors/erb_processor.rb +16 -0
- data/lib/i18nliner/processors/ruby_processor.rb +22 -0
- data/lib/i18nliner/processors.rb +11 -0
- data/lib/i18nliner/scope.rb +24 -0
- data/lib/i18nliner.rb +31 -0
- data/lib/tasks/i18nliner.rake +14 -0
- data/spec/extractors/ruby_extractor_spec.rb +61 -0
- data/spec/extractors/translate_call_spec.rb +154 -0
- metadata +182 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'i18nliner/call_helpers'
|
2
|
+
require 'i18nliner/errors'
|
3
|
+
|
4
|
+
module I18nliner
|
5
|
+
module Extractors
|
6
|
+
class TranslateCall
|
7
|
+
include CallHelpers
|
8
|
+
|
9
|
+
def initialize(scope, line, receiver, method, args)
|
10
|
+
@scope = scope
|
11
|
+
@line = line
|
12
|
+
@receiver = receiver
|
13
|
+
@method = method
|
14
|
+
|
15
|
+
normalize_arguments(args)
|
16
|
+
|
17
|
+
validate
|
18
|
+
normalize
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate
|
22
|
+
validate_key
|
23
|
+
validate_default
|
24
|
+
validate_options
|
25
|
+
end
|
26
|
+
|
27
|
+
def normalize
|
28
|
+
@key = normalize_key(@key, @scope, @receiver)
|
29
|
+
@default = normalize_default(@default, @options || {})
|
30
|
+
end
|
31
|
+
|
32
|
+
def translations
|
33
|
+
return [] unless @default
|
34
|
+
return [[@key, @default]] if @default.is_a?(String)
|
35
|
+
@default.map { |key, value|
|
36
|
+
["#{@key}.#{key}", value]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_key
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_default
|
44
|
+
return unless @default.is_a?(Hash)
|
45
|
+
if (keys = @default.keys - ALLOWED_PLURALIZATION_KEYS).size > 0
|
46
|
+
raise InvalidPluralizationKeyError.new(@line, keys)
|
47
|
+
elsif REQUIRED_PLURALIZATION_KEYS & (keys = @default.keys) != REQUIRED_PLURALIZATION_KEYS
|
48
|
+
raise MissingPluralizationKeyError.new(@line, keys)
|
49
|
+
else
|
50
|
+
@default.values.each do |value|
|
51
|
+
raise InvalidPluralizationDefaultError.new(@line, value) unless value.is_a?(String)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
unless I18nliner.infer_interpolation_values
|
56
|
+
if @default.is_a?(String)
|
57
|
+
validate_interpolation_values(@key, @default)
|
58
|
+
else
|
59
|
+
@default.each_pair do |sub_key, default|
|
60
|
+
validate_interpolation_values("#{@key}.#{sub_key}", default)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Possible translate signatures:
|
69
|
+
#
|
70
|
+
# key [, options]
|
71
|
+
# key, default_string [, options]
|
72
|
+
# key, default_hash, options
|
73
|
+
# default_string [, options]
|
74
|
+
# default_hash, options
|
75
|
+
def normalize_arguments(args)
|
76
|
+
raise InvalidSignatureError.new(@line, args) if args.empty?
|
77
|
+
|
78
|
+
has_key = key_provided?(@scope, @receiver, *args)
|
79
|
+
args.unshift infer_key(args[0]) if !has_key && args[0].is_a?(String) || args[0].is_a?(Hash)
|
80
|
+
|
81
|
+
# [key, options] -> [key, nil, options]
|
82
|
+
args.insert(1, nil) if has_key && args[1].is_a?(Hash) && args[2].nil?
|
83
|
+
|
84
|
+
@key, @default, @options, *others = args
|
85
|
+
|
86
|
+
raise InvalidSignatureError.new(@line, args) if !others.empty?
|
87
|
+
raise InvalidSignatureError.new(@line, args) unless @key.is_a?(Symbol) || @key.is_a?(String)
|
88
|
+
raise InvalidSignatureError.new(@line, args) unless @default.nil? || @default.is_a?(String) || @default.is_a?(Hash)
|
89
|
+
raise InvalidSignatureError.new(@line, args) unless @options.nil? || @options.is_a?(Hash)
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_interpolation_values(key, default)
|
93
|
+
default.scan(/%\{([^\}]+)\}/) do |match|
|
94
|
+
placeholder = match[0].to_sym
|
95
|
+
next if @options.include?(placeholder)
|
96
|
+
raise MissingInterpolationValueError.new(@line, placeholder)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_options
|
101
|
+
if @default.is_a?(Hash)
|
102
|
+
raise MissingCountValueError.new(@line) unless @options && @options.key?(:count)
|
103
|
+
end
|
104
|
+
return if @options.nil?
|
105
|
+
@options.keys.each do |key|
|
106
|
+
raise InvalidOptionKeyError.new(@line) unless key.is_a?(String) || key.is_a?(Symbol)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module I18nLine
|
2
|
+
module Extractors
|
3
|
+
class TranslationHash < Hash
|
4
|
+
attr_accessor :line
|
5
|
+
|
6
|
+
def self.new(hash)
|
7
|
+
hash.is_a?(self) ? hash : super
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
super
|
12
|
+
@total_size = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(key, value)
|
16
|
+
parts = key.split('.')
|
17
|
+
leaf = parts.pop
|
18
|
+
hash = self
|
19
|
+
while part = parts.shift
|
20
|
+
if hash[part]
|
21
|
+
unless hash[part].is_a?(Hash)
|
22
|
+
intermediate_key = key.sub((parts + [leaf]).join('.'), '')
|
23
|
+
raise KeyAsScopeError, intermediate_key
|
24
|
+
end
|
25
|
+
else
|
26
|
+
hash[part] = {}
|
27
|
+
end
|
28
|
+
hash = hash[part]
|
29
|
+
end
|
30
|
+
if hash[leaf]
|
31
|
+
if hash[leaf] != default
|
32
|
+
if hash[leaf].is_a?(Hash)
|
33
|
+
raise KeyAsScopeError.new(@line, key)
|
34
|
+
else
|
35
|
+
raise KeyInUseError.new(@line, key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@total_size += 1
|
40
|
+
hash[key] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Processors
|
3
|
+
class AbstractProcessor
|
4
|
+
def initialize(translations, options = {})
|
5
|
+
@translations = translations
|
6
|
+
@only = options[:only]
|
7
|
+
@checker = options[:checker] || methods(:noop_checker)
|
8
|
+
end
|
9
|
+
|
10
|
+
def noop_checker(file)
|
11
|
+
yield file
|
12
|
+
end
|
13
|
+
|
14
|
+
def files
|
15
|
+
@files ||= begin
|
16
|
+
files = Globby.select(@pattern)
|
17
|
+
files = files.select(@only) if @only
|
18
|
+
files.reject(I18nliner.ignore)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_files
|
23
|
+
files.each do |file|
|
24
|
+
@checker.call file, &methods(:check_file)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.inherited(klass)
|
29
|
+
Processors.register klass
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Processors
|
3
|
+
class ErbProcessor < RubyProcessor
|
4
|
+
def source_for(file)
|
5
|
+
# TODO: pre-process for block fu
|
6
|
+
Erubis::Eruby.new(super).src
|
7
|
+
end
|
8
|
+
|
9
|
+
def scope_for(path)
|
10
|
+
scope = path.gsub(/(\A|.*\/)app\/views\/|\.html\z|(\.html)?\.erb\z/, '')
|
11
|
+
scope = scope.gsub(/\/_?/, '.')
|
12
|
+
Scope.new(scope, :allow_relative => true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Processors
|
3
|
+
class RubyProcessor < AbstractProcessor
|
4
|
+
def check_file(file)
|
5
|
+
sexps = RubyParser.new.parse(source_for(file))
|
6
|
+
extractor = Extractors::RubyExtractor.new(sexps, scope_for(file))
|
7
|
+
extractor.each_translation do |key, value|
|
8
|
+
@translations.line = extractor.line
|
9
|
+
@translations[key] = value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def source_for(file)
|
14
|
+
File.read(file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def scope_for(path)
|
18
|
+
Scope.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module I18nliner
|
2
|
+
class Scope
|
3
|
+
attr_reader :scope
|
4
|
+
|
5
|
+
def initialize(scope = nil, options = {})
|
6
|
+
@scope = scope ? "#{scope}." : scope
|
7
|
+
@options = {
|
8
|
+
:allow_relative => false
|
9
|
+
}.merge(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def allow_relative?
|
13
|
+
@options[:allow_relative]
|
14
|
+
end
|
15
|
+
|
16
|
+
def normalize_key(key)
|
17
|
+
if allow_relative? && (key = key.dup) && key.sub!(/\A\./, '')
|
18
|
+
scope + key
|
19
|
+
else
|
20
|
+
key
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/i18nliner.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
|
3
|
+
module I18nliner
|
4
|
+
def self.translations
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.look_up(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.setting(key, value)
|
11
|
+
instance_eval <<-CODE
|
12
|
+
def #{key}(value = nil)
|
13
|
+
if value && block_given?
|
14
|
+
begin
|
15
|
+
value_was = @#{key}
|
16
|
+
@#{key} = value
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@#{key} = value_was
|
20
|
+
end
|
21
|
+
else
|
22
|
+
@#{key} = #{value.inspect} if @#{key}.nil?
|
23
|
+
@#{key}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
CODE
|
27
|
+
end
|
28
|
+
|
29
|
+
setting :inferred_key_format, :underscored_crc32
|
30
|
+
setting :infer_interpolation_values, true
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
namespace :i18nliner do
|
2
|
+
desc "Verifies all translation calls"
|
3
|
+
task :check => :environment do
|
4
|
+
options = {:only => ENV['ONLY'])}
|
5
|
+
@command = I18nliner::Commands::Check.run(options) or exit 1
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Generates a new [default_locale].yml file for all translations"
|
9
|
+
task :dump => :check do
|
10
|
+
options = {:translations => @command.translations}
|
11
|
+
@command = I18nliner::Commands::Dump.run(options) or exit 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'sexp_processor'
|
2
|
+
require 'ruby_parser'
|
3
|
+
require 'i18nliner'
|
4
|
+
require 'i18nliner/errors'
|
5
|
+
require 'i18nliner/scope'
|
6
|
+
require 'i18nliner/extractors/ruby_extractor'
|
7
|
+
require 'i18nliner/extractors/translate_call'
|
8
|
+
|
9
|
+
describe I18nliner::Extractors::RubyExtractor do
|
10
|
+
def extract(source, scope = I18nliner::Scope.new(nil))
|
11
|
+
sexps = RubyParser.new.parse(source)
|
12
|
+
extractor = I18nliner::Extractors::RubyExtractor.new(sexps, scope)
|
13
|
+
translations = []
|
14
|
+
extractor.each_translation { |translation| translations << translation }
|
15
|
+
Hash[translations]
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_error(*args)
|
19
|
+
error = args.pop
|
20
|
+
expect {
|
21
|
+
extract(*args)
|
22
|
+
}.to raise_error(error)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#each_translation" do
|
26
|
+
it "should ignore non-t calls" do
|
27
|
+
extract("foo 'Foo'").should == {}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should not extract t calls with no default" do
|
31
|
+
extract("t :foo").should == {}
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should extract valid t calls" do
|
35
|
+
if false
|
36
|
+
extract("t 'Foo'").should ==
|
37
|
+
{"foo_f44ad75d" => "Foo"}
|
38
|
+
extract("t :bar, 'Baz'").should ==
|
39
|
+
{"bar" => "Baz"}
|
40
|
+
extract("t 'lol', 'wut'").should ==
|
41
|
+
{"lol" => "wut"}
|
42
|
+
extract("translate 'one', {:one => '1', :other => '2'}, :count => 1").should ==
|
43
|
+
{"one.one" => "1", "one.other" => "2"}
|
44
|
+
extract("t({:one => 'just one', :other => 'zomg lots'}, :count => 1)").should ==
|
45
|
+
{"zomg_lots_a54248c9.one" => "just one", "zomg_lots_a54248c9.other" => "zomg lots"}
|
46
|
+
extract("t 'foo2', <<-STR\nFoo\nSTR").should ==
|
47
|
+
{'foo2' => "Foo"}
|
48
|
+
end
|
49
|
+
extract("t 'foo', 'F' + 'o' + 'o'").should ==
|
50
|
+
{'foo' => "Foo"}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should bail on invalid t calls" do
|
54
|
+
assert_error "t foo", I18nliner::InvalidSignatureError
|
55
|
+
assert_error "t :foo, foo", I18nliner::InvalidSignatureError
|
56
|
+
assert_error "t :foo, \"hello \#{man}\"", I18nliner::InvalidSignatureError
|
57
|
+
assert_error "t :a, \"a\", {}, {}", I18nliner::InvalidSignatureError
|
58
|
+
assert_error "t({:one => '1', :other => '2'})", I18nliner::MissingCountValueError
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'i18nliner'
|
2
|
+
require 'i18nliner/scope'
|
3
|
+
require 'i18nliner/extractors/translate_call'
|
4
|
+
|
5
|
+
describe I18nliner::Extractors::TranslateCall do
|
6
|
+
def call(scope, *args)
|
7
|
+
I18nliner::Extractors::TranslateCall.new(scope, nil, nil, :t, args)
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:no_scope) { I18nliner::Scope.new(nil) }
|
11
|
+
let(:scope) { I18nliner::Scope.new("foo", :auto => true, :allow_relative => true) }
|
12
|
+
|
13
|
+
describe "signature" do
|
14
|
+
it "should reject extra arguments" do
|
15
|
+
expect {
|
16
|
+
call(no_scope, :key, "value", {}, :wat)
|
17
|
+
}.to raise_error(I18nliner::InvalidSignatureError)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should accept a valid key or default" do
|
21
|
+
expect {
|
22
|
+
call(no_scope, "key", "value", {})
|
23
|
+
}.to_not raise_error
|
24
|
+
|
25
|
+
expect {
|
26
|
+
call(no_scope, "key_or_value", {})
|
27
|
+
}.to_not raise_error
|
28
|
+
|
29
|
+
expect {
|
30
|
+
call(no_scope, :key, {})
|
31
|
+
}.to_not raise_error
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should require at least a key or default" do
|
35
|
+
expect {
|
36
|
+
call(no_scope)
|
37
|
+
}.to raise_error(I18nliner::InvalidSignatureError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should require a literal default" do
|
41
|
+
expect {
|
42
|
+
call(no_scope, :key, Object.new)
|
43
|
+
}.to raise_error(I18nliner::InvalidSignatureError)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should ensure options is a hash, if provided" do
|
47
|
+
expect {
|
48
|
+
call(no_scope, :key, "value", Object.new)
|
49
|
+
}.to raise_error(I18nliner::InvalidSignatureError)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "key inference" do
|
54
|
+
it "should generate literal keys" do
|
55
|
+
I18nliner.inferred_key_format :literal do
|
56
|
+
call(no_scope, "zomg key").translations.should ==
|
57
|
+
[["zomg key", "zomg key"]]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should generate underscored keys" do
|
62
|
+
I18nliner.inferred_key_format :underscored do
|
63
|
+
call(no_scope, "zOmg key!!").translations.should ==
|
64
|
+
[["zomg_key", "zOmg key!!"]]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should generate underscored + crc32 keys" do
|
69
|
+
I18nliner.inferred_key_format :underscored_crc32 do
|
70
|
+
call(no_scope, "zOmg key!!").translations.should ==
|
71
|
+
[["zomg_key_90a85b0b", "zOmg key!!"]]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "normalization" do
|
77
|
+
it "should make keys absolute if scoped" do
|
78
|
+
call(scope, '.key', "value").translations[0][0].should =~ /\Afoo\.key/
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should strip whitespace from defaults" do
|
82
|
+
call(no_scope, "\t whitespace \n\t ").translations[0][1].should == "whitespace"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "pluralization" do
|
87
|
+
describe "defaults" do
|
88
|
+
it "should be inferred" do
|
89
|
+
translations = call(no_scope, "person", {:count => Object.new}).translations
|
90
|
+
translations.map(&:last).sort.should == ["%{count} people", "1 person"]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should not be inferred if given multiple words" do
|
94
|
+
translations = call(no_scope, "happy person", {:count => Object.new}).translations
|
95
|
+
translations.map(&:last).should == ["happy person"]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should accept valid hashes" do
|
100
|
+
call(no_scope, {:one => "asdf", :other => "qwerty"}, :count => 1).translations.sort.should ==
|
101
|
+
[["qwerty_98185351.one", "asdf"], ["qwerty_98185351.other", "qwerty"]]
|
102
|
+
call(no_scope, :some_stuff, {:one => "asdf", :other => "qwerty"}, :count => 1).translations.sort.should ==
|
103
|
+
[["some_stuff.one", "asdf"], ["some_stuff.other", "qwerty"]]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should reject invalid keys" do
|
107
|
+
expect {
|
108
|
+
call(no_scope, {:one => "asdf", :twenty => "qwerty"}, :count => 1)
|
109
|
+
}.to raise_error(I18nliner::InvalidPluralizationKeyError)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should require essential keys" do
|
113
|
+
expect {
|
114
|
+
call(no_scope, {:one => "asdf"}, :count => 1)
|
115
|
+
}.to raise_error(I18nliner::MissingPluralizationKeyError)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should reject invalid count defaults" do
|
119
|
+
expect {
|
120
|
+
call(no_scope, {:one => "asdf", :other => Object.new}, :count => 1)
|
121
|
+
}.to raise_error(I18nliner::InvalidPluralizationDefaultError)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should complain if no :count is provided" do
|
125
|
+
expect {
|
126
|
+
call(no_scope, {:one => "asdf", :other => "qwerty"})
|
127
|
+
}.to raise_error(I18nliner::MissingCountValueError)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "validation" do
|
132
|
+
it "should require all interpolation values" do
|
133
|
+
I18nliner.infer_interpolation_values false do
|
134
|
+
expect {
|
135
|
+
call(no_scope, "asdf %{bob}")
|
136
|
+
}.to raise_error(I18nliner::MissingInterpolationValueError)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should require all interpolation values in count defaults" do
|
141
|
+
I18nliner.infer_interpolation_values false do
|
142
|
+
expect {
|
143
|
+
call(no_scope, {:one => "asdf %{bob}", :other => "querty"})
|
144
|
+
}.to raise_error(I18nliner::MissingInterpolationValueError)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should ensure option keys are symbols or strings" do
|
149
|
+
expect {
|
150
|
+
call(no_scope, "hello", {Object.new => "okay"})
|
151
|
+
}.to raise_error(I18nliner::InvalidOptionKeyError)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|