air18n 0.0.1

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