rubi18n 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.
@@ -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 Rubi18n
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 and translated to
27
+ # +locale+
28
+ def initialize(str, locale)
29
+ super(str)
30
+ @locale = locale
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,269 @@
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 Rubi18n
24
+ # Translation for interface to i18n support.
25
+ #
26
+ # Translation files use YAML format and has name CODE.yml, where CODE is a
27
+ # language/country code (RFC 3066) like en_US. For example: en.yaml is a
28
+ # English translation, en_US.yml is a translation to USA English dialect.
29
+ #
30
+ # Rubi18n contain translations for common words (such as “OK”, “Cancel”, etc)
31
+ # for most supported locales. See <tt>base/</tt> dir.
32
+ #
33
+ # You may load several locales. Also for each locale will be load it
34
+ # sublocales – languages, which user often know in this locale. For example,
35
+ # many, who speak on Frensh dialects in Canada can understand original French.
36
+ # Also many people in Belarus can understand Russian.
37
+ #
38
+ # Translated strinsg will have locale methods, which return Locale of locale
39
+ # code if locale info file isn't exists.
40
+ #
41
+ # == Examples
42
+ # translations/fr.yaml
43
+ #
44
+ # one: Un
45
+ #
46
+ # translations/ru.yaml
47
+ #
48
+ # one: Один
49
+ # two: Два
50
+ #
51
+ # translations/en.yaml
52
+ #
53
+ # one: One
54
+ # two: Two
55
+ # three: Three
56
+ #
57
+ # entry:
58
+ # between: Between %1 and %2
59
+ # methods: Is %1 method
60
+ #
61
+ # comments: !!pl
62
+ # 0: no comments
63
+ # 1: one comment
64
+ # n: %1 comments
65
+ #
66
+ # sum: !!proc |x, y| "is #{x + y}"
67
+ #
68
+ # example.rb
69
+ #
70
+ # i18n = Rubi18n::Translation.load(["fr", "ru"], "translations/")
71
+ # i18n.one #=> "Un"
72
+ # i18n.two #=> "Два"
73
+ # i18n.three #=> "Three"
74
+ #
75
+ # i18n.three.locale["code"] #=> "en"
76
+ # i18n.three.locale["direction"] #=> "ltr"
77
+ #
78
+ # i18n.entry.between(2, 3) #=> "between 2 and 3"
79
+ # i18n["methods", "object"] #=> "Is object method"
80
+ #
81
+ # i18n.comments(0) #=> "no comments"
82
+ # i18n.comments(10) #=> "10 comments"
83
+ #
84
+ # i18n.sum(2, 3) #=> "is 5"
85
+ #
86
+ # == Extention translations
87
+ # You can add dir with translations, which will be used with application
88
+ # translations. If application translations with same locale isn't exists
89
+ # extention files willn't be used.
90
+ #
91
+ # It's useful for plugins for rubi18n. For example DB plugin may place
92
+ # translations for error messages in extention dir. Rubi18n contain
93
+ # translations for base words as extention dir too.
94
+ class Translation
95
+ @@default = "en"
96
+
97
+ # Set default locale code to use when any user locales willn't be founded.
98
+ # It should has all translations and locale file.
99
+ def self.default=(locale)
100
+ @@default = locale
101
+ end
102
+
103
+ # Get default locale code
104
+ def self.default
105
+ @@default
106
+ end
107
+
108
+ @@extension_translations = [
109
+ Pathname(__FILE__).dirname.expand_path + "../../base"]
110
+
111
+ # Get dirs with extention translations. If application translations with
112
+ # same locale isn't exists extention files willn't be used.
113
+ def self.extension_translations
114
+ @@extension_translations
115
+ end
116
+
117
+ # Return available translations in +translations_dir+
118
+ def self.available(translations_dir)
119
+ Dir.glob(File.join(translations_dir, "*.yml")).map do |i|
120
+ File.basename(i, ".yml")
121
+ end
122
+ end
123
+
124
+ # Load locale and translations. +locales+ may be string for one user locale
125
+ # or array with many.
126
+ #
127
+ # It load all available translations in +translations_dir+, which exists in
128
+ # +locales+. Next it load all translations for +sublocales+ in locale info
129
+ # for all loaded translations.
130
+ #
131
+ # == Example
132
+ # +locales+ is ["fr", "uk"], translations are available for "fr", "be", "ru"
133
+ # and "en". Translation will be founding firstly in "fr" next in "be" and
134
+ # next in "ru" and "en" (because many Belarusians know Russian and "be"
135
+ # locale contain in +sublocales+ "ru" and "en").
136
+ def self.load(locales, translations_dir)
137
+ locales = locales.to_a if String == locales.class
138
+ locales << @@default
139
+
140
+ locales += Locale.find(locales)["sublocales"]
141
+ locales.each_with_index do |locale, i|
142
+ # Add language locale for dialects
143
+ if "_" == locale[2..2]
144
+ locales.insert(i + 1, locale[0..1])
145
+ end
146
+ end
147
+ locales.uniq!
148
+
149
+ locales &= self.available(translations_dir)
150
+
151
+ translations = []
152
+ locales.map! do |locale|
153
+ locale.delete!("/", "\\")
154
+
155
+ translation = {}
156
+ @@extension_translations.each do |dir|
157
+ file = File.join(dir, "#{locale}.yml")
158
+ translation.merge! YAML::load_file(file) if File.exists? file
159
+ end
160
+ file = File.join(translations_dir, "#{locale}.yml")
161
+ translation.merge! YAML::load_file(file)
162
+ translations << translation
163
+
164
+ if Locale.exists? locale
165
+ Locale.new(locale)
166
+ else
167
+ locale
168
+ end
169
+ end
170
+
171
+ self.new(locales, translations)
172
+ end
173
+
174
+ # Default pluralization rule to translation without locale info
175
+ def self.pluralize(n)
176
+ n == 0 ? 0 : n == 1 ? 1 : 'n'
177
+ end
178
+
179
+ # Create translation hash with messages in +translations+ for +locales+.
180
+ #
181
+ # This is internal contructor to load translation use
182
+ # Rubi18n::Translation.load(locales, translations_dir).
183
+ def initialize(locales, translations)
184
+ @locales = locales
185
+ @translations = translations
186
+ end
187
+
188
+ # Short and pretty way to get traslation by method name. If translation
189
+ # has name of object methods ("methods", "strftime") use <tt>[]</tt> method
190
+ # to access.
191
+ #
192
+ # Translation can contain variable part. Just set is as %1, %2 etc in
193
+ # translations file and set values as methods params.
194
+ def method_missing(name, *params)
195
+ self[name.to_s, *params]
196
+ end
197
+
198
+ # Return traslation with special name.
199
+ #
200
+ # Translation can contain variable part. Just set is as %1, %2 etc in
201
+ # translations file and set values in next arguments.
202
+ def [](name, *params)
203
+ @translations.each_with_index do |translation, i|
204
+ result = translation[name]
205
+ next if result.nil?
206
+
207
+ if YAML::PrivateType == result.class
208
+ case result.type_id
209
+ when "proc"
210
+ return eval("proc {#{result.value}}").call(*params)
211
+ when "pl"
212
+ pluralizator = @locales[i]
213
+ pluralizator = self.class if Locale != pluralizator.class
214
+
215
+ params.each do |param|
216
+ type = pluralizator.pluralize(param)
217
+ type = "n" if not result.value.include? type
218
+ result = result.value[type]
219
+ break if YAML::PrivateType != result.class
220
+ end
221
+ else
222
+ return result
223
+ end
224
+ end
225
+
226
+ if String == result.class
227
+ params.each_with_index do |param, i|
228
+ result.gsub! "%#{i+1}", param.to_s
229
+ end
230
+ return TranslatedString.new(result, @locales[i])
231
+ elsif Hash == result.class
232
+ return self.class.new(@locales, @translations.map { |i|
233
+ i[name] or {}
234
+ })
235
+ else
236
+ return result
237
+ end
238
+ end
239
+
240
+ return nil
241
+ end
242
+
243
+ # Format +time+ according to the directives in the given format string and
244
+ # translate month, week days and meridian indicator (AM/PM) names (%A, %a,
245
+ # %B, %b, %p directives).
246
+ #
247
+ # See <tt>Time.strftime()</tt> method docs for info about all directives.
248
+ def strftime(time, format)
249
+ translated = ""
250
+ format.scan(/%[EO]?.|./o) do |c|
251
+ case c.sub(/^%[EO]?(.)$/o, "%\\1")
252
+ when "%A"
253
+ translated << self.week.days[time.wday - 1]
254
+ when "%a"
255
+ translated << self.week.abbr_days[time.wday - 1]
256
+ when "%B"
257
+ translated << self.months.names[time.month - 1]
258
+ when "%b"
259
+ translated << self.months.abbr_names[time.month - 1]
260
+ when "%p"
261
+ translated << if time.hour < 12 then self.am else self.pm end
262
+ else
263
+ translated << c
264
+ end
265
+ end
266
+ time.strftime(translated)
267
+ end
268
+ end
269
+ end
data/lib/rubi18n.rb ADDED
@@ -0,0 +1,29 @@
1
+ =begin
2
+ Main file to load all neccessary classes 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
+ require "pathname"
21
+
22
+ $KCODE = "u"
23
+
24
+ dir = Pathname(__FILE__).dirname.expand_path + "rubi18n"
25
+
26
+ require dir + "locale"
27
+ require dir + "translation"
28
+ require dir + "translated_string"
29
+ require dir + "formatters"
data/locales/en.yml ADDED
@@ -0,0 +1,13 @@
1
+ title: English
2
+ code: en
3
+ sublocales: []
4
+ direction: ltr
5
+
6
+ pluralization: "n == 0 ? 0 : n == 1 ? 1 : 'n'"
7
+
8
+ week_start: sunday
9
+
10
+ numbers:
11
+ separation: classic
12
+ decimal_separator: "."
13
+ group_delimiter: ","
data/locales/en_US.yml ADDED
@@ -0,0 +1,5 @@
1
+ include: en
2
+
3
+ code: en_US
4
+ title: English (US)
5
+ sublocales: [en]
data/locales/ru.yml ADDED
@@ -0,0 +1,13 @@
1
+ title: Русский
2
+ code: ru
3
+ sublocales: [en]
4
+ direction: ltr
5
+
6
+ pluralization: "n==0 ? 0 : n%10==1 && n%100!=11 ? 1 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 2 : 'n'"
7
+
8
+ week_start: monday
9
+
10
+ numbers:
11
+ separation: classic
12
+ decimal_separator: ","
13
+ group_delimiter: " "
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Rubi18n::Formatters do
4
+
5
+ it "should format number in local traditions" do
6
+ Rubi18n::Locale.current = "en"
7
+ -123456789.to_ls.should == "−123,456,789"
8
+
9
+ -123456789.to_ls(Rubi18n::Locale.new("ru")).should == "−123 456 789"
10
+ end
11
+
12
+ it "should format float in local traditions" do
13
+ Rubi18n::Locale.current = "en"
14
+ -12345.67.to_ls.should == "−12,345.67"
15
+
16
+ -12345.67.to_ls(Rubi18n::Locale.new("ru")).should == "−12 345,67"
17
+ end
18
+
19
+ end
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Rubi18n::Locale do
4
+
5
+ it "should check is locale exists" do
6
+ Rubi18n::Locale.exists?("ru").should be_true
7
+ Rubi18n::Locale.exists?("no_LC").should be_false
8
+ end
9
+
10
+ it "should load locale" do
11
+ locale = Rubi18n::Locale.new "ru"
12
+ locale["code"].should == "ru"
13
+ locale["title"].should == "Русский"
14
+ end
15
+
16
+ it "should include locale by 'include' option" do
17
+ en_US = Rubi18n::Locale.new "en_US"
18
+ en = Rubi18n::Locale.new "en"
19
+ en_US["title"].should == "English (US)"
20
+ en["title"].should == "English"
21
+ en_US["week"].should == en["week"]
22
+ end
23
+
24
+ it "should raise error if locale isn't exists" do
25
+ lambda {
26
+ Rubi18n::Locale.new "no_LC"
27
+ }.should raise_error
28
+ end
29
+
30
+ it "should be equal to another locale with same code" do
31
+ ru = Rubi18n::Locale.new "ru"
32
+ en = Rubi18n::Locale.new "en"
33
+ another = Rubi18n::Locale.new "en"
34
+ en.should_not == ru
35
+ en.should == another
36
+ end
37
+
38
+ it "should print human readable representation" do
39
+ Rubi18n::Locale.new("ru").inspect.should == "Locale ru (Русский)"
40
+ end
41
+
42
+ it "should has current locale to use in +to_ls+ methods" do
43
+ en = Rubi18n::Locale.new "en"
44
+ Rubi18n::Locale.current = en
45
+ Rubi18n::Locale.current.should == en
46
+ Rubi18n::Locale.current = "en"
47
+ Rubi18n::Locale.current.should == en
48
+ Rubi18n::Locale.current = "ru"
49
+ Rubi18n::Locale.current.should_not == en
50
+ end
51
+
52
+ it "should return all available locales" do
53
+ Rubi18n::Locale.locales.sort.should == ["en", "en_US", "ru"]
54
+ end
55
+
56
+ it "should load first available locale" do
57
+ Rubi18n::Locale.find(["no_LC", "en", "ru"])["code"].should == "en"
58
+ end
59
+
60
+ it "should find a short name of locale" do
61
+ Rubi18n::Locale.find("ru_RU.UTF-8")["code"].should == "ru"
62
+ end
63
+
64
+ it "should return pluralization type by elements count" do
65
+ locale = Rubi18n::Locale.new "en"
66
+ locale.pluralize(0).should == 0
67
+ locale.pluralize(1).should == 1
68
+ locale.pluralize(:n).should == 'n'
69
+ end
70
+
71
+ end
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), "..", "lib", "rubi18n")
2
+
3
+ $KCODE = "u"
@@ -0,0 +1,106 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Rubi18n::Translation do
4
+ DIR = File.join(File.dirname(__FILE__), "translations", "general")
5
+ EXT = File.join(File.dirname(__FILE__), "translations", "extention")
6
+
7
+ it "should return all available translations" do
8
+ Rubi18n::Translation.available(DIR).sort.should == ["en", "fr", "no_LC", "ru"]
9
+ end
10
+
11
+ it "should has default locale to use when there aren't any user locales" do
12
+ Rubi18n::Translation.default = "en"
13
+ Rubi18n::Translation.default.should == "en"
14
+ Rubi18n::Translation.load("no_TR", DIR).one.should == "One"
15
+ end
16
+
17
+ it "should load translations" do
18
+ translation = Rubi18n::Translation.load("en", DIR)
19
+ translation.one.should == "One"
20
+ translation["one"].should == "One"
21
+ end
22
+
23
+ it "should find in subtranslations" do
24
+ translation = Rubi18n::Translation.load(["fr", "ru"], DIR)
25
+ translation.one.should == "Un"
26
+ translation.two.should == "Два"
27
+ translation.three.should == "Three"
28
+ end
29
+
30
+ it "should return nil if translation isn't found" do
31
+ translation = Rubi18n::Translation.load("en", DIR)
32
+ translation.without_translation.should be_nil
33
+ end
34
+
35
+ it "should search language locale for dialect" do
36
+ translation = Rubi18n::Translation.load("fr_CA", DIR)
37
+ translation.one.should == "Un"
38
+ end
39
+
40
+ it "should can use params in translation" do
41
+ translation = Rubi18n::Translation.load("en", DIR)
42
+ translation.params(1, 2).should == "Is 1 between 1 and 2?"
43
+ translation["params", 1, 2].should == "Is 1 between 1 and 2?"
44
+ end
45
+
46
+ it "should load use hierarchical translations" do
47
+ translation = Rubi18n::Translation.load(["ru", "en"], DIR)
48
+ translation.in.another.should == "Иерархический"
49
+ translation["in"]["another"].should == "Иерархический"
50
+ translation.only.english.should == "Only in English"
51
+ end
52
+
53
+ it "should return string with locale info" do
54
+ translation = Rubi18n::Translation.load(["fr", "ru"], DIR)
55
+ translation.one.locale.should == "fr"
56
+ translation.two.locale.should == Rubi18n::Locale.new("ru")
57
+ translation.three.locale.should == Rubi18n::Locale.new("en")
58
+ end
59
+
60
+ it "should use extention translations" do
61
+ Rubi18n::Translation.extension_translations << EXT
62
+
63
+ translation = Rubi18n::Translation.load("en", DIR)
64
+ translation.ext.should == "Extention"
65
+ translation.one.should == "One"
66
+ end
67
+
68
+ it "shouldn't use extention without app translations with same locale" do
69
+ Rubi18n::Translation.extension_translations << EXT
70
+
71
+ translation = Rubi18n::Translation.load(["no_TR", "en"], DIR)
72
+ translation.ext.should == "Extention"
73
+ end
74
+
75
+ it "should translate month, week days and am/pm names in strftime" do
76
+ translation = Rubi18n::Translation.load("ru", DIR)
77
+
78
+ time = Time.at(0).utc
79
+ translation.strftime(time, "%a %A").should == 'Срд Среда'
80
+ translation.strftime(time, "%b %B").should == 'Янв Январь'
81
+ translation.strftime(time, "%H:%M%p").should == '00:00AM'
82
+ end
83
+
84
+ it "should call proc in translation" do
85
+ translation = Rubi18n::Translation.load("en", DIR)
86
+ translation.sum(2, 3).should == 5
87
+ end
88
+
89
+ it "should pluralize translation" do
90
+ translation = Rubi18n::Translation.load("en", DIR)
91
+ translation.comments(0, "article").should == "no comments for article"
92
+ translation.comments(1, "article").should == "one comment for article"
93
+ translation.comments(5, "article").should == "5 comments for article"
94
+
95
+ translation.files(0).should == "0 files"
96
+
97
+ translation.authors(1, 4).should == "One author with many comments"
98
+ end
99
+
100
+ it "should pluralize translation without locale" do
101
+ translation = Rubi18n::Translation.load("no_LC", DIR)
102
+ translation.entries(1).should == "ONE"
103
+ translation.entries(5).should == "N"
104
+ end
105
+
106
+ end
@@ -0,0 +1,2 @@
1
+ one: No one
2
+ ext: Extention
@@ -0,0 +1 @@
1
+ ext: NO
@@ -0,0 +1,31 @@
1
+ one: One
2
+ two: Two
3
+ three: Three
4
+
5
+ params: Is %1 between %1 and %2?
6
+
7
+ in:
8
+ another: Hierarchical
9
+
10
+ only:
11
+ english: Only in English
12
+
13
+ sum: !!proc |x, y| x + y
14
+
15
+ comments: !!pl
16
+ 0: no comments for %2
17
+ 1: one comment for %2
18
+ n: %1 comments for %2
19
+
20
+ files: !!pl
21
+ 1: 1 file
22
+ n: %1 files
23
+
24
+ authors: !!pl
25
+ 0: No authors
26
+ 1: !!pl
27
+ 1: One author with one comment
28
+ n: One author with many comments
29
+ n: !!pl
30
+ 1: Many authors with one comment
31
+ n: Many authors with many comments
@@ -0,0 +1 @@
1
+ one: Un
@@ -0,0 +1,4 @@
1
+ entries: !!pl
2
+ 1: ONE
3
+ 2: TWO
4
+ n: N
@@ -0,0 +1,5 @@
1
+ one: Один
2
+ two: Два
3
+
4
+ in:
5
+ another: Иерархический