air18n 0.1.15 → 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,7 @@ require 'i18n'
3
3
  require 'air18n/logging_helper'
4
4
  require 'air18n/prim_and_proper'
5
5
  require 'air18n/pseudo_locales'
6
+ require 'air18n/smart_count'
6
7
 
7
8
  module Air18n
8
9
  class Backend
@@ -111,6 +112,8 @@ module Air18n
111
112
  # 5) Makes the :xx locale translations.
112
113
  # 6) Asks PrimAndProper to make en-GB translations and other best-guess
113
114
  # translations if appropriate.
115
+ # 7) Chooses the correct translation form based on options[:smart_count] if
116
+ # present.
114
117
  def lookup(locale, key, scope = [], options = {})
115
118
  # Useful i18n logging for debugging translation lookup problems.
116
119
  # LoggingHelper.info "Lookup! key is #{key.inspect}, options are #{options.inspect}"
@@ -202,7 +205,7 @@ module Air18n
202
205
  end
203
206
 
204
207
  unless options[:disable_xss_check]
205
- xss_detection = XssDetector::safe?(default, result)
208
+ xss_detection = XssDetector::safe?(default, result, I18n.default_locale, locale_fallen_back_to)
206
209
  if !xss_detection[:safe]
207
210
  # Kill the translation if the result is unsafe.
208
211
  LoggingHelper.error "Killing unsafe translation! Default is #{default.inspect}, result is #{result.inspect}, reason for kill is #{xss_detection[:result]}"
@@ -210,9 +213,15 @@ module Air18n
210
213
  end
211
214
  end
212
215
 
216
+ # Strip whitespace from both sides. For fun?
217
+ result = result.strip if result.present?
218
+
213
219
  # Handle pseudo-locales.
214
220
  result = PseudoLocales.translate(locale, result)
215
221
 
222
+ # Handle smart counts.
223
+ result = SmartCount.choose(result, locale_fallen_back_to, options[SmartCount::INTERPOLATION_VARIABLE_NAME])
224
+
216
225
  result
217
226
  end
218
227
  end
@@ -14,7 +14,9 @@ module Air18n
14
14
  before_create :set_latest
15
15
  before_create :set_source_hash
16
16
 
17
+ validate :check_plural_forms
17
18
  validate :check_matching_variables
19
+ validate :check_max_length
18
20
 
19
21
  # Sets is_latest of this translation, and removes the is_latest flag from all
20
22
  # previous translations.
@@ -296,7 +298,14 @@ module Air18n
296
298
  true
297
299
  end
298
300
 
299
- def check_matching_variables
301
+ def check_plural_forms
302
+ result = SmartCount::valid?(self.value, self.locale)
303
+ if !result[:valid]
304
+ self.errors.add(:value, result[:reason])
305
+ end
306
+ end
307
+
308
+ def check_max_length
300
309
  if /maxlength:(\d+)/ =~ key
301
310
  max_length = $1.to_i
302
311
  length = value.size
@@ -304,29 +313,39 @@ module Air18n
304
313
  self.errors.add(:value, "Translation has #{length} characters; maximum length #{max_length} characters.")
305
314
  end
306
315
  end
307
- unless variables_match?
308
- our_variables = self.variables
309
- their_variables = self.phrase.variables
310
- our_extra = our_variables - their_variables
311
- their_extra = their_variables - our_variables
312
- problems = []
313
- def quote(vars)
314
- vars.map { |var| "%{#{var}}" }.join(", ")
315
- end
316
- unless their_extra.empty?
317
- problems << "Var #{quote their_extra} missing from translation"
318
- end
319
- unless our_extra.empty?
320
- problems << "Var #{quote our_extra} should not be in translation"
316
+ end
317
+
318
+ def check_matching_variables
319
+ our_variables = self.variables
320
+ their_variables = self.phrase.variables
321
+
322
+ if SmartCount::applies?(self.phrase.value) || SmartCount::applies?(self.value)
323
+ our_variables = SmartCount::dedupe_things_like_tags_or_variables(
324
+ self.locale, our_variables)
325
+ their_variables = SmartCount::dedupe_things_like_tags_or_variables(
326
+ I18n.default_locale, their_variables)
327
+ end
328
+
329
+ LoggingHelper.info "our variables: #{our_variables.inspect}"
330
+ LoggingHelper.info "their variables: #{their_variables.inspect}"
331
+
332
+ our_extra = our_variables - their_variables
333
+ their_extra = their_variables - our_variables
334
+ problems = []
335
+ if !their_extra.empty? || !our_extra.empty?
336
+ if !their_extra.empty?
337
+ problems << "Var #{quote_vars their_extra} missing from translation"
321
338
  end
322
- unless problems.empty?
323
- self.errors.add(:value, problems.join('; '))
339
+ if !our_extra.empty?
340
+ problems << "Var #{quote_vars our_extra} should not be in translation"
324
341
  end
342
+ elsif our_variables.group_by{|t| t} != their_variables.group_by{|t| t}
343
+ # This is to catch smart-count-related cases.
344
+ problems << "Vars don't match: #{quote_vars their_variables} vs. #{quote_vars our_variables}"
345
+ end
346
+ unless problems.empty?
347
+ self.errors.add(:value, problems.join('; '))
325
348
  end
326
- end
327
-
328
- def variables_match?
329
- self.variables == self.phrase.variables
330
349
  end
331
350
 
332
351
  def variables
@@ -335,10 +354,10 @@ module Air18n
335
354
 
336
355
  def self.detect_variables(search_in)
337
356
  case search_in
338
- when String then Set.new(search_in.scan(/\{\{(\w+)\}\}/).flatten + search_in.scan(/\%\{(\w+)\}/).flatten)
357
+ when String then search_in.scan(/\{\{(\w+)\}\}/).flatten + search_in.scan(/\%\{(\w+)\}/).flatten
339
358
  when Array then search_in.inject(Set[]) { |carry, item| carry + detect_variables(item) }
340
359
  when Hash then search_in.values.inject(Set[]) { |carry, item| carry + detect_variables(item) }
341
- else Set[]
360
+ else []
342
361
  end
343
362
  end
344
363
 
@@ -403,5 +422,10 @@ module Air18n
403
422
  end
404
423
  return [true]
405
424
  end
425
+
426
+ private
427
+ def quote_vars(vars)
428
+ vars.map { |var| "%{#{var}}" }.join(", ")
429
+ end
406
430
  end
407
431
  end
@@ -0,0 +1,174 @@
1
+ module Air18n
2
+ module SmartCount
3
+ # Class for choosing the right plural form of a phrase based on an
4
+ # interpolation variable with special name "smart_count" (set below in
5
+ # OPTION_NAME constant).
6
+ #
7
+ # If a phrase uses the smart_count interpolation variable, translators can
8
+ # specify multiple translations, one per possible pluralization form of the
9
+ # language. If there is more than one pluralization form, translations are
10
+ # separated by ";" (set below in DELIMITER constant).
11
+
12
+ INTERPOLATION_VARIABLE_NAME = :smart_count
13
+ INTERPOLATION_VARIABLE_PLACEHOLDER = "%{#{INTERPOLATION_VARIABLE_NAME}}"
14
+ DELIMITER = ';'
15
+
16
+ # C.F.
17
+ # https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
18
+ # http://translate.sourceforge.net/wiki/l10n/pluralforms
19
+ PLURAL_TYPES = {
20
+ :chinese_like => {
21
+ :num_forms => 1,
22
+ :doc => ['One form for all counts.'],
23
+ :rule => lambda { |n| 0 } },
24
+
25
+ :german_like => {
26
+ :num_forms => 2,
27
+ :doc => ['Count is 1', 'Everything else (0, 2, 3, ...)'],
28
+ :rule => lambda { |n| n != 1 ? 1 : 0 } },
29
+
30
+ :french_like => {
31
+ :num_forms => 2,
32
+ :doc => ['Count is 0 or 1', 'Everything else (2, 3, 4, ...)'],
33
+ :rule => lambda { |n| n > 1 ? 1 : 0 } },
34
+
35
+ :russian_like => {
36
+ :num_forms => 3,
37
+ :doc => ['Count ends in 1, excluding 11 (1, 21, 31, ...)', 'Count ends in 2-4, excluding 12-14 (2, 3, 4, 22, ...)', 'Everything else (0, 5, 6, ...)'],
38
+ :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 } },
39
+
40
+ :czech_like => {
41
+ :num_forms => 3,
42
+ :doc => ['Count is 1', 'Count is 2, 3, 4', 'Everything else (0, 5, 6, ...)'],
43
+ :rule => lambda { |n| (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 } },
44
+
45
+ :polish_like => {
46
+ :num_forms => 3,
47
+ :doc => ['Count is 1', 'Count ends in 2-4, excluding 12-14 (2, 3, 4, 22, ...)', 'Everything else (0, 5, 6, ...)'],
48
+ :rule => lambda { |n| (n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2) } },
49
+
50
+ :icelandic_like => {
51
+ :num_forms => 2,
52
+ :doc => ['Count ends in 1, excluding 11 (1, 21, 31, ...)', 'Everything else (0, 2, 3, ...)'],
53
+ :rule => lambda { |n| (n % 10 != 1 || n % 100 == 11) ? 1 : 0 } },
54
+ }
55
+
56
+ PLURAL_TYPE_NAME_TO_LANGUAGES = {
57
+ :chinese_like => [:id, :ja, :ko, :ms, :th, :tr, :zh, ],
58
+ :german_like => [:da, :de, :en, :es, :fi, :el, :he, :hu, :it, :nl, :no, :pt, :sv, ],
59
+ :french_like => [:fr, :tl, ],
60
+ :russian_like => [:hr, :ru, ],
61
+ :czech_like => [:cs],
62
+ :polish_like => [:pl],
63
+ :icelandic_like => [:is],
64
+ }
65
+
66
+ LANGUAGE_TO_PLURAL_TYPE_NAME = PLURAL_TYPE_NAME_TO_LANGUAGES.inject({}) do |carry, (type, languages)|
67
+ languages.each { |l| carry[l] = type }
68
+ carry
69
+ end
70
+
71
+ module_function
72
+
73
+ # Chooses the right version of 'text' for the value of the 'smart_count' in
74
+ # 'locale'.
75
+ #
76
+ # Examples:
77
+ # choose("%{smart_count} bewertung; %{smart_count} bewertungen",
78
+ # :de, 0)
79
+ # => %{smart_count} bewertungen
80
+ # choose("%{smart_count} bewertung; %{smart_count} bewertungen",
81
+ # :de, 1)
82
+ # => %{smart_count} bewertung
83
+ # choose("%{smart_count} bewertung; %{smart_count} bewertungen",
84
+ # :de, 2)
85
+ # => %{smart_count} bewertungen
86
+ #
87
+ # If 'count' is nil or 'text' contains too few alternative versions, this
88
+ # method degrades gracefully by returning the first version of text.
89
+ #
90
+ # Returns if nil if text is blank.
91
+ def choose(text, locale, count)
92
+ return text if text.blank?
93
+
94
+ texts = text.split(DELIMITER)
95
+ if count
96
+ chosen_text = texts[plural_type_index(locale, count)] || texts.first
97
+ chosen_text.strip
98
+ else
99
+ texts.first.strip
100
+ end
101
+ end
102
+
103
+ def plural_type_name(locale)
104
+ LANGUAGE_TO_PLURAL_TYPE_NAME[I18n.language_from_locale(locale)] ||
105
+ LANGUAGE_TO_PLURAL_TYPE_NAME[I18n.default_language]
106
+ end
107
+
108
+ def plural_type_index(locale, count)
109
+ PLURAL_TYPES[plural_type_name(locale)][:rule].call(count)
110
+ end
111
+
112
+ def hint(text, locale)
113
+ if applies?(text)
114
+ PLURAL_TYPES[plural_type_name(locale)][:doc].join(DELIMITER + ' ')
115
+ end
116
+ end
117
+
118
+ def applies?(text)
119
+ !!text.index(INTERPOLATION_VARIABLE_PLACEHOLDER)
120
+ end
121
+
122
+ def num_forms(locale)
123
+ PLURAL_TYPES[plural_type_name(locale)][:num_forms]
124
+ end
125
+
126
+ # Takes a list of things (for example tags or variables), counts up how
127
+ # many of each thing there are, then returns a new list that has each thing
128
+ # but with a possibly different count.
129
+ #
130
+ # The count of each thing in the returned list is determined by:
131
+ # - If the count is a multiple of the number of plural forms for 'locale',
132
+ # count is divided by the number of plural forms.
133
+ # - Otherwise count is set to 0.
134
+ #
135
+ # The returned list is reordered so that all instances of the same thing
136
+ # are consecutive, and are ordered in the same order as the first of each
137
+ # thing in the original list.
138
+ #
139
+ # This method is useful for ensuring that HTML tags or interpolation
140
+ # variables match in smart-count phrases between languages that might have
141
+ # different numbers of plural forms.
142
+ def dedupe_things_like_tags_or_variables(locale, things)
143
+ return things if things.blank?
144
+
145
+ num_forms = num_forms(locale)
146
+ grouped_things = things.group_by { |t| t }
147
+ deduped = grouped_things.inject([]) do |carry, (thing, list_of_things)|
148
+ count_of_thing = list_of_things.size
149
+ if count_of_thing % num_forms == 0
150
+ deduped_count_of_thing = count_of_thing / num_forms
151
+ else
152
+ deduped_count_of_thing = 0
153
+ end
154
+ deduped_count_of_thing.times { carry << thing }
155
+ carry
156
+ end
157
+ end
158
+
159
+ def valid?(text, locale)
160
+ if applies?(text)
161
+ size = text.split(DELIMITER).size
162
+ num_forms = num_forms(locale)
163
+ if size == num_forms
164
+ { :valid => true }
165
+ else
166
+ { :valid => false,
167
+ :reason => "Must specify #{num_forms} forms separated by \"#{DELIMITER}\" (#{size} #{size == 1? 'was' : 'were'} specified)" }
168
+ end
169
+ else
170
+ { :valid => true }
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,3 +1,3 @@
1
1
  module Air18n
2
- VERSION = "0.1.15"
2
+ VERSION = "0.1.16"
3
3
  end
@@ -2,11 +2,19 @@ module Air18n
2
2
  module XssDetector
3
3
  module_function
4
4
 
5
- def safe?(text_a, text_b)
5
+ def safe?(text_a, text_b, locale_a, locale_b)
6
6
  text_a = text_a.to_s
7
7
  text_b = text_b.to_s
8
8
  tags_a = normalize_tags(extract_tags(text_a))
9
9
  tags_b = normalize_tags(extract_tags(text_b))
10
+ if SmartCount::applies?(text_a) || SmartCount::applies?(text_b)
11
+ LoggingHelper.info "tags_a were: #{tags_a.inspect}"
12
+ LoggingHelper.info "tags_b were: #{tags_b.inspect}"
13
+ tags_a = SmartCount::dedupe_things_like_tags_or_variables(locale_a, tags_a)
14
+ tags_b = SmartCount::dedupe_things_like_tags_or_variables(locale_b, tags_b)
15
+ LoggingHelper.info "tags_a now: #{tags_a.inspect}"
16
+ LoggingHelper.info "tags_b now: #{tags_b.inspect}"
17
+ end
10
18
  if has_dubious_escape_characters?(text_a) || has_dubious_escape_characters?(text_b)
11
19
  { :safe => false, :reason => 'Backslashes are not allowed' }
12
20
  elsif tags_a.group_by{|t| t} != tags_b.group_by{|t| t}
@@ -148,6 +148,31 @@ describe Air18n::Backend do
148
148
  end
149
149
  end
150
150
 
151
+ context 'Smart counting' do
152
+ it 'should count smartly' do
153
+ backend = Air18n::Backend.new
154
+ backend.lookup(
155
+ :en, 'smart count reviews', @scope,
156
+ :default => "%{smart_count} review; %{smart_count} reviews",
157
+ :smart_count => 1
158
+ ).should == '%{smart_count} review'
159
+ backend.lookup(
160
+ :en, 'smart count reviews', @scope,
161
+ :default => "%{smart_count} review; %{smart_count} reviews",
162
+ :smart_count => 3
163
+ ).should == '%{smart_count} reviews'
164
+
165
+ # If a phrase is untranslated in Chinese, this can turn into "3 review"
166
+ # on the Chinese site if we are not careful and use the Chinese plural
167
+ # rules for this English fallback. This test catches this bug.
168
+ backend.lookup(
169
+ :zh, 'smart count reviews', @scope,
170
+ :default => "%{smart_count} review; %{smart_count} reviews",
171
+ :smart_count => 3
172
+ ).should == '%{smart_count} reviews'
173
+ end
174
+ end
175
+
151
176
  context 'Keeping track of which phrases have screenshots' do
152
177
  before do
153
178
  @backend = Air18n::Backend.new
@@ -54,6 +54,73 @@ describe Air18n::PhraseTranslation do
54
54
  translation.errors.should include :value
55
55
  translation.errors[:value].should include "Var %{time_zone} missing from translation; Var %{time_zon} should not be in translation"
56
56
  end
57
+
58
+ context 'smart_count' do
59
+ before :all do
60
+ @phrase = FactoryGirl.create(:phrase, :key => 'variables with smart count reviews', :value => "%{smart_count} review in %{location};%{smart_count} reviews in %{location}")
61
+ end
62
+
63
+ it 'Should check variables with smart count in language with one plural form' do
64
+ translation = FactoryGirl.create(
65
+ :phrase_translation,
66
+ :phrase => @phrase,
67
+ :locale => :ja,
68
+ :key => 'variables with smart count reviews',
69
+ :value => '%{location}で%{smart_count}レビュー')
70
+ validated = translation.save
71
+ validated.should == true
72
+ translation.value = '%{smart_count}レビュー'
73
+ translation.save.should be_false
74
+ translation.errors.should include :value
75
+ translation.errors[:value].should include "Var %{location} missing from translation"
76
+ end
77
+
78
+ it 'Should check variables with smart count in language with two plural forms' do
79
+ translation = FactoryGirl.create(
80
+ :phrase_translation,
81
+ :phrase => @phrase,
82
+ :locale => :fr,
83
+ :key => 'variables with smart count reviews',
84
+ :value => 'a %{location} %{smart_count} commentaire; a %{location} %{smart_count} commentaires')
85
+ validated = translation.save
86
+ validated.should == true
87
+ translation.value = 'a %{location} %{smart_count} commentaire; a %{location} %{smart_countz} commentaires'
88
+ translation.save.should be_false
89
+ translation.errors.should include :value
90
+ translation.errors[:value].should include "Var %{smart_count} missing from translation"
91
+ end
92
+
93
+ it 'Should check variables with smart count in language with three plural forms' do
94
+ translation = FactoryGirl.create(
95
+ :phrase_translation,
96
+ :phrase => @phrase,
97
+ :locale => :pl,
98
+ :key => 'variables with smart count reviews',
99
+ :value => 'a %{location} %{smart_count} ends-in-one-except-eleven; a %{location} %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen; a %{location} %{smart_count} everything-else')
100
+ validated = translation.save
101
+ validated.should == true
102
+ translation.value = 'a %{location} %{smart_count} ends-in-one-except-eleven; a %{location} %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen; a %{smart_count} everything-else'
103
+
104
+ translation.save.should be_false
105
+ translation.errors.should include :value
106
+ translation.errors[:value].should include "Var %{location} missing from translation"
107
+ end
108
+
109
+ it 'Should complain about missing plural forms' do
110
+ translation = FactoryGirl.create(
111
+ :phrase_translation,
112
+ :phrase => @phrase,
113
+ :locale => :de,
114
+ :key => 'variables with smart count reviews',
115
+ :value => 'a %{location} %{smart_count} bewertung; a %{location} %{smart_count} bewertungen')
116
+ validated = translation.save
117
+ validated.should == true
118
+ translation.value = 'a %{location} %{smart_count} bewertung, a %{location} %{smart_count} bewertungen'
119
+ translation.save.should be_false
120
+ translation.errors.should include :value
121
+ translation.errors[:value].should include "Must specify 2 forms separated by \";\" (1 was specified)"
122
+ end
123
+ end
57
124
  end
58
125
 
59
126
  context 'helpers' do
@@ -0,0 +1,146 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'air18n/smart_count'
5
+
6
+ describe Air18n::SmartCount do
7
+ def test_choose(text, locale, answers)
8
+ answers.each_with_index do |answer, n|
9
+ Air18n::SmartCount::choose(text, locale, n).should == answer
10
+ end
11
+ end
12
+
13
+ describe "choose" do
14
+ it "should work in Japanese" do
15
+ review_text = "%{smart_count} レビュー"
16
+ answers =
17
+ ["%{smart_count} レビュー"] * 26
18
+ test_choose(review_text, :de, answers)
19
+ end
20
+
21
+ it "should work in German" do
22
+ review_text = "%{smart_count} bewertung; %{smart_count} bewertungen"
23
+ answers =
24
+ ["%{smart_count} bewertungen"] * 1 +
25
+ ["%{smart_count} bewertung"] * 1 +
26
+ ["%{smart_count} bewertungen"] * 24
27
+ test_choose(review_text, :de, answers)
28
+ end
29
+
30
+ it "should work in French" do
31
+ review_text = "%{smart_count} commentaire; %{smart_count} commentaires"
32
+ answers =
33
+ ["%{smart_count} commentaire"] * 2 +
34
+ ["%{smart_count} commentaires"] * 24
35
+ test_choose(review_text, :fr, answers)
36
+ end
37
+
38
+ it "should work in Russian" do
39
+ review_text = "%{smart_count} ends-in-one-except-eleven; %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen; %{smart_count} everything-else"
40
+ answers =
41
+ ["%{smart_count} everything-else"] * 1 +
42
+ ["%{smart_count} ends-in-one-except-eleven"] * 1 +
43
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
44
+ ["%{smart_count} everything-else"] * 16 +
45
+ ["%{smart_count} ends-in-one-except-eleven"] * 1 +
46
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
47
+ ["%{smart_count} everything-else"] * 1
48
+ test_choose(review_text, :ru, answers)
49
+ end
50
+
51
+ it "should work in Czech" do
52
+ review_text = "%{smart_count} one; %{smart_count} two-through-four; %{smart_count} everything-else"
53
+ answers =
54
+ ["%{smart_count} everything-else"] * 1 +
55
+ ["%{smart_count} one"] * 1 +
56
+ ["%{smart_count} two-through-four"] * 3 +
57
+ ["%{smart_count} everything-else"] * 21
58
+ test_choose(review_text, :cs, answers)
59
+ end
60
+
61
+ it "should work in Polish" do
62
+ review_text = "%{smart_count} one; %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen; %{smart_count} everything-else"
63
+ answers =
64
+ ["%{smart_count} everything-else"] * 1 +
65
+ ["%{smart_count} one"] * 1 +
66
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
67
+ ["%{smart_count} everything-else"] * 17 +
68
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
69
+ ["%{smart_count} everything-else"] * 1
70
+ test_choose(review_text, :pl, answers)
71
+ end
72
+ end
73
+
74
+ describe "degenerate choose" do
75
+ it "should work if too few translated forms are given" do
76
+ # One of the three Russian forms, the "everything else" form, is missing.
77
+ # The fallback should be the first form, "ends in one except eleven".
78
+ review_text = "%{smart_count} ends-in-one-except-eleven; %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"
79
+ answers =
80
+ ["%{smart_count} ends-in-one-except-eleven"] * 1 +
81
+ ["%{smart_count} ends-in-one-except-eleven"] * 1 +
82
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
83
+ ["%{smart_count} ends-in-one-except-eleven"] * 16 +
84
+ ["%{smart_count} ends-in-one-except-eleven"] * 1 +
85
+ ["%{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"] * 3 +
86
+ ["%{smart_count} ends-in-one-except-eleven"] * 1
87
+ test_choose(review_text, :ru, answers)
88
+ end
89
+
90
+ it "should work with nil count" do
91
+ review_text = "%{smart_count} ends-in-one-except-eleven; %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen"
92
+ Air18n::SmartCount::choose(review_text, :ru, nil).should == "%{smart_count} ends-in-one-except-eleven"
93
+ end
94
+
95
+ it "should work with nil text" do
96
+ Air18n::SmartCount::choose(nil, :ru, nil).should == nil
97
+ end
98
+ end
99
+
100
+ describe 'hint' do
101
+ it 'should work' do
102
+ Air18n::SmartCount::hint('hi there', :pl).should be_nil
103
+ Air18n::SmartCount::hint('%{smart_count} reviews', :pl).should ==
104
+ 'Count is 1; Count ends in 2-4, excluding 12-14 (2, 3, 4, 22, ...); Everything else (0, 5, 6, ...)'
105
+ end
106
+ end
107
+
108
+ describe 'dedupe_things_like_tags_or_variables' do
109
+ it 'should work' do
110
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
111
+ :fr, [:a, :b, :a, :b]).should == [:a, :b]
112
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
113
+ :ja, [:a, :b, :a, :b]).should == [:a, :a, :b, :b]
114
+
115
+ # Polish has 3 plural forms.
116
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
117
+ :pl, [:a, :b, :a, :b]).should == []
118
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
119
+ :pl, [:a, :b, :a, :b, :a, :b]).should == [:a, :b]
120
+
121
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
122
+ :pl, [:a, :b, :a, :a, :b, :a, :a, :b, :a]).should == [:a, :a, :b]
123
+
124
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
125
+ :pl, []).should == []
126
+ end
127
+
128
+ it 'should be unlenient if one variable is repeated less often than others' do
129
+ Air18n::SmartCount::dedupe_things_like_tags_or_variables(
130
+ :fr, [:a, :b, :a]).should == [:a]
131
+ end
132
+ end
133
+
134
+ describe "valid?" do
135
+ it 'should work' do
136
+ Air18n::SmartCount::valid?('%{smart_count} レビュー', :ja).should == { :valid => true }
137
+ Air18n::SmartCount::valid?('%{smart_count} commentaire; %{smart_count} commentaires', :fr).should == { :valid => true }
138
+ Air18n::SmartCount::valid?('%{smart_count} ends-in-one-except-eleven; %{smart_count} ends-in-two-through-four-except-twelve-through-fourteen; %{smart_count} everything-else', :pl).should == { :valid => true }
139
+
140
+ Air18n::SmartCount::valid?('%{smart_count} a; %{smart_count} b; %{smart_count} c', :fr).should == {
141
+ :valid => false,
142
+ :reason => "Must specify 2 forms separated by \";\" (3 were specified)"
143
+ }
144
+ end
145
+ end
146
+ end
@@ -1,6 +1,8 @@
1
- require 'spec_helper'
1
+ # encoding: utf-8
2
2
 
3
+ require 'spec_helper'
3
4
  require 'air18n/xss_detector'
5
+ require 'air18n/logging_helper'
4
6
 
5
7
  describe Air18n::XssDetector do
6
8
  describe "has_dubious_escape_characters?" do
@@ -12,31 +14,78 @@ describe Air18n::XssDetector do
12
14
 
13
15
  describe "safe?" do
14
16
  it "should detect safe texts" do
15
- Air18n::XssDetector::safe?("safe", "safe").should == { :safe => true }
16
- Air18n::XssDetector::safe?("<tag>", "<tag>").should == { :safe => true }
17
- Air18n::XssDetector::safe?("safe", { "safe" => "whatevs" }).should == { :safe => true }
17
+ Air18n::XssDetector::safe?("safe", "safe", :en, :fr).should == { :safe => true }
18
+ Air18n::XssDetector::safe?("<tag>", "<tag>", :en, :fr).should == { :safe => true }
19
+ Air18n::XssDetector::safe?("safe", { "safe" => "whatevs" }, :en, :fr).should == { :safe => true }
18
20
  end
19
21
 
20
22
  it "should detect dubious escape characters" do
21
- Air18n::XssDetector::safe?("Let's escape a \\' there", "safe").should == { :safe => false, :reason => "Backslashes are not allowed" }
22
- Air18n::XssDetector::safe?("safe", "Let's escape a \\' there").should == { :safe => false, :reason => "Backslashes are not allowed" }
23
+ Air18n::XssDetector::safe?("Let's escape a \\' there", "safe", :en, :fr).should == { :safe => false, :reason => "Backslashes are not allowed" }
24
+ Air18n::XssDetector::safe?("safe", "Let's escape a \\' there", :en, :fr).should == { :safe => false, :reason => "Backslashes are not allowed" }
23
25
  end
24
26
 
25
27
  it "should detect tag mismatches" do
26
- Air18n::XssDetector::safe?("<tag>", "safe").should == { :safe => false, :reason => "HTML tags don't match: #{['<tag>'].inspect} vs. #{[].inspect}" }
27
- Air18n::XssDetector::safe?("safe", "<tag>").should == { :safe => false, :reason => "HTML tags don't match: #{[].inspect} vs. #{['<tag>'].inspect}" }
28
- Air18n::XssDetector::safe?("<b>safe</b>", "<b>safe</b><b>").should == { :safe => false, :reason => "HTML tags don't match: #{['<b>', '</b>'].inspect} vs. #{['<b>', '</b>', '<b>'].inspect}" }
28
+ Air18n::XssDetector::safe?("<tag>", "safe", :en, :fr).should == { :safe => false, :reason => "HTML tags don't match: #{['<tag>'].inspect} vs. #{[].inspect}" }
29
+ Air18n::XssDetector::safe?("safe", "<tag>", :en, :fr).should == { :safe => false, :reason => "HTML tags don't match: #{[].inspect} vs. #{['<tag>'].inspect}" }
30
+ Air18n::XssDetector::safe?("<b>safe</b>", "<b>safe</b><b>", :en, :fr).should == { :safe => false, :reason => "HTML tags don't match: #{['<b>', '</b>'].inspect} vs. #{['<b>', '</b>', '<b>'].inspect}" }
29
31
  end
30
32
 
31
33
  it "should allow quote mismatches in tags" do
32
- Air18n::XssDetector::safe?("<a href='/help/question/280' target='_blank'>Why is this required?</a>", "<a href='/help/question/280' target='_blank'>...</a>").should == { :safe => true }
33
- Air18n::XssDetector::safe?("<tag href='hi'>", "<tag href=\"hi\">").should == { :safe => true }
34
+ Air18n::XssDetector::safe?("<a href='/help/question/280' target='_blank'>Why is this required?</a>", "<a href='/help/question/280' target='_blank'>...</a>", :en, :fr).should == { :safe => true }
35
+ Air18n::XssDetector::safe?("<tag href='hi'>", "<tag href=\"hi\">", :en, :fr).should == { :safe => true }
34
36
  end
35
37
 
36
38
  it "should allow reordering tags" do
37
39
  Air18n::XssDetector::safe?(
38
40
  "<span class='count'>%{count}</span> %{pluralized_review} left in <a href='#listings/search/%{encoded_search}' class='search_link'>%{city}, %{state} %{country}</a>",
39
- "in <a href='#listings/search/%{encoded_search}' class='search_link'>%{city}, %{state} %{country}</a> <span class='count'>%{count}</span> %{pluralized_review} left"
41
+ "in <a href='#listings/search/%{encoded_search}' class='search_link'>%{city}, %{state} %{country}</a> <span class='count'>%{count}</span> %{pluralized_review} left",
42
+ :en, :ko
43
+ ).should == { :safe => true }
44
+ end
45
+
46
+ it "should work with smart count with one plural form" do
47
+ Air18n::XssDetector::safe?(
48
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
49
+ "<span class='text'>レビュー</span> <span class='count'>%{smart_count}</span>",
50
+ :en, :ja
51
+ ).should == { :safe => true }
52
+
53
+ # "spank" instead of "span" in the last tag.
54
+ Air18n::XssDetector::safe?(
55
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
56
+ "<span class='text'>レビュー</span> <span class='count'>%{smart_count}</spank>",
57
+ :en, :ja
58
+ ).should == { :safe => false, :reason => "HTML tags don't match: [\"<span class='count'>\", \"</span>\", \"</span>\", \"<span class='text'>\"] vs. [\"<span class='text'>\", \"</span>\", \"<span class='count'>\", \"</spank>\"]" }
59
+ end
60
+
61
+ it "should work with smart count with two plural forms" do
62
+ Air18n::XssDetector::safe?(
63
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
64
+ "<span class='text'>commentaire</span> <span class='count'>%{smart_count}</span>;<span class='text'>commentaires</span> <span class='count'>%{smart_count}</span>",
65
+ :en, :fr
66
+ ).should == { :safe => true }
67
+
68
+ # Second form not styled.
69
+ Air18n::LoggingHelper.info "GRRRRR"
70
+ Air18n::XssDetector::safe?(
71
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
72
+ "<span class='text'>commentaire</span> <span class='count'>%{smart_count}</span>;commentaires %{smart_count}",
73
+ :en, :fr
74
+ ).should == { :safe => false, :reason => "HTML tags don't match: [\"<span class='count'>\", \"</span>\", \"</span>\", \"<span class='text'>\"] vs. [\"</span>\"]" }
75
+
76
+ # Only one form being given leads to the same error.
77
+ Air18n::XssDetector::safe?(
78
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
79
+ "<span class='text'>commentaire</span> <span class='count'>%{smart_count}</span>",
80
+ :en, :fr
81
+ ).should == { :safe => false, :reason => "HTML tags don't match: [\"<span class='count'>\", \"</span>\", \"</span>\", \"<span class='text'>\"] vs. [\"</span>\"]" }
82
+ end
83
+
84
+ it "should work with smart count with three plural forms" do
85
+ Air18n::XssDetector::safe?(
86
+ "<span class='count'>%{smart_count}</span> <span class='text'>review</span>;<span class='count'>%{smart_count}</span> <span class='text'>reviews</span>",
87
+ "<span class='text'>ends-in-one-except-eleven</span> <span class='count'>%{smart_count}</span>;<span class='text'>ends-in-two-through-four-except-twelve-through-fourteen</span> <span class='count'>%{smart_count}</span>;<span class='text'>everything-else</span> <span class='count'>%{smart_count}</span>",
88
+ :en, :ru
40
89
  ).should == { :safe => true }
41
90
  end
42
91
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: air18n
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.1.16
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2012-09-29 00:00:00.000000000 Z
16
+ date: 2012-10-02 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: i18n
@@ -161,6 +161,7 @@ files:
161
161
  - lib/air18n/pseudo_locales.rb
162
162
  - lib/air18n/reflection.rb
163
163
  - lib/air18n/screenshot.rb
164
+ - lib/air18n/smart_count.rb
164
165
  - lib/air18n/testing_support/factories.rb
165
166
  - lib/air18n/testing_support/factories/phrase.rb
166
167
  - lib/air18n/testing_support/factories/phrase_screenshot.rb
@@ -177,6 +178,7 @@ files:
177
178
  - spec/lib/air18n/phrase_translation_spec.rb
178
179
  - spec/lib/air18n/prim_and_proper_spec.rb
179
180
  - spec/lib/air18n/pseudo_locales_spec.rb
181
+ - spec/lib/air18n/smart_count_spec.rb
180
182
  - spec/lib/air18n/xss_detector_spec.rb
181
183
  - spec/spec_helper.rb
182
184
  homepage: http://www.github.com/airbnb/air18n
@@ -212,5 +214,6 @@ test_files:
212
214
  - spec/lib/air18n/phrase_translation_spec.rb
213
215
  - spec/lib/air18n/prim_and_proper_spec.rb
214
216
  - spec/lib/air18n/pseudo_locales_spec.rb
217
+ - spec/lib/air18n/smart_count_spec.rb
215
218
  - spec/lib/air18n/xss_detector_spec.rb
216
219
  - spec/spec_helper.rb