air18n 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/.gitignore +18 -0
  2. data/.rvmrc +48 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +9 -0
  7. data/air18n.gemspec +25 -0
  8. data/lib/air18n.rb +83 -0
  9. data/lib/air18n/backend.rb +194 -0
  10. data/lib/air18n/class_methods.rb +292 -0
  11. data/lib/air18n/less_silly_chain.rb +54 -0
  12. data/lib/air18n/logging_helper.rb +13 -0
  13. data/lib/air18n/mock_priority.rb +25 -0
  14. data/lib/air18n/phrase.rb +92 -0
  15. data/lib/air18n/phrase_screenshot.rb +76 -0
  16. data/lib/air18n/phrase_translation.rb +348 -0
  17. data/lib/air18n/prim_and_proper.rb +94 -0
  18. data/lib/air18n/priority.rb +13 -0
  19. data/lib/air18n/pseudo_locales.rb +53 -0
  20. data/lib/air18n/reflection.rb +10 -0
  21. data/lib/air18n/screenshot.rb +45 -0
  22. data/lib/air18n/testing_support/factories.rb +3 -0
  23. data/lib/air18n/testing_support/factories/phrase.rb +8 -0
  24. data/lib/air18n/testing_support/factories/phrase_screenshot.rb +8 -0
  25. data/lib/air18n/testing_support/factories/phrase_translation.rb +17 -0
  26. data/lib/air18n/version.rb +3 -0
  27. data/lib/air18n/xss_detector.rb +47 -0
  28. data/lib/generators/air18n/migration/migration_generator.rb +39 -0
  29. data/lib/generators/air18n/migration/templates/active_record/migration.rb +63 -0
  30. data/spec/database.yml +3 -0
  31. data/spec/factories.rb +2 -0
  32. data/spec/lib/air18n/air18n_spec.rb +144 -0
  33. data/spec/lib/air18n/backend_spec.rb +173 -0
  34. data/spec/lib/air18n/phrase_translation_spec.rb +80 -0
  35. data/spec/lib/air18n/prim_and_proper_spec.rb +21 -0
  36. data/spec/lib/air18n/pseudo_locales_spec.rb +17 -0
  37. data/spec/lib/air18n/xss_detector_spec.rb +47 -0
  38. data/spec/spec_helper.rb +62 -0
  39. metadata +212 -0
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ *.log
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
+ # Only full ruby name is supported here, for short names use:
8
+ # echo "rvm use 1.9.3" > .rvmrc
9
+ environment_id="ruby-1.9.3-p125@air18n"
10
+
11
+ # Uncomment the following lines if you want to verify rvm version per project
12
+ # rvmrc_rvm_version="1.12.3 (stable)" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+
18
+ # First we attempt to load the desired environment directly from the environment
19
+ # file. This is very fast and efficient compared to running through the entire
20
+ # CLI and selector. If you want feedback on which environment was used then
21
+ # insert the word 'use' after --create as this triggers verbose mode.
22
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
23
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
24
+ then
25
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
26
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
27
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
28
+ else
29
+ # If the environment file has not yet been created, use the RVM CLI to select.
30
+ rvm --create "$environment_id" || {
31
+ echo "Failed to create RVM environment '${environment_id}'."
32
+ return 1
33
+ }
34
+ fi
35
+
36
+ # If you use bundler, this might be useful to you:
37
+ # if [[ -s Gemfile ]] && {
38
+ # ! builtin command -v bundle >/dev/null ||
39
+ # builtin command -v bundle | grep $rvm_path/bin/bundle >/dev/null
40
+ # }
41
+ # then
42
+ # printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
43
+ # gem install bundler
44
+ # fi
45
+ # if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
46
+ # then
47
+ # bundle install | grep -vE '^Using|Your bundle is complete'
48
+ # fi
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in air18n.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jason Katz-Brown
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Air18n
2
+
3
+ Rails plugin for amazing internationalization.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'air18n'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install air18n
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.pattern = "spec/**/*_spec.rb"
7
+ end
8
+
9
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/air18n/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jason Katz-Brown", "Nick Grandy", "Naseem Hakim", "Horace Ko", "Matt Baker"]
6
+ gem.email = ["jason@airbnb.com", "nick@airbnb.com", "naseem@airbnb.com", "horace@airbnb.com", "matt.baker@airbnb.com"]
7
+ gem.summary = %q{Dynamic I18n backend}
8
+ gem.description = %q{Rails plugin with dyanmic I18n backend for amazing internationalization.}
9
+ gem.homepage = "http://www.github.com/airbnb/air18n"
10
+
11
+ gem.add_runtime_dependency 'i18n', '>= 0.5.0'
12
+ gem.add_runtime_dependency 'activerecord', '~> 3.0'
13
+ gem.add_development_dependency "rspec"
14
+ gem.add_development_dependency 'sqlite3'
15
+ gem.add_development_dependency 'guard'
16
+ gem.add_development_dependency 'guard-rspec'
17
+ gem.add_development_dependency 'factory_girl', '~> 3.0'
18
+
19
+ gem.files = `git ls-files`.split($\)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.name = "air18n"
23
+ gem.require_paths = ["lib"]
24
+ gem.version = Air18n::VERSION
25
+ end
@@ -0,0 +1,83 @@
1
+ require "i18n"
2
+
3
+ require 'air18n/class_methods'
4
+ require 'air18n/backend'
5
+ require 'air18n/priority'
6
+
7
+ require 'air18n/reflection'
8
+
9
+ module Air18n
10
+ AIR18N_RESERVED_KEYS = [:routes_context, :suppress_ct, :default_is_low_priority, :disable_xss_check]
11
+
12
+ def self.included(base)
13
+ base.class_eval do
14
+ # Currencies that correspond to users who want distance unit to be
15
+ # miles.
16
+ self.const_set(:MILES_CURRENCIES, ['USD', 'GBP'])
17
+
18
+ class << self
19
+ extend ActiveSupport::Memoizable
20
+
21
+ module_eval do
22
+ include ClassMethods
23
+ end
24
+
25
+ # Country, as a symbol. This is separate from locale, and should set based
26
+ # only on the domain name or IP address or some other kind of geographic
27
+ # location.
28
+ attr_accessor :country
29
+
30
+ # Like country; set it to :US if we're on the global domain to show
31
+ # correct terms/conditions
32
+ attr_accessor :legal_country
33
+
34
+ # An instance of I18n::Locale::Fallbacks for computing locale fallbacks.
35
+ attr_reader :fallbacks
36
+
37
+ # Manages prioritization of translation keys. If set, must implement
38
+ # methods of Air18n::Priority.
39
+ attr_accessor :priority
40
+
41
+ # Used as routes context if not specified in options passed to translate().
42
+ attr_accessor :fallback_routes_context
43
+
44
+ # Currency for the current request.
45
+ @currency = 'USD'
46
+ attr_reader :currency
47
+
48
+ # Distance unit, 'MI' or 'KM'.
49
+ @distance_unit = 'KM'
50
+ attr_reader :distance_unit
51
+
52
+ # Whether to wrap phrases in HTML that allows them to be translated by
53
+ # the user from directly on the page.
54
+ @contextual_translation = false
55
+
56
+ alias_method :t_with_routes_context, :translate_with_routes_context
57
+ alias_method_chain :t, :routes_context
58
+ alias_method_chain :translate, :routes_context
59
+
60
+ memoize :localized_path_explicit_locale
61
+ memoize :language_from_locale
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Make sure we don't try to interpolate our Air18n-specific reserved keys.
68
+ I18n::Backend::Base.module_eval do
69
+ def interpolate_with_new_reserved_keys(locale, string, values = {})
70
+ interpolate_without_new_reserved_keys(locale, string, values.except(*Air18n::AIR18N_RESERVED_KEYS))
71
+ end
72
+ alias_method_chain :interpolate, :new_reserved_keys
73
+ end
74
+
75
+ I18n.send(:include, Air18n)
76
+ I18n.reset(:en)
77
+
78
+ # This should really be done by default! Make the simple backend also use
79
+ # fallbacks.
80
+ I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
81
+
82
+ # Use the fancy version of I18n locale tags.
83
+ I18n::Locale::Tag.implementation = I18n::Locale::Tag::Rfc4646
@@ -0,0 +1,194 @@
1
+ require 'i18n'
2
+
3
+ require 'air18n/logging_helper'
4
+ require 'air18n/prim_and_proper'
5
+ require 'air18n/pseudo_locales'
6
+
7
+ module Air18n
8
+ class Backend
9
+ include I18n::Backend::Base
10
+
11
+ attr_accessor :translation_data, :initialized, :phrase_screenshots
12
+
13
+ # Stores translations for a given locale.
14
+ def store_translations locale, data, options = {}
15
+ @translation_data ||= {}
16
+ @translation_data[locale.to_sym] = data
17
+ end
18
+
19
+ def available_locales
20
+ if @translation_data
21
+ @translation_data.inject([]) do |carry, (locale, translations)|
22
+ carry << locale unless translations.empty?
23
+ carry
24
+ end
25
+ else
26
+ []
27
+ end
28
+ end
29
+
30
+ def reload_translations(locales)
31
+ # We don't want to load translations of unused phrases, so we exclude
32
+ # unused from the set of translations we initialize.
33
+ translations_hash = PhraseTranslation.translations_for_locales(locales, :exclude_unused => true)
34
+
35
+ # In development, people might not be running the counter server to keep
36
+ # track of which phrases are used. In this ase, load all phrases, except UGC.
37
+ if translations_hash.all? { |locale, translations| translations.empty? }
38
+ translations_hash = PhraseTranslation.translations_for_locales(locales, :exclude_ugc => true)
39
+ end
40
+
41
+ num_translations = translations_hash.inject(0) { |carry, (locale, translations)| carry + translations.size }
42
+ LoggingHelper.info "Loaded #{num_translations} translations for locales #{locales.inspect}."
43
+
44
+ number_of_translations = 0
45
+ translations_hash.each_pair do |locale, data|
46
+ store_translations(locale, data)
47
+ number_of_translations += data.size
48
+
49
+ @translations_last_loaded_at ||= {}
50
+ @translations_last_loaded_at[locale] = Time.now
51
+ end
52
+
53
+ LoggingHelper.info "Translation data size: #{Marshal.dump(@translation_data).size}"
54
+ end
55
+
56
+ def init_translations(locale)
57
+ @phrase_screenshots ||= PhraseScreenshot.all_phrase_urls
58
+ if @translation_data.nil? || !@translation_data.include?(locale)
59
+ reload_translations([locale])
60
+ end
61
+ end
62
+
63
+ def check_for_new_translations(locale)
64
+ # When TranslateController makes a new translation, it sets
65
+ # translations_last_updated_at to the current time in the cache.
66
+ # If we haven't reset the i18n backends since then, we take the opportunity to reset them.
67
+ last_updated_at = Rails.cache.read("#{locale}_translations_last_updated_at")
68
+ if last_updated_at && last_updated_at.to_i > @translations_last_loaded_at[locale].to_i
69
+ reload_translations([locale])
70
+ end
71
+ end
72
+
73
+ def guess_translation(text, orig_locale, other_locale)
74
+ @prim_and_proper ||= PrimAndProper.new
75
+ @prim_and_proper.guess(text, orig_locale, other_locale)
76
+ end
77
+
78
+ # We will only take 10 screenshots per Rails instance per key.
79
+ # This is to avoid having to moderate 200 screenshots for every new site-wide
80
+ # phrase.
81
+ # Because this is a per-Rails-thread cap, we call it approximate.
82
+ APPROX_MAX_SCREENSHOTS_PER_KEY = 10
83
+
84
+ # This method does the meat of Air18n functionality.
85
+ # 1) Finds the translations for specified key and locale, falling back to
86
+ # appropriate locales if necessary based on I18n.fallbacks.
87
+ # 2) Queues up screenshot-taking jobs if we don't have a screenshot for a
88
+ # (key, options[:routes_context])
89
+ # 3) Populates the phrases database if key and options[:default] aren't
90
+ # already in there.
91
+ # 4) Updates the phrases database if options[:default] doesn't match the
92
+ # English text in the database. Alternatively, you can use
93
+ # options[:default_is_low_priority] if you don't want this behavior.
94
+ # 5) Makes the :xx locale translations.
95
+ # 6) Asks PrimAndProper to make en-GB translations and other best-guess
96
+ # translations if appropriate.
97
+ def lookup(locale, key, scope = [], options = {})
98
+ # Useful i18n logging for debugging translation lookup problems.
99
+ # LoggingHelper.info "Lookup! key is #{key.inspect}, options are #{options.inspect}"
100
+ # caller.each { |l| LoggingHelper.info " " + l }
101
+
102
+ # Sometimes translate() is called with an array of keys. We don't handle that case.
103
+ if key.blank? || !key.is_a?(String)
104
+ return nil
105
+ end
106
+
107
+ # Force locale to symbol to allow e.g.
108
+ # I18n.t('foo', :default => 'Foo', :locale => @current_user.preferred_locale)
109
+ locale = locale.to_sym
110
+
111
+ default = options[:default] && options[:default].is_a?(String) && options[:default]
112
+ overrides_previous_default = !options[:default_is_low_priority]
113
+
114
+ init_translations(locale)
115
+
116
+ # Only create new screenshots while using default locale, and ignore keys
117
+ # that come in hash format or with a wacky namespace
118
+ if options[:routes_context] && locale == I18n.default_locale
119
+ # Check to see if we have screenshot for this phrase/routes context combo
120
+ unless @phrase_screenshots[key] && (@phrase_screenshots[key].include?(options[:routes_context]) || @phrase_screenshots[key].size >= APPROX_MAX_SCREENSHOTS_PER_KEY)
121
+ I18n.phrase_needs_screenshot(options[:routes_context], {key => @translation_data[I18n.default_locale][key] || default || key})
122
+ @phrase_screenshots[key] = (@phrase_screenshots[key] || []) << options[:routes_context]
123
+ end
124
+ end
125
+
126
+ fallback_chain = I18n.fallbacks_for(locale)
127
+ result = nil
128
+ locale_fallen_back_to = nil
129
+ for fallback_locale in fallback_chain
130
+ if @translation_data.include?(fallback_locale)
131
+ result = @translation_data[fallback_locale][key]
132
+ locale_fallen_back_to = fallback_locale
133
+ break if result
134
+ end
135
+ end
136
+
137
+ if result && locale_fallen_back_to != locale
138
+ # See if we can guess a translation instead of using the fallback
139
+ # translation.
140
+ guess = guess_translation(result, locale_fallen_back_to, locale)
141
+ result = guess if guess
142
+ end
143
+
144
+ # If there was a default-language translation, check if it matches the default.
145
+ # If the phrase is not in the phrases table, it will be created.
146
+ if locale == I18n.default_locale && default && result != default
147
+ # If it doesn't, we need to update the 'phrases' table so that
148
+ # the 'value' column reflects the latest English default text.
149
+ LoggingHelper.info "Default English text for key '#{key}' changed! Old default '#{result}' != new default '#{default}'. Route context: #{options[:routes_context]}"
150
+ phrase = Phrase.find_or_create_by_key(key)
151
+ if phrase
152
+ if (overrides_previous_default && phrase.value != default) ||
153
+ (!overrides_previous_default && phrase.value != default && phrase.value.blank?)
154
+ phrase.value = default
155
+
156
+ begin
157
+ phrase.save
158
+ rescue Exception => e
159
+ # If many requests happen simultaneously for a page with a new
160
+ # phrase, a "duplicate entry" exception will happen naturally.
161
+ # And if phrase creation fails for some other reason, we will try
162
+ # again to create it next time automatically.
163
+ end
164
+ end
165
+ end
166
+
167
+ @translation_data[locale][key] = default
168
+ result = default
169
+ end
170
+
171
+ # Helps debug translation loading and default setting.
172
+ # LoggingHelper.info "Airbnb Backend looking up key #{key.inspect}. result is #{result.inspect}. Default is #{default.inspect}"
173
+
174
+ # if a default is given, use it here
175
+ if result.nil? && default
176
+ result = default
177
+ end
178
+
179
+ unless options[:disable_xss_check]
180
+ xss_detection = XssDetector::safe?(default, result)
181
+ if !xss_detection[:safe]
182
+ # Kill the translation if the result is unsafe.
183
+ LoggingHelper.error "Killing unsafe translation! Default is #{default.inspect}, result is #{result.inspect}, reason for kill is #{xss_detection[:result]}"
184
+ result = default
185
+ end
186
+ end
187
+
188
+ # Handle pseudo-locales.
189
+ result = PseudoLocales.translate(locale, result)
190
+
191
+ result
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,292 @@
1
+ require 'air18n/less_silly_chain'
2
+ require 'air18n/logging_helper'
3
+ require 'air18n/xss_detector'
4
+
5
+ module Air18n
6
+ module ClassMethods
7
+ # Returns only the language part of the current locale, as a symbol.
8
+ # E.g. "pt" if the locale is pt-BR.
9
+ def language
10
+ language_from_locale full_locale
11
+ end
12
+
13
+ # Returns the language code of I18n.default_locale, as a symbol.
14
+ def default_language
15
+ language_from_locale default_locale
16
+ end
17
+
18
+ # Deprecated.
19
+ # This returns the same as language().
20
+ # TODO(jason) Switch all use of locale() to either language() or
21
+ # full_locale().
22
+ def locale_with_language_hack
23
+ language
24
+ end
25
+
26
+ # Returns full locale as a symbol, like :es, :en, :"es-419" (Latin-American
27
+ # Spanish) or :"pt-BR" or :"en-GB".
28
+ def full_locale
29
+ config.locale
30
+ end
31
+
32
+ # The one true way to set the locale.
33
+ def full_locale=(locale)
34
+ config.locale = locale.to_sym
35
+ end
36
+
37
+ # Deprecated.
38
+ # Sets locale. Using full_locale= is preferable.
39
+ def locale=(locale)
40
+ LoggingHelper.info "I18n.locale=() is deprecated. Use I18n.full_locale=()."
41
+ self.full_locale = locale
42
+ end
43
+
44
+ # For a locale (like :en, :en-GB, :pt-PT), returns the "language"
45
+ # part (like :en or :pt).
46
+ # If locale is invalid or nil, returns the default language.
47
+ def language_from_locale locale
48
+ tags = I18n::Locale::Tag.tag(locale)
49
+ tags ? tags.language.to_sym : language_from_locale(I18n.default_locale)
50
+ end
51
+
52
+ # Gets a hash containing phrases that need to be screenshot in the
53
+ # specified routes_context.
54
+ # After this method returns the phrases, it forgets about them, so you
55
+ # can only retrieve them once.
56
+ def retrieve_phrases_needing_screenshot(routes_context)
57
+ return {} unless @phrases_needing_screenshot && @phrases_needing_screenshot.include?(routes_context)
58
+ @phrases_needing_screenshot.delete(routes_context)
59
+ end
60
+
61
+ def phrase_needs_screenshot(routes_context, phrase)
62
+ @phrases_needing_screenshot ||= Hash.new { |h, routes_context| h[routes_context] = {} }
63
+ @phrases_needing_screenshot[routes_context].merge!(phrase)
64
+ end
65
+
66
+ def contextual_translation
67
+ if full_locale != default_locale
68
+ @contextual_translation
69
+ else
70
+ false
71
+ end
72
+ end
73
+
74
+ def contextual_translation= x
75
+ @contextual_translation= x
76
+ end
77
+
78
+ # Returns OrderedHash mapping key => times looked up today or in the last week.
79
+ def still_used_keys_hash
80
+ # If you want to test with the full allotment of still-used phrases from
81
+ # production, do this on mainframe:
82
+ # File.open('/tmp/still_used', 'w') { |f| f.write Counter.retrieve_counts_from_today_and_last_week("t").to_json }
83
+ # Then copy /tmp/still_used to your local machine, and uncomment the next line.
84
+ # @still_used_keys ||= File.open('/tmp/still_used') { |f| JSON.parse(f.read()) }
85
+
86
+ @priority ? @priority.key_usage : {}
87
+ end
88
+
89
+ # Returns list of keys looked up today or in the last week.
90
+ def still_used_keys
91
+ still_used_keys_hash.keys
92
+ end
93
+
94
+ # Returns list of phrase IDs that are still used.
95
+ def still_used_phrase_ids
96
+ @still_used_phrase_ids ||= begin
97
+ key_to_phrase_id = {}
98
+ # Uses raw SQL for speed.
99
+ Phrase.connection.select_all("SELECT `id`, `key` FROM phrases").each do |record|
100
+ key_to_phrase_id[record['key']] = record['id']
101
+ end
102
+
103
+ still_used_keys.map do |key|
104
+ LoggingHelper.error "Mystery phrase key: #{key}" if !key_to_phrase_id.include? key
105
+ key_to_phrase_id[key]
106
+ end
107
+ end
108
+ end
109
+
110
+ # Phrases that are from user-generated content (like hosting names) have a
111
+ # key prefixed with "ugc.".
112
+ def phrase_key_is_ugc?(key)
113
+ key.starts_with?('ugc.')
114
+ end
115
+
116
+ # Returns true if key should be translated.
117
+ # A key needs to be translated if it is either UGC or was looked up by t()
118
+ # today or in the last week.
119
+ def needs_translation?(key)
120
+ phrase_key_is_ugc?(key) || still_used_keys_hash.include?(key)
121
+ end
122
+
123
+ # Create Air18n backend with given default_locale and translation prioritizer.
124
+ def reset(default_locale)
125
+ self.default_locale = default_locale
126
+
127
+ # Use locale fallbacks that always include default locale as the last
128
+ # fallback.
129
+ @fallbacks = I18n::Locale::Fallbacks.new(default_locale)
130
+
131
+ # Make the single instance of Air18n::Backend.
132
+ @air18n_backend = Air18n::Backend.new
133
+
134
+ # This activates Air18n by putting Air18n::Backend before I18n.backend.
135
+ self.backend = Air18n::LessSillyChain.new(@air18n_backend, I18n::Backend::Simple.new)
136
+ end
137
+
138
+ # Return the fallback locales for the_locale.
139
+ # If opts[:exclude_default] is set, the default locale, which is otherwise
140
+ # always the last one in the returned list, will be excluded.
141
+ #
142
+ # For example, fallbacks_for(:"pt-BR") is [:"pt-BR", :pt, :en] with
143
+ # exclude_default = false and [:"pt-BR", :pt] with exclude_default = true.
144
+ def fallbacks_for(the_locale, opts = {})
145
+ # We dup() because otherwise the fallbacks hash might itself be modified
146
+ # by the user of this method, and that would be terrible.
147
+ ret = @fallbacks[the_locale].dup
148
+
149
+ # We make two assumptions here:
150
+ # 1) There is only one default locale (that is, it has no less-specific
151
+ # children)
152
+ # 1) The default locale is just a language. (Like :en, and not :"en-US".)
153
+ if opts[:exclude_default] && ret.last == self.default_locale && ret.last != language_from_locale(the_locale)
154
+ ret.pop
155
+ end
156
+
157
+ ret
158
+ end
159
+
160
+ # Returns whether the_locale has other_locale in its non-default fallbacks.
161
+ def falls_back_to?(the_locale, other_locale)
162
+ fallbacks_for(the_locale, :exclude_default => true).include?(other_locale)
163
+ end
164
+
165
+ def check_for_new_translations
166
+ @air18n_backend.check_for_new_translations(I18n.full_locale)
167
+ end
168
+
169
+ def currency=(c)
170
+ @currency = c
171
+ @distance_unit = self::MILES_CURRENCIES.include?(c) ? "MI" : "KM"
172
+ end
173
+
174
+ # Parses a date using the current locale setting.
175
+ # 2-digit years are interpreted as 19XX (>= 69) or 20XX (< 69).
176
+ def parse_date s, locale = nil
177
+ return s if s.nil?
178
+
179
+ if /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\Z/.match s
180
+ # If this looks like an ISO date, YYYY-mm-dd, let [Date.parse] handle
181
+ # parsing since [Date.strptime] varies by platform (darwin vs. linux)
182
+ Date.parse(s, true)
183
+ else
184
+ locale ||= I18n.full_locale
185
+ s = s.to_str
186
+ format = I18n.t('date.formats.default', :locale => locale.to_sym)
187
+ begin
188
+ date = Date.strptime(s, format)
189
+ if date.year < 100
190
+ date = Date.strptime(s, format.gsub("%Y", "%y"))
191
+ end
192
+ rescue
193
+ # When date is not parsable in user's locale attempt default
194
+ # date handling for cases like booking where dates can be
195
+ # in the international format "%Y-%m-%d". Failure to parse
196
+ # after this will raise an error.
197
+ date = Date.parse(s, true)
198
+ end
199
+ date
200
+ end
201
+ end
202
+
203
+ # Use when formatted string will be used by jQuery UI or calendar text fields
204
+ def format_date d, locale = nil
205
+ # temp handle iso date strings
206
+ if d.is_a?(String)
207
+ d = Date.parse(d)
208
+ end
209
+
210
+ locale ||= I18n.full_locale
211
+ format = I18n.t('date.formats.default', :locale => locale.to_sym)
212
+ if d.nil?
213
+ format.gsub('%m', 'mm').gsub('%d', 'dd').gsub('%Y', 'yy')
214
+ else
215
+ I18n.l(d, :format => format)
216
+ end
217
+ end
218
+
219
+ # Replaces LOCALE and COUNTRY, if they are present in path, with
220
+ # I18n.locale and I18n.country. You can explicitly set the locale and
221
+ # country with options[:locale] and options[:country].
222
+ #
223
+ # Use this if the resource you reference lives under public/. For example,
224
+ # to reference an image file, pass just call e.g.
225
+ # localized_public_path("/images/buttons/awesome-LOCALE.png").
226
+ def localized_public_path(path_relative_to_public, options = {})
227
+ localized_path(path_relative_to_public, options.merge({:absolute_prefix_on_disk => Rails.public_path}))
228
+ end
229
+
230
+ # Replaces LOCALE and COUNTRY, if they are present in path, with
231
+ # I18n.locale and I18n.country. You can explicitly set the locale and
232
+ # country with options[:locale] and options[:country].
233
+ #
234
+ # If path is not absolute, you must also set
235
+ # options[:absolute_prefix_on_disk] so that absolute_prefix_on_disk+path is
236
+ # a valid absolute path (after locale or country interpolations).
237
+ # If you are referencing something in public/, you should use
238
+ # localized_public_path.
239
+ #
240
+ # If the file does not exist after interpolation, LOCALE will be replaced
241
+ # with "en" and COUNTRY will be replaced with "us".
242
+ def localized_path(path, options = {})
243
+ options[:locale] ||= I18n.locale
244
+ options[:country] ||= I18n.country
245
+ localized_path_explicit_locale(path, options)
246
+ end
247
+
248
+ # Same as localized_path but explicitly uses options[:locale] and
249
+ # options[:country] so that it can be memoized.
250
+ def localized_path_explicit_locale(path, options = {})
251
+ replaced = path.gsub('LOCALE', options[:locale].to_s).gsub('COUNTRY', options[:country].to_s)
252
+
253
+ # TODO(jason) Use full locale and its fallbacks.
254
+ absolute_path = options[:absolute_prefix_on_disk] ? File.join(options[:absolute_prefix_on_disk], "images", replaced) : replaced
255
+ if File.exist?(absolute_path)
256
+ replaced
257
+ else
258
+ path.gsub('COUNTRY', "US").gsub('LOCALE', "en")
259
+ end
260
+ end
261
+
262
+ # Add :routes_context option, which is used for translation screenshotting.
263
+ # I18n.fallback_routes_context is set by an ApplicationController
264
+ # before_filter.
265
+ def translate_with_routes_context(key, options = {})
266
+ if !options.include?(:routes_context) && I18n.fallback_routes_context
267
+ options = options.merge(:routes_context => I18n.fallback_routes_context)
268
+ end
269
+ result = translate_without_routes_context(key, options)
270
+
271
+ # Handle contextual translation.
272
+ if result && I18n.contextual_translation && (!options[:suppress_ct])
273
+ phrase = Phrase.by_key(key)
274
+
275
+ if phrase
276
+ # Provides styling to wrap a string into a div so that it can be
277
+ # clicked on for easy translation.
278
+ result = "<phrase id='#{Digest::MD5.hexdigest(result + rand(1000).to_s)[0..8]}' data-phrase-id='#{phrase.id}'>#{result}</phrase>"
279
+ end
280
+ end
281
+
282
+ if key.is_a?(String)
283
+ # Handle bookkeeping of which translation keys are still used.
284
+ @priority.key_used(key) if @priority
285
+ end
286
+
287
+ result = result.html_safe if result && result.respond_to?(:html_safe)
288
+
289
+ result
290
+ end
291
+ end
292
+ end