r18n-core 0.2.3 → 0.3

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