util 0.2.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.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/CECILL-C.EN +517 -0
- data/CECILL-C.FR +521 -0
- data/LICENSES.md +21 -0
- data/README.md +325 -0
- data/lib/util.rb +8 -0
- data/lib/util/arg.rb +90 -0
- data/lib/util/communia.rb +8 -0
- data/lib/util/console_logger.rb +178 -0
- data/lib/util/i18n.rb +328 -0
- data/lib/util/lists.rb +6 -0
- data/lib/util/lists/iso639.rb +149 -0
- data/lib/util/test.rb +225 -0
- data/lib/util/yaml.rb +18 -0
- data/share/lists/iso639-3.yml +35528 -0
- data/test/unit.rb +20 -0
- data/test/unit/i18n.rb +216 -0
- data/test/unit/i18n/CamelCase/eng.yml +2 -0
- data/test/unit/i18n/CamelCase/fra.yml +2 -0
- data/test/unit/i18n/aaj.yml +2 -0
- data/test/unit/i18n/en.yml +2 -0
- data/test/unit/i18n/fra.yml +2 -0
- data/test/unit/i18n/i18n +0 -0
- data/test/unit/i18n/prv.yml +2 -0
- data/test/unit/i18n/void/fra.yml/Yes-git-I-need-this-folder-even-if-it-is-empty +0 -0
- data/test/unit/i18n//316/261/316/273/316/271/316/261/317/202/fra.yml +2 -0
- data/test/unit/lists/iso639.rb +78 -0
- data/tools/create-iso639-3.rb +235 -0
- metadata +74 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
module Util
|
2
|
+
# Help write formatted messages to the console, for friendlier
|
3
|
+
# command-line interface. Uses generally available ANSI codes, so
|
4
|
+
# it should work on all UNIXes and on recent Windows.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# cl = ConsoleLogger.new e: { :stderr => true }
|
8
|
+
# cl.warning 'Errors will be logged on STDERR.'
|
9
|
+
# # Message written in yellow
|
10
|
+
# begin
|
11
|
+
# text = File.read 'secret.msg'
|
12
|
+
# rescue Exception => e
|
13
|
+
# msg = 'Cannot go any further because of %E%, aborting.'
|
14
|
+
# cl.error msg, 'E': e.message
|
15
|
+
# # Message written in red
|
16
|
+
# end
|
17
|
+
class ConsoleLogger
|
18
|
+
require 'util/arg'
|
19
|
+
|
20
|
+
# ANSI code to reset all formatting
|
21
|
+
RESET = "\x1b[0m"
|
22
|
+
# @no_doc
|
23
|
+
BASE = "\x1b[%CODE%m"
|
24
|
+
# @no_doc
|
25
|
+
COLORS = { :black => 0, :red => 1, :green => 2, :yellow => 3,
|
26
|
+
:blue => 4, :magenta => 5, :cyan => 6, :white => 7 }
|
27
|
+
# @no_doc
|
28
|
+
COLOR_TYPES = { :fg => 30, :bg => 40, :bright => 60 }
|
29
|
+
# @no_doc
|
30
|
+
DECORS = { :bold => 1, :faint => 2, :italic => 3, :underline => 4,
|
31
|
+
:blink => 5, :reverse => 7, :conceal => 8, :crossed => 9,
|
32
|
+
:dbl_underline => 21, :overline => 53 }
|
33
|
+
# @no_doc
|
34
|
+
CL = ConsoleLogger
|
35
|
+
|
36
|
+
# Generate the ANSI code to obtain a given formatting.
|
37
|
+
# @param opts [Hash] the wanted formatting
|
38
|
+
# @option opts [:black, :blue, :cyan, :green, :magenta,
|
39
|
+
# :red, :white, :yellow] :color font color
|
40
|
+
# @option opts [idem] :bgcolor background color
|
41
|
+
# @option opts [Boolean] :bright use bright font color
|
42
|
+
# @option opts [Boolean] :bgbright use bright background color
|
43
|
+
# @option opts [:blink, :bold, :conceal, :crossed,
|
44
|
+
# :dbl_underline, :faint, :italic, :overline, :reverse,
|
45
|
+
# :underline, Array<idem>] :decor text decorations
|
46
|
+
def self.escape_code opts={}
|
47
|
+
opts = Arg.check opts, Hash, {}
|
48
|
+
return RESET if opts.empty?
|
49
|
+
code = ''
|
50
|
+
|
51
|
+
if opts.has_key? :color then
|
52
|
+
color = Arg.check opts[:color], Symbol, :white
|
53
|
+
color = :white unless COLORS.has_key? color
|
54
|
+
bright = Arg.check opts[:bright], 'Boolean', false
|
55
|
+
|
56
|
+
cur = COLOR_TYPES[:fg] + COLORS[color]
|
57
|
+
cur += COLOR_TYPES[:bright] if bright
|
58
|
+
code += cur.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
if opts.has_key? :bgcolor then
|
62
|
+
color = Arg.check opts[:bgcolor], Symbol, :black
|
63
|
+
color = :black unless COLORS.has_key? color
|
64
|
+
bright = Arg.check opts[:bgbright], 'Boolean', false
|
65
|
+
|
66
|
+
cur = COLOR_TYPES[:bg] + COLORS[color]
|
67
|
+
cur += COLOR_TYPES[:bright] if bright
|
68
|
+
code += ';' unless code.empty?
|
69
|
+
code += cur.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
if opts.has_key? :decor then
|
73
|
+
decors = Arg.check opts[:decor], Array, [opts[:decor]]
|
74
|
+
cur = ''
|
75
|
+
decors.each do |d|
|
76
|
+
cur += ';' + DECORS[d].to_s if DECORS.has_key? d
|
77
|
+
end
|
78
|
+
cur = cur.sub ';', '' if code.empty?
|
79
|
+
code += cur
|
80
|
+
end
|
81
|
+
|
82
|
+
code.empty? ? RESET : BASE.sub('%CODE%', code)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create a new ConsoleLogger.
|
86
|
+
# @param config [Hash] initial configuration: for each kind of
|
87
|
+
# message, whether to use STDERR or STDOUT, and which formatting
|
88
|
+
# @option config [Hash { :stderr => Boolean, :code => String }]
|
89
|
+
# e configuration for error (defaults to red text)
|
90
|
+
# @option config [Hash { :stderr => Boolean, :code => String }]
|
91
|
+
# i configuration for information (defaults to cyan text)
|
92
|
+
# @option config [Hash { :stderr => Boolean, :code => String }]
|
93
|
+
# n configuration for normal (defaults to no formatting)
|
94
|
+
# @option config [Hash { :stderr => Boolean, :code => String }]
|
95
|
+
# o configuration for ok (defaults to green text)
|
96
|
+
# @option config [Hash { :stderr => Boolean, :code => String }]
|
97
|
+
# w configuration for warning (defaults to yellow text)
|
98
|
+
def initialize config={}
|
99
|
+
config = Arg.check config, Hash, {}
|
100
|
+
@config = {
|
101
|
+
:e => { :io => $stdout, :code => CL.escape_code(color: :red) },
|
102
|
+
:i => { :io => $stdout, :code => CL.escape_code(color: :cyan) },
|
103
|
+
:n => { :io => $stdout, :code => '' },
|
104
|
+
:o => { :io => $stdout, :code => CL.escape_code(color: :green) },
|
105
|
+
:w => { :io => $stdout, :code => CL.escape_code(color: :yellow) },
|
106
|
+
}
|
107
|
+
|
108
|
+
config.each_pair do |k, v|
|
109
|
+
next unless @config.has_key? k
|
110
|
+
v = Arg.check v, Hash, {}
|
111
|
+
@config[k][:io] = (v[:stderr] == true) ? $stderr : $stdout
|
112
|
+
@config[k][:code] = CL.escape_code v
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Print an error to the console.
|
117
|
+
# @param msg [String] message to print
|
118
|
+
# @param payload [Hash<#to_s, #to_s>] parts to replace in the base
|
119
|
+
# message: '%KEY%' will be replaced by 'VALUE'.
|
120
|
+
# @example
|
121
|
+
# msg = 'The array contains only %I% objects of type %T%.'
|
122
|
+
# cl.error msg, 'T': Float, 'I': arr.how_many?(Float)
|
123
|
+
#
|
124
|
+
# # Results in `The array contains only 42 objects of type Float.`
|
125
|
+
# @return nil
|
126
|
+
def error msg, payload={}
|
127
|
+
self.printf :e, msg, payload
|
128
|
+
end
|
129
|
+
|
130
|
+
# Print an important message to the console.
|
131
|
+
# @param (see #error)
|
132
|
+
# @example (see #error)
|
133
|
+
# @return (see #error)
|
134
|
+
def important msg, payload={}
|
135
|
+
self.printf :i, msg, payload
|
136
|
+
end
|
137
|
+
|
138
|
+
# Print a normal message to the console.
|
139
|
+
# @param (see #error)
|
140
|
+
# @example (see #error)
|
141
|
+
# @return (see #error)
|
142
|
+
def normal msg, payload={}
|
143
|
+
self.printf :n, msg, payload
|
144
|
+
end
|
145
|
+
|
146
|
+
# Print an approval to the console.
|
147
|
+
# @param (see #error)
|
148
|
+
# @example (see #error)
|
149
|
+
# @return (see #error)
|
150
|
+
def ok msg, payload={}
|
151
|
+
self.printf :o, msg, payload
|
152
|
+
end
|
153
|
+
|
154
|
+
# Print a warning to the console.
|
155
|
+
# @param (see #error)
|
156
|
+
# @example (see #error)
|
157
|
+
# @return (see #error)
|
158
|
+
def warning msg, payload={}
|
159
|
+
self.printf :w, msg, payload
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
# Common parts to all messaging methods.
|
165
|
+
# @param type [:e, :i, :n, :o, :w] kind of message
|
166
|
+
# @param msg (see #error)
|
167
|
+
# @param payload (see #error)
|
168
|
+
# @return nil
|
169
|
+
def printf type, msg, payload
|
170
|
+
msg = Arg.check msg, String, ''
|
171
|
+
payload = Arg.check payload, Hash, {}
|
172
|
+
payload.each_pair do |k, v|
|
173
|
+
msg = msg.gsub "%#{k}%", v.to_s
|
174
|
+
end
|
175
|
+
@config[type][:io].puts @config[type][:code] + msg + RESET
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/util/i18n.rb
ADDED
@@ -0,0 +1,328 @@
|
|
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/arg'
|
57
|
+
id, opts = Arg.check_a [[id, String, ''], [opts, Hash, {}]]
|
58
|
+
o_lang, o_mod = Arg.check_h 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/arg'
|
198
|
+
init unless initialized?
|
199
|
+
name, path, relative, recursive = Arg.check_a [
|
200
|
+
[o_name, String, ''], [path, String, ''],
|
201
|
+
[relative, FalseClass, true], [recursive, TrueClass, false]
|
202
|
+
]
|
203
|
+
return false if check_t name.empty?, :reg_no_name, o_name
|
204
|
+
|
205
|
+
path = relative ? './temp.rb' : '.' if path.empty?
|
206
|
+
if relative then
|
207
|
+
dir = File.dirname(File.expand_path path)
|
208
|
+
path = File.join dir, 'i18n'
|
209
|
+
end
|
210
|
+
|
211
|
+
return false if check_f File.exist?(path), :reg_path_not_exist, path
|
212
|
+
return false if check_f File.directory?(path), :reg_path_not_dir, path
|
213
|
+
[name, path, relative, recursive]
|
214
|
+
end
|
215
|
+
|
216
|
+
# Get the folder(s) in which to search for YAML files, and the
|
217
|
+
# associated module names.
|
218
|
+
# @param [String] name name of the root module
|
219
|
+
# @param [String] path path to the root folder
|
220
|
+
# @param [Boolean] recursive wether the search should be recursive
|
221
|
+
# @return [Array<Array<String, String>>]
|
222
|
+
def self.register_get_locations name, path, recursive
|
223
|
+
locations = []
|
224
|
+
dirs = [path]
|
225
|
+
begin
|
226
|
+
current = dirs.shift
|
227
|
+
glob = File.join current.gsub(/([?*{\[])/, "\\$1"), '*.yml'
|
228
|
+
# These characters have a meaning for +Dir.glob+ and might
|
229
|
+
# yield very strange results if they are not escaped.
|
230
|
+
|
231
|
+
slug = slugify(current.sub(path, '').sub(File::SEPARATOR, ''))
|
232
|
+
full_name = slug.empty? ? name : (name + '-' + slug)
|
233
|
+
locations << [glob, full_name]
|
234
|
+
Dir[File.join current, '*'].each do |f|
|
235
|
+
dirs << f if File.directory? f
|
236
|
+
end if recursive
|
237
|
+
end until dirs.empty?
|
238
|
+
locations
|
239
|
+
end
|
240
|
+
|
241
|
+
# Extract the messages from a given folder. Will use all files with
|
242
|
+
# the form +lang-code.yml+ that contain valid YAML for a hash, then
|
243
|
+
# convert the values to strings, and finally merge it in the
|
244
|
+
# existing list.
|
245
|
+
# @param [Array<String, String>] loc glob and module name to use
|
246
|
+
# @param [Hash] messages existing list
|
247
|
+
# @return [Hash] updated list
|
248
|
+
def self.register_get_messages loc, messages
|
249
|
+
require 'util/yaml'
|
250
|
+
glob, name = loc
|
251
|
+
langs = {}
|
252
|
+
|
253
|
+
Dir[glob].each do |f|
|
254
|
+
lang = File.basename(f, '.yml')
|
255
|
+
next unless lang = valid_lang?(lang)
|
256
|
+
lang = lang.to_sym
|
257
|
+
|
258
|
+
content = YAML.from_file f
|
259
|
+
check_f content, :reg_yaml_cant_open, f
|
260
|
+
next unless content.is_a? Hash
|
261
|
+
|
262
|
+
langs[lang] = {} unless content.empty?
|
263
|
+
content.each_pair do |k, v|
|
264
|
+
langs[lang][k.to_s] = v.to_s
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
return messages if langs.empty?
|
269
|
+
|
270
|
+
if messages.has_key? name then
|
271
|
+
messages[name].merge! langs do |k, ov, nv|
|
272
|
+
messages[name][k] = ov.merge nv
|
273
|
+
end
|
274
|
+
else
|
275
|
+
messages[name] = langs
|
276
|
+
end
|
277
|
+
|
278
|
+
messages
|
279
|
+
end
|
280
|
+
|
281
|
+
# Escape a path so that it can be used as a Hash key. Rules are as follows.
|
282
|
+
# - Any non-word character becomes a hyphen.
|
283
|
+
# - Multiple hyphens are reduced to just one.
|
284
|
+
# - Starting and trailing hyphens are removed.
|
285
|
+
# - Everything is downcased.
|
286
|
+
# - The {https://en.wikipedia.org/wiki/Base64 base64} representation
|
287
|
+
# of the path is used instead if the resulting slug is empty.
|
288
|
+
# - All directories’ slugs are joined by hyphens.
|
289
|
+
# @param [String] string path to transform in slug
|
290
|
+
# @return [String]
|
291
|
+
# @example
|
292
|
+
# I18n.slugify 'alias' # 'alias'
|
293
|
+
# I18n.slugify '今日は' # '今日は'
|
294
|
+
# I18n.slugify 'La vie de ma mère' # 'la-vie-de-ma-mère'
|
295
|
+
# I18n.slugify 'Ça va? Oui, et toi?' # 'ça-va-oui-et-toi'
|
296
|
+
# I18n.slugify '??!?' # 'Pz8hPw=='
|
297
|
+
# I18n.slugify 'hello/Darkneß/m41_0!d' # 'hello-darkneß-m41_0-d'
|
298
|
+
# I18n.slugify 'Estie/de/tabarnak/!!§!' # 'estie-de-tabarnak-ISHCpyE='
|
299
|
+
# @note {https://www.youtube.com/watch?v=DvR6-SQzqO8 For those who did
|
300
|
+
# not get the last example…}
|
301
|
+
def self.slugify string
|
302
|
+
require 'base64'
|
303
|
+
result = []
|
304
|
+
string.split(File::SEPARATOR).each do |part|
|
305
|
+
res = part.gsub(/[^[:word:]]/, '-').gsub(/-{2,}/, '-')
|
306
|
+
res = res.sub(/^-/, '').sub(/-$/, '').downcase
|
307
|
+
res = Base64.urlsafe_encode64(part) if res.empty?
|
308
|
+
result << res
|
309
|
+
end
|
310
|
+
result.join '-'
|
311
|
+
end
|
312
|
+
|
313
|
+
# From a given ISO 639 code, either get the valid corresponding
|
314
|
+
# ISO 639-3 code, or false.
|
315
|
+
# @param [#to_sym] lang language code to check
|
316
|
+
# @return [#to_sym] existing ISO 639-3 code
|
317
|
+
# @return [false] could not find a matching ISO 639-3 code
|
318
|
+
def self.valid_lang? lang
|
319
|
+
require 'util/lists/iso639'
|
320
|
+
iso = Util::Lists::ISO639
|
321
|
+
# Typecheck is already done by ISO639
|
322
|
+
return lang if iso::P3.exist?(lang)
|
323
|
+
if n_lang = iso::P3.from2(lang) then return n_lang; end
|
324
|
+
if n_lang = iso::P3.from1(lang) then return n_lang; end
|
325
|
+
false
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|