thedarkone-i18n 0.1.4 → 0.2.0

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 (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