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