refinerycms-i18n 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/REFINERY_README +8 -0
  2. data/Rakefile +34 -0
  3. data/config/locales/ar.yml +121 -0
  4. data/config/locales/bg.yml +198 -0
  5. data/config/locales/bn-IN.yml +180 -0
  6. data/config/locales/bs.yml +114 -0
  7. data/config/locales/ca-ES.yml +155 -0
  8. data/config/locales/cz.rb +166 -0
  9. data/config/locales/da.yml +167 -0
  10. data/config/locales/de-AT.yml +140 -0
  11. data/config/locales/de.yml +141 -0
  12. data/config/locales/dsb.yml +182 -0
  13. data/config/locales/el.yml +191 -0
  14. data/config/locales/es-AR.yml +168 -0
  15. data/config/locales/es-CO.yml +146 -0
  16. data/config/locales/es-MX.yml +118 -0
  17. data/config/locales/es.yml +186 -0
  18. data/config/locales/et.yml +109 -0
  19. data/config/locales/fa.yml +119 -0
  20. data/config/locales/fi.yml +154 -0
  21. data/config/locales/fr-CH.yml +123 -0
  22. data/config/locales/fr.yml +140 -0
  23. data/config/locales/fun/en-AU.rb +105 -0
  24. data/config/locales/fun/gibberish.rb +109 -0
  25. data/config/locales/fur.yml +141 -0
  26. data/config/locales/gl-ES.yml +193 -0
  27. data/config/locales/he.yml +103 -0
  28. data/config/locales/hr.yml +116 -0
  29. data/config/locales/hsb.yml +190 -0
  30. data/config/locales/hu.yml +144 -0
  31. data/config/locales/id.yml +122 -0
  32. data/config/locales/is.yml +142 -0
  33. data/config/locales/it.yml +146 -0
  34. data/config/locales/ja.yml +139 -0
  35. data/config/locales/ko.yml +153 -0
  36. data/config/locales/lo.yml +201 -0
  37. data/config/locales/lt.yml +141 -0
  38. data/config/locales/lv.yml +132 -0
  39. data/config/locales/mk.yml +115 -0
  40. data/config/locales/nb.yml +103 -0
  41. data/config/locales/nl.yml +173 -0
  42. data/config/locales/nn.yml +96 -0
  43. data/config/locales/pl.yml +127 -0
  44. data/config/locales/pt-BR.yml +148 -0
  45. data/config/locales/pt-PT.yml +220 -0
  46. data/config/locales/rm.yml +134 -0
  47. data/config/locales/ro.yml +152 -0
  48. data/config/locales/ru.yml +210 -0
  49. data/config/locales/sk.yml +139 -0
  50. data/config/locales/sl.yml +190 -0
  51. data/config/locales/sr-Latn.yml +116 -0
  52. data/config/locales/sr.yml +116 -0
  53. data/config/locales/sv-SE.yml +191 -0
  54. data/config/locales/sw.yml +123 -0
  55. data/config/locales/th.rb +222 -0
  56. data/config/locales/tr.yml +139 -0
  57. data/config/locales/uk.yml +237 -0
  58. data/config/locales/vi.yml +198 -0
  59. data/config/locales/zh-CN.yml +131 -0
  60. data/config/locales/zh-TW.yml +132 -0
  61. data/i18n-js-readme.rdoc +249 -0
  62. data/lib/gemspec.rb +29 -0
  63. data/lib/refinery/i18n-filter.rb +38 -0
  64. data/lib/refinery/i18n-js.rb +132 -0
  65. data/lib/refinery/i18n-js.yml +4 -0
  66. data/lib/refinery/i18n.js +339 -0
  67. data/lib/refinery/i18n.rb +146 -0
  68. data/lib/refinery/translate.rb +13 -0
  69. data/lib/tasks/i18n-js_tasks.rake +16 -0
  70. data/lib/tasks/translate.rake +200 -0
  71. data/lib/translate/file.rb +35 -0
  72. data/lib/translate/keys.rb +123 -0
  73. data/lib/translate/log.rb +39 -0
  74. data/lib/translate/storage.rb +20 -0
  75. data/test/i18n-test.html +50 -0
  76. data/test/i18n-test.js +661 -0
  77. data/test/i18n_js_test.rb +168 -0
  78. data/test/jsunittest/jsunittest.js +1017 -0
  79. data/test/jsunittest/unittest.css +54 -0
  80. data/test/resources/custom_path.yml +4 -0
  81. data/test/resources/default.yml +4 -0
  82. data/test/resources/locales.yml +76 -0
  83. data/test/resources/multiple_files.yml +6 -0
  84. data/test/resources/no_scope.yml +3 -0
  85. data/test/resources/simple_scope.yml +4 -0
  86. data/test/test_helper.rb +24 -0
  87. data/translate-readme.md +61 -0
  88. metadata +154 -0
@@ -0,0 +1,146 @@
1
+ # Encoding: UTF-8 <-- required, please leave this in.
2
+ require 'refinery'
3
+
4
+ module Refinery
5
+ module I18n
6
+ class Engine < Rails::Engine
7
+ config.before_initialize do
8
+ require File.expand_path('../i18n-filter', __FILE__)
9
+ require File.expand_path('../i18n-js', __FILE__)
10
+ require File.expand_path('../translate', __FILE__)
11
+
12
+ # TODO: Use new method available_locales once Rails is upgraded, see:
13
+ # http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4
14
+ def I18n.valid_locales
15
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
16
+ backend.send(:translations).keys.reject { |locale| locale == :root }
17
+ end
18
+ end
19
+
20
+ config.to_prepare do
21
+ ::Refinery::I18n.setup! if defined?(RefinerySetting) and RefinerySetting.table_exists?
22
+ end
23
+ end
24
+
25
+ class << self
26
+
27
+ attr_accessor :enabled, :current_locale, :locales, :default_locale, :default_frontend_locale
28
+
29
+ def enabled?
30
+ # cache this lookup as it gets very expensive.
31
+ if defined?(@enabled) && !@enabled.nil?
32
+ @enabled
33
+ else
34
+ @enabled = RefinerySetting.find_or_set(:i18n_translation_enabled, true, {
35
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!},
36
+ :scoping => 'refinery'
37
+ })
38
+ end
39
+ end
40
+
41
+ def current_locale
42
+ unless enabled?
43
+ ::Refinery::I18n.current_locale = ::Refinery::I18n.default_locale
44
+ else
45
+ @current_locale ||= RefinerySetting.find_or_set(:i18n_translation_current_locale,
46
+ ::Refinery::I18n.default_locale, {
47
+ :scoping => 'refinery',
48
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!}
49
+ })
50
+ end
51
+ end
52
+
53
+ def current_locale=(locale)
54
+ @current_locale = locale.to_sym
55
+ RefinerySetting[:i18n_translation_current_locale] = {
56
+ :value => locale.to_sym,
57
+ :scoping => 'refinery',
58
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!}
59
+ }
60
+ ::I18n.locale = locale.to_sym
61
+ end
62
+
63
+ def default_locale
64
+ @default_locale ||= RefinerySetting.find_or_set(:i18n_translation_default_locale,
65
+ :en, {
66
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!},
67
+ :scoping => 'refinery'
68
+ })
69
+ end
70
+
71
+ def default_frontend_locale
72
+ @default_frontend_locale ||= RefinerySetting.find_or_set(:i18n_translation_default_frontend_locale,
73
+ :en, {
74
+ :scoping => 'refinery',
75
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!}
76
+ })
77
+ end
78
+
79
+ def locales
80
+ @locales ||= RefinerySetting.find_or_set(:i18n_translation_locales, {
81
+ :en => 'English',
82
+ :fr => 'Français',
83
+ :nl => 'Nederlands',
84
+ :'pt-BR' => 'Português',
85
+ :da => 'Dansk',
86
+ :nb => 'Norsk Bokmål',
87
+ :sl => 'Slovenian',
88
+ :es => 'Español',
89
+ :it => 'Italiano',
90
+ :'zh-CN' => 'Simple Chinese',
91
+ :de => 'Deutsch',
92
+ :lv => 'Latviski',
93
+ :ru => 'Русский'
94
+ },
95
+ {
96
+ :scoping => 'refinery',
97
+ :callback_proc_as_string => %q{::Refinery::I18n.setup!}
98
+ }
99
+ )
100
+ end
101
+
102
+ def has_locale?(locale)
103
+ locales.has_key?(locale.try(:to_sym))
104
+ end
105
+
106
+ def setup!
107
+ # Re-initialize variables.
108
+ @enabled = nil
109
+ @locales = nil
110
+ @default_locale = nil
111
+ @default_frontend_locale = nil
112
+ @current_locale = nil
113
+
114
+ self.load_base_locales!
115
+ self.load_refinery_locales!
116
+ self.load_app_locales!
117
+ self.set_default_locale!
118
+ end
119
+
120
+ def load_base_locales!
121
+ load_locales Pathname.new(__FILE__).parent.join "..", "config", "locales", "*.yml"
122
+ end
123
+
124
+ def load_refinery_locales!
125
+ load_locales Refinery.root.join "vendor", "engines", "*", "config", "locales", "*.yml"
126
+ end
127
+
128
+ def load_app_locales!
129
+ load_locales Rails.root.join "config", "locales", "*.yml"
130
+ end
131
+
132
+ def set_default_locale!
133
+ ::I18n.default_locale = ::Refinery::I18n.default_locale
134
+ end
135
+
136
+ def load_locales(locale_files)
137
+ locale_files = locale_files.to_s if locale_files.is_a? Pathname
138
+ locale_files = Dir[locale_files] if locale_files.is_a? String
139
+ locale_files.each do |locale_file|
140
+ ::I18n.load_path.unshift locale_file
141
+ end
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,13 @@
1
+ module Translate
2
+ def self.locales_dir=(dir)
3
+ @locales_dir = dir.to_s
4
+ end
5
+
6
+ def self.locales_dir
7
+ @locales_dir || Rails.root.join("config", "locales").to_s
8
+ end
9
+ end
10
+
11
+ Dir[File.join(File.dirname(__FILE__), "translate", "*.rb")].each do |file|
12
+ require file
13
+ end
@@ -0,0 +1,16 @@
1
+ namespace :i18n do
2
+ desc "Copy i18n.js and configuration file"
3
+ task :setup => :environment do
4
+ SimplesIdeias::I18n.setup!
5
+ end
6
+
7
+ desc "Export the messages files"
8
+ task :export => :environment do
9
+ SimplesIdeias::I18n.export!
10
+ end
11
+
12
+ desc "Update the JavaScript library"
13
+ task :update => :environment do
14
+ SimplesIdeias::I18n.update!
15
+ end
16
+ end
@@ -0,0 +1,200 @@
1
+ require 'yaml'
2
+
3
+ class Hash
4
+ def deep_merge(other)
5
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
6
+ merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
7
+ merge(other, &merger)
8
+ end
9
+
10
+ def set(keys, value)
11
+ key = keys.shift
12
+ if keys.empty?
13
+ self[key] = value
14
+ else
15
+ self[key] ||= {}
16
+ self[key].set keys, value
17
+ end
18
+ end
19
+
20
+ if ENV['SORT']
21
+ # copy of ruby's to_yaml method, prepending sort.
22
+ # before each so we get an ordered yaml file
23
+ def to_yaml( opts = {} )
24
+ YAML::quick_emit( self, opts ) do |out|
25
+ out.map( taguri, to_yaml_style ) do |map|
26
+ sort.each do |k, v| #<- Adding sort.
27
+ map.add( k, v )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ namespace :translate do
36
+
37
+ def find_missing_translations(locale)
38
+ keys = []
39
+ result = []
40
+ locale_hash = {}
41
+
42
+ puts "Searching for missing translations for the locale: #{locale}"
43
+ default_locale_files = Dir.glob(Rails.root.join('vendor', 'plugins', '*', 'config', 'locales', "#{locale}.yml").to_s)
44
+ default_locale_files += Dir.glob(Refinery.root.join('vendor', 'plugins', '*', 'config', 'locales', "#{locale}.yml").to_s)
45
+ default_locale_files += Dir.glob(Refinery.root.join('vendor', 'refinerycms', '*', 'config', 'locales', "#{locale}.yml").to_s)
46
+ default_locale_files += Dir.glob(File.join(Translate.locales_dir, "**","#{locale}.yml").to_s)
47
+
48
+ default_locale_files.uniq.each do |locale_file_name|
49
+ yaml = YAML::load(File.open(locale_file_name))
50
+ locale_hash = locale_hash.deep_merge(yaml[locale.to_s])
51
+ end
52
+
53
+ lookup_pattern = /\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]*.[a-z0-9_.]+)'\)?/
54
+ templates = Dir.glob(Rails.root.join('app', '**', '*.{erb,rb}').to_s)
55
+ templates += Dir.glob(Rails.root.join.join('vendor', 'plugins', '**', 'app', '**', '*.{erb,rb}').to_s)
56
+ templates += Dir.glob(Refinery.root.join('vendor', 'plugins', '**', 'app', '**', '*.{erb,rb}').to_s)
57
+ templates += Dir.glob(Refinery.root.join('vendor', 'refinerycms', '**', 'app', '**', '*.{erb,rb}').to_s)
58
+
59
+ templates.reject{|t| t =~ /\/lib\/generators\/.+?\/templates\//}.uniq.each do |file_name|
60
+ File.open(file_name, "r+").each do |line|
61
+ line.scan(lookup_pattern) do |key_string|
62
+ # qualify the namespace if beginning with . like t('.log_out')
63
+ if key_string.first =~ /^\./
64
+ namespace = file_name.split("app/").last.split(/^.+?\//).last.split('/')
65
+ namespace = namespace | [namespace.pop.gsub(/^\_/, '').split('.').first]
66
+ key_string = ["#{namespace.join('.')}#{key_string.first}"]
67
+ end
68
+
69
+ unless key_exist?(key_string.first.split("."), locale_hash)
70
+ result << "#{key_string} in \t #{file_name} \t is not in any #{locale} locale file"
71
+ end
72
+ end
73
+ end unless file_name =~ /translate\/spec/
74
+ end
75
+ puts result.empty? ? "No missing translations for locale: #{locale}" : "#{result.join("\n\n")}\n\nNumber of missing translations for #{locale}: #{result.length}"
76
+ end
77
+
78
+ desc "Show I18n keys that are missing in the specified locale YAML file. Defaults to I18n.default_locale, unless LOCALE env is specified"
79
+ task :lost_in_translation => :environment do
80
+ locale = ENV['LOCALE'] || I18n.default_locale
81
+ find_missing_translations(locale)
82
+ end
83
+
84
+ desc "Show I18n keys that are missing in all locale YAML files."
85
+ task :lost_in_translation_all => :environment do
86
+ ::Refinery::I18n.locales.keys.each do |locale|
87
+ find_missing_translations(locale)
88
+ puts "--"
89
+ end
90
+ end
91
+
92
+ def key_exist?(key_arr, locale_hash)
93
+ key = key_arr.slice!(0)
94
+ if key
95
+ key_exist?(key_arr, locale_hash[key]) if locale_hash && locale_hash.include?(key)
96
+ elsif locale_hash
97
+ true
98
+ else
99
+ false
100
+ end
101
+ end
102
+
103
+ desc "Merge I18n keys from log/translations.yml into config/locales/*.yml (for use with the Rails I18n TextMate bundle)"
104
+ task :merge_keys => :environment do
105
+ I18n.backend.send(:init_translations)
106
+ new_translations = YAML::load(IO.read(File.join(Rails.root, "log", "translations.yml")))
107
+ raise("Can only merge in translations in single locale") if new_translations.keys.size > 1
108
+ locale = new_translations.keys.first
109
+
110
+ overwrites = false
111
+ Translate::Keys.new.send(:extract_i18n_keys, new_translations[locale]).each do |key|
112
+ new_text = key.split(".").inject(new_translations[locale]) { |hash, sub_key| hash[sub_key] }
113
+ existing_text = I18n.backend.send(:lookup, locale.to_sym, key)
114
+ if existing_text && new_text != existing_text
115
+ puts "ERROR: key #{key} already exists with text '#{existing_text.inspect}' and would be overwritten by new text '#{new_text}'. " +
116
+ "Set environment variable OVERWRITE=1 if you really want to do this."
117
+ overwrites = true
118
+ end
119
+ end
120
+
121
+ if !overwrites || ENV['OVERWRITE']
122
+ I18n.backend.store_translations(locale, new_translations[locale])
123
+ Translate::Storage.new(locale).write_to_file
124
+ end
125
+ end
126
+
127
+ desc "Apply Google translate to auto translate all texts in locale ENV['FROM'] to locale ENV['TO']"
128
+ task :google => :environment do
129
+ raise "Please specify FROM and TO locales as environment variables" if ENV['FROM'].blank? || ENV['TO'].blank?
130
+
131
+ # Depends on httparty gem
132
+ # http://www.robbyonrails.com/articles/2009/03/16/httparty-goes-foreign
133
+ class GoogleApi
134
+ include HTTParty
135
+ base_uri 'ajax.googleapis.com'
136
+ def self.translate(string, to, from)
137
+ tries = 0
138
+ begin
139
+ get("/ajax/services/language/translate",
140
+ :query => {:langpair => "#{from}|#{to}", :q => string, :v => 1.0},
141
+ :format => :json)
142
+ rescue
143
+ tries += 1
144
+ puts("SLEEPING - retrying in 5...")
145
+ sleep(5)
146
+ retry if tries < 10
147
+ end
148
+ end
149
+ end
150
+
151
+ I18n.backend.send(:init_translations)
152
+
153
+ start_at = Time.now
154
+ translations = {}
155
+ Translate::Keys.new.i18n_keys(ENV['FROM']).each do |key|
156
+ from_text = I18n.backend.send(:lookup, ENV['FROM'], key).to_s
157
+ to_text = I18n.backend.send(:lookup, ENV['TO'], key)
158
+ if !from_text.blank? && to_text.blank?
159
+ print "#{key}: '#{from_text[0, 40]}' => "
160
+ if !translations[from_text]
161
+ response = GoogleApi.translate(from_text, ENV['TO'], ENV['FROM'])
162
+ translations[from_text] = response["responseData"] && response["responseData"]["translatedText"]
163
+ end
164
+ if !(translation = translations[from_text]).blank?
165
+ translation.gsub!(/\(\(([a-z_.]+)\)\)/i, '{{\1}}')
166
+ # Google translate sometimes replaces %{foobar} with (()) foobar. We skip these
167
+ if translation !~ /\(\(\)\)/
168
+ puts "'#{translation[0, 40]}'"
169
+ I18n.backend.store_translations(ENV['TO'].to_sym, Translate::Keys.to_deep_hash({key => translation}))
170
+ else
171
+ puts "SKIPPING since interpolations were messed up: '#{translation[0,40]}'"
172
+ end
173
+ else
174
+ puts "NO TRANSLATION - #{response.inspect}"
175
+ end
176
+ end
177
+ end
178
+
179
+ puts "\nTime elapsed: #{(((Time.now - start_at) / 60) * 10).to_i / 10.to_f} minutes"
180
+ Translate::Storage.new(ENV['TO'].to_sym).write_to_file
181
+ end
182
+
183
+ desc "List keys that have changed I18n texts between YAML file ENV['FROM_FILE'] and YAML file ENV['TO_FILE']. Set ENV['VERBOSE'] to see changes"
184
+ task :changed => :environment do
185
+ from_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['FROM_FILE']).read)
186
+ to_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['TO_FILE']).read)
187
+ from_hash.each do |key, from_value|
188
+ if (to_value = to_hash[key]) && to_value != from_value
189
+ key_without_locale = key[/^[^.]+\.(.+)$/, 1]
190
+ if ENV['VERBOSE']
191
+ puts "KEY: #{key_without_locale}"
192
+ puts "FROM VALUE: '#{from_value}'"
193
+ puts "TO VALUE: '#{to_value}'"
194
+ else
195
+ puts key_without_locale
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,35 @@
1
+ require 'fileutils'
2
+
3
+ class Translate::File
4
+ attr_accessor :path
5
+
6
+ def initialize(path)
7
+ self.path = path
8
+ end
9
+
10
+ def write(keys)
11
+ FileUtils.mkdir_p File.dirname(path)
12
+ File.open(path, "w") do |file|
13
+ file.puts keys_to_yaml(Translate::File.deep_stringify_keys(keys))
14
+ end
15
+ end
16
+
17
+ def read
18
+ File.exists?(path) ? YAML::load(IO.read(path)) : {}
19
+ end
20
+
21
+ # Stringifying keys for prettier YAML
22
+ def self.deep_stringify_keys(hash)
23
+ hash.inject({}) { |result, (key, value)|
24
+ value = deep_stringify_keys(value) if value.is_a? Hash
25
+ result[(key.to_s rescue key) || key] = value
26
+ result
27
+ }
28
+ end
29
+
30
+ private
31
+ def keys_to_yaml(keys)
32
+ # Using ya2yaml, if available, for UTF8 support
33
+ keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ require 'pathname'
2
+
3
+ class Translate::Keys
4
+ # Allows keys extracted from lookups in files to be cached
5
+ def self.files
6
+ @@files ||= Translate::Keys.new.files
7
+ end
8
+
9
+ # Allows flushing of the files cache
10
+ def self.files=(files)
11
+ @@files = files
12
+ end
13
+
14
+ def files
15
+ @files ||= extract_files
16
+ end
17
+ alias_method :to_hash, :files
18
+
19
+ def keys
20
+ files.keys
21
+ end
22
+ alias_method :to_a, :keys
23
+
24
+ def i18n_keys(locale)
25
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
26
+ extract_i18n_keys(I18n.backend.send(:translations)[locale.to_sym]).sort
27
+ end
28
+
29
+ # Convert something like:
30
+ #
31
+ # {
32
+ # :pressrelease => {
33
+ # :label => {
34
+ # :one => "Pressmeddelande"
35
+ # }
36
+ # }
37
+ # }
38
+ #
39
+ # to:
40
+ #
41
+ # {'pressrelease.label.one' => "Pressmeddelande"}
42
+ #
43
+ def self.to_shallow_hash(hash)
44
+ hash.inject({}) do |shallow_hash, (key, value)|
45
+ if value.is_a?(Hash)
46
+ to_shallow_hash(value).each do |sub_key, sub_value|
47
+ shallow_hash[[key, sub_key].join(".")] = sub_value
48
+ end
49
+ else
50
+ shallow_hash[key.to_s] = value
51
+ end
52
+ shallow_hash
53
+ end
54
+ end
55
+
56
+ # Convert something like:
57
+ #
58
+ # {'pressrelease.label.one' => "Pressmeddelande"}
59
+ #
60
+ # to:
61
+ #
62
+ # {
63
+ # :pressrelease => {
64
+ # :label => {
65
+ # :one => "Pressmeddelande"
66
+ # }
67
+ # }
68
+ # }
69
+ def self.to_deep_hash(hash)
70
+ hash.inject({}) do |deep_hash, (key, value)|
71
+ keys = key.to_s.split('.').reverse
72
+ leaf_key = keys.shift
73
+ key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
74
+ deep_merge!(deep_hash, key_hash)
75
+ deep_hash
76
+ end
77
+ end
78
+
79
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
80
+ def self.deep_merge!(hash1, hash2)
81
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
82
+ hash1.merge!(hash2, &merger)
83
+ end
84
+
85
+ private
86
+ def extract_i18n_keys(hash, parent_keys = [])
87
+ hash.inject([]) do |keys, (key, value)|
88
+ full_key = parent_keys + [key]
89
+ if value.is_a?(Hash)
90
+ # Nested hash
91
+ keys += extract_i18n_keys(value, full_key)
92
+ elsif value.present?
93
+ # String leaf node
94
+ keys << full_key.join(".")
95
+ end
96
+ keys
97
+ end
98
+ end
99
+
100
+ def extract_files
101
+ files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
102
+ IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
103
+ files[key] ||= []
104
+ path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
105
+ files[key] << path unless files[key].include?(path)
106
+ end
107
+ files
108
+ end
109
+ end
110
+
111
+ def i18n_lookup_pattern
112
+ /\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
113
+ end
114
+
115
+ def files_to_scan
116
+ Dir.glob(File.join(files_root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
117
+ Dir.glob(File.join(files_root_dir, "public", "javascripts", "**","*.js"))
118
+ end
119
+
120
+ def files_root_dir
121
+ Rails.root
122
+ end
123
+ end