r18n-core 0.2.3 → 0.3

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 (47) hide show
  1. data/README.rdoc +157 -35
  2. data/base/cs.yml +34 -0
  3. data/base/de.yml +24 -0
  4. data/base/en.yml +24 -0
  5. data/base/fr.yml +24 -0
  6. data/base/pl.yml +24 -0
  7. data/base/ru.yml +30 -0
  8. data/lib/r18n-core/filters.rb +245 -0
  9. data/lib/r18n-core/i18n.rb +57 -40
  10. data/lib/r18n-core/locale.rb +116 -19
  11. data/lib/r18n-core/translated.rb +186 -0
  12. data/lib/r18n-core/translation.rb +33 -73
  13. data/lib/r18n-core/unsupported_locale.rb +27 -9
  14. data/lib/r18n-core/utils.rb +49 -0
  15. data/lib/r18n-core/version.rb +1 -1
  16. data/lib/r18n-core.rb +3 -15
  17. data/locales/cs.rb +23 -0
  18. data/locales/cs.yml +26 -0
  19. data/locales/de.yml +3 -8
  20. data/locales/en-us.rb +8 -0
  21. data/locales/en-us.yml +9 -0
  22. data/locales/en.rb +26 -0
  23. data/locales/en.yml +3 -8
  24. data/locales/eo.yml +3 -8
  25. data/locales/fr.rb +14 -0
  26. data/locales/fr.yml +3 -8
  27. data/locales/kk.yml +3 -8
  28. data/locales/pl.yml +4 -11
  29. data/locales/ru.yml +3 -8
  30. data/spec/filters_spec.rb +167 -0
  31. data/spec/i18n_spec.rb +61 -16
  32. data/spec/locale_spec.rb +46 -19
  33. data/spec/locales/cs_spec.rb +22 -0
  34. data/spec/locales/en-us_spec.rb +14 -0
  35. data/spec/locales/en_spec.rb +14 -0
  36. data/spec/locales/fr_spec.rb +10 -0
  37. data/spec/locales/pl_spec.rb +17 -17
  38. data/spec/locales/ru_spec.rb +2 -2
  39. data/spec/r18n_spec.rb +7 -3
  40. data/spec/spec_helper.rb +2 -0
  41. data/spec/translated_spec.rb +108 -0
  42. data/spec/translation_spec.rb +24 -56
  43. data/spec/translations/extension/{no_TR.yml → no-tr.yml} +0 -0
  44. data/spec/translations/general/en.yml +15 -2
  45. data/spec/translations/general/{no_LC.yml → no-lc.yml} +0 -0
  46. metadata +46 -31
  47. data/locales/en_US.yml +0 -14
@@ -48,9 +48,9 @@ module R18n
48
48
  # i18n.one #=> "Один"
49
49
  # i18n.two #=> "Two"
50
50
  #
51
- # i18n.locale['title'] #=> "Русский"
52
- # i18n.locale['code'] #=> "ru"
53
- # i18n.locale['direction'] #=> "ltr"
51
+ # i18n.locale.title #=> "Русский"
52
+ # i18n.locale.code #=> "ru"
53
+ # i18n.locale.ltr? #=> true
54
54
  #
55
55
  # i18n.l -11000.5 #=> "−11 000,5"
56
56
  # i18n.l Time.now #=> "Вск, 21 сен 2008, 22:10:10 MSD"
@@ -91,56 +91,58 @@ module R18n
91
91
  attr_reader :locales
92
92
 
93
93
  # Dirs with translations files
94
- attr_reader :translations_dirs
94
+ attr_reader :translation_dirs
95
95
 
96
96
  # First locale with locale file
97
97
  attr_reader :locale
98
98
 
99
- # Create i18n for +locales+ with translations from +translations_dirs+ and
99
+ # Create i18n for +locales+ with translations from +translation_dirs+ and
100
100
  # locales data. Translations will be also loaded for default locale,
101
101
  # +sublocales+ from first in +locales+ and general languages for dialects
102
102
  # (it will load +fr+ for +fr_CA+ too).
103
103
  #
104
104
  # +Locales+ must be a locale code (RFC 3066) or array, ordered by priority.
105
- # +Translations_dirs+ must be a string with path or array.
106
- def initialize(locales, translations_dirs = nil)
105
+ # +Translation_dirs+ must be a string with path or array.
106
+ def initialize(locales, translation_dirs = nil)
107
107
  locales = [locales] if locales.is_a? String
108
108
 
109
- @locales = locales.map { |i| Locale.load(i) }
110
-
111
- locales << @@default
112
- if @locales.first.kind_of? Locale
113
- locales += @locales.first['sublocales']
109
+ if not locales.empty? and Locale.exists? locales.first
110
+ locales += Locale.load(locales.first)['sublocales']
114
111
  end
112
+ locales << @@default
115
113
  locales.each_with_index do |locale, i|
116
- if "_" == locale[2..2]
117
- locales.insert(i + 1, locale[0..1])
114
+ if locale =~ /[_-]/
115
+ locales.insert(i + 1, locale.match(/([^_-]+)[_-]/)[1])
118
116
  end
119
117
  end
120
118
  locales.uniq!
119
+ @locales = locales.map { |i| Locale.load(i) }
121
120
 
122
- locales.each do |locale|
123
- if Locale.exists? locale
124
- @locale = Locale.load(locale)
125
- break
126
- end
121
+ if translation_dirs.nil?
122
+ @translation_dirs = []
123
+ @translation = Translation.load(@locales,
124
+ Translation.extension_translations)
125
+ else
126
+ @translation_dirs = translation_dirs
127
+ @translation = Translation.load(@locales, @translation_dirs)
127
128
  end
128
129
 
129
- if not translations_dirs.nil?
130
- @translations_dirs = translations_dirs
131
- @translation = Translation.load(locales, @translations_dirs)
130
+ @locale = @locales.first
131
+ unless @locale.supported?
132
+ @locales.each do |locale|
133
+ if locale.supported?
134
+ @locale.base = locale
135
+ break
136
+ end
137
+ end
132
138
  end
133
139
  end
134
140
 
135
141
  # Return Hash with titles (or code for unsupported locales) for available
136
142
  # translations.
137
143
  def translations
138
- Translation.available(@translations_dirs).inject({}) do |all, code|
139
- all[code] = if Locale.exists? code
140
- Locale.load(code)['title']
141
- else
142
- code
143
- end
144
+ Translation.available(@translation_dirs).inject({}) do |all, code|
145
+ all[code] = Locale.load(code).title
144
146
  all
145
147
  end
146
148
  end
@@ -148,22 +150,37 @@ module R18n
148
150
  # Convert +object+ to String, according to the rules of the current locale.
149
151
  # It support Fixnum, Bignum, Float, Time, Date and DateTime.
150
152
  #
151
- # For time classes you can set +format+ in standart +strftime+ form, or
152
- # Symbol to use format from locale file (<tt>:time</tt>, <tt>:date</tt>,
153
- # <tt>:short_data</tt>, <tt>:long_data</tt>, <tt>:datetime</tt>,
154
- # <tt>:short_datetime</tt> or <tt>:long_datetime</tt>). Without format it
155
- # use <tt>:datetime</tt> for Time and DateTime and <tt>:date</tt> for Date.
156
- def localize(object, format = nil)
153
+ # For time classes you can set +format+ in standard +strftime+ form,
154
+ # <tt>:full</tt> (“01 Jule, 2009”), <tt>:human</tt> (“yesterday”),
155
+ # <tt>:standard</tt> (“07/01/09”) or <tt>:month</tt> for standalone month
156
+ # name. Default format is <tt>:standard</tt>.
157
+ #
158
+ # i18n.l -12000.5 #=> "−12,000.5"
159
+ # i18n.l Time.now #=> "07/01/09 12:59"
160
+ # i18n.l Time.now.to_date #=> "07/01/09"
161
+ # i18n.l Time.now, :human #=> "now"
162
+ # i18n.l Time.now, :full #=> "Jule 1st, 2009 12:59"
163
+ def localize(object, format = nil, *params)
157
164
  if object.is_a? Integer
158
165
  locale.format_integer(object)
159
166
  elsif object.is_a? Float
160
167
  locale.format_float(object)
161
- elsif object.is_a? Time or object.is_a? DateTime
162
- format = :datetime if format.nil?
163
- locale.strftime(object, format)
164
- elsif object.is_a? Date
165
- format = :date if format.nil?
166
- locale.strftime(object, format)
168
+ elsif object.is_a? Time or object.is_a? DateTime or object.is_a? Date
169
+ if format.is_a? String
170
+ locale.strftime(object, format)
171
+ else
172
+ if :month == format
173
+ return locale.data['months']['standalone'][object.month - 1]
174
+ end
175
+ type = object.is_a?(Date) ? 'date' : 'time'
176
+ format = :standard unless format
177
+
178
+ unless [:human, :full, :standard].include? format
179
+ raise ArgumentError, "Unknown time formatter #{format}"
180
+ end
181
+
182
+ locale.send "format_#{type}_#{format}", self, object, *params
183
+ end
167
184
  end
168
185
  end
169
186
  alias :l :localize
@@ -38,9 +38,9 @@ module R18n
38
38
  # Get Russian locale and print it information
39
39
  #
40
40
  # ru = R18n::Locale.load('ru')
41
- # ru['title'] #=> "Русский"
42
- # ru['code'] #=> "ru"
43
- # ru['direction'] #=> "ltr"
41
+ # ru.title #=> "Русский"
42
+ # ru.code #=> "ru"
43
+ # ru.ltr? #=> true
44
44
  #
45
45
  # == Available data
46
46
  #
@@ -52,6 +52,12 @@ module R18n
52
52
  #
53
53
  # You can see more available data about locale in samples in
54
54
  # <tt>locales/</tt> dir.
55
+ #
56
+ # == Extend locale
57
+ # If language need some special logic (for example, another pluralization or
58
+ # time formatters) you can just change Locale class. Create
59
+ # R18n::Locales::_Code_ class in base/_code_.rb, extend R18n::Locale and
60
+ # rewrite methods (for example, +pluralization+ or +format_date_full+).
55
61
  class Locale
56
62
  LOCALES_DIR = Pathname(__FILE__).dirname.expand_path + '../../locales/'
57
63
 
@@ -69,11 +75,14 @@ module R18n
69
75
 
70
76
  # Load locale by RFC 3066 +code+
71
77
  def self.load(code)
78
+ code = code.to_s
72
79
  code.delete! '/'
73
80
  code.delete! '\\'
74
81
  code.delete! ';'
82
+ original = code
83
+ code = code.downcase
75
84
 
76
- return UnsupportedLocale.new(code) unless exists? code
85
+ return UnsupportedLocale.new(original) unless exists? code
77
86
 
78
87
  data = {}
79
88
  klass = R18n::Locale
@@ -85,7 +94,8 @@ module R18n
85
94
 
86
95
  if R18n::Locale == klass and File.exists? LOCALES_DIR + "#{code}.rb"
87
96
  require LOCALES_DIR + "#{code}.rb"
88
- klass = eval 'R18n::Locales::' + code.capitalize
97
+ name = code.gsub(/[\w\d]+/) { |i| i.capitalize }.gsub('-', '')
98
+ klass = eval 'R18n::Locales::' + name
89
99
  end
90
100
 
91
101
  loaded = YAML.load_file(file)
@@ -114,6 +124,21 @@ module R18n
114
124
  def initialize(data)
115
125
  @data = data
116
126
  end
127
+
128
+ # Locale RFC 3066 code.
129
+ def code
130
+ @data['code']
131
+ end
132
+
133
+ # Locale title.
134
+ def title
135
+ @data['title']
136
+ end
137
+
138
+ # Is locale has left-to-right write direction.
139
+ def ltr?
140
+ @data['direction'] == 'ltr'
141
+ end
117
142
 
118
143
  # Get information about locale
119
144
  def [](name)
@@ -122,12 +147,17 @@ module R18n
122
147
 
123
148
  # Is another locale has same code
124
149
  def ==(locale)
125
- @data['code'] == locale['code']
150
+ code.downcase == locale.code.downcase
151
+ end
152
+
153
+ # Is locale has information file. In this class always return true.
154
+ def supported?
155
+ true
126
156
  end
127
157
 
128
158
  # Human readable locale code and title
129
159
  def inspect
130
- "Locale #{@data['code']} (#{@data['title']})"
160
+ "Locale #{code} (#{title})"
131
161
  end
132
162
 
133
163
  # Returns the integer in String form, according to the rules of the locale.
@@ -151,19 +181,8 @@ module R18n
151
181
 
152
182
  # Same that <tt>Time.strftime</tt>, but translate months and week days
153
183
  # names. In +time+ you can use Time, DateTime or Date object. In +format+
154
- # you can use String with standart +strftime+ format (see
155
- # <tt>Time.strftime</tt> docs) or Symbol with format from locale file
156
- # (<tt>:month</tt>, <tt>:time</tt>, <tt>:date</tt>, <tt>:short_data</tt>,
157
- # <tt>:long_data</tt>, <tt>:datetime</tt>, <tt>:short_datetime</tt> or
158
- # <tt>:long_datetime</tt>).
184
+ # you can use standard +strftime+ format.
159
185
  def strftime(time, format)
160
- if format.is_a? Symbol
161
- if :month == format
162
- return @data['months']['standalone'][time.month - 1]
163
- end
164
- format = @data['formats'][format.to_s]
165
- end
166
-
167
186
  translated = ''
168
187
  format.scan(/%[EO]?.|./o) do |c|
169
188
  case c.sub(/^%[EO]?(.)$/o, '%\\1')
@@ -187,6 +206,84 @@ module R18n
187
206
  end
188
207
  time.strftime(translated)
189
208
  end
209
+
210
+ # Format +time+ without date. For example, “12:59”.
211
+ def format_time(time)
212
+ strftime(time, @data['time']['time'])
213
+ end
214
+
215
+ # Format +time+ in human usable form. For example “5 minutes ago” or
216
+ # “yesterday”. In +now+ you can set base time, which be used to get relative
217
+ # time. For special cases you can replace it in locale’s class.
218
+ def format_time_human(i18n, time, now = Time.now)
219
+ minutes = (time - now) / 60.0
220
+ if time.mday != now.mday and minutes.abs > 720 # 12 hours
221
+ format_date_human(i18n, R18n::Utils.to_date(time),
222
+ R18n::Utils.to_date(now)) + format_time(time)
223
+ else
224
+ case minutes
225
+ when -60..-1
226
+ i18n.human_time.minutes_ago(minutes.round.abs)
227
+ when 1..60
228
+ i18n.human_time.after_minutes(minutes.round)
229
+ when -1..1
230
+ i18n.human_time.now
231
+ else
232
+ hours = (minutes / 60.0).abs.floor
233
+ if time > now
234
+ i18n.human_time.after_hours(hours)
235
+ else
236
+ i18n.human_time.hours_ago(hours)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ # Format +time+ in compact form. For example, “12/31/09 12:59”.
243
+ def format_time_standard(i18n, time)
244
+ format_date_standard(i18n, time) + format_time(time)
245
+ end
246
+
247
+ # Format +time+ in most official form. For example, “December 31st, 2009
248
+ # 12:59”. For special cases you can replace it in locale’s class.
249
+ def format_time_full(i18n, time)
250
+ format_date_full(i18n, time) + format_time(time)
251
+ end
252
+
253
+ # Format +date+ in human usable form. For example “5 days ago” or
254
+ # “yesterday”. In +now+ you can set base time, which be used to get relative
255
+ # time. For special cases you can replace it in locale’s class.
256
+ def format_date_human(i18n, date, now = Date.today)
257
+ days = (date - now).to_i
258
+ case days
259
+ when -6..-2
260
+ i18n.human_time.days_ago(days.abs)
261
+ when -1
262
+ i18n.human_time.yesterday
263
+ when 0
264
+ i18n.human_time.today
265
+ when 1
266
+ i18n.human_time.tomorrow
267
+ when 2..6
268
+ i18n.human_time.after_days(days)
269
+ else
270
+ format_date_full(i18n, date, date.year != now.year)
271
+ end
272
+ end
273
+
274
+ # Format +date+ in compact form. For example, “12/31/09”.
275
+ def format_date_standard(i18n, date)
276
+ strftime(date, @data['time']['date'])
277
+ end
278
+
279
+ # Format +date+ in most official form. For example, “December 31st, 2009”.
280
+ # For special cases you can replace it in locale’s class. If +year+ is false
281
+ # date will be without year.
282
+ def format_date_full(i18n, date, year = true)
283
+ format = @data['time']['full']
284
+ format = @data['time']['year'].sub('_', format) if year
285
+ strftime(date, format)
286
+ end
190
287
 
191
288
  # Return pluralization type for +n+ items. This is simple form. For special
192
289
  # cases you can replace it in locale’s class.
@@ -0,0 +1,186 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Add i18n support to any class.
4
+
5
+ Copyright (C) 2008 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Lesser General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Lesser General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Lesser General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ module R18n
22
+ # Module to add i18n support to any class. It will be useful for ORM or
23
+ # R18n plugin with out-of-box i18n support.
24
+ #
25
+ # Module can add proxy-methods to find translation in object methods. For
26
+ # example, if you class have +title_en+ and +title_ru+ methods, you can add
27
+ # proxy-method +title+, which will use +title_ru+ for Russian users and
28
+ # +title_en+ for English:
29
+ #
30
+ # require 'r18n-core/translated'
31
+ #
32
+ # class Product
33
+ # include DataMapper::Resource
34
+ # property :title_ru, String
35
+ # property :title_en, String
36
+ # property :desciption_ru, String
37
+ # property :desciption_en, String
38
+ #
39
+ # include R18n::Translated
40
+ # translations :title, :desciption
41
+ # end
42
+ #
43
+ # # User know only Russian
44
+ # R18n.set(R18n::I18n.new('ru'))
45
+ #
46
+ # product.title #=> Untranslated
47
+ #
48
+ # # Set value to English (default) title
49
+ # product.title_en = "Anthrax"
50
+ # product.title #=> "Anthrax"
51
+ # product.title.locale #=> Locale en (English)
52
+ #
53
+ # # Set value to title on user locale (Russian)
54
+ # product.title = "Сибирская язва"
55
+ # product.title #=> "Сибирская язва"
56
+ # product.title.locale #=> Locale ru (Russian)
57
+ #
58
+ # product.title_en #=> "Anthrax"
59
+ # product.title_ru #=> "Сибирская язва"
60
+ #
61
+ # Proxy-method support all funtion from I18n: global and type filters,
62
+ # pluralization, variables. It also return TranslatedString or Untranslated.
63
+ #
64
+ # Note, you must set your I18n object by <tt>R18n.set</tt> and don’t forget
65
+ # to require <tt>'r18n-core/translated'</tt>. R18n plugins (sinatra-r18n,
66
+ # r18-desktop) set I18n object by <tt>R18n.set</tt> automatically, but you
67
+ # must call <tt>i18n</tt> helper in Sinatra before use models.
68
+ #
69
+ # See R18n::Translated::Base for class method documentation.
70
+ #
71
+ # == Options
72
+ # You can set option for proxy-method as Hash with keys;
73
+ # * +type+ – YAML type for filters. For example, "markdown" or "escape_html".
74
+ # * +methods+ – manual method map as Hash of locale codes to method names.
75
+ # * +no_params+ – set it to true if you proxy-method must send it parameters
76
+ # only to filters.
77
+ # * +no_write+ – set it to true if you don’t want to create proxy-setters.
78
+ #
79
+ # Method +translation+ will be more useful for options:
80
+ #
81
+ # translation :title, :methods => {:ru => :russian, :en => :english}
82
+ module Translated
83
+ class << self
84
+ def included(base) #:nodoc:
85
+ base.send :extend, Base
86
+ base.instance_variable_set '@unlocalized_getters', {}
87
+ base.instance_variable_set '@unlocalized_setters', {}
88
+ base.instance_variable_set '@translation_types', {}
89
+ end
90
+ end
91
+
92
+ # Module with class methods, which be added after R18n::Translated include.
93
+ module Base
94
+ # Hash of translation method names to it type for filters.
95
+ attr_reader :translation_types
96
+
97
+ # Add several proxy +methods+. See R18n::Translated for description.
98
+ # It’s more compact, that +translation+.
99
+ #
100
+ # translations :title, :keywords, [:desciption, {:type => 'markdown'}]
101
+ def translations(*methods)
102
+ methods.each do |method|
103
+ translation(*method)
104
+ end
105
+ end
106
+
107
+ # Add proxy-method +name+. See R18n::Translated for description.
108
+ # It’s more useful to set options.
109
+ #
110
+ # translation :desciption, :type => 'markdown'
111
+ def translation(name, options = {})
112
+ if options[:methods]
113
+ @unlocalized_getters[name] = Hash[
114
+ options[:methods].map { |l, i| [l.to_s, i.to_s] } ]
115
+ unless options[:no_write]
116
+ @unlocalized_setters[name] = Hash[
117
+ options[:methods].map { |l, i| [l.to_s, i.to_s + '='] } ]
118
+ end
119
+ end
120
+
121
+ @translation_types[name] = options[:type]
122
+ call = options[:no_params] ? 'call' : 'call(*params)'
123
+
124
+ class_eval <<-EOS, __FILE__, __LINE__
125
+ def #{name}(*params)
126
+ path = "\#{self.class.name}##{name}"
127
+
128
+ unlocalized = self.class.unlocalized_getters(#{name.inspect})
129
+ R18n.get.locales.each do |locale|
130
+ code = locale.code
131
+ next unless unlocalized.has_key? code
132
+ result = method(unlocalized[code]).#{call}
133
+ next unless result
134
+
135
+ type = self.class.translation_types[#{name.inspect}]
136
+ return R18n::Filters.process(result, locale, path, type, params)
137
+ end
138
+
139
+ R18n::Untranslated.new(path, '#{name}')
140
+ end
141
+ EOS
142
+
143
+ unless options[:no_write]
144
+ class_eval <<-EOS, __FILE__, __LINE__
145
+ def #{name}=(*params)
146
+ unlocalized = self.class.unlocalized_setters(#{name.inspect})
147
+ R18n.get.locales.each do |locale|
148
+ code = locale.code
149
+ next unless unlocalized.has_key? code
150
+ return method(unlocalized[code]).call(*params)
151
+ end
152
+ end
153
+ EOS
154
+ end
155
+ end
156
+
157
+ # Return Hash of locale code to getter method for proxy +method+. If you
158
+ # didn’t set map in +translation+ option +methods+, it will be detect
159
+ # automatically.
160
+ def unlocalized_getters(method)
161
+ matcher = Regexp.new('^' + Regexp.escape(method.to_s) + '_(.*[^=])$')
162
+ unless @unlocalized_getters.has_key? method
163
+ @unlocalized_getters[method] = {}
164
+ self.instance_methods.reject { |i| not i =~ matcher }.each do |i|
165
+ @unlocalized_getters[method][i.to_s.match(matcher)[1]] = i.to_s
166
+ end
167
+ end
168
+ @unlocalized_getters[method]
169
+ end
170
+
171
+ # Return Hash of locale code to setter method for proxy +method+. If you
172
+ # didn’t set map in +translation+ option +methods+, it will be detect
173
+ # automatically.
174
+ def unlocalized_setters(method)
175
+ matcher = Regexp.new('^' + Regexp.escape(method.to_s) + '_(.*)=$')
176
+ unless @unlocalized_setters.has_key? method
177
+ @unlocalized_setters[method] = {}
178
+ self.instance_methods.reject { |i| not i =~ matcher }.each do |i|
179
+ @unlocalized_setters[method][i.to_s.match(matcher)[1]] = i.to_s
180
+ end
181
+ end
182
+ @unlocalized_setters[method]
183
+ end
184
+ end
185
+ end
186
+ end