rails-i18nterface 0.1.7 → 0.2.0

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.
Files changed (39) hide show
  1. data/README.md +17 -10
  2. data/app/controllers/rails_i18nterface/application_controller.rb +2 -0
  3. data/app/controllers/rails_i18nterface/translate_controller.rb +29 -80
  4. data/app/helpers/rails_i18nterface/application_helper.rb +2 -0
  5. data/app/helpers/rails_i18nterface/translate_helper.rb +2 -0
  6. data/app/views/rails_i18nterface/translate/index.html.erb +14 -23
  7. data/config/routes.rb +2 -0
  8. data/lib/rails-i18nterface.rb +3 -1
  9. data/lib/rails-i18nterface/cache.rb +40 -0
  10. data/lib/rails-i18nterface/engine.rb +2 -0
  11. data/lib/rails-i18nterface/keys.rb +24 -112
  12. data/lib/rails-i18nterface/sourcefiles.rb +14 -12
  13. data/lib/rails-i18nterface/storage.rb +2 -4
  14. data/lib/rails-i18nterface/utils.rb +51 -0
  15. data/lib/rails-i18nterface/version.rb +3 -1
  16. data/lib/rails-i18nterface/yamlfile.rb +15 -17
  17. data/lib/tasks/rails-i18nterface.rake +1 -0
  18. data/spec/controllers/translate_controller_spec.rb +18 -33
  19. data/spec/internal/app/controllers/application_controller.rb +2 -0
  20. data/spec/internal/app/models/article.rb +8 -2
  21. data/spec/internal/config/routes.rb +2 -0
  22. data/spec/internal/db/combustion_test.sqlite +0 -0
  23. data/spec/internal/log/test.log +241 -1608
  24. data/spec/lib/cache_spec.rb +51 -0
  25. data/spec/lib/keys_spec.rb +59 -88
  26. data/spec/lib/sourcefiles_spec.rb +8 -6
  27. data/spec/lib/storage_spec.rb +2 -0
  28. data/spec/lib/utils_spec.rb +34 -0
  29. data/spec/lib/yamlfile_spec.rb +10 -5
  30. data/spec/requests/search_spec.rb +2 -0
  31. data/spec/spec_helper.rb +3 -3
  32. metadata +104 -67
  33. checksums.yaml +0 -7
  34. data/app/models/rails_i18nterface/translation.rb +0 -23
  35. data/db/migrate/20110921112044_create_translations.rb +0 -18
  36. data/db/migrate/20130422115639_rename_translation_to_namespace.rb +0 -8
  37. data/lib/rails-i18nterface/log.rb +0 -40
  38. data/spec/lib/log_spec.rb +0 -47
  39. data/spec/models/translation_spec.rb +0 -9
data/README.md CHANGED
@@ -40,11 +40,7 @@ In routes.rb
40
40
  ```ruby
41
41
  mount RailsI18nterface::Engine => "/translate", :as => "translate_engine"
42
42
  ```
43
- Copy migration to store translated values and migrate
44
- ```
45
- rake railties:install:migrations
46
- rake db:migrate
47
- ```
43
+
48
44
  ### Protect access
49
45
 
50
46
  You may want to protect the translation engine to admin and create a constraint
@@ -55,6 +51,11 @@ constraints AdminConstraint.new do
55
51
  mount RailsI18nterface::Engine => "/translate", :as => "translate_engine"
56
52
  end
57
53
  end
54
+ # this second route will be then used if the user is not an admin
55
+ get 'translate' => redirect do |p, req|
56
+ req.flash['error'] = I18n.t('errors.permission_denied')
57
+ "/signin"
58
+ end
58
59
  ```
59
60
 
60
61
  Then create a `config/initializers/admin_constraint.rb` containing:
@@ -81,16 +82,22 @@ Where `[:en]` and `[:ja, :es, :fr]` could be replaced by locale list of your cho
81
82
  ## Todo
82
83
 
83
84
  * fix the code smell reported by code climate
84
- * extract code from the controller to a lib
85
- * refactor the libs in a cleaner way
86
- * apply rubocop and follow his law
85
+ * extract code from the controller to a lib (in progress)
86
+ * refactor the libs in a cleaner way (in progress)
87
+ * apply rubocop and follow his law (done)
87
88
  * make the application thread-safe
88
- * remove those damn global variables
89
- * extend testing to refactored libs
89
+ * remove those damn global variables (in progress)
90
+ * extend testing to refactored libs (in progress)
90
91
  * change navigation to an ajax-driven reload
91
92
  * add a way to gather .one and .other and .few under same translation line
92
93
  * add support for other i18n backends (gettext)
93
94
 
95
+ ## Note for upgrade 0.1.x to 0.2.x
96
+
97
+ The database is not used anymore, back to the good old way.
98
+ So you can remove the table rails_i18nterface_translations (v0.1.7)
99
+ or translations (< 0.1.7).
100
+
94
101
  ## License
95
102
 
96
103
  ```
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module RailsI18nterface
2
4
  class ApplicationController < ActionController::Base
3
5
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module RailsI18nterface
2
4
  class TranslateController < RailsI18nterface::ApplicationController
3
5
 
@@ -8,108 +10,68 @@ module RailsI18nterface
8
10
 
9
11
  def index
10
12
  @dbvalues = {}
11
- @keys = initialize_keys
12
- load_db_translations
13
- @deep_keys = RailsI18nterface::Keys.to_deep_hash(@keys)
13
+ @keys = RailsI18nterface::Keys.new(Rails.root, @from_locale, @to_locale)
14
+ @files = @keys.files
15
+ @yaml_keys = @keys.yaml_keys
16
+ @all_keys = @keys.all_keys
17
+ @deep_keys = to_deep_hash(@all_keys)
14
18
  filter_by_key_pattern
15
19
  filter_by_text_pattern
16
20
  filter_by_translated_or_changed
17
21
  sort_keys
18
22
  paginate_keys
19
- @total_entries = @keys.size
23
+ @total_entries = @all_keys.size
24
+ @page_title = 'Translate'
25
+ @show_filters = ['all', 'untranslated', 'translated']
20
26
  end
21
27
 
22
28
  def destroy
23
- puts params
24
- term = RailsI18nterface::Translation.find_by_key(params[:del])
25
- if term and term.destroy
26
- flash[:success] = "Translations removed from database"
27
- else
28
- flash[:notice] = "Translations not found in database"
29
- end
30
29
  params[:key] = { params[:del] => '' }
31
30
  update
32
31
  end
33
32
 
34
- def load_db_translations
35
- @versions = {}
36
- @dbvalues = {@to_locale => {}}
37
- (RailsI18nterface::Translation.where(:locale => @to_locale) || []).each do |translation|
38
- @versions[translation.key] = translation.updated_at.to_i
39
- yaml_value = I18n.backend.send(:lookup, @to_locale, translation.key)
40
- if yaml_value && translation.value != yaml_value
41
- translation.value = yaml_value
42
- RailsI18nterface::Translation.where(key: translation.key).first.update_attribute(:value, yaml_value)
43
- end
44
- @dbvalues[@to_locale][translation.key] = translation.value
45
- @keys << translation.key
46
- end
47
- @keys.uniq!
48
- end
49
-
50
33
  def export
51
34
  locale = params[:locale].to_sym
52
35
  keys = {locale => I18n.backend.send(:translations)[locale] || {}}
53
- RailsI18nterface::Translation.where(:locale => @to_locale).each do |translation|
54
- next if !translation.value or translation.value == ''
55
- set_nested(keys[locale], translation.key.split('.'), translation.value)
56
- end
57
36
  remove_blanks keys
58
- puts keys
59
- yaml = RailsI18nterface::Yamlfile.new(nil).keys_to_yaml(keys)
37
+ yaml = keys_to_yaml(keys)
60
38
  response.headers['Content-Disposition'] = "attachment; filename=#{locale}.yml"
61
39
  render :text => yaml
62
40
  end
63
41
 
64
42
  def update
65
- RailsI18nterface::Translation.update(@to_locale, params[:key])
66
43
  if I18n.backend.respond_to? :store_translations
67
- I18n.backend.store_translations(@to_locale, RailsI18nterface::Keys.to_deep_hash(params[:key]))
44
+ I18n.backend.store_translations(@to_locale, to_deep_hash(params[:key]))
68
45
  end
69
- RailsI18nterface::Storage.new(@to_locale).write_to_file
70
- RailsI18nterface::Log.new(@from_locale, @to_locale, params[:key].keys).write_to_file
71
- force_init_translations # Force reload from YAML file
72
- flash[:notice] = "Translations stored"
46
+ yaml = RailsI18nterface::Yamlfile.new(Rails.root, @to_locale)
47
+ yaml.write_to_file
48
+ force_init_translations
49
+ flash[:notice] = 'Translations stored'
73
50
  redirect_to root_path(params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern))
74
51
  end
75
52
 
76
53
  def reload
77
- RailsI18nterface::Keys.files = nil
54
+ @keys.reload
78
55
  redirect_to root_path(params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern))
79
56
  end
80
57
 
81
58
  private
82
59
 
83
- def initialize_keys
84
- @files = RailsI18nterface::Keys.files
85
- keys = (@files.keys.map(&:to_s) + RailsI18nterface::Keys.new.i18n_keys(@from_locale)).uniq
86
- keys.reject! do |key|
87
- from_text = lookup(@from_locale, key)
88
- # When translating from one language to another,
89
- # make sure there is a text to translate from.
90
- # Always exclude non string translation objects
91
- # as we don't support editing them in the UI.
92
- (@from_locale != @to_locale && !from_text.present?) ||
93
- (from_text.present? && !from_text.is_a?(String))
94
- end
95
- end
96
60
 
97
61
  def lookup(locale, key)
98
- (@dbvalues[locale] && @dbvalues[locale][key]) || I18n.backend.send(:lookup, locale, key)
62
+ I18n.backend.send(:lookup, locale, key)
99
63
  end
100
64
  helper_method :lookup
101
65
 
102
66
  def filter_by_translated_or_changed
103
67
  params[:filter] ||= 'all'
104
68
  return if params[:filter] == 'all'
105
- @keys.reject! do |key|
69
+ @all_keys.reject! do |key|
106
70
  case params[:filter]
107
71
  when 'untranslated'
108
72
  lookup(@to_locale, key).present?
109
73
  when 'translated'
110
74
  lookup(@to_locale, key).blank?
111
- when 'changed'
112
- old_from_text(key).blank? || lookup(@from_locale, key) == old_from_text(key)
113
75
  else
114
76
  raise "Unknown filter '#{params[:filter]}'"
115
77
  end
@@ -118,15 +80,15 @@ module RailsI18nterface
118
80
 
119
81
  def filter_by_key_pattern
120
82
  return if params[:key_pattern].blank?
121
- @keys.reject! do |key|
83
+ @all_keys.reject! do |key|
122
84
  case params[:key_type]
123
- when "starts_with"
85
+ when 'starts_with'
124
86
  if params[:key_pattern] == '.'
125
87
  key.match(/\./)
126
88
  else
127
89
  !key.starts_with?(params[:key_pattern])
128
90
  end
129
- when "contains"
91
+ when 'contains'
130
92
  key.index(params[:key_pattern]).nil?
131
93
  else
132
94
  raise "Unknown key_type '#{params[:key_type]}'"
@@ -136,7 +98,7 @@ module RailsI18nterface
136
98
 
137
99
  def filter_by_text_pattern
138
100
  return if params[:text_pattern].blank?
139
- @keys.reject! do |key|
101
+ @all_keys.reject! do |key|
140
102
  lookup_key = lookup(@from_locale, key)
141
103
  case params[:text_type]
142
104
  when 'contains'
@@ -150,12 +112,12 @@ module RailsI18nterface
150
112
  end
151
113
 
152
114
  def sort_keys
153
- params[:sort_by] ||= "key"
115
+ params[:sort_by] ||= 'key'
154
116
  case params[:sort_by]
155
- when "key"
156
- @keys.sort!
157
- when "text"
158
- @keys.sort! do |key1, key2|
117
+ when 'key'
118
+ @all_keys.sort!
119
+ when 'text'
120
+ @all_keys.sort! do |key1, key2|
159
121
  if lookup(@from_locale, key1).present? && lookup(@from_locale, key2).present?
160
122
  lookup(@from_locale, key1).to_s.downcase <=> lookup(@from_locale, key2).to_s.downcase
161
123
  elsif lookup(@from_locale, key1).present?
@@ -171,7 +133,7 @@ module RailsI18nterface
171
133
 
172
134
  def paginate_keys
173
135
  params[:page] ||= 1
174
- @paginated_keys = @keys[offset, per_page]
136
+ @paginated_keys = @all_keys[offset, per_page]
175
137
  end
176
138
 
177
139
  def offset
@@ -204,18 +166,5 @@ module RailsI18nterface
204
166
  @to_locale = session[:to_locale].to_sym
205
167
  end
206
168
 
207
- def old_from_text(key)
208
- return @old_from_text[key] if @old_from_text && @old_from_text[key]
209
- @old_from_text = {}
210
- text = key.split(".").reduce(log_hash) do |hash, k|
211
- hash ? hash[k] : nil
212
- end
213
- @old_from_text[key] = text
214
- end
215
- helper_method :old_from_text
216
-
217
- def log_hash
218
- @log_hash ||= RailsI18nterface::Log.new(@from_locale, @to_locale, {}).read
219
- end
220
169
  end
221
170
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module RailsI18nterface
2
4
  module ApplicationHelper
3
5
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module RailsI18nterface
2
4
  module TranslateHelper
3
5
 
@@ -1,29 +1,21 @@
1
- <%
2
- @page_title = "Translate"
3
- show_filters = ["all", "untranslated", "translated"]
4
- show_filters << "changed" if @from_locale != @to_locale
5
- %>
6
1
  <%= form_tag(params, :method => :get, :name => 'filter_form') do %>
7
2
  <%= hidden_field_tag(:filter, params[:filter]) %>
8
3
  <%= hidden_field_tag(:sort_by, params[:sort_by]) %>
9
4
  <div id="top">
10
- <% if @page_title %>
11
- <h1>
12
- <div class="right">
13
- <%= select_tag(:from_locale, options_for_select(I18n.available_locales, @from_locale.to_sym)) %> <span>to</span>
14
- <%= select_tag(:to_locale, options_for_select(I18n.available_locales, @to_locale.to_sym)) %>
15
- <%= submit_tag "Display" %>
16
- </div>
17
- <%= link_to '.. /', '../' %>
18
- <%= link_to @page_title, root_path %></a>
19
- <div class="center">
20
- <label>Show:</label> <%= simple_filter(show_filters).html_safe %>
21
- Found <strong><%= @total_entries %></strong> messages
22
- <%= link_to "Reload messages", translate_reload_path(params) %>
23
- </div>
24
-
25
- </h1>
26
- <% end %>
5
+ <h1>
6
+ <div class="right">
7
+ <%= select_tag(:from_locale, options_for_select(I18n.available_locales, @from_locale.to_sym)) %> <span>to</span>
8
+ <%= select_tag(:to_locale, options_for_select(I18n.available_locales, @to_locale.to_sym)) %>
9
+ <%= submit_tag "Display" %>
10
+ </div>
11
+ <%= link_to '.. /', '../' %>
12
+ <%= link_to @page_title, root_path %></a>
13
+ <div class="center">
14
+ <label>Show:</label> <%= simple_filter(@show_filters).html_safe %>
15
+ Found <strong><%= @total_entries %></strong> messages
16
+ <%= link_to "Reload messages", translate_reload_path(params) %>
17
+ </div>
18
+ </h1>
27
19
  <% flash.each do |key, msg| %>
28
20
  <div class="flash" id="<%= key %>"><%= msg %></div>
29
21
  <% end %>
@@ -78,7 +70,6 @@
78
70
  %>
79
71
  <div class="translation">
80
72
  <p class="edit-form">
81
- <%= hidden_field_tag("version[#{key}]", @versions[key] || '0') %>
82
73
  <% if n_lines > 1 %>
83
74
  <div class="right">
84
75
  <% if @files[key] %>
data/config/routes.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  RailsI18nterface::Engine.routes.draw do
2
4
  root to: 'translate#index'
3
5
 
@@ -1,9 +1,11 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'rails-i18nterface/engine'
2
4
  require 'rails-i18nterface/utils'
5
+ require 'rails-i18nterface/cache'
3
6
  require 'rails-i18nterface/yamlfile'
4
7
  require 'rails-i18nterface/sourcefiles'
5
8
  require 'rails-i18nterface/keys'
6
- require 'rails-i18nterface/log'
7
9
  require 'rails-i18nterface/storage'
8
10
 
9
11
  module RailsI18nterface
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ module RailsI18nterface
4
+ module Cache
5
+
6
+ def cache_save(obj, uri, options={})
7
+ FileUtils.rm uri if File.exists? uri
8
+ File.open(uri, 'wb') do |f|
9
+ Marshal.dump(obj, f)
10
+ end
11
+ end
12
+
13
+ def cache_load(uri, file, &process)
14
+ mtime = File.mtime(file)
15
+ if cached?(uri) && uptodate?(uri, file)
16
+ File.open(uri) do |f|
17
+ Marshal.load f
18
+ end
19
+ else
20
+ if block_given?
21
+ obj = yield
22
+ cache_save(obj, uri)
23
+ File.utime(mtime, mtime, uri)
24
+ obj
25
+ else
26
+ nil
27
+ end
28
+ end
29
+ end
30
+
31
+ def cached?(uri)
32
+ File.file? uri
33
+ end
34
+
35
+ def uptodate?(uri, file)
36
+ File.mtime(uri) == File.mtime(file)
37
+ end
38
+
39
+ end
40
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'rails'
2
4
 
3
5
  module RailsI18nterface
@@ -1,57 +1,50 @@
1
- #require 'pathname'
1
+ # encoding: utf-8
2
2
 
3
3
  module RailsI18nterface
4
4
  class Keys
5
5
 
6
- # Allows keys extracted from lookups in files to be cached
7
- def self.files
8
- @files ||= new.files
9
- end
10
-
11
- def self.names
12
- @names || new.names
13
- end
14
- # Allows flushing of the files cache
15
- def self.files=(files)
16
- @files = files
17
- end
6
+ include Utils
18
7
 
19
- def files
20
- @files ||= RailsI18nterface::Sourcefiles.extract_files
21
- end
22
- alias_method :to_hash, :files
8
+ attr_accessor :files, :keys
9
+ attr_reader :yaml_keys, :all_keys
23
10
 
24
- def names
25
- @names ||= i18n_keys
11
+ def initialize(root_dir, from, to)
12
+ @root_dir = root_dir
13
+ @files = RailsI18nterface::Sourcefiles.extract_files(root_dir)
14
+ @yaml_keys = i18n_keys(I18n.default_locale)
15
+ @from_locale = from
16
+ @to_locale = to
17
+ @all_keys = (@files.keys.map(&:to_s) + @yaml_keys).uniq
26
18
  end
27
- alias_method :to_tree, :names
28
19
 
29
- def keys
30
- files.keys
20
+ def reload
21
+ @files = RailsI18nterface::Sourcefiles.extract_files(root_dir)
22
+ @yaml_keys = i18n_keys(I18n.default_locale)
23
+ @all_keys = (@files.keys.map(&:to_s) + @yaml_keys).uniq
31
24
  end
32
- alias_method :to_a, :keys
33
25
 
34
26
  def i18n_keys(locale)
35
27
  I18n.backend.send(:init_translations) unless I18n.backend.initialized?
36
- self.class.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
28
+ to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
37
29
  end
38
30
 
39
31
  def untranslated_keys
40
- self.class.translated_locales.reduce({}) do |missing, locale|
41
- missing[locale] = i18n_keys(I18n.default_locale).map do |key|
42
- I18n.backend.send(:lookup, locale, key).nil? ? key : nil
32
+ self.class.translated_locales.reduce({}) do |a, e|
33
+ a[e] = i18n_keys(I18n.default_locale).map do |key|
34
+ I18n.backend.send(:lookup, e, key).nil? ? key : nil
43
35
  end.compact
44
- missing
36
+ a
45
37
  end
46
38
  end
47
39
 
48
40
  def missing_keys
49
41
  locale = I18n.default_locale
42
+ filepath = Dir.glob(File.join(@root_dir, 'config', 'locales', '**', "#{locale}.yml"))
50
43
  yaml_keys = {}
51
- yaml_keys = Storage.file_paths(locale).reduce({}) do |keys, path|
52
- keys = keys.deep_merge(Yamlfile.new(path).read[locale.to_s])
44
+ yaml_keys = filepath.reduce({}) do |a, e|
45
+ a = a.deep_merge(Yamlfile.new(@root_dir, locale).read[locale.to_s])
53
46
  end
54
- files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
47
+ @files.keys.reject { |key, file| contains_key?(yaml_keys, key) }
55
48
  end
56
49
 
57
50
  def self.translated_locales
@@ -60,86 +53,5 @@ module RailsI18nterface
60
53
  end
61
54
  end
62
55
 
63
- # Checks if a nested hash contains the keys in dot separated I18n key.
64
- #
65
- # Example:
66
- #
67
- # hash = {
68
- # :foo => {
69
- # :bar => {
70
- # :baz => 1
71
- # }
72
- # }
73
- # }
74
- #
75
- # contains_key?("foo", key) # => true
76
- # contains_key?("foo.bar", key) # => true
77
- # contains_key?("foo.bar.baz", key) # => true
78
- # contains_key?("foo.bar.baz.bla", key) # => false
79
- #
80
- def self.contains_key?(hash, key)
81
- keys = key.to_s.split('.')
82
- return false if keys.empty?
83
- !keys.reduce(HashWithIndifferentAccess.new(hash)) do |memo, k|
84
- memo.is_a?(Hash) ? memo.try(:[], k) : nil
85
- end.nil?
86
- end
87
-
88
- # Convert something like:
89
- #
90
- # {
91
- # :pressrelease => {
92
- # :label => {
93
- # :one => "Pressmeddelande"
94
- # }
95
- # }
96
- # }
97
- #
98
- # to:
99
- #
100
- # {'pressrelease.label.one' => "Pressmeddelande"}
101
- #
102
- def self.to_shallow_hash(hash)
103
- hash.reduce({}) do |shallow_hash, (key, value)|
104
- if value.is_a?(Hash)
105
- to_shallow_hash(value).each do |sub_key, sub_value|
106
- shallow_hash[[key, sub_key].join('.')] = sub_value
107
- end
108
- else
109
- shallow_hash[key.to_s] = value
110
- end
111
- shallow_hash
112
- end
113
- end
114
-
115
- # Convert something like:
116
- #
117
- # {'pressrelease.label.one' => "Pressmeddelande"}
118
- #
119
- # to:
120
- #
121
- # {
122
- # :pressrelease => {
123
- # :label => {
124
- # :one => "Pressmeddelande"
125
- # }
126
- # }
127
- # }
128
- def self.to_deep_hash(hash)
129
- hash.reduce({}) do |deep_hash, (key, value)|
130
- keys = key.to_s.split('.').reverse
131
- leaf_key = keys.shift
132
- key_hash = keys.reduce({leaf_key.to_sym => value}) { |h, k| { k.to_sym => h } }
133
- deep_merge!(deep_hash, key_hash)
134
- deep_hash
135
- end
136
- end
137
-
138
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
139
- def self.deep_merge!(hash1, hash2)
140
- merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
141
- hash1.merge!(hash2, &merger)
142
- end
143
-
144
56
  end
145
57
  end