i18nliner 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|