rubi18n 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: Иерархический