r18n-core 0.1

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