util 0.4.0

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,327 @@
1
+ module Util
2
+ # A class for simple internationalization.
3
+ # Do not expect anything extraordinary.
4
+ class I18n
5
+ # If the provided default language does not work, the default default
6
+ # language is French.
7
+ DEFAULT_LANG = :fra
8
+
9
+ private_class_method :new
10
+
11
+ # Initialize the internationalization manager. Automatically called when
12
+ # using another method of the class.
13
+ def self.init
14
+ @default_lang = DEFAULT_LANG
15
+ @errors = []
16
+ @messages = {}
17
+ end
18
+
19
+ # Simpler variant of register, where the module name is generated
20
+ # from its filename. No recursivity.
21
+ # @param [String] path path relative to which the +i18n+ folder is located
22
+ # @return [Boolean] true on success, false on error
23
+ def self.<< path
24
+ name = File.basename path.to_s, '.rb'
25
+ register name, path
26
+ end
27
+
28
+ # Get the current default language.
29
+ # @return [Symbol]
30
+ def self.default_lang
31
+ init unless initialized?
32
+ @default_lang
33
+ end
34
+
35
+ # Check whether the internationalization system is initialized.
36
+ # @return [Boolean]
37
+ def self.initialized?
38
+ not @default_lang.nil?
39
+ end
40
+
41
+ # Obtain the translated message associated to a given
42
+ # internationalization token. If no options are provided, the
43
+ # default language and last used module will be used.
44
+ # @param [String] id internationalization token
45
+ # @param [Hash] opts options
46
+ # @option opts [#to_sym] :lang wanted language
47
+ # @option opts [String] :mod in which module to find the token
48
+ # @return [String] translated message, or +''+ in case of error
49
+ def self.message id, opts={}
50
+ init unless initialized?
51
+ if @messages.empty? then
52
+ @errors << [:messages_empty, nil]
53
+ return ''
54
+ end
55
+
56
+ require 'util/args'
57
+ id = Util::Args.check id, String, ''
58
+ o_lang, o_mod = Util::Opts.check opts, :lang, Symbol, @default_lang, \
59
+ :mod, String, @last_used_mod
60
+
61
+ mod = o_mod.empty? ? @last_used_mod : o_mod
62
+ mod = @messages.keys[0] if mod.empty?
63
+ if @messages[mod].nil? or @messages[mod].empty? then
64
+ @errors << [:msg_no_module_content, mod]
65
+ return ''
66
+ end
67
+
68
+ lang = (@messages[mod][o_lang].nil? or @messages[mod][o_lang].empty?) \
69
+ ? @default_lang : o_lang
70
+ lang = (@messages[mod][lang].nil? or @messages[mod][lang].empty?) \
71
+ ? DEFAULT_LANG : lang
72
+ if @messages[mod][lang].nil? or @messages[mod][lang].empty? then
73
+ data = [o_lang, @default_lang, DEFAULT_LANG].to_s
74
+ @errors << [:msg_no_valid_lang, data]
75
+ return ''
76
+ end
77
+
78
+ @last_used_mod = mod
79
+ @messages[mod][lang][id].to_s
80
+ end
81
+
82
+ # Get the next error encountered while using I18n. Possible
83
+ # errors are the following.
84
+ # - +:messages_empty+ (data is +nil+): attempted to get a tranlated
85
+ # message before having registered any.
86
+ # - +:msg_no_module_content+ (data is the module name): attempted to get
87
+ # a translated message from a module that does not exist or
88
+ # contains no messages.
89
+ # - +:msg_no_valid_lang+ (data is an array of tried languages): attempted
90
+ # to get a translated message from a module where neither the asked
91
+ # language nor the default languages contain any messages.
92
+ # - +:reg_no_file+ (data is the provided path): the path provided to
93
+ # register messages contains no valid YAML file to use.
94
+ # - +:reg_no_name+ (data is the provided name): attempted to
95
+ # register messages with a module name that resolves to an empty string.
96
+ # - +:reg_path_not_dir+ (data is the provided path): the path provided to
97
+ # register messages is not a directory.
98
+ # - +:reg_path_not_exist+ (data is the provided path): the path provided
99
+ # to register messages does not exist.
100
+ # - +:reg_yaml_cant_open+ (data is the file path): the file cannot be
101
+ # opened or parsed by YAML.
102
+ # - +:set_def_unknown+ (data is the provided language code): attempted
103
+ # to set the default language to an invalid value.
104
+ # @return [Array<Symbol, Object>] error name and error data
105
+ def self.next_error
106
+ init unless initialized?
107
+ @errors.shift
108
+ end
109
+
110
+ # Register a set of internationalization tokens. Will search the
111
+ # same directory as the given path for YAML files whose file name
112
+ # is a valid language code, and extract the internationalized
113
+ # strings from them.
114
+ # @param [String] o_name name of the token-set
115
+ # @param [String] path file relative to which the YAML files
116
+ # will be searched (it is meant to be used with +__FILE__+).
117
+ # If nothing is provided, present working directory will be used.
118
+ # @param [Boolean] relative if true, will search +i18n+ folder
119
+ # next to the given path; if false, will search YAML files
120
+ # directly inside the given path
121
+ # @param [Boolean] recursive search subfolders too, and generate
122
+ # token-set names derived from the subfolders’ name
123
+ # @return [Boolean] true on success, false on error
124
+ def self.register o_name='', path='', relative=true, recursive=false
125
+ name, path, relative, recursive = \
126
+ register_check_args o_name, path, relative, recursive
127
+ return false if name == false
128
+
129
+ locations = register_get_locations name, path, recursive
130
+
131
+ messages = {}
132
+ locations.each do |loc|
133
+ register_get_messages loc, messages
134
+ end
135
+ return false if check_t messages.empty?, :reg_no_file, path
136
+
137
+ @messages.merge! messages do |k, ov, nv|
138
+ @messages[k] = ov.merge nv do |l, lov, lnv|
139
+ ov[l] = lov.merge lnv
140
+ end
141
+ end
142
+
143
+ @last_used_mod = name if @last_used_mod.nil?
144
+ true
145
+ end
146
+
147
+ # Set the default language to use from now on. Recommended is an
148
+ # ISO 639-3 language code, but any ISO 639 code, IETF BCP 47 language
149
+ # tag or ISO/IEC 15897 locale (a. k. a. POSIX locale) will work.
150
+ # @param [#to_sym] n_lang new default language
151
+ # @return [Boolean]
152
+ def self.set_default_lang n_lang
153
+ init unless initialized?
154
+ lang = n_lang.to_s.downcase
155
+ lang = $1 if lang.match /^(\w+)(?:_|-)/
156
+ lang = valid_lang? lang
157
+ return false if check_f lang, :set_def_unknown, n_lang
158
+
159
+ @default_lang = lang.to_sym
160
+ true
161
+ end
162
+
163
+ private
164
+
165
+ # Register an error if a given test yields a given result.
166
+ # @param [Boolean] trigger test result that shall trigger the error
167
+ # @param [Boolean] test test that might trigger the error
168
+ # @param [Symbol] err_name error name
169
+ # @param [Object] err_data data giving details on the error
170
+ # @return [Boolean] whether the error was triggered
171
+ def self.check trigger, test, err_name, err_data
172
+ @errors << [err_name, err_data] if test == trigger
173
+ test == trigger
174
+ end
175
+
176
+ # Register an error if the test is false
177
+ # @param [Boolean] test test that might trigger the error
178
+ # @param [Symbol] err_name error name
179
+ # @param [Object] err_data data giving details on the error
180
+ # @return [Boolean] whether the error was triggered
181
+ def self.check_f test, err_name, err_data=nil
182
+ check false, test, err_name, err_data
183
+ end
184
+
185
+ # Register an error if the test is true
186
+ # @param (see check_f)
187
+ # @return (see check_f)
188
+ def self.check_t test, err_name, err_data=nil
189
+ check true, test, err_name, err_data
190
+ end
191
+
192
+ # Verify the arguments given to I18n.register.
193
+ # @param (see register)
194
+ # @return [String, String, Boolean, Boolean] success
195
+ # @return [false] failure
196
+ def self.register_check_args o_name, path, relative, recursive
197
+ require 'util/args'
198
+ init unless initialized?
199
+ name, path, relative, recursive = Util::Args.check \
200
+ o_name, String, '', path, String, '', \
201
+ relative, FalseClass, true, recursive, TrueClass, false
202
+ return false if check_t name.empty?, :reg_no_name, o_name
203
+
204
+ path = relative ? './temp.rb' : '.' if path.empty?
205
+ if relative then
206
+ dir = File.dirname(File.expand_path path)
207
+ path = File.join dir, 'i18n'
208
+ end
209
+
210
+ return false if check_f File.exist?(path), :reg_path_not_exist, path
211
+ return false if check_f File.directory?(path), :reg_path_not_dir, path
212
+ [name, path, relative, recursive]
213
+ end
214
+
215
+ # Get the folder(s) in which to search for YAML files, and the
216
+ # associated module names.
217
+ # @param [String] name name of the root module
218
+ # @param [String] path path to the root folder
219
+ # @param [Boolean] recursive wether the search should be recursive
220
+ # @return [Array<Array<String, String>>]
221
+ def self.register_get_locations name, path, recursive
222
+ locations = []
223
+ dirs = [path]
224
+ begin
225
+ current = dirs.shift
226
+ glob = File.join current.gsub(/([?*{\[])/, "\\$1"), '*.yml'
227
+ # These characters have a meaning for +Dir.glob+ and might
228
+ # yield very strange results if they are not escaped.
229
+
230
+ slug = slugify(current.sub(path, '').sub(File::SEPARATOR, ''))
231
+ full_name = slug.empty? ? name : (name + '-' + slug)
232
+ locations << [glob, full_name]
233
+ Dir[File.join current, '*'].each do |f|
234
+ dirs << f if File.directory? f
235
+ end if recursive
236
+ end until dirs.empty?
237
+ locations
238
+ end
239
+
240
+ # Extract the messages from a given folder. Will use all files with
241
+ # the form +lang-code.yml+ that contain valid YAML for a hash, then
242
+ # convert the values to strings, and finally merge it in the
243
+ # existing list.
244
+ # @param [Array<String, String>] loc glob and module name to use
245
+ # @param [Hash] messages existing list
246
+ # @return [Hash] updated list
247
+ def self.register_get_messages loc, messages
248
+ require 'util/yaml'
249
+ glob, name = loc
250
+ langs = {}
251
+
252
+ Dir[glob].each do |f|
253
+ lang = File.basename(f, '.yml')
254
+ next unless lang = valid_lang?(lang)
255
+ lang = lang.to_sym
256
+
257
+ content = YAML.from_file f
258
+ check_f content, :reg_yaml_cant_open, f
259
+ next unless content.is_a? Hash
260
+
261
+ langs[lang] = {} unless content.empty?
262
+ content.each_pair do |k, v|
263
+ langs[lang][k.to_s] = v.to_s
264
+ end
265
+ end
266
+
267
+ return messages if langs.empty?
268
+
269
+ if messages.has_key? name then
270
+ messages[name].merge! langs do |k, ov, nv|
271
+ messages[name][k] = ov.merge nv
272
+ end
273
+ else
274
+ messages[name] = langs
275
+ end
276
+
277
+ messages
278
+ end
279
+
280
+ # Escape a path so that it can be used as a Hash key. Rules are as follows.
281
+ # - Any non-word character becomes a hyphen.
282
+ # - Multiple hyphens are reduced to just one.
283
+ # - Starting and trailing hyphens are removed.
284
+ # - Everything is downcased.
285
+ # - The {https://en.wikipedia.org/wiki/Base64 base64} representation
286
+ # of the path is used instead if the resulting slug is empty.
287
+ # - All directories’ slugs are joined by hyphens.
288
+ # @param [String] string path to transform in slug
289
+ # @return [String]
290
+ # @example
291
+ # I18n.slugify 'alias' # 'alias'
292
+ # I18n.slugify '今日は' # '今日は'
293
+ # I18n.slugify 'La vie de ma mère' # 'la-vie-de-ma-mère'
294
+ # I18n.slugify 'Ça va? Oui, et toi?' # 'ça-va-oui-et-toi'
295
+ # I18n.slugify '??!?' # 'Pz8hPw=='
296
+ # I18n.slugify 'hello/Darkneß/m41_0!d' # 'hello-darkneß-m41_0-d'
297
+ # I18n.slugify 'Estie/de/tabarnak/!!§!' # 'estie-de-tabarnak-ISHCpyE='
298
+ # @note {https://www.youtube.com/watch?v=DvR6-SQzqO8 For those who did
299
+ # not get the last example…}
300
+ def self.slugify string
301
+ require 'base64'
302
+ result = []
303
+ string.split(File::SEPARATOR).each do |part|
304
+ res = part.gsub(/[^[:word:]]/, '-').gsub(/-{2,}/, '-')
305
+ res = res.sub(/^-/, '').sub(/-$/, '').downcase
306
+ res = Base64.urlsafe_encode64(part) if res.empty?
307
+ result << res
308
+ end
309
+ result.join '-'
310
+ end
311
+
312
+ # From a given ISO 639 code, either get the valid corresponding
313
+ # ISO 639-3 code, or false.
314
+ # @param [#to_sym] lang language code to check
315
+ # @return [#to_sym] existing ISO 639-3 code
316
+ # @return [false] could not find a matching ISO 639-3 code
317
+ def self.valid_lang? lang
318
+ require 'util/lists/iso639'
319
+ iso = Util::Lists::ISO639
320
+ # Typecheck is already done by ISO639
321
+ return lang if iso::P3.exist?(lang)
322
+ if n_lang = iso::P3.from2(lang) then return n_lang; end
323
+ if n_lang = iso::P3.from1(lang) then return n_lang; end
324
+ false
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,6 @@
1
+ require 'util/lists/iso369'
2
+
3
+ module Util
4
+ # A collection of modules representing useful lists, like standards.
5
+ module Lists; end
6
+ end
@@ -0,0 +1,149 @@
1
+ require 'util/communia'
2
+
3
+ module Util
4
+ module Lists
5
+ # A module for {https://en.wikipedia.org/wiki/ISO_639 ISO 639} language codes.
6
+ module ISO639
7
+ private
8
+
9
+ # Methods common to all parts of ISO 639 standard.
10
+ class Common
11
+ private_class_method :new
12
+
13
+ # Get the data from the file into memory. Automatically called when
14
+ # using another method of the class.
15
+ # @param [String] filename file inside +share/lists+ to use
16
+ def self.init filename
17
+ require 'util/yaml'
18
+ path = File.join Util::SHARE_PATH, 'lists'
19
+
20
+ @complete = YAML.from_file File.join(path, filename), {}
21
+ @complete.freeze
22
+
23
+ @codes = @complete.keys
24
+ @codes.freeze
25
+ end
26
+
27
+ # Check whether the data is in memory and readable.
28
+ # @return [Boolean]
29
+ def self.initialized?
30
+ not @complete.nil?
31
+ end
32
+
33
+ # Full list of codes in a given part of the stadard.
34
+ # @return [Array<Symbol>]
35
+ def self.codes
36
+ init if @complete.nil?
37
+ @codes
38
+ end
39
+
40
+ # Full data for a given part of the standard.
41
+ # @return [Hash<Symbol, Hash>]
42
+ def self.complete
43
+ init if @complete.nil?
44
+ @complete
45
+ end
46
+
47
+ # Check whether a given code exists in a given standard.
48
+ # @param [#to_sym] code
49
+ # @return [Boolean]
50
+ def self.exist? code
51
+ not category(code).nil?
52
+ end
53
+
54
+ # Check whether a given code is deprecated in a given standard.
55
+ # @param [#to_sym] code
56
+ # @return [Boolean]
57
+ def self.deprecated? code
58
+ category(code) == :dep
59
+ end
60
+
61
+ # Check whether a given code is for private use in a given standard.
62
+ # @param [#to_sym] code
63
+ # @return [Boolean]
64
+ def self.private? code
65
+ category(code) == :priv
66
+ end
67
+
68
+ # Check whether a given code is valid to date in a given standard.
69
+ # @param [#to_sym] code
70
+ # @return [Boolean]
71
+ def self.valid? code
72
+ category(code) == :val
73
+ end
74
+
75
+ private
76
+
77
+ # Check which kind of code the argument is.
78
+ # @param [#to_sym] code
79
+ # @return [:dep, :priv, :val, nil]
80
+ def self.category code
81
+ init if @complete.nil?
82
+ require 'util/args'
83
+ code = Util::Args.check code, Symbol, false
84
+ return nil unless code
85
+
86
+ info = @complete[code]
87
+ base = code.to_s[0..1]
88
+ priv = (base <=> 'pz') + (base <=> 'qu') == 0
89
+ info.nil? \
90
+ ? (priv ? :priv : nil)
91
+ : (info[:deprecated] ? :dep : :val)
92
+ end
93
+ end
94
+
95
+ public
96
+
97
+ # Codes from the ISO 639-3 standard.
98
+ # @example
99
+ # codes = Util::Lists::ISO639
100
+ # puts codes::P3.from1 'fr' # :fre
101
+ # puts codes::P3.exist? :prv # true
102
+ # puts codes::P3.valid? :prv # false
103
+ class P3 < Common
104
+ # Get the data into memory. Automatically called when using
105
+ # another method of the class.
106
+ def self.init
107
+ super 'iso639-3.yml'
108
+
109
+ @from1 = {}
110
+ @from2 = {}
111
+ @complete.each_pair do |k, v|
112
+ @from1[v[:p1]] = k unless v[:p1].nil?
113
+ @from2[v[:p2b]] = k unless v[:p2b].nil?
114
+ @from2[v[:p2t]] = k unless v[:p2t].nil?
115
+ end
116
+ @from1.freeze
117
+
118
+ ('a'..'t').each do |a|
119
+ ('a'..'z').each do |b|
120
+ code = "q#{a}#{b}".to_sym
121
+ @from2[code] = code
122
+ end
123
+ end
124
+ @from2.freeze
125
+ end
126
+
127
+ # Get the ISO 639-3 code associated to a given ISO 639-1 code.
128
+ # @param [#to_sym] code
129
+ # @return [Symbol]
130
+ def self.from1 code
131
+ init if @complete.nil?
132
+ require 'util/args'
133
+ code = Util::Args.check code, Symbol, false
134
+ @from1[code]
135
+ end
136
+
137
+ # Get the ISO 639-3 code associated to a given ISO 639-2 code.
138
+ # @param [#to_sym] code
139
+ # @return [Symbol]
140
+ def self.from2 code
141
+ init if @complete.nil?
142
+ require 'util/args'
143
+ code = Util::Args.check code, Symbol, false
144
+ @from2[code]
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end