thedarkone-i18n 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/CHANGELOG.textile +57 -0
  2. data/README.textile +43 -9
  3. data/Rakefile +21 -0
  4. data/VERSION +1 -0
  5. data/lib/i18n.rb +87 -16
  6. data/lib/i18n/backend/base.rb +251 -0
  7. data/lib/i18n/backend/cache.rb +71 -0
  8. data/lib/i18n/backend/chain.rb +64 -0
  9. data/lib/i18n/backend/fallbacks.rb +53 -0
  10. data/lib/i18n/backend/fast.rb +53 -22
  11. data/lib/i18n/backend/fast/interpolation_compiler.rb +84 -0
  12. data/lib/i18n/backend/gettext.rb +65 -0
  13. data/lib/i18n/backend/lazy_reloading.rb +60 -0
  14. data/lib/i18n/backend/pluralization.rb +56 -0
  15. data/lib/i18n/backend/simple.rb +17 -240
  16. data/lib/i18n/exceptions.rb +13 -5
  17. data/lib/i18n/gettext.rb +25 -0
  18. data/lib/i18n/helpers/gettext.rb +35 -0
  19. data/lib/i18n/locale/fallbacks.rb +100 -0
  20. data/lib/i18n/locale/tag.rb +27 -0
  21. data/lib/i18n/locale/tag/parents.rb +24 -0
  22. data/lib/i18n/locale/tag/rfc4646.rb +78 -0
  23. data/lib/i18n/locale/tag/simple.rb +44 -0
  24. data/test/all.rb +5 -7
  25. data/test/api/basics.rb +15 -0
  26. data/test/api/interpolation.rb +85 -0
  27. data/test/api/lambda.rb +52 -0
  28. data/test/api/link.rb +47 -0
  29. data/test/api/localization/date.rb +65 -0
  30. data/test/api/localization/date_time.rb +63 -0
  31. data/test/api/localization/lambda.rb +26 -0
  32. data/test/api/localization/time.rb +63 -0
  33. data/test/api/pluralization.rb +37 -0
  34. data/test/api/translation.rb +51 -0
  35. data/test/backend/cache/cache_test.rb +57 -0
  36. data/test/backend/chain/api_test.rb +80 -0
  37. data/test/backend/chain/chain_test.rb +64 -0
  38. data/test/backend/fallbacks/api_test.rb +79 -0
  39. data/test/backend/fallbacks/fallbacks_test.rb +29 -0
  40. data/test/backend/fast/all.rb +5 -0
  41. data/test/backend/fast/api_test.rb +91 -0
  42. data/test/backend/fast/interpolation_compiler_test.rb +84 -0
  43. data/test/backend/fast/lookup_test.rb +24 -0
  44. data/test/backend/fast/setup.rb +22 -0
  45. data/test/backend/fast/translations_test.rb +89 -0
  46. data/test/backend/lazy_reloading/reloading_test.rb +67 -0
  47. data/test/backend/pluralization/api_test.rb +81 -0
  48. data/test/backend/pluralization/pluralization_test.rb +39 -0
  49. data/test/backend/simple/all.rb +5 -0
  50. data/test/backend/simple/api_test.rb +90 -0
  51. data/test/backend/simple/lookup_test.rb +24 -0
  52. data/test/backend/simple/setup.rb +151 -0
  53. data/test/backend/simple/translations_test.rb +89 -0
  54. data/test/fixtures/locales/de.po +61 -0
  55. data/test/fixtures/locales/en.rb +3 -0
  56. data/test/fixtures/locales/en.yml +3 -0
  57. data/test/fixtures/locales/plurals.rb +112 -0
  58. data/test/gettext/api_test.rb +78 -0
  59. data/test/gettext/backend_test.rb +35 -0
  60. data/test/i18n_exceptions_test.rb +6 -25
  61. data/test/i18n_load_path_test.rb +23 -0
  62. data/test/i18n_test.rb +56 -18
  63. data/test/locale/fallbacks_test.rb +128 -0
  64. data/test/locale/tag/rfc4646_test.rb +147 -0
  65. data/test/locale/tag/simple_test.rb +35 -0
  66. data/test/test_helper.rb +72 -0
  67. data/test/with_options.rb +34 -0
  68. metadata +109 -19
  69. data/i18n.gemspec +0 -31
  70. data/lib/i18n/backend/fast/pluralization_compiler.rb +0 -50
  71. data/test/backend_test.rb +0 -633
  72. data/test/fast_backend_test.rb +0 -34
  73. data/test/locale/en.rb +0 -1
  74. data/test/locale/en.yml +0 -3
  75. data/test/pluralization_compiler_test.rb +0 -35
@@ -0,0 +1,60 @@
1
+ # Speeds up the reload! method (usefull in development mode) by first making sure
2
+ # the locale files have actually changed (this is done using their last mtime).
3
+ # Usage:
4
+ #
5
+ # I18n::Backend::Simple.send(:include, I18n::Backend::LazyReloading)
6
+
7
+ module I18n
8
+ module Backend
9
+ module LazyReloading
10
+ def reload!
11
+ flush_translations if stale?
12
+ end
13
+
14
+ protected
15
+ def init_translations
16
+ load_translations(*load_paths)
17
+ @initialized = true
18
+ end
19
+
20
+ def load_paths
21
+ I18n.load_path.flatten
22
+ end
23
+
24
+ def flush_translations
25
+ @initialized = false
26
+ @translations = nil
27
+ @file_mtimes = {}
28
+ end
29
+
30
+ def file_mtimes
31
+ @file_mtimes ||= {}
32
+ end
33
+
34
+ def load_file(filename)
35
+ super
36
+ record_mtime_of(filename)
37
+ end
38
+
39
+ def record_mtime_of(filename)
40
+ file_mtimes[filename] = File.mtime(filename)
41
+ end
42
+
43
+ def stale_translation_file?(filename)
44
+ (mtime = file_mtimes[filename]).nil? || !File.file?(filename) || mtime < File.mtime(filename)
45
+ end
46
+
47
+ def stale?
48
+ translation_path_removed? || translation_file_updated_or_added?
49
+ end
50
+
51
+ def translation_path_removed?
52
+ (file_mtimes.keys - load_paths).any?
53
+ end
54
+
55
+ def translation_file_updated_or_added?
56
+ load_paths.any? {|path| stale_translation_file?(path)}
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ # I18n locale fallbacks are useful when you want your application to use
4
+ # translations from other locales when translations for the current locale are
5
+ # missing. E.g. you might want to use :en translations when translations in
6
+ # your applications main locale :de are missing.
7
+ #
8
+ # To enable locale specific pluralizations you can simply include the
9
+ # Pluralization module to the Simple backend - or whatever other backend you
10
+ # are using.
11
+ #
12
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
13
+ #
14
+ # You also need to make sure to provide pluralization algorithms to the
15
+ # backend, i.e. include them to your I18n.load_path accordingly.
16
+ module I18n
17
+ module Backend
18
+ module Pluralization
19
+ # Overwrites the Base backend translate method so that it will check the
20
+ # translation meta data space (:i18n) for locale specific pluralizers
21
+ # and use them to pluralize the given entry.
22
+ #
23
+ # Pluralizers are expected to respond to #call(entry, count) and return
24
+ # a pluralization key. Valid keys depend on the translation data hash
25
+ # (entry) but it is generally recommended to follow CLDR's style, i.e.
26
+ # return one of the keys :zero, :one, :few, :many, :other.
27
+ #
28
+ # The :zero key is always picked directly when count equals 0 AND the
29
+ # translation data has the key :zero. This way translators are free to
30
+ # either pick a special :zero translation even for languages where the
31
+ # pluralizer does not return a :zero key.
32
+ def pluralize(locale, entry, count)
33
+ return entry unless entry.is_a?(Hash) and count
34
+
35
+ pluralizer = pluralizer(locale)
36
+ if pluralizer.respond_to?(:call)
37
+ key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
38
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
39
+ entry[key]
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def pluralizers
48
+ @pluralizers ||= {}
49
+ end
50
+
51
+ def pluralizer(locale)
52
+ pluralizers[locale] ||= lookup(locale, :"i18n.pluralize")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,246 +1,23 @@
1
- require 'yaml'
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/backend/base'
4
+
5
+ # Stub class for the Simple backend. The actual implementation is provided by
6
+ # the backend Base class. This makes it easier to extend the Simple backend's
7
+ # behaviour by including modules. E.g.:
8
+ #
9
+ # module I18n::Backend::Pluralization
10
+ # def pluralize(*args)
11
+ # # extended pluralization logic
12
+ # super
13
+ # end
14
+ # end
15
+ #
16
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
2
17
 
3
18
  module I18n
4
19
  module Backend
5
- class Simple
6
- INTERPOLATION_RESERVED_KEYS = [:scope, :default]
7
- MATCH = /(\\\\)?(\{\{([^\}]+)\}\})/
8
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
9
- DEEP_MERGE_PROC = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &DEEP_MERGE_PROC) : v2 }
10
-
11
- # Accepts a list of paths to translation files. Loads translations from
12
- # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
13
- # for details.
14
- def load_translations(*filenames)
15
- filenames.each { |filename| load_file(filename) }
16
- end
17
-
18
- # Stores translations for the given locale in memory.
19
- # This uses a deep merge for the translations hash, so existing
20
- # translations will be overwritten by new ones only at the deepest
21
- # level of the hash.
22
- def store_translations(locale, data)
23
- merge_translations(locale, data)
24
- end
25
-
26
- def translate(locale, key, opts = nil)
27
- raise InvalidLocale.new(locale) unless locale
28
- return key.map { |k| translate(locale, k, opts) } if key.is_a? Array
29
-
30
- if opts
31
- count, scope = opts.values_at(:count, :scope)
32
-
33
- if entry = lookup(locale, key, scope) || ((default = opts.delete(:default)) && default(locale, default, opts))
34
- entry = pluralize(locale, entry, count) if count
35
- entry = interpolate(entry, opts)
36
- entry
37
- end
38
- else
39
- lookup(locale, key)
40
- end || raise(I18n::MissingTranslationData.new(locale, key, opts))
41
- end
42
-
43
- # Acts the same as +strftime+, but returns a localized version of the
44
- # formatted date string. Takes a key from the date/time formats
45
- # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
46
- def localize(locale, object, format = :default)
47
- raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
48
-
49
- format = decide_on_localization_format(locale, object, format)
50
-
51
- # TODO only translate these if the format string is actually present
52
- # TODO check which format strings are present, then bulk translate then, then replace them
53
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
54
- format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
55
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
56
- format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
57
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
58
- object.strftime(format)
59
- end
60
-
61
- def initialized?
62
- @initialized ||= false
63
- end
64
-
65
- # Returns an array of locales for which translations are available
66
- def available_locales
67
- init_translations unless initialized?
68
- translations.keys
69
- end
70
-
71
- def reload!
72
- flush_translations if stale?
73
- end
74
-
75
- def stale?
76
- translation_path_removed? || translation_file_updated_or_added?
77
- end
78
-
79
- protected
80
- def init_translations
81
- load_translations(*load_paths)
82
- @initialized = true
83
- end
84
-
85
- def flush_translations
86
- @initialized = false
87
- @translations = nil
88
- @file_mtimes = {}
89
- end
90
-
91
- def load_paths
92
- I18n.load_path.flatten
93
- end
94
-
95
- def translations
96
- @translations ||= {}
97
- end
98
-
99
- def decide_on_localization_format(locale, object, format)
100
- type = object.respond_to?(:sec) ? 'time' : 'date'
101
- # TODO raise exception unless format found?
102
- lookup(locale, :"#{type}.formats.#{format}") || format.to_s.dup
103
- end
104
-
105
- # Looks up a translation from the translations hash. Returns nil if
106
- # eiher key is nil, or locale, scope or key do not exist as a key in the
107
- # nested translations hash. Splits keys or scopes containing dots
108
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
109
- # <tt>%w(currency format)</tt>.
110
- def lookup(locale, key, scope = [])
111
- return unless key
112
- init_translations unless initialized?
113
- keys = I18n.send(:normalize_translation_keys, locale, key, scope)
114
- keys.inject(translations) do |result, k|
115
- if (x = result[k.to_sym]).nil?
116
- return nil
117
- else
118
- x
119
- end
120
- end
121
- end
122
- alias lookup_with_count lookup
123
-
124
- # Evaluates a default translation.
125
- # If the given default is a String it is used literally. If it is a Symbol
126
- # it will be translated with the given options. If it is an Array the first
127
- # translation yielded will be returned.
128
- #
129
- # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
130
- # <tt>translate(locale, :foo)</tt> does not yield a result.
131
- def default(locale, default, options = {})
132
- case default
133
- when String then default
134
- when Symbol then translate locale, default, options
135
- when Array then default.each do |obj|
136
- result = default(locale, obj, options.dup) and return result
137
- end and nil
138
- end
139
- rescue MissingTranslationData
140
- nil
141
- end
142
-
143
- # Picks a translation from an array according to English pluralization
144
- # rules. It will pick the first translation if count is not equal to 1
145
- # and the second translation if it is equal to 1. Other backends can
146
- # implement more flexible or complex pluralization rules.
147
- def pluralize(locale, entry, count)
148
- return entry unless entry.is_a?(Hash) and count
149
- # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
150
- key = :zero if count == 0 && entry.has_key?(:zero)
151
- key ||= count == 1 ? :one : :other
152
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
153
- entry[key]
154
- end
155
-
156
- # Interpolates values into a given string.
157
- #
158
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
159
- # # => "file test.txt opened by {{user}}"
160
- #
161
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
162
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
163
- # interpolation).
164
- def interpolate(string, values = {})
165
- return string unless string.is_a?(String)
166
-
167
- string.gsub(MATCH) do
168
- escaped, pattern, key = $1, $2, $3.to_sym
169
-
170
- if escaped
171
- pattern
172
- elsif INTERPOLATION_RESERVED_KEYS.include?(key)
173
- raise ReservedInterpolationKey.new(key, string)
174
- elsif !values.include?(key)
175
- raise MissingInterpolationArgument.new(key, string)
176
- else
177
- values[key].to_s
178
- end
179
- end
180
- end
181
-
182
- # Loads a single translations file by delegating to #load_rb or
183
- # #load_yml depending on the file extension and directly merges the
184
- # data to the existing translations. Raises I18n::UnknownFileType
185
- # for all other file extensions.
186
- def load_file(filename)
187
- type = File.extname(filename).tr('.', '').downcase
188
- raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
189
- record_mtime_of(filename)
190
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
191
- data.each { |locale, d| merge_translations(locale, d) }
192
- end
193
-
194
- def record_mtime_of(filename)
195
- file_mtimes[filename] = File.mtime(filename)
196
- end
197
-
198
- def stale_translation_file?(filename)
199
- (mtime = file_mtimes[filename]).nil? || !File.file?(filename) || mtime < File.mtime(filename)
200
- end
201
-
202
- def file_mtimes
203
- @file_mtimes ||= {}
204
- end
205
-
206
- def translation_path_removed?
207
- (file_mtimes.keys - load_paths).any?
208
- end
209
-
210
- def translation_file_updated_or_added?
211
- load_paths.any? {|path| stale_translation_file?(path)}
212
- end
213
-
214
- # Loads a plain Ruby translations file. eval'ing the file must yield
215
- # a Hash containing translation data with locales as toplevel keys.
216
- def load_rb(filename)
217
- eval(IO.read(filename), binding, filename)
218
- end
219
-
220
- # Loads a YAML translations file. The data must have locales as
221
- # toplevel keys.
222
- def load_yml(filename)
223
- YAML::load(IO.read(filename))
224
- end
225
-
226
- # Deep merges the given translations hash with the existing translations
227
- # for the given locale
228
- def merge_translations(locale, data)
229
- locale = locale.to_sym
230
- translations[locale] ||= {}
231
- data = deep_symbolize_keys(data)
232
-
233
- translations[locale].merge!(data, &DEEP_MERGE_PROC)
234
- end
235
-
236
- # Return a new hash with all keys and nested keys converted to symbols.
237
- def deep_symbolize_keys(hash)
238
- hash.inject({}) do |result, (key, value)|
239
- value = deep_symbolize_keys(value) if value.is_a? Hash
240
- result[(key.to_sym rescue key) || key] = value
241
- result
242
- end
243
- end
20
+ class Simple < Base
244
21
  end
245
22
  end
246
23
  end
@@ -1,3 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ class KeyError < IndexError
4
+ def initialize(message = nil)
5
+ super(message || "key not found")
6
+ end
7
+ end unless defined?(KeyError)
8
+
1
9
  module I18n
2
10
  class ArgumentError < ::ArgumentError; end
3
11
 
@@ -13,7 +21,7 @@ module I18n
13
21
  attr_reader :locale, :key, :options
14
22
  def initialize(locale, key, options)
15
23
  @key, @locale, @options = key, locale, options
16
- keys = I18n.send(:normalize_translation_keys, locale, key, options && options[:scope])
24
+ keys = I18n.send(:normalize_translation_keys, locale, key, options[:scope])
17
25
  keys << 'no key' if keys.size < 2
18
26
  super "translation missing: #{keys.join(', ')}"
19
27
  end
@@ -28,10 +36,10 @@ module I18n
28
36
  end
29
37
 
30
38
  class MissingInterpolationArgument < ArgumentError
31
- attr_reader :key, :string
32
- def initialize(key, string)
33
- @key, @string = key, string
34
- super "interpolation argument #{key.inspect} missing in #{string.inspect}"
39
+ attr_reader :values, :string
40
+ def initialize(values, string)
41
+ @values, @string = values, string
42
+ super "missing interpolation argument in #{string.inspect} (#{values.inspect} given)"
35
43
  end
36
44
  end
37
45
 
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Gettext
5
+ PLURAL_SEPARATOR = "\001"
6
+ CONTEXT_SEPARATOR = "\004"
7
+
8
+ @@plural_keys = { :en => [:one, :other] }
9
+
10
+ class << self
11
+ # returns an array of plural keys for the given locale so that we can
12
+ # convert from gettext's integer-index based style
13
+ # TODO move this information to the pluralization module
14
+ def plural_keys(locale)
15
+ @@plural_keys[locale] || @@plural_keys[:en]
16
+ end
17
+
18
+ def extract_scope(msgid, separator = nil)
19
+ scope = msgid.to_s.split(separator || '|')
20
+ msgid = scope.pop
21
+ [scope, msgid]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Helpers
5
+ # Implements classical Gettext style accessors. To use this include the
6
+ # module to the global namespace or wherever you want to use it.
7
+ #
8
+ # include I18n::Helpers::Gettext
9
+ module Gettext
10
+ def _(msgid, options = {})
11
+ I18n.t(msgid, { :default => msgid, :separator => '|' }.merge(options))
12
+ end
13
+
14
+ def sgettext(msgid, separator = '|')
15
+ scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
16
+ I18n.t(msgid, :scope => scope, :default => msgid)
17
+ end
18
+
19
+ def pgettext(msgctxt, msgid, separator = I18n::Gettext::CONTEXT_SEPARATOR)
20
+ sgettext([msgctxt, msgid].join(separator), separator)
21
+ end
22
+
23
+ def ngettext(msgid, msgid_plural, n = 1)
24
+ nsgettext(msgid, msgid_plural, n, nil)
25
+ end
26
+
27
+ def nsgettext(msgid, msgid_plural, n = 1, separator = nil)
28
+ scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
29
+ default = { :one => msgid, :other => msgid_plural }
30
+ msgid = [msgid, I18n::Gettext::PLURAL_SEPARATOR, msgid_plural].join
31
+ I18n.t(msgid, :default => default, :count => n, :scope => scope)
32
+ end
33
+ end
34
+ end
35
+ end