i18n_backend_database 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/LICENSE +29 -0
  2. data/README.textile +125 -0
  3. data/data/locales.yml +4 -0
  4. data/generators/i18n_backend_database/i18n_backend_database_generator.rb +8 -0
  5. data/generators/i18n_backend_database/templates/migrate/create_i18n_tables.rb +25 -0
  6. data/init.rb +1 -0
  7. data/lib/controllers/locales_controller.rb +86 -0
  8. data/lib/controllers/translations_controller.rb +141 -0
  9. data/lib/ext/i18n.rb +68 -0
  10. data/lib/google_language.rb +33 -0
  11. data/lib/i18n_backend_database.rb +9 -0
  12. data/lib/i18n_backend_database/database.rb +263 -0
  13. data/lib/i18n_util.rb +148 -0
  14. data/lib/models/locale.rb +67 -0
  15. data/lib/models/translation.rb +46 -0
  16. data/lib/models/translation_option.rb +26 -0
  17. data/lib/public/images/custom1_bar.gif +0 -0
  18. data/lib/public/images/custom1_box.gif +0 -0
  19. data/lib/public/images/percentImage.png +0 -0
  20. data/lib/public/images/percentImage_back.png +0 -0
  21. data/lib/public/images/percentImage_back1.png +0 -0
  22. data/lib/public/images/percentImage_back2.png +0 -0
  23. data/lib/public/images/percentImage_back3.png +0 -0
  24. data/lib/public/images/percentImage_back4.png +0 -0
  25. data/lib/public/javascripts/jsProgressBarHandler.js +509 -0
  26. data/lib/routing.rb +15 -0
  27. data/lib/views/layouts/translations.html.erb +23 -0
  28. data/lib/views/locales/edit.html.erb +20 -0
  29. data/lib/views/locales/index.html.erb +22 -0
  30. data/lib/views/locales/new.html.erb +19 -0
  31. data/lib/views/locales/show.html.erb +14 -0
  32. data/lib/views/translations/asset_translations.html.erb +32 -0
  33. data/lib/views/translations/edit.html.erb +35 -0
  34. data/lib/views/translations/index.html.erb +27 -0
  35. data/lib/views/translations/new.html.erb +19 -0
  36. data/lib/views/translations/show.html.erb +32 -0
  37. data/lib/views/translations/translations.html.erb +28 -0
  38. data/lib/views/translations/update.rjs +7 -0
  39. data/routes.rb +3 -0
  40. data/spec/assets/public/es/favicons/favicon1.gif +0 -0
  41. data/spec/assets/public/es/images/icons/icon1.gif +0 -0
  42. data/spec/assets/public/es/images/image1.gif +0 -0
  43. data/spec/assets/public/favicons/favicon1.gif +0 -0
  44. data/spec/assets/public/favicons/favicon2.gif +0 -0
  45. data/spec/assets/public/images/icons/icon1.gif +0 -0
  46. data/spec/assets/public/images/icons/icon2.gif +0 -0
  47. data/spec/assets/public/images/image1.gif +0 -0
  48. data/spec/assets/public/images/image2.gif +0 -0
  49. data/spec/assets/public/images/promo/sfc08_140x400_3.gif +0 -0
  50. data/spec/assets/views/test_view1.html.erb +1 -0
  51. data/spec/assets/views/test_view2.html.erb +30 -0
  52. data/spec/caching_spec.rb +87 -0
  53. data/spec/controllers/locales_controller_spec.rb +173 -0
  54. data/spec/controllers/locales_routing_spec.rb +59 -0
  55. data/spec/controllers/translations_controller_spec.rb +189 -0
  56. data/spec/controllers/translations_routing_spec.rb +59 -0
  57. data/spec/database_spec.rb +199 -0
  58. data/spec/i18n_ext_spec.rb +40 -0
  59. data/spec/localize_spec.rb +66 -0
  60. data/spec/localize_text_spec.rb +76 -0
  61. data/spec/models/locale_spec.rb +111 -0
  62. data/spec/models/translation_spec.rb +44 -0
  63. data/spec/spec_helper.rb +16 -0
  64. data/spec/translate_asset_spec.rb +57 -0
  65. data/spec/translate_spec.rb +546 -0
  66. data/tasks/i18n.rake +60 -0
  67. metadata +155 -0
@@ -0,0 +1,33 @@
1
+ require 'cgi'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ class GoogleLanguage
6
+
7
+ # Thanks http://ruby.geraldbauer.ca/google-translation-api.html
8
+ def self.translate( text, to, from='en' )
9
+
10
+ base = 'http://ajax.googleapis.com/ajax/services/language/translate'
11
+
12
+ # assemble query params
13
+ params = {
14
+ :langpair => "#{from}|#{to}",
15
+ :q => text,
16
+ :v => 1.0
17
+ }
18
+
19
+ query = params.map{ |k,v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
20
+
21
+ # send get request
22
+ response = Net::HTTP.get_response( URI.parse( "#{base}?#{query}" ) )
23
+
24
+ json = JSON.parse( response.body )
25
+
26
+ if json['responseStatus'] == 200
27
+ json['responseData']['translatedText']
28
+ else
29
+ raise StandardError, response['responseDetails']
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,9 @@
1
+ require File.dirname(__FILE__) + '/models/locale'
2
+ require File.dirname(__FILE__) + '/models/translation'
3
+ require File.dirname(__FILE__) + '/models/translation_option'
4
+ require File.dirname(__FILE__) + '/routing'
5
+ require File.dirname(__FILE__) + '/controllers/locales_controller'
6
+ require File.dirname(__FILE__) + '/controllers/translations_controller'
7
+ require File.dirname(__FILE__) + '/i18n_backend_database/database'
8
+ require File.dirname(__FILE__) + '/ext/i18n'
9
+ ActionController::Routing::RouteSet::Mapper.send(:include, I18n::BackendDatabase::Routing)
@@ -0,0 +1,263 @@
1
+
2
+ module I18n
3
+ module Backend
4
+ class Database
5
+ INTERPOLATION_RESERVED_KEYS = %w(scope default)
6
+ MATCH = /(\\\\)?\{\{([^\}]+)\}\}/
7
+
8
+ attr_accessor :locale
9
+ attr_accessor :cache_store
10
+ attr_accessor :localize_text_tag
11
+
12
+ def initialize(options = {})
13
+ store = options.delete(:cache_store)
14
+ text_tag = options.delete(:localize_text_tag)
15
+ @cache_store = store ? ActiveSupport::Cache.lookup_store(store) : Rails.cache
16
+ @localize_text_tag = text_tag ? text_tag : '^^'
17
+ end
18
+
19
+ def locale=(code)
20
+ @locale = Locale.find_by_code(code)
21
+ end
22
+
23
+ def cache_store=(store)
24
+ @cache_store = ActiveSupport::Cache.lookup_store(store)
25
+ end
26
+
27
+ # Handles the lookup and addition of translations to the database
28
+ #
29
+ # On an initial translation, the locale is checked to determine if
30
+ # this is the default locale. If it is, we'll create a complete
31
+ # translation record for this locale with both the key and value.
32
+ #
33
+ # If the current locale is checked, and it differs from the default
34
+ # locale, we'll create a translation record with a nil value. This
35
+ # allows for the lookup of untranslated records in a given locale.
36
+ def translate(locale, key, options = {})
37
+ @locale = locale_in_context(locale)
38
+
39
+ options[:scope] = [options[:scope]] unless options[:scope].is_a?(Array) || options[:scope].blank?
40
+ key = "#{options[:scope].join('.')}.#{key}".to_sym if options[:scope] && key.is_a?(Symbol)
41
+ count = options[:count]
42
+ # pull out values for interpolation
43
+ values = options.reject { |name, value| [:scope, :default].include?(name) }
44
+
45
+ entry = lookup(@locale, key)
46
+ cache_lookup = true unless entry.nil?
47
+
48
+ # if no entry exists for the current locale and the current locale is not the default locale then lookup translations for the default locale for this key
49
+ unless entry || @locale.default_locale?
50
+ entry = use_and_copy_default_locale_translations_if_they_exist(@locale, key)
51
+ end
52
+
53
+ # if we have no entry and some defaults ... start looking them up
54
+ unless entry || key.is_a?(String) || options[:default].blank?
55
+ default = options[:default].is_a?(Array) ? options[:default].shift : options.delete(:default)
56
+ return translate(@locale.code, default, options.dup)
57
+ end
58
+
59
+ # this needs to be folded into the above at some point.
60
+ # this handles the case where the default of the string key is a space
61
+ if !entry && key.is_a?(String) && options[:default] == " "
62
+ default = options[:default].is_a?(Array) ? options[:default].shift : options.delete(:default)
63
+ return translate(@locale.code, default, options.dup)
64
+ end
65
+
66
+ # The requested key might not be a parent node in a hierarchy of keys instead of a regular 'leaf' node
67
+ # that would simply result in a string return. If so, check the database for possible children
68
+ # and return them in a nested hash if we find them.
69
+ # We can safely ignore pluralization indeces here since they should never apply to a hash return
70
+ if !entry && (key.is_a?(String) || key.is_a?(Symbol))
71
+ #We need to escape % and \. Rails will handle the rest.
72
+ escaped_key = key.to_s.gsub('\\', '\\\\\\\\').gsub(/%/, '\%')
73
+ children = @locale.translations.find :all, :conditions => ["raw_key like ?", "#{escaped_key}.%"]
74
+ if children.size > 0
75
+ entry = hashify_record_array(key.to_s, children)
76
+ @cache_store.write(Translation.ck(@locale, key), entry) unless cache_lookup == true
77
+ return entry
78
+ end
79
+ end
80
+
81
+ # we check the database before creating a translation as we can have translations with nil values
82
+ # if we still have no blasted translation just go and create one for the current locale!
83
+ unless entry
84
+ pluralization_index = (options[:count].nil? || options[:count] == 1) ? 1 : 0
85
+ translation = @locale.translations.find_by_key_and_pluralization_index(Translation.hk(key), pluralization_index) ||
86
+ @locale.create_translation(key, key, pluralization_index)
87
+ entry = translation.value_or_default
88
+ end
89
+
90
+ # write to cache unless we've already had a successful cache hit
91
+ @cache_store.write(Translation.ck(@locale, key), entry) unless cache_lookup == true
92
+
93
+ entry = pluralize(@locale, entry, count)
94
+ entry = interpolate(@locale.code, entry, values)
95
+ entry.is_a?(Array) ? entry.dup : entry # array's can get frozen with cache writes
96
+ end
97
+
98
+ # Acts the same as +strftime+, but returns a localized version of the
99
+ # formatted date string. Takes a key from the date/time formats
100
+ # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
101
+ def localize(locale, object, format = :default)
102
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
103
+
104
+ type = object.respond_to?(:sec) ? 'time' : 'date'
105
+ format = translate(locale, "#{type}.formats.#{format.to_s}") unless format.to_s.index('%') # lookup keyed formats unless a custom format is passed
106
+
107
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
108
+ format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
109
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
110
+ format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
111
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
112
+
113
+ object.strftime(format)
114
+ end
115
+
116
+ # Returns the text string with the text within the localize text tags translated.
117
+ def localize_text(locale, text)
118
+ text_tag = Regexp.escape(localize_text_tag).to_s
119
+ expression = Regexp.new(text_tag + "(.*?)" + text_tag)
120
+ tagged_text = text[expression, 1]
121
+ while tagged_text do
122
+ text = text.sub(expression, translate(locale, tagged_text))
123
+ tagged_text = text[expression, 1]
124
+ end
125
+ return text
126
+ end
127
+
128
+ def available_locales
129
+ Locale.available_locales
130
+ end
131
+
132
+ def reload!
133
+ # get's called on initialization
134
+ # let's not do anything yet
135
+ end
136
+
137
+ protected
138
+ # keep a local copy of the locale in context for use within the translation
139
+ # routine, and also accept an arbitrary locale for one time locale lookups
140
+ def locale_in_context(locale)
141
+ return @locale if @locale && @locale.code == locale.to_s
142
+ #Locale.find_by_code(locale.to_s) rescue nil && (raise InvalidLocale.new(locale))
143
+ locale = Locale.find_by_code(locale.to_s)
144
+ raise InvalidLocale.new(locale) unless locale
145
+ locale
146
+ end
147
+
148
+ # lookup key in cache and db, if the db is hit the value is cached
149
+ def lookup(locale, key)
150
+ cache_key = Translation.ck(locale, key)
151
+ if @cache_store.exist?(cache_key) && value = @cache_store.read(cache_key)
152
+ return value
153
+ else
154
+ translations = locale.translations.find_all_by_key(Translation.hk(key))
155
+ case translations.size
156
+ when 0
157
+ value = nil
158
+ when 1
159
+ value = translations.first.value_or_default
160
+ else
161
+ value = translations.inject([]) do |values, t|
162
+ values[t.pluralization_index] = t.value_or_default
163
+ values
164
+ end
165
+ end
166
+
167
+ @cache_store.write(cache_key, (value.nil? ? nil : value))
168
+ return value
169
+ end
170
+ end
171
+
172
+ # looks up translations for the default locale, and if they exist untranslated records are created for the locale and the default locale values are returned
173
+ def use_and_copy_default_locale_translations_if_they_exist(locale, key)
174
+ default_locale_entry = lookup(Locale.default_locale, key)
175
+ return unless default_locale_entry
176
+
177
+ if default_locale_entry.is_a?(Array)
178
+ default_locale_entry.each_with_index do |entry, index|
179
+ locale.create_translation(key, nil, index) if entry
180
+ end
181
+ else
182
+ locale.create_translation(key, nil)
183
+ end
184
+
185
+ return default_locale_entry
186
+ end
187
+
188
+ def pluralize(locale, entry, count)
189
+ return entry unless entry.is_a?(Array) and count
190
+ count = count == 1 ? 1 : 0
191
+ entry.compact[count]
192
+ end
193
+
194
+ # Interpolates values into a given string.
195
+ #
196
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
197
+ # # => "file test.txt opened by {{user}}"
198
+ #
199
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
200
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
201
+ # interpolation).
202
+ def interpolate(locale, string, values = {})
203
+ return string unless string.is_a?(String)
204
+
205
+ if string.respond_to?(:force_encoding)
206
+ original_encoding = string.encoding
207
+ string.force_encoding(Encoding::BINARY)
208
+ end
209
+
210
+ result = string.gsub(MATCH) do
211
+ escaped, pattern, key = $1, $2, $2.to_sym
212
+
213
+ if escaped
214
+ pattern
215
+ elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
216
+ raise ReservedInterpolationKey.new(pattern, string)
217
+ elsif !values.include?(key)
218
+ raise MissingInterpolationArgument.new(pattern, string)
219
+ else
220
+ values[key].to_s
221
+ end
222
+ end
223
+
224
+ result.force_encoding(original_encoding) if original_encoding
225
+ result
226
+ end
227
+
228
+ def strip_root_key(root_key, key)
229
+ return nil if key.nil?
230
+ return key.gsub(/^#{root_key}\./, '')
231
+ end
232
+
233
+ def hashify_record_array(root_key, record_array)
234
+ return nil if record_array.nil? || record_array.empty?
235
+
236
+ #Make sure that all of our records have raw_keys
237
+ record_array.reject! {|record| record.raw_key.nil?}
238
+
239
+ # Start building our return hash
240
+ result = {}
241
+ record_array.each { |record|
242
+ key = strip_root_key(root_key, record.raw_key)
243
+ next unless key.present?
244
+
245
+ # If we contain a period delimiter, we need to add a sub-hash.
246
+ # Otherwise, we just insert the value at this level.
247
+ if key.index(".")
248
+ internal_node = key.slice(0, key.index('.'))
249
+ new_root = root_key + '.' + internal_node
250
+ new_record_array = record_array.select {|record| record.raw_key.starts_with? new_root}
251
+ result[internal_node.to_sym] = hashify_record_array(new_root, new_record_array)
252
+ else
253
+ value = record.value
254
+ value = value.to_i if value == "0" || value.to_i != 0 #simple integer cast
255
+ result[key.to_sym] = value
256
+ end
257
+ }
258
+ result
259
+ end
260
+
261
+ end
262
+ end
263
+ end
data/lib/i18n_util.rb ADDED
@@ -0,0 +1,148 @@
1
+ class I18nUtil
2
+
3
+ # Create tanslation records from the YAML file. Will create the required locales if they do not exist.
4
+ def self.load_from_yml(file_name)
5
+ data = YAML::load(IO.read(file_name))
6
+ data.each do |code, translations|
7
+ locale = Locale.find_or_create_by_code(code)
8
+ backend = I18n::Backend::Simple.new
9
+ keys = extract_i18n_keys(translations)
10
+ keys.each do |key|
11
+ value = backend.send(:lookup, code, key)
12
+
13
+ pluralization_index = 1
14
+
15
+ if key.ends_with?('.one')
16
+ key.gsub!('.one', '')
17
+ end
18
+
19
+ if key.ends_with?('.other')
20
+ key.gsub!('.other', '')
21
+ pluralization_index = 0
22
+ end
23
+
24
+ if value.is_a?(Array)
25
+ value.each_with_index do |v, index|
26
+ create_translation(locale, "#{key}", index, v) unless v.nil?
27
+ end
28
+ else
29
+ create_translation(locale, key, pluralization_index, value)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+
36
+ # Finds or creates a translation record and updates the value
37
+ def self.create_translation(locale, key, pluralization_index, value)
38
+ translation = locale.translations.find_by_key_and_pluralization_index(Translation.hk(key), pluralization_index) # find existing record by hash key
39
+ unless translation # or build new one with raw key
40
+ translation = locale.translations.build(:key =>key, :pluralization_index => pluralization_index)
41
+ puts "from yaml create translation for #{locale.code} : #{key} : #{pluralization_index}" unless RAILS_ENV['test']
42
+ end
43
+ translation.value = value
44
+ translation.save!
45
+ end
46
+
47
+ def self.extract_i18n_keys(hash, parent_keys = [])
48
+ hash.inject([]) do |keys, (key, value)|
49
+ full_key = parent_keys + [key]
50
+ if value.is_a?(Hash)
51
+ # Nested hash
52
+ keys += extract_i18n_keys(value, full_key)
53
+ elsif !value.nil?
54
+ # String leaf node
55
+ keys << full_key.join(".")
56
+ end
57
+ keys
58
+ end
59
+ end
60
+
61
+ # Create translation records for all existing locales from translation calls with the application. Ignores errors from tranlations that require objects.
62
+ def self.seed_application_translations(dir='app')
63
+ translated_objects(dir).each do |object|
64
+ interpolation_arguments= object.scan(/\{\{(.*?)\}\}/).flatten
65
+ object = object[/'(.*?)'/, 1] || object[/"(.*?)"/, 1]
66
+ options = {}
67
+ interpolation_arguments.each { |arg| options[arg.to_sym] = nil }
68
+ next if object.nil?
69
+
70
+ puts "translating for #{object} with options #{options.inspect}" unless RAILS_ENV['test']
71
+ I18n.t(object, options) # default locale first
72
+ locales = Locale.available_locales
73
+ locales.delete(I18n.default_locale)
74
+ # translate for other locales
75
+ locales.each do |locale|
76
+ I18n.t(object, options.merge(:locale => locale))
77
+ end
78
+
79
+ end
80
+ end
81
+
82
+ def self.translated_objects(dir='app')
83
+ assets = []
84
+ Dir.glob("#{dir}/*").each do |item|
85
+ if File.directory?(item)
86
+ assets += translated_objects(item) unless item.ends_with?('i18n_backend_database') # ignore self
87
+ else
88
+ File.readlines(item).each do |l|
89
+ assets += l.scan(/I18n.t\((.*?)\)/).flatten
90
+ end
91
+ end
92
+ end
93
+ assets.uniq
94
+ end
95
+
96
+ # Populate translation records from the default locale to other locales if no record exists.
97
+ def self.synchronize_translations
98
+ non_default_locales = Locale.non_defaults
99
+ Locale.default_locale.translations.each do |t|
100
+ non_default_locales.each do |locale|
101
+ unless locale.translations.exists?(:key => t.key, :pluralization_index => t.pluralization_index)
102
+ value = t.value =~ /^---(.*)\n/ ? t.value : nil # well will copy across YAML, like symbols
103
+ locale.translations.create!(:key => t.raw_key, :value => value, :pluralization_index => t.pluralization_index)
104
+ puts "synchronizing has created translation for #{locale.code} : #{t.raw_key} : #{t.pluralization_index}" unless RAILS_ENV['test']
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def self.google_translate
111
+ Locale.non_defaults.each do |locale|
112
+ locale.translations.untranslated.each do |translation|
113
+ default_locale_value = translation.default_locale_value
114
+ unless needs_human_eyes?(default_locale_value)
115
+ interpolation_arguments= default_locale_value.scan(/\{\{(.*?)\}\}/).flatten
116
+
117
+ if interpolation_arguments.empty?
118
+ translation.value = GoogleLanguage.translate(default_locale_value, locale.code, Locale.default_locale.code)
119
+ translation.save!
120
+ else
121
+ placeholder_value = 990 # at least in :es it seems to leave a 3 digit number in the postion on the string
122
+ placeholders = {}
123
+
124
+ # replace {{interpolation_arguments}} with a numeric place holder
125
+ interpolation_arguments.each do |interpolation_argument|
126
+ default_locale_value.gsub!("{{#{interpolation_argument}}}", "#{placeholder_value}")
127
+ placeholders[placeholder_value] = interpolation_argument
128
+ placeholder_value += 1
129
+ end
130
+
131
+ # translate string
132
+ translated_value = GoogleLanguage.translate(default_locale_value, locale.code, Locale.default_locale.code)
133
+
134
+ # replace numeric place holders with {{interpolation_arguments}}
135
+ placeholders.each {|placeholder_value,interpolation_argument| translated_value.gsub!("#{placeholder_value}", "{{#{interpolation_argument}}}") }
136
+ translation.value = translated_value
137
+ translation.save!
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ def self.needs_human_eyes?(value)
145
+ return true if value.index('%') # date formats
146
+ return true if value =~ /^---(.*)\n/ # YAML
147
+ end
148
+ end