r18n-core 0.1

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.
@@ -0,0 +1,191 @@
1
+ =begin
2
+ I18n support.
3
+
4
+ Copyright (C) 2008 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ require 'date'
21
+
22
+ module R18n
23
+ # General class to i18n support in your application. It load Translation and
24
+ # Locale classes and create pretty way to use it.
25
+ #
26
+ # To get translation you can use same with Translation way – use method with
27
+ # translation’s name or <tt>[name]</tt> method. Translations will be also
28
+ # loaded for default locale, +sublocales+ from first in +locales+ and general
29
+ # languages for dialects (it will load +fr+ for +fr_CA+ too).
30
+ #
31
+ # See Translation and Locale documentation.
32
+ #
33
+ # == Usage
34
+ # translations/ru.yml
35
+ #
36
+ # one: Один
37
+ #
38
+ # translations/en.yml
39
+ #
40
+ # one: One
41
+ # two: Two
42
+ #
43
+ # example.rb
44
+ #
45
+ # i18n = R18n::I18n.new(['ru', 'en'], 'translations/')
46
+ #
47
+ # i18n.one #=> "Один"
48
+ # i18n.two #=> "Two"
49
+ #
50
+ # i18n.locale['title'] #=> "Русский"
51
+ # i18n.locale['code'] #=> "ru"
52
+ # i18n.locale['direction'] #=> "ltr"
53
+ #
54
+ # i18n.l -11000.5 #=> "−11 000,5"
55
+ # i18n.l Time.now #=> "Вск, 21 сен 2008, 22:10:10 MSD"
56
+ # i18n.l Time.now, :date #=> "21.09.2008"
57
+ # i18n.l Time.now, :time #=> "22:10"
58
+ # i18n.l Time.now, '%A' #=> "Воскресенье"
59
+ class I18n
60
+ @@default = 'en'
61
+
62
+ # Set default locale code to use when any user locales willn't be founded.
63
+ # It should has all translations and locale file.
64
+ def self.default=(locale)
65
+ @@default = locale
66
+ end
67
+
68
+ # Get default locale code
69
+ def self.default
70
+ @@default
71
+ end
72
+
73
+ # Parse HTTP_ACCEPT_LANGUAGE and return array of user locales
74
+ def self.parse_http(str)
75
+ return [] if str.nil?
76
+ locales = str.split(',')
77
+ locales.map! do |locale|
78
+ locale = locale.split ';q='
79
+ if 1 == locale.size
80
+ [locale[0], 1.0]
81
+ else
82
+ [locale[0], locale[1].to_f]
83
+ end
84
+ end
85
+ locales.sort! { |a, b| b[1] <=> a[1] }
86
+ locales.map! { |i| i[0] }
87
+ end
88
+
89
+ # User locales, ordered by priority
90
+ attr_reader :locales
91
+
92
+ # Dir with translations files
93
+ attr_reader :translations_dir
94
+
95
+ # First locale with locale file
96
+ attr_reader :locale
97
+
98
+ # Create i18n for +locales+ with translations from +translations_dir+ and
99
+ # locales data. Translations will be also loaded for default locale,
100
+ # +sublocales+ from first in +locales+ and general languages for dialects
101
+ # (it will load +fr+ for +fr_CA+ too).
102
+ #
103
+ # +Locales+ must be a locale code (RFC 3066) or array, ordered by priority.
104
+ def initialize(locales, translations_dir)
105
+ locales = locales.to_a if String == locales.class
106
+
107
+ @locales = locales.map do |locale|
108
+ if Locale.exists? locale
109
+ Locale.new locale
110
+ else
111
+ locale
112
+ end
113
+ end
114
+
115
+ locales << @@default
116
+ if Locale == @locales.first.class
117
+ locales += @locales.first['sublocales']
118
+ end
119
+ locales.each_with_index do |locale, i|
120
+ if "_" == locale[2..2]
121
+ locales.insert(i + 1, locale[0..1])
122
+ end
123
+ end
124
+ locales.uniq!
125
+
126
+ locales.each do |locale|
127
+ if Locale.exists? locale
128
+ @locale = Locale.new(locale)
129
+ break
130
+ end
131
+ end
132
+
133
+ @translations_dir = File.expand_path(translations_dir)
134
+ @translation = Translation.load(locales, @translations_dir)
135
+ end
136
+
137
+ # Return Hash with titles (or code for translation without locale file) of
138
+ # available translations.
139
+ def translations
140
+ Translation.available(@translations_dir).inject({}) do |all, code|
141
+ all[code] = if Locale.exists? code
142
+ Locale.new(code)['title']
143
+ else
144
+ code
145
+ end
146
+ all
147
+ end
148
+ end
149
+
150
+ # Convert +object+ to String, according to the rules of the current locale.
151
+ # It support Fixnum, Bignum, Float, Time, Date and DateTime.
152
+ #
153
+ # For time classes you can set +format+ in standart +strftime+ form, or
154
+ # Symbol to use format from locale file (<tt>:time</tt>, <tt>:date</tt>,
155
+ # <tt>:short_data</tt>, <tt>:long_data</tt>, <tt>:datetime</tt>,
156
+ # <tt>:short_datetime</tt> or <tt>:long_datetime</tt>). Without format it
157
+ # use <tt>:datetime</tt> for Time and DateTime and <tt>:date</tt> for Date.
158
+ def localize(object, format = nil)
159
+ if Fixnum == object.class or Bignum == object.class
160
+ locale.format_number(object)
161
+ elsif Float == object.class
162
+ locale.format_float(object)
163
+ elsif Time == object.class or DateTime == object.class
164
+ format = :datetime if format.nil?
165
+ locale.strftime(object, format)
166
+ elsif Date == object.class
167
+ format = :date if format.nil?
168
+ locale.strftime(object, format)
169
+ end
170
+ end
171
+ alias :l :localize
172
+
173
+ # Short and pretty way to get translation by method name. If translation
174
+ # has name like object methods (+new+, +to_s+, +methods+) use <tt>[]</tt>
175
+ # method to access.
176
+ #
177
+ # Translation can contain variable part. Just set is as <tt>%1</tt>,
178
+ # <tt>%2</tt>, etc in translations file and set values as methods params.
179
+ def method_missing(name, *params)
180
+ self[name.to_s, *params]
181
+ end
182
+
183
+ # Return translation with special +name+.
184
+ #
185
+ # Translation can contain variable part. Just set is as <tt>%1</tt>,
186
+ # <tt>%2</tt>, etc in translations file and set values in next +params+.
187
+ def [](name, *params)
188
+ @translation[name, *params]
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,171 @@
1
+ =begin
2
+ Locale to i18n support.
3
+
4
+ Copyright (C) 2008 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ require 'pathname'
21
+ require 'yaml'
22
+
23
+ module R18n
24
+ # Information about locale (language, country and other special variant
25
+ # preferences). Locale was named by RFC 3066. For example, locale for French
26
+ # speaking people in Canada will be +fr_CA+.
27
+ #
28
+ # Locale files is placed in <tt>locales/</tt> dir in YAML files.
29
+ #
30
+ # Each locale has +sublocales+ – often known languages for people from this
31
+ # locale. For example, many Belorussians know Russian and English. If there
32
+ # is’t translation for Belorussian, it will be searched in Russian and next in
33
+ # English translations.
34
+ #
35
+ # == Usage
36
+ #
37
+ # Get Russian locale and print it information
38
+ #
39
+ # ru = R18n::Locale.new('ru')
40
+ # ru['title'] #=> "Русский"
41
+ # ru['code'] #=> "ru"
42
+ # ru['direction'] #=> "ltr"
43
+ #
44
+ # == Available data
45
+ #
46
+ # * +code+: locale RFC 3066 code;
47
+ # * +title+: locale name on it language;
48
+ # * +direction+: writing direction, +ltr+ or +rtl+ (for Arabic and Hebrew);
49
+ # * +sublocales+: often known languages for people from this locale;
50
+ # * +pluralization+: function to get pluralization type for +n+ items;
51
+ # * +include+: locale code to include it data, optional.
52
+ #
53
+ # You can see more available data about locale in samples in
54
+ # <tt>locales/</tt> dir.
55
+ class Locale
56
+ LOCALES_DIR = Pathname(__FILE__).dirname.expand_path + '../../locales/'
57
+
58
+ # All available locales
59
+ def self.available
60
+ Dir.glob(LOCALES_DIR + '*.yml').map do |i|
61
+ File.basename(i, '.yml')
62
+ end
63
+ end
64
+
65
+ # Is +locale+ has info file
66
+ def self.exists?(locale)
67
+ File.exists?(File.join(LOCALES_DIR, locale + '.yml'))
68
+ end
69
+
70
+ # Default pluralization rule to translation without locale file
71
+ def self.default_pluralize(n)
72
+ n == 0 ? 0 : n == 1 ? 1 : 'n'
73
+ end
74
+
75
+ # Load locale by RFC 3066 +code+
76
+ def initialize(code)
77
+ code.delete! '/'
78
+ code.delete! '\\'
79
+
80
+ @locale = {}
81
+ while code
82
+ file = LOCALES_DIR + "#{code}.yml"
83
+ raise "Locale #{code} isn't exists" if not File.exists? file
84
+ loaded = YAML.load_file(file)
85
+ @locale = loaded.merge @locale
86
+ code = loaded['include']
87
+ end
88
+
89
+ eval("def pluralize(n); #{@locale["pluralization"]}; end", binding)
90
+ end
91
+
92
+ # Get information about locale
93
+ def [](name)
94
+ @locale[name]
95
+ end
96
+
97
+ # Is another locale has same code
98
+ def ==(locale)
99
+ @locale['code'] == locale['code']
100
+ end
101
+
102
+ # Human readable locale code and title
103
+ def inspect
104
+ "Locale #{@locale['code']} (#{@locale['title']})"
105
+ end
106
+
107
+ # Returns the integer in String form, according to the rules of the locale.
108
+ # It will also put real typographic minus.
109
+ def format_number(number)
110
+ str = number.to_s
111
+ str[0] = '−' if 0 > number # Real typographic minus
112
+ group = @locale['numbers']['group_delimiter']
113
+
114
+ if 'indian' == @locale['numbers']['separation']
115
+ str.gsub(/(\d)(?=((\d\d\d)(?!\d))|((\d\d)+(\d\d\d)(?!\d)))/) do |match|
116
+ match + group
117
+ end
118
+ else
119
+ str.gsub(/(\d)(?=(\d\d\d)+(?!\d))/) do |match|
120
+ match + group
121
+ end
122
+ end
123
+ end
124
+
125
+ # Returns the float in String form, according to the rules of the locale.
126
+ # It will also put real typographic minus.
127
+ def format_float(float)
128
+ decimal = @locale['numbers']['decimal_separator']
129
+ self.format_number(float.to_i) + decimal + float.to_s.split('.').last
130
+ end
131
+
132
+ # Same that <tt>Time.strftime</tt>, but translate months and week days
133
+ # names. In +time+ you can use Time, DateTime or Date object. In +format+
134
+ # you can use String with standart +strftime+ format (see
135
+ # <tt>Time.strftime</tt> docs) or Symbol with format from locale file
136
+ # (<tt>:time</tt>, <tt>:date</tt>, <tt>:short_data</tt>, <tt>:long_data</tt>,
137
+ # <tt>:datetime</tt>, <tt>:short_datetime</tt> or <tt>:long_datetime</tt>).
138
+ def strftime(time, format)
139
+ if Symbol == format.class
140
+ format = @locale['formats'][format.to_s]
141
+ end
142
+
143
+ translated = ''
144
+ format.scan(/%[EO]?.|./o) do |c|
145
+ case c.sub(/^%[EO]?(.)$/o, '%\\1')
146
+ when '%A'
147
+ translated << @locale['week']['days'][time.wday]
148
+ when '%a'
149
+ translated << @locale['week']['abbrs'][time.wday]
150
+ when '%B'
151
+ translated << @locale['months']['names'][time.month - 1]
152
+ when '%b'
153
+ translated << @locale['months']['abbrs'][time.month - 1]
154
+ when '%p'
155
+ translated << if time.hour < 12
156
+ @locale['time']['am']
157
+ else
158
+ @locale['time']['pm']
159
+ end
160
+ else
161
+ translated << c
162
+ end
163
+ end
164
+ time.strftime(translated)
165
+ end
166
+
167
+ # Return pluralization type for +n+ items. It will be replacing by code
168
+ # from locale file.
169
+ def pluralize(n); end
170
+ end
171
+ end
@@ -0,0 +1,33 @@
1
+ =begin
2
+ Translation string for i18n support.
3
+
4
+ Copyright (C) 2008 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ module R18n
21
+ # String, which is translated to some locale and loading from Translation.
22
+ class TranslatedString < String
23
+ # String locale
24
+ attr_reader :locale
25
+
26
+ # Returns a new string object containing a copy of +str+, which translated
27
+ # to +locale+
28
+ def initialize(str, locale)
29
+ super(str)
30
+ @locale = locale
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,203 @@
1
+ =begin
2
+ Translation to i18n support.
3
+
4
+ Copyright (C) 2008 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ require 'pathname'
21
+ require 'yaml'
22
+
23
+ module R18n
24
+ # Translation for interface to i18n support. You can load several locales and
25
+ # if translation willn’t be found in first, r18n will be search it in next.
26
+ #
27
+ # Translation files use YAML format and has name like en.yml (English) or
28
+ # en_US.yml (USA English dialect) with language/country code (RFC 3066). In
29
+ # translation file you can use strings, numbers, floats (any YAML types),
30
+ # procedures (<tt>!!proc</tt>) and pluralizable values (<tt>!!pl</tt>). You
31
+ # can use params in string values, which you can replace in program. Just
32
+ # write <tt>%1</tt>, <tt>%2</tt>, etc and set it values as method arguments,
33
+ # when you will be get value.
34
+ #
35
+ # To get translation value use method with same name. If translation name
36
+ # is equal with Object methods (+new+, +to_s+, +methods+) use
37
+ # <tt>[name, params…]</tt>. If you want to get pluralizable value, just set
38
+ # value for pluralization in fisrt argument of method. See samples below.
39
+ #
40
+ # Translated strings will have +locale+ methods, which return Locale or it
41
+ # code, if locale file isn’t exists.
42
+ #
43
+ # R18n contain translations for common words (such as “OK”, “Cancel”, etc)
44
+ # for most supported locales. See <tt>base/</tt> dir.
45
+ #
46
+ # == Examples
47
+ # translations/ru.yml
48
+ #
49
+ # one: Один
50
+ #
51
+ # translations/en.yml
52
+ #
53
+ # one: One
54
+ # two: Two
55
+ #
56
+ # entry:
57
+ # between: Between %1 and %2
58
+ # methods: Is %1 method
59
+ #
60
+ # comments: !!pl
61
+ # 0: no comments
62
+ # 1: one comment
63
+ # n: %1 comments
64
+ #
65
+ # sum: !!proc |x, y| "is #{x + y}"
66
+ #
67
+ # example.rb
68
+ #
69
+ # i18n = R18n::Translation.load(['ru', 'en'], 'translations/')
70
+ # i18n.one #=> "Один"
71
+ # i18n.two #=> "Two"
72
+ #
73
+ # i18n.two.locale['code'] #=> "en"
74
+ # i18n.two.locale['direction'] #=> "ltr"
75
+ #
76
+ # i18n.entry.between(2, 3) #=> "between 2 and 3"
77
+ # i18n['methods', 'object'] #=> "Is object method"
78
+ #
79
+ # i18n.comments(0) #=> "no comments"
80
+ # i18n.comments(10) #=> "10 comments"
81
+ #
82
+ # i18n.sum(2, 3) #=> "is 5"
83
+ #
84
+ # i18n.yes #=> "Yes"
85
+ # i18n.ok #=> "OK"
86
+ # i18n.cancel #=> "Cancel"
87
+ #
88
+ # == Extension translations
89
+ # For r18n plugin you can add dir with translations, which will be used with
90
+ # application translations. For example, DB plugin may place translations for
91
+ # error messages in extension dir. R18n contain translations for base words as
92
+ # extension dir too.
93
+ class Translation
94
+ @@extension_translations = [
95
+ Pathname(__FILE__).dirname.expand_path + '../../base']
96
+
97
+ # Get dirs with extension translations. If application translations with
98
+ # same locale isn’t exists, extension file willn’t be used.
99
+ def self.extension_translations
100
+ @@extension_translations
101
+ end
102
+
103
+ # Return available translations in +translations_dir+
104
+ def self.available(translations_dir)
105
+ Dir.glob(File.join(translations_dir, '*.yml')).map do |i|
106
+ File.basename(i, '.yml')
107
+ end
108
+ end
109
+
110
+ # Load all available translations for +locales+. +locales+ may be string
111
+ # with one user locale or array with many.
112
+ def self.load(locales, translations_dir)
113
+ locales = locales.to_a if Array != locales.class
114
+
115
+ locales &= self.available(translations_dir)
116
+
117
+ translations = []
118
+ locales.map! do |locale|
119
+ translation = {}
120
+ @@extension_translations.each do |dir|
121
+ file = File.join(dir, "#{locale}.yml")
122
+ translation.merge! YAML::load_file(file) if File.exists? file
123
+ end
124
+ file = File.join(translations_dir, "#{locale}.yml")
125
+ translation.merge! YAML::load_file(file)
126
+ translations << translation
127
+
128
+ if Locale.exists? locale
129
+ Locale.new(locale)
130
+ else
131
+ locale
132
+ end
133
+ end
134
+
135
+ self.new(locales, translations)
136
+ end
137
+
138
+ # Create translation hash with messages in +translations+ for +locales+.
139
+ #
140
+ # This is internal a constructor. To load translation use
141
+ # <tt>R18n::Translation.load(locales, translations_dir)</tt>.
142
+ def initialize(locales, translations)
143
+ @locales = locales
144
+ @translations = translations
145
+ end
146
+
147
+ # Short and pretty way to get translation by method name. If translation
148
+ # has name like object methods (+new+, +to_s+, +methods+) use <tt>[]</tt>
149
+ # method to access.
150
+ #
151
+ # Translation can contain variable part. Just set is as <tt>%1</tt>,
152
+ # <tt>%2</tt>, etc in translations file and set values as methods params.
153
+ def method_missing(name, *params)
154
+ self[name.to_s, *params]
155
+ end
156
+
157
+ # Return translation with special +name+.
158
+ #
159
+ # Translation can contain variable part. Just set is as <tt>%1</tt>,
160
+ # <tt>%2</tt>, etc in translations file and set values in next +params+.
161
+ def [](name, *params)
162
+ @translations.each_with_index do |translation, i|
163
+ result = translation[name]
164
+ next if result.nil?
165
+
166
+ if YAML::PrivateType == result.class
167
+ case result.type_id
168
+ when 'proc'
169
+ return eval("proc {#{result.value}}").call(*params)
170
+ when 'pl'
171
+ locale = @locales[i]
172
+
173
+ type = if Locale == locale.class
174
+ locale.pluralize(params.first)
175
+ else
176
+ Locale.default_pluralize(params.first)
177
+ end
178
+
179
+ type = 'n' if not result.value.include? type
180
+ result = result.value[type]
181
+ else
182
+ return result
183
+ end
184
+ end
185
+
186
+ if String == result.class
187
+ params.each_with_index do |param, i|
188
+ result.gsub! "%#{i+1}", param.to_s
189
+ end
190
+ return TranslatedString.new(result, @locales[i])
191
+ elsif Hash == result.class
192
+ return self.class.new(@locales, @translations.map { |i|
193
+ i[name] or {}
194
+ })
195
+ else
196
+ return result
197
+ end
198
+ end
199
+
200
+ return nil
201
+ end
202
+ end
203
+ end