t_t 1.1.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b73436334b6c70340cf56c69878f5645ddecbd12
4
- data.tar.gz: 6f9f3913be2190fc19abce30109a1816621562b8
3
+ metadata.gz: 5e2d2727092a795c91c2ed55be7121285f17fe67
4
+ data.tar.gz: c60c9c93024393bc55c3ab325d8bc5c5137dab9a
5
5
  SHA512:
6
- metadata.gz: a9c423a3ab34a17a0f4f858a773d7c0d60250e32f18679e469e136de30b22b68b5fe5af7e30820c83fbedbcd47d097a65807050989ec875422daa0ba577946ad
7
- data.tar.gz: 42c316f2a5355a2b8379724847f8088582c3f46782811fff42595f6e49f7795ac711dfefe090d41f225db956c08507d02d4cb7a598bdfcbf900d9d393d787ab7
6
+ metadata.gz: ecb7f85dc79c4ad72f4351f6956b37cb5731af57191c11b6fe7a93fc41053466e9006f8adf183c0dfb025bedb8ec53f06eb83cd6b49ffb7437daef752a72d02c
7
+ data.tar.gz: c50045f1db29052474c838ebdcf7ffa0fdb551d4b3922e198f28361d569dfc98eec32863003985b6251c394addba1a07b9cfff492c5591c52c6d59f529e8b713
@@ -1,13 +1,13 @@
1
+ language: ruby
2
+ cache: bundler
1
3
  rvm:
2
4
  - 1.9.3
3
- - rbx-2
4
5
  - jruby
5
6
  - 2.0.0
6
7
  - 2.1
7
8
  - 2.2
8
9
  - ruby-head
9
10
  gemfile:
10
- - gemfiles/Gemfile.actionpack-3.1.x
11
11
  - gemfiles/Gemfile.actionpack-3.2.x
12
12
  - gemfiles/Gemfile.actionpack-4.0.x
13
13
  - gemfiles/Gemfile.actionpack-4.1.x
@@ -124,8 +124,8 @@ For other `active_model` based orms please specify configuration in an initializ
124
124
  # attributes:
125
125
  # user: "Email"
126
126
 
127
- # app/config/tt.rb
128
- TT.config(prefix: :mongoid)
127
+ # app/config/t_t.rb
128
+ TT::Rails.config(prefix: :mongoid)
129
129
  ```
130
130
 
131
131
  ## Resources
@@ -302,8 +302,8 @@ For example, words related to a user tips is good to place into `tips`, words re
302
302
  the gem provides a configuration block:
303
303
 
304
304
  ```ruby
305
- # app/config/tt.rb
306
- TT.config do
305
+ # app/config/t_t.rb
306
+ TT::Rails.config do
307
307
  lookup_key_method :tip, :tips
308
308
  lookup_key_method :f, :forms
309
309
  end
@@ -317,13 +317,13 @@ all it's not a problem. Let's look at a possible cases:
317
317
 
318
318
  ### You don't have ActionPack or want to use Dos-T outside the views
319
319
 
320
- `tt` is an instance of `TT::Translator`. To create a variable you need `namespace` & `section` (optional) keys. In the rails
320
+ `tt` is an instance of `TT::Base`. To create a variable you need `namespace` & `section` (optional) keys. In the rails
321
321
  environment it's `controller_path` and `action_name`.
322
322
 
323
323
  ```ruby
324
324
  class EmailApp < Sinatra::Base
325
325
  before do
326
- @tt = TT::Translator.new('emails')
326
+ @tt = TT::Base.new('emails')
327
327
  end
328
328
 
329
329
  post '/' do
@@ -336,7 +336,7 @@ class EmailSender
336
336
  attr_reader :tt
337
337
 
338
338
  def initialize
339
- @tt = TT::Translator.new('services/email_sender')
339
+ @tt = TT::Base.new('services/email_sender')
340
340
  end
341
341
 
342
342
  def work
@@ -352,6 +352,8 @@ end
352
352
 
353
353
  Just specify an orm i18n scope:
354
354
  ```ruby
355
- # app/config/tt.rb
356
- TT.config(prefix: :mongoid)
355
+ # app/config/t_t.rb
356
+
357
+ # activerecord and mongoid is supported out of the box
358
+ TT::Rails.config(prefix: :mongoid)
357
359
  ```
@@ -1,10 +1,9 @@
1
1
  # Overview
2
2
 
3
- Dos-T provides a factory (not required by default) to generate 'action'-translations in a few lines.
3
+ Dos-T provides the factory to generate 'action'-translations in a few lines.
4
4
 
5
5
  ```ruby
6
6
  # config/locales/actions.rb
7
- require 't_t/action_factory'
8
7
 
9
8
  TT.define_actions(:en, :de) do |f|
10
9
  f.add :see_all, en: "See all %{rs}", de: "Siehe alle %{RS}"
@@ -17,6 +16,7 @@ texts. The most popular case is an English "a/an" rule. For example, an applicat
17
16
 
18
17
  ```ruby
19
18
  # config/locales/actions.en.yml
19
+
20
20
  en:
21
21
  actions:
22
22
  base:
@@ -38,14 +38,13 @@ can teach DSL some grammar and it will generates all translation for you:
38
38
 
39
39
  ```ruby
40
40
  # config/locales/actions.rb
41
- require 't_t/action_factory'
42
41
 
43
42
  TT.define_actions(:en) do |f|
44
43
  f.for(:en) do |l|
45
44
  # defines `a/an` rule for English where:
46
45
  # base - a base action translation or a result of the previous rule processing
47
- # a_meta - a action-related metadata (could be specified on adding an action)
48
- # r_meta - a resource-related metadata (could be specified on marking a resource to use a rule)
46
+ # a_meta - a action-related metadata (specified on adding an action)
47
+ # r_meta - a resource-related metadata (specified on marking a resource to use a rule)
49
48
  l.rule(:an) { |base, a_meta, r_meta| a_meta }
50
49
 
51
50
  # registers a resources which should use the rule
@@ -62,7 +61,6 @@ end
62
61
  Here an another example with a more complex grammar rules:
63
62
 
64
63
  ```ruby
65
- require 't_t/action_factory'
66
64
  # de:
67
65
  # models:
68
66
  # article:
@@ -0,0 +1,39 @@
1
+ # Synchronisation of a translation files
2
+
3
+ When you work on a multi-language application it's easy to add a key for one language and forget to add for another.
4
+ With Dos-T you have a file watcher which watches on the main locale files (`:en` translations by default). Like the asset
5
+ pipeline in development environment, you can enable the file synchronisation and on each page reload the gem will check
6
+ if an English translation was changed and apply changes for other languages. To enable it add the next in
7
+ `config/environments/development.rb`:
8
+
9
+ ```ruby
10
+ # config/environments/development.rb
11
+
12
+ Rails.application.configure do
13
+ # other configuration
14
+ config.tt.sync = true
15
+ end
16
+ ```
17
+
18
+ If your default translation is not English or the translation files located not in `config/locales` use the next
19
+ configuration:
20
+
21
+ ```ruby
22
+ # config/environments/development.rb
23
+
24
+ Rails.application.configure do
25
+ # a custom default locale
26
+ config.tt.sync = :de
27
+ # a custom default glob
28
+ config.tt.sync = { locale: :fr, glob: 'other/locale/folder/**/*.yml' }
29
+ end
30
+ ```
31
+
32
+ Also you can synchronise files without a page reload. Add the next line at the bottom `%rails_root%/Rakefile`:
33
+
34
+ ```ruby
35
+ require 't_t/tasks'
36
+ ```
37
+
38
+ You will have two additional rake tasks - `tt:s` (synchronises the translation files) and `tt:m` (prints the missing
39
+ translations for all languages)
data/lib/t_t.rb CHANGED
@@ -1,226 +1,41 @@
1
- require "active_support/inflector"
2
- require "active_support/lazy_load_hooks"
3
- require "active_support/multibyte/chars"
4
- require "i18n"
1
+ require 't_t/base'
5
2
 
6
3
  module TT
7
- module Utils
8
- extend self
9
-
10
- DOWNCASE = lambda { |str, locale| (locale == :en) ? str.downcase : str.mb_chars.downcase.to_s }
11
-
12
- def lookup(prefix, base_suffix)
13
- prefix ? prefix_lookup(prefix, base_suffix) : simple_lookup(base_suffix)
14
- end
15
-
16
- def to_parts(str)
17
- str.to_s.underscore.split(/\.|\//)
18
- end
19
-
20
- private
21
-
22
- def simple_lookup(base_suffix)
23
- lambda do |ns, type|
24
- parts = to_parts(ns)
25
- model_path = parts.join('.')
26
-
27
- root = "#{ type }.#{ model_path }"
28
-
29
- defaults = []
30
- defaults << :"#{ type }.#{ parts.last }" if parts.length > 1
31
- if base_suffix
32
- defaults << :"#{ type }.#{ base_suffix }"
33
- else
34
- defaults << type
35
- end
36
-
37
- [root, defaults]
38
- end
39
- end
40
-
41
- def prefix_lookup(prefix, base_suffix)
42
- lambda do |ns, type|
43
- parts = to_parts(ns)
44
- model_path = parts.join('.')
45
-
46
- root = "#{ prefix }.#{ type }.#{ model_path }"
47
- defaults = [:"#{ type }.#{ model_path }"]
48
-
49
- if parts.length > 1
50
- pure_model = parts.last
51
- defaults << :"#{ prefix }.#{ type }.#{ pure_model }"
52
- defaults << :"#{ type }.#{ pure_model }"
53
- end
54
-
55
- if base_suffix
56
- defaults << :"#{ prefix }.#{ type }.#{ base_suffix }"
57
- defaults << :"#{ type }.#{ base_suffix }"
58
- else
59
- defaults << :"#{ prefix }.#{ type }"
60
- defaults << type
61
- end
62
-
63
- [root, defaults]
64
- end
65
- end
4
+ def self.fork(*args, &block)
5
+ klass = Class.new(Base)
6
+ klass.config(*args, &block)
7
+ klass
66
8
  end
67
9
 
68
- class Translator
69
- class << self
70
- def lookup_key_method(meth_name, path)
71
- class_eval <<-RUBY
72
- def #{ meth_name }(key, options = {})
73
- I18n.t "\#{ _config.fetch(:ns) }.#{ path }.\#{ key }",
74
- { default: [:"#{ path }.\#{ key }"] }.merge(options)
75
- end
76
- RUBY
77
- end
78
-
79
- def settings(custom = nil)
80
- @settings ||= {}
81
-
82
- if custom
83
- unknown = custom.keys.detect { |key| ![:downcase, :prefix].include?(key) }
84
- if unknown
85
- raise ArgumentError, "TT doesn't know `#{ unknown }` option in the configuration"
86
- else
87
- @settings.merge!(custom)
88
- end
89
- end
90
-
91
- @settings
92
- end
93
- end
94
-
95
- lookup_key_method :c, :common
96
-
97
- def initialize(ns, section = nil)
98
- @lookup = Utils.lookup(self.class.settings[:prefix], nil)
99
- @b_lookup = Utils.lookup(self.class.settings[:prefix], :base)
100
- @e_lookup = Utils.lookup(self.class.settings[:prefix], :messages)
101
-
102
- ns = Utils.to_parts(ns).join('.')
103
- @config = { ns: ns, root: (section ? "#{ ns }.#{ section }" : ns) }
104
- default_model = ns.to_s.singularize
105
-
106
- @config[:attributes] = @lookup.call(default_model, :attributes)
107
- @config[:models] = @lookup.call(default_model, :models)
108
- @config[:actions] = @b_lookup.call(default_model, :actions)
109
- @config[:enums] = @b_lookup.call(default_model, :enums)
110
- @config[:errors] = @e_lookup.call(default_model, :errors)
111
-
112
- @downcase = self.class.settings.fetch(:downcase, Utils::DOWNCASE)
113
- end
114
-
115
- def a(name, model_name = nil, custom = {})
116
- path, defaults = _resolve(@b_lookup, model_name, :actions, name)
117
-
118
- resource = r(model_name)
119
- resources = rs(model_name)
120
- I18n.t path, {
121
- default: defaults, r: @downcase.call(resource, I18n.locale), R: resource,
122
- rs: @downcase.call(resources, I18n.locale), RS: resources
123
- }.merge!(custom)
124
- end
125
-
126
- def attr(name, model_name = nil)
127
- path, defaults = _resolve(@lookup, model_name, :attributes, name)
128
- I18n.t path, default: defaults
129
- end
130
-
131
- def enum(name, kind, model_name = nil)
132
- path, defaults = _resolve(@b_lookup, model_name, :enums, "#{ name }.#{ kind }")
133
- I18n.t path, default: defaults
134
- end
135
-
136
- def e(attr_name, error_name, *args)
137
- custom = args.last.is_a?(Hash) ? args.pop : {}
138
- model_name = args.first
139
- path, defaults = _resolve_errors(model_name, attr_name, error_name)
140
- I18n.t path, { default: defaults }.merge!(custom)
141
- end
142
-
143
- def r(model_name = nil)
144
- rs(model_name, 1)
145
- end
146
-
147
- def rs(model_name = nil, count = 10)
148
- path, defaults = _resolve(@lookup, model_name, :models, nil)
149
- # cut from defaults :"#{ orm }.models", :models
150
- I18n.t path, default: defaults[0...-2], count: count
151
- end
152
-
153
- def t(key, custom = {})
154
- defaults = [:"#{ _config.fetch(:ns) }.common.#{ key }"].concat(Array(custom[:default]))
155
- I18n.t "#{ _config.fetch(:root) }.#{ key }", custom.merge(default: defaults)
156
- end
157
-
158
- private
159
-
160
- def _config
161
- @config
162
- end
163
-
164
- def _resolve_errors(model_name, attr_name, error_name)
165
- if attr_name == :base
166
- _resolve(@e_lookup, model_name, :errors, error_name)
167
- else
168
- path, _defaults = _resolve(@lookup, model_name, :errors, "#{ attr_name }.#{ error_name }")
169
- defaults = _defaults + ["errors.messages.#{ error_name }".to_sym]
170
- return path, defaults
171
- end
172
- end
173
-
174
- def _resolve(lookup, model_name, type, key)
175
- paths = model_name ? lookup.call(model_name, type) : _config.fetch(type)
176
- if key
177
- return "#{ paths.first }.#{ key }", paths.last.map { |i| :"#{ i }.#{ key }" }
178
- else
179
- return *paths
180
- end
181
- end
182
-
183
- ActiveSupport.run_load_hooks(:tt, self)
10
+ def self.config(*args, &block)
11
+ base.config(*args, &block)
184
12
  end
185
13
 
186
- def self.fork(&block)
187
- Class.new(Translator, &block)
14
+ def self.base(value = nil)
15
+ @base || Base
188
16
  end
189
17
 
190
- def self.config(options = {}, &block)
191
- Translator.settings(options)
192
- Translator.instance_exec(&block) if block_given?
18
+ def self.base=(value)
19
+ @base = value
193
20
  end
194
- end
195
-
196
- if defined?(ActionPack) || defined?(ActionMailer)
197
- module TT::Helper
198
- extend ::ActiveSupport::Concern
199
21
 
200
- included do
201
- helper_method :tt
202
-
203
- prepend_before_filter { instance_variable_set(:@tt, ::TT::Translator.new(controller_path, action_name)) }
204
- end
205
-
206
- private
207
-
208
- def tt(*args)
209
- args.empty? ? @tt : @tt.t(*args)
210
- end
22
+ def self.raise_error(base)
23
+ raise ArgumentError, "t_t: #{ base }"
211
24
  end
212
25
 
213
- ActiveSupport.on_load(:action_controller) do
214
- include ::TT::Helper
26
+ def self.define_actions(*args)
27
+ require "t_t/action_factory"
28
+ f = ActionFactory.new(*args)
29
+ yield f
30
+ f.as_hash
215
31
  end
216
32
 
217
- ActiveSupport.on_load(:action_mailer) do
218
- include ::TT::Helper
33
+ def self.const_missing(name)
34
+ super unless name.to_s == 'Translator'
35
+ puts ""
36
+ puts "t_t: TT::Translator is deprecated. Please, use #{ base } instead"
37
+ base
219
38
  end
220
39
  end
221
40
 
222
- if defined?(ActiveRecord)
223
- ActiveSupport.on_load(:active_record) do
224
- TT.config(prefix: :activerecord)
225
- end
226
- end
41
+ require 't_t/rails' if defined?(Rails)
@@ -1,5 +1,3 @@
1
- require 't_t/builtin_rules'
2
-
3
1
  module TT
4
2
  class ActionFactory
5
3
  Action = Struct.new(:base, :rules)
@@ -57,17 +55,18 @@ module TT
57
55
  end
58
56
 
59
57
  def for(key, &block)
60
- yield @locales.fetch(key) { raise_error "`#{ key }` is unknown" }
58
+ yield @locales.fetch(key) { TT.raise_error "`#{ key }` is unknown" }
61
59
  end
62
60
 
63
61
  def activate_rules(*list)
62
+ require 't_t/builtin_rules'
64
63
  list.each { |rkey| BuiltinRules.send(rkey, self) }
65
64
  end
66
65
 
67
66
  def add(akey, list)
68
67
  @locales.each do |lkey, locale|
69
68
  unless action = list[lkey]
70
- raise_error "action `#{ akey }` is missing for `#{ lkey }` locale"
69
+ TT.raise_error "action `#{ akey }` is missing for `#{ lkey }` locale"
71
70
  end
72
71
 
73
72
  action = Action.new(action, []) if action.is_a?(String)
@@ -75,10 +74,10 @@ module TT
75
74
  if action.is_a?(Action)
76
75
  action.rules.each do |rule|
77
76
  next if locale.knows_rule?(rule.key)
78
- raise_error "`#{ rule.key }` is an unknown rule for `#{ lkey }` locale"
77
+ TT.raise_error "`#{ rule.key }` is an unknown rule for `#{ lkey }` locale"
79
78
  end
80
79
  else
81
- raise_error "the value of `#{ akey }` action for `#{ lkey }` locale has a wrong type"
80
+ TT.raise_error "the value of `#{ akey }` action for `#{ lkey }` locale has a wrong type"
82
81
  end
83
82
 
84
83
  @actions[lkey][akey] = action
@@ -91,11 +90,11 @@ module TT
91
90
 
92
91
  def add_exception(mkey, schema)
93
92
  schema.each do |lkey, list|
94
- raise_error("`#{ lkey }` is an unknown locale") unless @locales.has_key?(lkey)
93
+ TT.raise_error("`#{ lkey }` is an unknown locale") unless @locales.has_key?(lkey)
95
94
 
96
95
  list.each do |akey, str|
97
96
  unless @actions[lkey].has_key?(akey)
98
- raise_error "`#{ akey }` action is not specified. Do it before add an exception"
97
+ TT.raise_error "`#{ akey }` action is not specified. Do it before add an exception"
99
98
  end
100
99
 
101
100
  @exceptions[lkey][akey] ||= {}
@@ -121,17 +120,5 @@ module TT
121
120
  hash.merge!(lkey => { actions: actions })
122
121
  end
123
122
  end
124
-
125
- private
126
-
127
- def raise_error(base)
128
- raise ArgumentError, "t_t: #{ base }"
129
- end
130
- end
131
-
132
- def self.define_actions(*args)
133
- f = ActionFactory.new(*args)
134
- yield f
135
- f.as_hash
136
123
  end
137
124
  end
@@ -0,0 +1,185 @@
1
+ require "active_support/inflector"
2
+ require "active_support/lazy_load_hooks"
3
+ require "active_support/multibyte/chars"
4
+ require "i18n"
5
+
6
+ module TT
7
+ module Utils
8
+ extend self
9
+
10
+ DOWNCASE = lambda { |str, locale| (locale == :en) ? str.downcase : str.mb_chars.downcase.to_s }
11
+
12
+ def lookup(prefix, base_suffix)
13
+ prefix ? prefix_lookup(prefix, base_suffix) : simple_lookup(base_suffix)
14
+ end
15
+
16
+ def to_parts(str)
17
+ str.to_s.underscore.split(/\.|\//)
18
+ end
19
+
20
+ private
21
+
22
+ def simple_lookup(base_suffix)
23
+ lambda do |ns, type|
24
+ parts = to_parts(ns)
25
+ model_path = parts.join('.')
26
+
27
+ root = "#{ type }.#{ model_path }"
28
+
29
+ defaults = []
30
+ defaults << :"#{ type }.#{ parts.last }" if parts.length > 1
31
+ if base_suffix
32
+ defaults << :"#{ type }.#{ base_suffix }"
33
+ else
34
+ defaults << type
35
+ end
36
+
37
+ [root, defaults]
38
+ end
39
+ end
40
+
41
+ def prefix_lookup(prefix, base_suffix)
42
+ lambda do |ns, type|
43
+ parts = to_parts(ns)
44
+ model_path = parts.join('.')
45
+
46
+ root = "#{ prefix }.#{ type }.#{ model_path }"
47
+ defaults = [:"#{ type }.#{ model_path }"]
48
+
49
+ if parts.length > 1
50
+ pure_model = parts.last
51
+ defaults << :"#{ prefix }.#{ type }.#{ pure_model }"
52
+ defaults << :"#{ type }.#{ pure_model }"
53
+ end
54
+
55
+ if base_suffix
56
+ defaults << :"#{ prefix }.#{ type }.#{ base_suffix }"
57
+ defaults << :"#{ type }.#{ base_suffix }"
58
+ else
59
+ defaults << :"#{ prefix }.#{ type }"
60
+ defaults << type
61
+ end
62
+
63
+ [root, defaults]
64
+ end
65
+ end
66
+ end
67
+
68
+ class Base
69
+ def self.lookup_key_method(meth_name, path)
70
+ class_eval <<-RUBY
71
+ def #{ meth_name }(key, options = {})
72
+ I18n.t "\#{ _config.fetch(:ns) }.#{ path }.\#{ key }",
73
+ { default: [:"#{ path }.\#{ key }"] }.merge(options)
74
+ end
75
+ RUBY
76
+ end
77
+
78
+ def self.config(custom = nil, &block)
79
+ @settings ||= {}
80
+
81
+ if custom
82
+ unknown = custom.keys.detect { |key| ![:downcase, :prefix].include?(key) }
83
+ if unknown
84
+ TT.raise_error "`#{ unknown }` is a wrong key in the configuration"
85
+ else
86
+ @settings.merge!(custom)
87
+ end
88
+ end
89
+
90
+ instance_exec(&block) if block_given?
91
+
92
+ @settings
93
+ end
94
+
95
+ lookup_key_method :c, :common
96
+
97
+ def initialize(ns, section = nil)
98
+ @lookup = Utils.lookup(self.class.config[:prefix], nil)
99
+ @b_lookup = Utils.lookup(self.class.config[:prefix], :base)
100
+ @e_lookup = Utils.lookup(self.class.config[:prefix], :messages)
101
+
102
+ ns = Utils.to_parts(ns).join('.')
103
+ @config = { ns: ns, root: (section ? "#{ ns }.#{ section }" : ns) }
104
+ default_model = ns.to_s.singularize
105
+
106
+ @config[:attributes] = @lookup.call(default_model, :attributes)
107
+ @config[:models] = @lookup.call(default_model, :models)
108
+ @config[:actions] = @b_lookup.call(default_model, :actions)
109
+ @config[:enums] = @b_lookup.call(default_model, :enums)
110
+ @config[:errors] = @e_lookup.call(default_model, :errors)
111
+
112
+ @downcase = self.class.config.fetch(:downcase, Utils::DOWNCASE)
113
+ end
114
+
115
+ def a(name, model_name = nil, custom = {})
116
+ path, defaults = _resolve(@b_lookup, model_name, :actions, name)
117
+
118
+ resource = r(model_name)
119
+ resources = rs(model_name)
120
+ I18n.t path, {
121
+ default: defaults, r: @downcase.call(resource, I18n.locale), R: resource,
122
+ rs: @downcase.call(resources, I18n.locale), RS: resources
123
+ }.merge!(custom)
124
+ end
125
+
126
+ def attr(name, model_name = nil)
127
+ path, defaults = _resolve(@lookup, model_name, :attributes, name)
128
+ I18n.t path, default: defaults
129
+ end
130
+
131
+ def enum(name, kind, model_name = nil)
132
+ path, defaults = _resolve(@b_lookup, model_name, :enums, "#{ name }.#{ kind }")
133
+ I18n.t path, default: defaults
134
+ end
135
+
136
+ def e(attr_name, error_name, *args)
137
+ custom = args.last.is_a?(Hash) ? args.pop : {}
138
+ model_name = args.first
139
+ path, defaults = _resolve_errors(model_name, attr_name, error_name)
140
+ I18n.t path, { default: defaults }.merge!(custom)
141
+ end
142
+
143
+ def r(model_name = nil)
144
+ rs(model_name, 1)
145
+ end
146
+
147
+ def rs(model_name = nil, count = 10)
148
+ path, defaults = _resolve(@lookup, model_name, :models, nil)
149
+ # cut from defaults :"#{ orm }.models", :models
150
+ I18n.t path, default: defaults[0...-2], count: count
151
+ end
152
+
153
+ def t(key, custom = {})
154
+ defaults = [:"#{ _config.fetch(:ns) }.common.#{ key }"].concat(Array(custom[:default]))
155
+ I18n.t "#{ _config.fetch(:root) }.#{ key }", custom.merge(default: defaults)
156
+ end
157
+
158
+ private
159
+
160
+ def _config
161
+ @config
162
+ end
163
+
164
+ def _resolve_errors(model_name, attr_name, error_name)
165
+ if attr_name == :base
166
+ _resolve(@e_lookup, model_name, :errors, error_name)
167
+ else
168
+ path, _defaults = _resolve(@lookup, model_name, :errors, "#{ attr_name }.#{ error_name }")
169
+ defaults = _defaults + ["errors.messages.#{ error_name }".to_sym]
170
+ return path, defaults
171
+ end
172
+ end
173
+
174
+ def _resolve(lookup, model_name, type, key)
175
+ paths = model_name ? lookup.call(model_name, type) : _config.fetch(type)
176
+ if key
177
+ return "#{ paths.first }.#{ key }", paths.last.map { |i| :"#{ i }.#{ key }" }
178
+ else
179
+ return *paths
180
+ end
181
+ end
182
+
183
+ ActiveSupport.run_load_hooks(:tt, self)
184
+ end
185
+ end
@@ -0,0 +1,138 @@
1
+ require 'active_support/file_update_checker'
2
+ require 'yaml'
3
+
4
+ module TT
5
+ class I18nSync
6
+ MARK = ":t_t: "
7
+
8
+ module Utils
9
+ extend self
10
+
11
+ def flat_hash(value, key = nil)
12
+ case value
13
+ when Hash
14
+ value.inject({}) { |r, (k, v)| r.merge! flat_hash(v, [key, k].compact.join('/')) }
15
+ else
16
+ { key => value }
17
+ end
18
+ end
19
+
20
+ def flat_file(*args)
21
+ flat_hash(load_file(*args))
22
+ end
23
+
24
+ def load_file(path, locale, &block)
25
+ content = YAML.load_file(path)
26
+ yield content if block_given?
27
+ content.fetch(locale.to_s) do
28
+ TT.raise_error "expected #{ path } should contain `#{ locale }` translations"
29
+ end
30
+ end
31
+
32
+ def sync_file(path, locale, standard)
33
+ old_review = {}
34
+ source = load_file(path, locale) { |content| old_review.merge!(content.fetch('review', {})) }
35
+ new_review = {}
36
+ content = { locale => sync_level(standard, source, new_review) }
37
+ review = old_review.merge(flat_hash(new_review))
38
+ content['review'] = review unless review.empty?
39
+ write_file(path, content)
40
+ end
41
+
42
+ def write_file(path, content)
43
+ File.open("#{ path }", "wb") { |stream| YAML.dump(content, stream, line_width: 1000) }
44
+ end
45
+
46
+ private
47
+
48
+ def sync_level(st_level, source, review)
49
+ level = st_level.inject({}) do |r, (key, st_node)|
50
+ node = source[key]
51
+
52
+ r[key] = case st_node
53
+ when Hash
54
+ sub_review = {}
55
+ sub_level = sync_level(st_node, (node.is_a?(Hash) ? node : {}), sub_review)
56
+ review[key] = sub_review unless sub_review.empty?
57
+ sub_level
58
+ when Array then node.is_a?(Array) ? node : st_node.map { |v| "#{ MARK }#{ v }" }
59
+ else
60
+ node.nil? ? "#{ MARK }#{ st_node }" : node
61
+ end
62
+ r
63
+ end
64
+
65
+ (source.keys - st_level.keys).each { |key| review[key] = source[key] }
66
+
67
+ level
68
+ end
69
+ end
70
+
71
+ class FileGroup
72
+ attr_reader :st_locale, :standard, :list
73
+
74
+ def initialize(st_locale, standard, list)
75
+ @st_locale = st_locale
76
+ @standard = standard
77
+ @list = list
78
+ end
79
+
80
+ def execute
81
+ file_updated_at = File.mtime(standard)
82
+ return if file_updated_at == @prev_updated_at
83
+
84
+ st_source = Utils.load_file(standard, st_locale)
85
+ list.each { |l, path| Utils.sync_file(path, l, st_source) }
86
+
87
+ @prev_updated_at = file_updated_at
88
+ end
89
+
90
+ def missed
91
+ flat_list = list.inject({}) { |r, (l, path)| r.merge!(l => Utils.flat_file(path, l)) }
92
+
93
+ Utils.flat_file(standard, st_locale).inject([]) do |list, (k, st_v)|
94
+ item = flat_list.inject({ st_locale => st_v }) { |r, (l, h)| r.merge!(l => h[k]) }
95
+ list << item if item.any? { |l, v| v.nil? }
96
+ list
97
+ end
98
+ end
99
+ end
100
+
101
+ attr_reader :checker, :groups
102
+
103
+ def initialize(st_locale, files)
104
+ @groups = []
105
+
106
+ files.inject({}) do |r, file|
107
+ parts = file.split('.')
108
+ k = parts[0...-2].join('.')
109
+ l = File.basename(parts[-2])
110
+ r[k] ||= {}
111
+ r[k][l] = file
112
+ r
113
+ end.each_value do |group|
114
+ locales = group.keys
115
+ next unless locales.include?(st_locale) && locales.size > 1
116
+ list = group.reject { |l, v| l == st_locale }
117
+ groups << FileGroup.new(st_locale, group[st_locale], list)
118
+ end
119
+
120
+ @checker = ActiveSupport::FileUpdateChecker.new(groups.map(&:standard)) { execute }
121
+ end
122
+
123
+ def execute
124
+ groups.each(&:execute)
125
+ end
126
+
127
+ def missed
128
+ groups.inject({}) do |r, group|
129
+ unless (list = group.missed).empty?
130
+ base_path = group.standard.split(group.st_locale)[0]
131
+ key = "#{ base_path }(*).yml"
132
+ r[key] = list
133
+ end
134
+ r
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,63 @@
1
+ options = {}
2
+ if defined?(Mongoid)
3
+ options[:prefix] = :mongoid
4
+ elsif defined?(ActiveRecord)
5
+ options[:prefix] = :activerecord
6
+ end
7
+
8
+ TT.base = TT::Rails = TT.fork(options) do
9
+ def self.sync(value = nil)
10
+ @sync = value unless value.nil?
11
+ @sync
12
+ end
13
+ end
14
+
15
+ module TT
16
+ module Helper
17
+ extend ::ActiveSupport::Concern
18
+
19
+ included do
20
+ helper_method :tt
21
+
22
+ prepend_before_filter { instance_variable_set(:@tt, ::TT::Rails.new(controller_path, action_name)) }
23
+ end
24
+
25
+ private
26
+
27
+ def tt(*args)
28
+ args.empty? ? @tt : @tt.t(*args)
29
+ end
30
+ end
31
+
32
+ class Railtie < ::Rails::Railtie
33
+ config.tt = ActiveSupport::OrderedOptions.new
34
+
35
+ config.after_initialize do |app|
36
+ if options = app.config.tt.sync
37
+ require 't_t/i18n_sync'
38
+
39
+ locale = :en
40
+ glob = 'config/locales/**/*.yml'
41
+ if options.is_a?(Symbol) || options.is_a?(String)
42
+ locale = options
43
+ elsif options.is_a?(Hash)
44
+ locale = options[:locale] if options.has_key?(:locale)
45
+ glob = options[:glob] if options.has_key?(:glob)
46
+ end
47
+
48
+ file_sync = ::TT::I18nSync.new(locale.to_s, Dir.glob(glob))
49
+ TT::Rails.sync(file_sync)
50
+ ::Rails.application.reloaders << file_sync.checker
51
+ ActionDispatch::Reloader.to_prepare { file_sync.checker.execute_if_updated }
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ ActiveSupport.on_load(:action_controller) do
58
+ include ::TT::Helper
59
+ end
60
+
61
+ ActiveSupport.on_load(:action_mailer) do
62
+ include ::TT::Helper
63
+ end
@@ -0,0 +1,23 @@
1
+ namespace :tt do
2
+ desc "Synchronises the translation file groups"
3
+ task :s => :environment do
4
+ if sync = TT::Rails.sync
5
+ sync.execute
6
+ else
7
+ puts "t_t: Please, setup the synchronisation first"
8
+ end
9
+ end
10
+
11
+ desc "Shows a missed keys in the translation file groups"
12
+ task :m => [:s] do
13
+ if sync = TT::Rails.sync
14
+ sync.missed.each do |group, list|
15
+ puts "# #{ group }"
16
+ puts line.inject("") { |r, (k, v)| r + "#{ k.upcase }: #{ v.to_s.encode('utf-8') }\n" }
17
+ puts '---'
18
+ end
19
+ else
20
+ puts "t_t: Please, setup the synchronisation first"
21
+ end
22
+ end
23
+ end
data/readme.md CHANGED
@@ -87,16 +87,24 @@ The result will be the next:
87
87
  = link_to 'Add a new user', new_user_path
88
88
  ```
89
89
 
90
+ ## Additional features
91
+
92
+ #### [The action factory](./docs/action_factory.md)
93
+
94
+ #### [Synchronisation of a translation files](./docs/synchronisation.md)
95
+
90
96
  ## Setup
91
97
 
92
98
  Just add `gem "t_t"` into your Gemfile and run `bundle`.
93
99
 
94
100
  ## Requirements
95
101
 
96
- Dos-T is tested against Ruby 1.9.3+. If your application uses Ruby on Rails the framework version should be 3.2+
102
+ Dos-T is tested against Ruby 1.9.3+ & JRuby(1.9+ compatible). If your application uses Ruby on Rails the framework version should be 3.2+
97
103
 
98
104
  ## Changelog
99
-
105
+ - 1.2.0
106
+ - Deprecate TT::Translator in favour of TT::Base & TT::Rails
107
+ - Introduce a real-time watcher for [translation file synchronisation](./docs/synchronisation.md)
100
108
  - 1.1.0:
101
109
  - Added [the action factory](./docs/action_factory.md)
102
110
  - Improve #attr, #r, #rs methods to make them more compatible with ActiveModel methods
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "t_t"
5
- spec.version = "1.1.0"
5
+ spec.version = "1.2.0"
6
6
  spec.authors = ["Sergey Pchelintsev"]
7
7
  spec.email = ["mail@sergeyp.me"]
8
8
  spec.summary = %q{An opinioned I18n helper}
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.add_dependency "i18n", ">= 0.6.0"
19
19
  spec.add_dependency "activesupport", ">= 3.0.0"
20
20
 
21
- spec.add_development_dependency "actionpack", ">= 3.0.0"
21
+ spec.add_development_dependency "rails", ">= 3.0.0"
22
22
  spec.add_development_dependency "bundler", "~> 1.7"
23
23
  spec.add_development_dependency "minitest", ">= 4.7"
24
24
  spec.add_development_dependency "rack-test"
@@ -11,7 +11,7 @@ describe "ActionPack integration" do
11
11
  end
12
12
 
13
13
  it 'returns tt instance if the method was called without args' do
14
- assert_equal @controller.tt.class, TT::Translator
14
+ assert_equal @controller.tt.class, TT::Rails
15
15
  end
16
16
 
17
17
  it 'calls #t if args were passed' do
@@ -0,0 +1,97 @@
1
+ require 'test_helper'
2
+
3
+ describe 'I18n synchronisation' do
4
+ it 'add a missing keys' do
5
+ store = {
6
+ 'en' => { 'a' => 'a', 'b' => 'b', 'c' => { 'd' => 'd' }, 'e' => { 'f' => ['k', 'l'] } },
7
+ 'de' => { 'b' => 'de-b' }
8
+ }
9
+
10
+ YAML.stub :load_file, store do
11
+ group = TT::I18nSync::FileGroup.new('en', __FILE__, { 'de' => 'de.yml' })
12
+ expectation = lambda do |path, content|
13
+ assert_equal path, 'de.yml'
14
+ assert_equal content, {
15
+ 'de' => {
16
+ 'a' => ':t_t: a', 'b' => 'de-b',
17
+ 'c' => { 'd' => ':t_t: d' },
18
+ 'e' => { 'f' => [':t_t: k', ':t_t: l'] }
19
+ }
20
+ }
21
+ end
22
+
23
+ utils = TT::I18nSync::Utils
24
+ utils.stub(:write_file, expectation) { group.execute }
25
+
26
+ should_not_call_twice = proc { assert false, "should not call twice" }
27
+ utils.stub(:write_file, should_not_call_twice) { group.execute }
28
+ end
29
+ end
30
+
31
+ it 'saves the previous review and adds a new' do
32
+ store = {
33
+ 'review' => { 'c/d' => 'c', 'r' => 'r' },
34
+ 'en' => { 'a' => 'a', 'b' => 'b', 'c' => { 'd' => 'd' } },
35
+ 'de' => { 'b' => 'de-b' }
36
+ }
37
+ YAML.stub :load_file, store do
38
+ group = TT::I18nSync::FileGroup.new('de', __FILE__, { 'en' => 'en.yml' })
39
+ expectation = lambda do |path, content|
40
+ assert_equal path, 'en.yml'
41
+ assert_equal content['en'], { 'b' => 'b' }
42
+ assert_equal content['review'].sort, [['a', 'a'], ['c/d', 'd'], ['r', 'r']]
43
+ end
44
+
45
+ utils = TT::I18nSync::Utils
46
+ utils.stub(:write_file, expectation) { group.execute }
47
+ end
48
+ end
49
+
50
+ it 'combines files into a full groups' do
51
+ groupMock = Struct.new(:locale, :standard, :list) do
52
+ def execute
53
+ end
54
+ end
55
+
56
+ TT::I18nSync::FileGroup.stub(:new, proc { |*args| groupMock.new(*args) }) do
57
+ sync = TT::I18nSync.new('de', [
58
+ 'config/locales/fr.yml', 'config/locales/views.bg.yml', 'config/locales/models/orm.nl.yml',
59
+ 'config/locales/de.yml', 'config/locales/views.de.yml', 'config/locales/models/orm.en-US.yml',
60
+ 'config/locales/es.yml', 'config/locales/views.en-US.yml', 'config/locales/models/orm.de.yml',
61
+ 'config/locales/en-US.yml', 'config/locales/views.es.yml', 'config/locales/models/orm.es.yml',
62
+ 'config/locales/fix.en-US.yml', 'config/locales/skip.de.yml'
63
+ ])
64
+
65
+ assert_equal 3, sync.groups.length
66
+
67
+ sync.groups[0].tap do |group|
68
+ assert_equal 'config/locales/de.yml', group.standard
69
+ assert_equal [
70
+ ['en-US', 'config/locales/en-US.yml'],
71
+ ['es', 'config/locales/es.yml'],
72
+ ['fr', 'config/locales/fr.yml']
73
+ ], group.list.sort
74
+ end
75
+ end
76
+ end
77
+
78
+ it 'shows all missed translations' do
79
+ store = {
80
+ 'en' => { 'a' => 'a', 'b' => 'b', 'c' => { 'd' => 'd' } },
81
+ 'de' => { 'b' => 'de-b' }
82
+ }
83
+ YAML.stub :load_file, store do
84
+ sync = TT::I18nSync.new('en', ['en.yml', 'de.yml'])
85
+ result = sync.missed
86
+
87
+ assert_equal 1, result.size
88
+ assert_equal '(*).yml', result.keys.first
89
+
90
+ result['(*).yml'].tap do |list|
91
+ assert_equal 2, list.size
92
+ assert_equal({ 'en' => 'a', 'de' => nil }, list.first)
93
+ assert_equal({ 'en' => 'd', 'de' => nil }, list.last)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  describe 'Methods related to models' do
4
4
  before do
5
- @tt = TT::Translator.new("admin/users", "spec")
5
+ @tt = TT::Rails.new("admin/users", "spec")
6
6
  end
7
7
 
8
8
  describe 'actions' do
@@ -134,7 +134,7 @@ describe 'Methods related to models' do
134
134
 
135
135
  describe 'resource names' do
136
136
  before do
137
- @tt = TT::Translator.new('public/people')
137
+ @tt = TT::Rails.new('public/people')
138
138
  load_i18n({
139
139
  models: { person: { one: "whatever", other: "whatever" }, user: { one: "User", other: "Users" } },
140
140
  activerecord: { models: {
@@ -1,14 +1,11 @@
1
- # emulate activerecord presence
2
- ActiveRecord = nil
3
-
4
1
  require "minitest/autorun"
5
2
  require "minitest/mock"
6
3
  require "rack/test"
7
- require "action_controller"
4
+ require "action_controller/railtie"
5
+ require "active_record/railtie"
8
6
  require "t_t"
9
- require "t_t/action_factory"
7
+ require "t_t/i18n_sync"
10
8
 
11
- ActiveSupport.run_load_hooks(:active_record, self)
12
9
  ViewTranslator = TT.fork do
13
10
  lookup_key_method :f, :form
14
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: t_t
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Pchelintsev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-25 00:00:00.000000000 Z
11
+ date: 2016-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 3.0.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: actionpack
42
+ name: rails
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -121,20 +121,25 @@ files:
121
121
  - Rakefile
122
122
  - cheatsheet.md
123
123
  - docs/action_factory.md
124
+ - docs/synchronisation.md
124
125
  - examples/simple_app.yml
125
- - gemfiles/Gemfile.actionpack-3.1.x
126
126
  - gemfiles/Gemfile.actionpack-3.2.x
127
127
  - gemfiles/Gemfile.actionpack-4.0.x
128
128
  - gemfiles/Gemfile.actionpack-4.1.x
129
129
  - gemfiles/Gemfile.actionpack-4.2.x
130
130
  - lib/t_t.rb
131
131
  - lib/t_t/action_factory.rb
132
+ - lib/t_t/base.rb
132
133
  - lib/t_t/builtin_rules.rb
134
+ - lib/t_t/i18n_sync.rb
135
+ - lib/t_t/rails.rb
136
+ - lib/t_t/tasks.rb
133
137
  - readme.md
134
138
  - t_t.gemspec
135
139
  - tests/lib/action_factory_test.rb
136
140
  - tests/lib/action_pack_test.rb
137
141
  - tests/lib/builtin_rules_test.rb
142
+ - tests/lib/i18n_sync_test.rb
138
143
  - tests/lib/model_test.rb
139
144
  - tests/lib/view_test.rb
140
145
  - tests/test_helper.rb
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec path: '../'
4
- gem 'activesupport', '~> 3.1.0'