t_t 1.1.0 → 1.2.0

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