lolita-i18n 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +29 -0
  2. data/Gemfile +11 -25
  3. data/History.rdoc +7 -0
  4. data/README.md +1 -1
  5. data/Rakefile +4 -46
  6. data/app/assets/javascripts/lolita/i18n/application.js +1 -1
  7. data/app/assets/javascripts/lolita/i18n/i18n.js +87 -0
  8. data/app/assets/stylesheets/lolita/i18n/application.scss +8 -7
  9. data/app/controllers/lolita/i18n_controller.rb +27 -9
  10. data/app/helpers/lolita/i18n_helper.rb +40 -7
  11. data/app/views/lolita/i18n/index.html.haml +23 -14
  12. data/config/locales/en.yml +7 -2
  13. data/config/locales/lv.yml +7 -2
  14. data/config/routes.rb +2 -10
  15. data/lib/lolita-i18n.rb +11 -77
  16. data/lib/lolita-i18n/configuration.rb +56 -0
  17. data/lib/lolita-i18n/exceptions.rb +6 -0
  18. data/lib/lolita-i18n/rails.rb +0 -1
  19. data/lib/lolita-i18n/request.rb +219 -0
  20. data/lib/lolita-i18n/version.rb +11 -0
  21. data/lolita-i18n.gemspec +18 -96
  22. data/spec/controllers/lolita/i18n_controller_spec.rb +64 -30
  23. data/spec/helpers/lolita/i18n_helper_spec.rb +70 -0
  24. data/spec/lolita-i18n/configuration_spec.rb +63 -0
  25. data/spec/lolita-i18n/exceptions_spec.rb +15 -0
  26. data/spec/lolita-i18n/request_spec.rb +238 -0
  27. data/spec/lolita-i18n/version_spec.rb +8 -0
  28. data/spec/lolita_i18n_spec.rb +38 -0
  29. data/spec/rails_spec_helper.rb +11 -0
  30. data/spec/requests/translating_spec.rb +51 -0
  31. data/spec/requests/translation_list_spec.rb +36 -0
  32. data/spec/routing/routes_spec.rb +11 -0
  33. data/spec/spec_helper.rb +26 -18
  34. data/spec/{rails_app → test_app}/app/controllers/application_controller.rb +2 -2
  35. data/spec/test_app/config/application.rb +19 -0
  36. data/spec/test_app/config/boot.rb +11 -0
  37. data/spec/{rails_app → test_app}/config/enviroment.rb +4 -4
  38. data/spec/test_app/config/enviroments/test.rb +44 -0
  39. data/spec/test_app/config/initializers/lolita_i18n.rb +4 -0
  40. data/spec/test_app/config/initializers/token.rb +7 -0
  41. data/spec/test_app/config/locales/en.yml +10 -0
  42. data/spec/test_app/config/locales/lv.yml +2 -0
  43. data/spec/test_app/config/locales/ru.yml +2 -0
  44. data/spec/test_app/config/mongoid.yml +6 -0
  45. data/spec/{rails_app → test_app}/config/routes.rb +2 -2
  46. data/spec/test_app/log/.gitkeep +0 -0
  47. data/spec/test_app/log/development.log +734 -0
  48. metadata +69 -80
  49. data/VERSION +0 -1
  50. data/app/assets/javascripts/lolita/i18n/i18n.js.coffee +0 -135
  51. data/lib/lolita-i18n/backend.rb +0 -87
  52. data/spec/lolita-i18n/backend_spec.rb +0 -33
  53. data/spec/rails_app/config/application.rb +0 -18
  54. data/spec/rails_app/config/initializers/lolita_i18n.rb +0 -16
  55. data/spec/rails_app/config/locales/en.yml +0 -10
  56. data/spec/rails_app/config/locales/lv.yml +0 -2
@@ -4,9 +4,14 @@ en:
4
4
  ru: Russian
5
5
  de: German
6
6
  sv: Swedish
7
+ lt: Lithuanian
8
+ ee: Estonian
7
9
  lolita-i18n:
8
10
  title: "Static content translation"
9
11
  choose-other-language: "Choose other language"
10
- show-untranslated: "Show only untranslated translations"
11
- remove-old: "Remove old translations"
12
+ "Successful update": "Successful update"
13
+ show-untranslated: "Show only untranslated"
14
+ lolita:
15
+ navigation:
16
+ system: "System"
12
17
 
@@ -4,8 +4,13 @@ lv:
4
4
  ru: Krievu
5
5
  de: Vācu
6
6
  sv: Zviedru
7
+ lt: Lietuviešu
8
+ ee: Igauņu
7
9
  lolita-i18n:
8
10
  title: "Statiskā satura tulkošana"
9
11
  choose-other-language: "Izvēlies citu valodu"
10
- show-untranslated: "Rādīt tikai neiztulkotos tulkojumus"
11
- remove-old: "Noņemt vecos tulkojumus"
12
+ "Successful update": "Atslēga saglabāta veiksmīgi"
13
+ show-untranslated: "Rādīt tikai neiztulkotos"
14
+ lolita:
15
+ navigation:
16
+ system: "Sistēma"
data/config/routes.rb CHANGED
@@ -1,11 +1,3 @@
1
- Rails.application.routes.draw do
2
-
3
- namespace "lolita" do
4
- resources :i18n, :only=>[:update],:constraints=>{:id=>/.*/} do
5
- collection do
6
- put 'translate_untranslated'
7
- get 'index'
8
- end
9
- end
10
- end
1
+ Rails.application.routes.draw do
2
+ lolita_for :i18n, :append_to => "system", :to => "Lolita::I18n", :only => [:update, :index], :controller => "lolita/i18n", :title => "I18n"
11
3
  end
data/lib/lolita-i18n.rb CHANGED
@@ -3,71 +3,19 @@ require 'redis'
3
3
  require 'yajl'
4
4
  require 'lolita'
5
5
 
6
+
6
7
  module Lolita
7
8
  # === Uses Redis DB as backend
8
9
  # All translations ar stored with full key like "en.home.index.title" -> Hello world.
9
- # Translations whitch are translated with Google translate have prefix "g" -> "g.en.home.index.title".
10
- # These translations should be accepted/edited and approved then they will become as normal for general use.
11
- #
12
- # === Other stored data
13
- # => :unapproved_keys_<locale> - a SET containing all unapproved keys from GoogleTranslate
14
- #
10
+
15
11
  # In your lolita initializer add this line in setup block.
16
- # config.i18n.store = {:db => 1}
12
+ # config.i18n.store = {redis_confguration_goes_here}
17
13
  # # or
18
- # config.i18n.store = Redis.new()
14
+ # config.i18n.store = Redis.new() # default store
19
15
  module I18n
20
- autoload :Backend, 'lolita-i18n/backend'
16
+ autoload :Request, 'lolita-i18n/request'
21
17
  autoload :Exceptions, 'lolita-i18n/exceptions'
22
-
23
- class Configuration
24
-
25
- attr_accessor :yaml_backend
26
-
27
- def store
28
- unless @store
29
- warn "Lolita::I18n No store specified. See Lolita::I18n"
30
- @store = Redis.new
31
- end
32
- @store
33
- end
34
-
35
- def store=(possible_store)
36
- @store = if possible_store.is_a?(Hash)
37
- Redis.new(possible_store)
38
- else
39
- possible_store
40
- end
41
- @store
42
- end
43
-
44
- def backend
45
- @backend ||= ::I18n::Backend::KeyValue.new(self.store)
46
- end
47
-
48
- # returns Array of flattened keys as "home.index.title"
49
- def flatten_keys
50
- load_translations
51
- self.yaml_backend.flatten_translations(nil, self.yaml_backend.send(:translations)[::I18n.default_locale] || {}, ::I18n::Backend::Flatten::SEPARATOR_ESCAPE_CHAR, false).keys
52
- end
53
-
54
- def load_translations
55
- # don't cache
56
- self.yaml_backend.load_translations
57
- end
58
-
59
- def initialize_chain
60
- ::I18n::Backend::Chain.new(self.backend, self.yaml_backend)
61
- end
62
-
63
- def include_modules
64
- ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Flatten)
65
- ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Pluralization)
66
- ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Metadata)
67
- ::I18n::Backend::Simple.send(:include, ::I18n::Backend::InterpolationCompiler)
68
- end
69
-
70
- end
18
+ autoload :Configuration, 'lolita-i18n/configuration'
71
19
  end
72
20
  end
73
21
 
@@ -77,7 +25,7 @@ module LolitaI18nConfiguration
77
25
  end
78
26
  end
79
27
 
80
- Lolita.scope.extend(LolitaI18nConfiguration)
28
+ Lolita.configuration.extend(LolitaI18nConfiguration)
81
29
 
82
30
  Lolita.after_setup do
83
31
  Lolita.i18n.yaml_backend = ::I18n.backend
@@ -89,25 +37,11 @@ Lolita.after_setup do
89
37
  rescue Errno::ECONNREFUSED => e
90
38
  warn "Warning: Lolita was unable to connect to Redis DB: #{e}"
91
39
  end
92
-
40
+ true
93
41
  end
94
42
 
95
- require 'lolita-i18n/module'
43
+ Lolita.i18n.load_rails!
96
44
 
97
- if Lolita.rails3?
98
- require 'lolita-i18n/rails'
99
- end
45
+ require 'lolita-i18n/module'
46
+ require 'lolita-i18n/version'
100
47
 
101
- Lolita.after_routes_loaded do
102
- if tree=Lolita::Navigation::Tree[:"left_side_navigation"]
103
- unless tree.branches.detect { |b| b.title=="System" }
104
- branch=tree.append(nil, :title=>"System")
105
- branch.append(Object, :title=>"I18n", :url=>Proc.new { |view, branch|
106
- view.send(:lolita_i18n_index_path)
107
- }, :active=>Proc.new { |view, parent_branch, branch|
108
- params=view.send(:params)
109
- params[:controller].to_s.match(/lolita\/i18n/)
110
- })
111
- end
112
- end
113
- end
@@ -0,0 +1,56 @@
1
+ module Lolita
2
+ module I18n
3
+ class Configuration
4
+
5
+ attr_accessor :yaml_backend
6
+
7
+ def load_rails!
8
+ if Lolita.rails3?
9
+ require 'lolita-i18n/rails'
10
+ end
11
+ end
12
+ # Rerturn existing store or create new Redis connection without any arguments.
13
+ def store
14
+ unless @store
15
+ warn "Lolita::I18n No store specified. See Lolita::I18n"
16
+ @store = Redis.new
17
+ end
18
+ @store
19
+ end
20
+
21
+ # Set current store to new Redis connection with given Hash options or accept Redis connection itself.
22
+ def store=(possible_store)
23
+ @store = if possible_store.is_a?(Hash)
24
+ Redis.new(possible_store)
25
+ else
26
+ possible_store
27
+ end
28
+ @store
29
+ end
30
+
31
+ # Lazy create new KeyValue backend with current store.
32
+ def backend
33
+ @backend ||= ::I18n::Backend::KeyValue.new(self.store)
34
+ end
35
+
36
+ # Load translation from yaml.
37
+ def load_translations
38
+ self.yaml_backend.load_translations
39
+ end
40
+
41
+ # Create chain where new KeyValue backend is first and Yaml backend is second.
42
+ def initialize_chain
43
+ ::I18n::Backend::Chain.new(self.backend, self.yaml_backend)
44
+ end
45
+
46
+ # Add modules for SimpleBackend that is used for Yaml translations
47
+ def include_modules
48
+ ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Flatten)
49
+ ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Pluralization)
50
+ ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Metadata)
51
+ ::I18n::Backend::Simple.send(:include, ::I18n::Backend::InterpolationCompiler)
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -6,6 +6,12 @@ module Lolita
6
6
  super "Translation should contain all these variables #{missing_arguments.join(', ')}"
7
7
  end
8
8
  end
9
+
10
+ class TranslationDoesNotMatch < ArgumentError
11
+ def initialize translation, original
12
+ super "Translation #{translation} does not match #{original}"
13
+ end
14
+ end
9
15
  end
10
16
  end
11
17
  end
@@ -1,4 +1,3 @@
1
-
2
1
  module LolitaI18n
3
2
  class Engine < Rails::Engine
4
3
 
@@ -0,0 +1,219 @@
1
+ require "json"
2
+ require "unicode_utils/upcase"
3
+
4
+ module Lolita
5
+ module I18n
6
+ class Request
7
+
8
+ class Validator
9
+
10
+ def validate(key,value)
11
+ if value.is_a?(Array)
12
+ validate_array(key,value)
13
+ elsif value.is_a?(Hash)
14
+ validate_hash(key,value)
15
+ else
16
+ validate_value(key,value)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def validate_array(key,values)
23
+ translation = Translation.new(key,values)
24
+ if translation.original.class != values.class || translation.original.size != values.size
25
+ raise Exceptions::TranslationDoesNotMatch.new(values,translation.original)
26
+ end
27
+ values.each_with_index do |value,index|
28
+ validate_value(key,value,:index => index)
29
+ end
30
+ end
31
+
32
+ def validate_hash(key,hash)
33
+ translation = Translation.new(key,hash)
34
+ if translation.original.class != hash.class || (hash.keys.map(&:to_sym) - translation.original.keys).any?
35
+ raise Exceptions::TranslationDoesNotMatch.new(hash,translation.original)
36
+ end
37
+ hash.each do |hash_key,value|
38
+ validate_value(key,value, :key => hash_key)
39
+ end
40
+ end
41
+
42
+ def validate_value(key,value, options = {})
43
+ value = value.to_s
44
+ original_value = current_value_for_original(key,value,options)
45
+ unless interpolations(value) == interpolations(original_value)
46
+ raise Exceptions::MissingInterpolationArgument.new(interpolations(original_value))
47
+ end
48
+ end
49
+
50
+ def current_value_for_original(key,value,options = {})
51
+ translation = Translation.new(key,value)
52
+ original = translation.original
53
+ if original.is_a?(Hash)
54
+ original[options[:key].to_sym]
55
+ elsif original.is_a?(Array)
56
+ original[options[:index].to_i]
57
+ else
58
+ original
59
+ end
60
+ end
61
+
62
+ def interpolations(value)
63
+ value.to_s.scan(/(%{\w+})/).map{|m| m.first}.sort
64
+ end
65
+ end
66
+
67
+ class Translation
68
+
69
+ def initialize(key,translation)
70
+ @key,@translation = key, translation
71
+ @key_parts = @key.to_s.split(".")
72
+ end
73
+
74
+ def value
75
+ Yajl::Parser.parse(@translation.to_json)
76
+ end
77
+
78
+ def locale
79
+ (locale_from_key || ::I18n.default_locale).to_sym
80
+ end
81
+
82
+ def for_store
83
+ [locale, { key => value }, :escape => false]
84
+ end
85
+
86
+ def key
87
+ @key_parts[1..-1].join(".")
88
+ end
89
+
90
+ def original
91
+ @original ||= ::I18n.t(self.key, :locale => ::I18n.default_locale)
92
+ end
93
+
94
+ private
95
+
96
+ def locale_from_key
97
+ @key_parts.first
98
+ end
99
+
100
+ end
101
+
102
+ class Translations
103
+ def initialize(translations_hash)
104
+ @translations = translations_hash
105
+ end
106
+
107
+ def normalized(locale)
108
+ unless @normalized
109
+ @normalized = {}
110
+ flatten_keys(@translations,locale) do |key,value,original_value|
111
+ @normalized[key] = {:translation => value, :original_translation => original_value}
112
+ end
113
+ end
114
+ @normalized
115
+ end
116
+
117
+ def flatten_keys hash,locale, prev_key = nil, &block
118
+ hash.each_pair do |key,value|
119
+ current_key = [prev_key, key].compact.join(".").to_sym
120
+ if final_value?(value)
121
+ yield current_key, translation_value(current_key,value,locale), original_translation_value(current_key,value)
122
+ else
123
+ flatten_keys(value,locale,current_key, &block)
124
+ end
125
+ end
126
+ end
127
+
128
+ def final_value?(value)
129
+ !value.is_a?(Hash) ||
130
+ (value.is_a?(Hash) && value.keys.map(&:to_sym).include?(:other) && value.keys.size > 1 && !value.values.detect{|value| value.is_a?(Array) || value.is_a?(Hash)})
131
+ end
132
+
133
+ def translation_value key, value, locale
134
+ translation_value = ::I18n.t(key,:locale => locale, :default => default_value_from(value))
135
+ translation_value = {} if value.is_a?(Hash) && !translation_value.is_a?(Hash)
136
+ translation_value = [] if value.is_a?(Array) && !translation_value.is_a?(Array)
137
+ translation_value
138
+ end
139
+
140
+ private
141
+
142
+ def original_translation_value(key,value)
143
+ options = {:locale => ::I18n.default_locale}
144
+ if value.is_a?(Hash) # workaround for I18n::Chain, this allow to load Hash from translations.
145
+ options = options.merge(:count => nil)
146
+ end
147
+ ::I18n.t(key, options)
148
+ end
149
+
150
+ def default_value_from value
151
+ if value.is_a?(Array)
152
+ []
153
+ elsif value.is_a?(Hash)
154
+ {}
155
+ else
156
+ ""
157
+ end
158
+ end
159
+ end
160
+
161
+ attr_accessor :params
162
+
163
+ def initialize(params)
164
+ self.params = params
165
+ end
166
+
167
+ def translations locale
168
+ Lolita.i18n.load_translations
169
+ translations = Translations.new(Lolita.i18n.yaml_backend.send(:translations)[::I18n.default_locale])
170
+ translations.normalized(locale)
171
+ end
172
+
173
+ def sort_translations(unsorted_translations)
174
+ unsorted_translations.sort do |pair_a,pair_b|
175
+ value_a,value_b = pair_a[1][:original_translation],pair_b[1][:original_translation]
176
+
177
+ if both_values_complex?(value_a, value_b)
178
+ 0
179
+ elsif complex_value?(value_a,value_b)
180
+ -1
181
+ elsif complex_value?(value_b,value_a)
182
+ 1
183
+ else
184
+ UnicodeUtils.upcase(value_a.to_s) <=> UnicodeUtils.upcase(value_b.to_s)
185
+ end
186
+ end
187
+ end
188
+
189
+ def update_key
190
+ set(Base64.decode64(params[:id]),params[:translation])
191
+ end
192
+
193
+ def validator
194
+ @validator ||= Validator.new()
195
+ end
196
+
197
+ def del key
198
+ Lolita.i18n.store.del key
199
+ end
200
+
201
+ private
202
+
203
+ def both_values_complex?(value_a, value_b)
204
+ (value_a.is_a?(Hash) || value_a.is_a?(Array)) && [Array,Hash].include?(value_b.class)
205
+ end
206
+
207
+ def complex_value?(value_a, value_b)
208
+ (value_a.is_a?(Hash) || value_a.is_a?(Array)) && ![Array,Hash].include?(value_b.class)
209
+ end
210
+
211
+ def set(key,value)
212
+ translation = Translation.new(key,value)
213
+ validator.validate(key,translation.value)
214
+ !!Lolita.i18n.backend.store_translations(*translation.for_store)
215
+ end
216
+
217
+ end
218
+ end
219
+ end