rfc5646 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rfc5646.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Rfc5646
2
+ require 'rfc5646/version'
3
+ require 'rfc5646/locale'
4
+ end
@@ -0,0 +1,4 @@
1
+ module Rfc5646
2
+ class Engine
3
+ end
4
+ end
@@ -0,0 +1,338 @@
1
+ require 'active_support/core_ext/object/try'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'i18n'
4
+
5
+ I18n.backend.load_translations('config/locales/locales.en.yml')
6
+
7
+ module Rfc5646
8
+ # Represents a locale that can be localized to. Locales are identified by
9
+ # their RFC 5646 code, which can be as simple as just a language (e.g., "en" for
10
+ # English), or arbitrary complex (e.g., "zh-cmn-Hans-CN" for Mandarin Chinese
11
+ # as spoken in China, simplified Han orthography). The entire RFC 5646 spec is
12
+ # supported by this class.
13
+ class Locale
14
+ # @private
15
+ RFC5646_EXTLANG = /(?<extlang>[a-zA-Z]{3})(-[a-zA-Z]{3}){0,2}/
16
+ # @private
17
+ RFC5646_ISO639 = /(?<iso639>[a-zA-Z]{2,3})(-#{RFC5646_EXTLANG.source})?/
18
+ # @private
19
+ RFC5646_RESERVED = /(?<reserved>[a-zA-Z]{4})/
20
+ # @private
21
+ RFC5646_SUBTAG = /(?<subtag>[a-zA-Z]{5,8})/
22
+ # @private
23
+ RFC5646_REGION = /(?<region>([a-zA-Z]{2}|\d{3}))/
24
+ # @private
25
+ RFC5646_VARIANT = /([a-zA-Z0-9]{5,8}|\d[a-zA-Z0-9]{3})/
26
+ # @private
27
+ RFC5646_SCRIPT = /(?<script>[a-zA-Z]{4})/
28
+ # @private
29
+ RFC5646_EXTENSION = /([0-9A-WY-Za-wy-z](-[a-zA-Z0-9]{2,8}){1,})/
30
+ # @private
31
+ RFC5646_LANGUAGE = /(#{RFC5646_ISO639.source}|#{RFC5646_RESERVED.source}|#{RFC5646_SUBTAG.source})/
32
+ # @private
33
+ RFC5646_PRIVATE = /x(?<privates>(-[a-zA-Z0-9]{1,8}){1,})/
34
+ # @private
35
+ RFC5646_FORMAT = /\A#{RFC5646_LANGUAGE.source}(-#{RFC5646_SCRIPT.source})?(-#{RFC5646_REGION.source})?(?<variants>(-#{RFC5646_VARIANT.source})*)(?<extensions>(-#{RFC5646_EXTENSION.source})*)(-#{RFC5646_PRIVATE.source})?\z/ # rubocop:disable Metrics/LineLength
36
+
37
+ # @return [String] The ISO 639 code for the base language (e.g., "de" for
38
+ # German).
39
+ attr_reader :iso639
40
+ # @return [String] The RFC 5646 code for the orthography (e.g., "Arab" for
41
+ # Arabic script).
42
+ attr_reader :script
43
+ # @return [String] The ISO 3166 country code for the regional dialect (e.g.,
44
+ # "BZ" for Belize). Some special values are also supported (e.g., "013" for
45
+ # Central America); see the spec for details.
46
+ attr_reader :region
47
+ # @return [Array<String>] The variant or nested subvariant of this locale.
48
+ # The full path to a subvariant is listed as a top-level Array; an example
49
+ # is `["sl", "rozaj", "1994"]`, indicating the 1994 standardization of the
50
+ # Resian orthography of the Rozaj dialect of Slovenian (in case we should
51
+ # ever want to localize one of our projects thusly). Variants can be
52
+ # regional or temporal dialects, or orthographies, or both, and are very
53
+ # specific.
54
+ attr_reader :variants
55
+ # @return [String] The dialect (not associated with a specific region or
56
+ # period in time) specifier. For example, "yue" indicates Yue Chinese
57
+ # (Cantonese).
58
+ attr_reader :extended_language
59
+ # @return [Array<String>] The user-defined extensions applied to this
60
+ # locale. The meaning of these is not specified in the spec, and left up
61
+ # to private use, and is ignored by this class, but stored for completeness.
62
+ attr_reader :extensions
63
+
64
+ class << self
65
+ # Generates a new instance from an RFC 5646 code.
66
+ #
67
+ # @param [String] ident The RFC 5646 code for the locale.
68
+ # @return [Locale] The instance representing that locale.
69
+
70
+ def from_rfc5646(ident)
71
+ ident = ident.tr('_', '-')
72
+ return nil unless (matches = RFC5646_FORMAT.match(ident))
73
+ attrs = RFC5646_FORMAT.named_captures.each_with_object({}) do |(name, offsets), hsh|
74
+ hsh[name] = offsets.map { |offset| matches[offset] }.compact
75
+ end
76
+
77
+ iso639 = attrs['iso639'].first
78
+ script = attrs['script'].first
79
+ return nil unless iso639
80
+ region = attrs['region'].first
81
+ if (variants = attrs['variants'].first)
82
+ variants = variants.split('-')
83
+ variants.shift
84
+ end
85
+ if (extensions = attrs['extensions'].first)
86
+ extensions = extensions.split('-')
87
+ extensions.shift
88
+ end
89
+ extlang = attrs['extlang'].first
90
+
91
+ Locale.new iso639, script, extlang, region, variants || [], extensions || []
92
+ end
93
+
94
+ # Returns an array of Locales representing all possible completions given a
95
+ # prefix portion of an RFC 5646 code. The resolution of the resultant array is
96
+ # determined by the resolution if the input prefix. Some examples:
97
+ #
98
+ # * If just the letter "e" is entered, Locales whose ISO 639 codes begin
99
+ # with the letter "e" will be returned (English, Spanish, etc.). These
100
+ # Locale instances will have no other fields specified.
101
+ # * If "en-U" is specified, Locale instances representing "en-US" and
102
+ # "en-UA", among others, will be returned, as well as "en-Ugar" (for all the
103
+ # sense it makes). "en-US-Ugar" would not be returned, as it is of a higher
104
+ # resolution than the input.
105
+ #
106
+ # @param [String] prefix A portion of an RFC 5646 code.
107
+ # @param [Fixnum] max A maximum number of completions to return.
108
+ # @return [Array<Locale>] Candidate completions as Locale instances.
109
+
110
+ def from_rfc5646_prefix(prefix, max = nil)
111
+ if prefix.include?('-')
112
+ prefix_path = prefix.split('-')
113
+ prefix = prefix_path.pop
114
+ parent_prefix = prefix_path.join('-')
115
+ parent = from_rfc5646(parent_prefix)
116
+ return [] unless parent
117
+
118
+ search_paths = if parent.variants.present?
119
+ # TODO: subvariants
120
+ []
121
+ elsif parent.region # only possible completions are variants
122
+ []
123
+ # TODO: variants
124
+ elsif parent.script # can be followed with variant or region
125
+ %w(locale.region)
126
+ else # can be followed with script, region, or variant
127
+ %w(locale.region locale.script)
128
+ end
129
+
130
+ keys = search_paths.map do |path|
131
+ I18n.t(path).select { |k, v| Locale.matches_prefix? prefix, k, v }.keys
132
+ end.flatten
133
+ keys.delete '_END_'
134
+ keys = keys[0, max] if max
135
+
136
+ return keys.map { |key| from_rfc5646 "#{parent_prefix}-#{key}" }
137
+ else
138
+ keys = I18n.t('locale.name').select { |k, v| Locale.matches_prefix? prefix, k, v }.keys
139
+ keys = keys[0, max] if max
140
+ return keys.map { |key| from_rfc5646 key }
141
+ end
142
+ end
143
+
144
+ # @private
145
+ def matches_prefix?(prefix, key, value)
146
+ return false unless value.is_a?(String)
147
+ return true if key.to_s.downcase.starts_with?(prefix.downcase)
148
+ return true if value.split(/\w+/).any? { |word| word.downcase.starts_with? prefix.downcase }
149
+ false
150
+ end
151
+
152
+ private
153
+
154
+ def fallbacks
155
+ @fallbacks ||= YAML.load_file(Rails.root.join('data', 'fallbacks.yml'))
156
+ end
157
+ end
158
+
159
+ # @private
160
+ def initialize(iso639, script = nil, extlang = nil, region = nil, variants = [], extensions = [])
161
+ @iso639 = iso639.try!(:downcase)
162
+ @region = region.try!(:upcase)
163
+ @variants = variants.map(&:downcase)
164
+ @extended_language = extlang.try!(:downcase)
165
+ @extensions = extensions
166
+ @script = script
167
+ end
168
+
169
+ # @return [String] The full RFC 5646 code for this locale.
170
+
171
+ def rfc5646
172
+ [iso639, script, extended_language, region, *variants].compact.join('-')
173
+ end
174
+
175
+ alias_method :to_param, :rfc5646
176
+
177
+ # Returns a human-readable localized name of the locale.
178
+ #
179
+ # @param [String] locale The locale to use (default locale is used by
180
+ # default).
181
+ # @return [String] The localized name of the locale.
182
+
183
+ def name(locale = nil)
184
+ I18n.with_locale(locale || I18n.locale) do
185
+ i18n_language = if extended_language
186
+ I18n.t "locale.extended.#{iso639}.#{extended_language}"
187
+ else
188
+ I18n.t "locale.name.#{iso639}"
189
+ end
190
+
191
+ i18n_dialect = if variants.present?
192
+ I18n.t "locale.variant.#{iso639}.#{variants.join '.'}._END_"
193
+ end
194
+
195
+ i18n_script = script ? I18n.t("locale.script.#{script}") : nil
196
+ i18n_region = region ? I18n.t("locale.region.#{region}") : nil
197
+
198
+ if i18n_region && i18n_dialect && i18n_script
199
+ I18n.t('locale.format.scripted_regional_dialectical',
200
+ script: i18n_script,
201
+ dialect: i18n_dialect,
202
+ region: i18n_region,
203
+ language: i18n_language
204
+ )
205
+ elsif i18n_region && i18n_dialect
206
+ I18n.t('locale.format.regional_dialectical',
207
+ dialect: i18n_dialect,
208
+ region: i18n_region,
209
+ language: i18n_language
210
+ )
211
+ elsif i18n_region && i18n_script
212
+ I18n.t('locale.format.scripted_regional',
213
+ script: i18n_script,
214
+ region: i18n_region,
215
+ language: i18n_language
216
+ )
217
+ elsif i18n_dialect && i18n_script
218
+ I18n.t('locale.format.scripted_dialectical',
219
+ script: i18n_script,
220
+ dialect: i18n_dialect,
221
+ language: i18n_language
222
+ )
223
+ elsif i18n_script
224
+ I18n.t('locale.format.scripted',
225
+ script: i18n_script,
226
+ language: i18n_language
227
+ )
228
+ elsif i18n_dialect
229
+ I18n.t('locale.format.dialectical',
230
+ dialect: i18n_dialect,
231
+ language: i18n_language
232
+ )
233
+ elsif i18n_region
234
+ I18n.t('locale.format.regional',
235
+ region: i18n_region,
236
+ language: i18n_language
237
+ )
238
+ else
239
+ i18n_language
240
+ end
241
+ end
242
+ end
243
+
244
+ # Tests for equality between two locales. Their full RFC 5646 codes must be
245
+ # equal.
246
+ #
247
+ # @param [Locale] other Another Locale.
248
+ # @return [true, false] Whether it is the same Locale as the receiver.
249
+ # @raise [ArgumentError] If `other` is not a Locale.
250
+
251
+ def ==(other)
252
+ case other
253
+ when Locale
254
+ rfc5646 == other.rfc5646
255
+ else
256
+ false
257
+ end
258
+ end
259
+
260
+ alias_method :eql?, :==
261
+ alias_method :equal?, :==
262
+ alias_method :===, :==
263
+
264
+ # @private
265
+ def hash
266
+ rfc5646.hash
267
+ end
268
+
269
+ # Returns the fallback order for this Locale. For example, fr-CA might
270
+ # fall back to fr, which then falls back to en. The fallback order is
271
+ # described in the `fallbacks.yml` file.
272
+ #
273
+ # @return [Array<Locale>] The fallback order of this locale, from most
274
+ # specific to most general. Note that this array includes the receiver.
275
+
276
+ def fallbacks
277
+ fallbacks = Array.wrap(self.class.fallbacks[rfc5646])
278
+ .map { |l| self.class.from_rfc5646 l }
279
+ fallbacks.unshift self
280
+ fallbacks
281
+ end
282
+
283
+ # Returns whether this Languge is a subset of the given locale. "en-US" is a
284
+ # child of "en".
285
+ #
286
+ # @param [Locale] parent Another locale.
287
+ # @return [true, false] Whether this locale is a child of `parent`.
288
+
289
+ def child_of?(parent)
290
+ return false if iso639 != parent.iso639
291
+ return false if parent.specificity > specificity
292
+ parent.specified_parts.all? { |part| specified_parts.include?(part) }
293
+ end
294
+
295
+ # @return [true, false] Whether this locale is a pseudo-locale.
296
+ def pseudo?
297
+ variants.include? 'pseudo'
298
+ end
299
+
300
+ # @private
301
+ def specificity
302
+ specificity = 1
303
+ specificity += 1 if script
304
+ specificity += 1 if region
305
+ specificity += variants.size
306
+ specificity += 1 if extended_language
307
+ specificity + extensions.size
308
+ end
309
+
310
+ # @private
311
+ def specified_parts
312
+ # relies on the fact that the namespace for each element of the code is
313
+ # *globally* unique, not just unique to the code element
314
+ (variants + extensions + [script, region, extended_language]).compact
315
+ end
316
+
317
+ # @private
318
+ def as_json(_options = nil)
319
+ {
320
+ rfc5646: rfc5646,
321
+ components: {
322
+ iso639: iso639,
323
+ script: script,
324
+ extended_language: extended_language,
325
+ region: region,
326
+ variants: variants,
327
+ exensions: extensions
328
+ },
329
+ name: name
330
+ }
331
+ end
332
+
333
+ # @private
334
+ def inspect
335
+ "#<#{self.class} #{rfc5646}>"
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,3 @@
1
+ module Rfc5646
2
+ VERSION = '0.1.0'
3
+ end
data/rfc5646.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rfc5646/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'rfc5646'
8
+ spec.version = Rfc5646::VERSION
9
+ spec.authors = ['Artyom Bolshakov']
10
+ spec.email = ['abolshakov@spbtv.com']
11
+
12
+ spec.summary = 'Parsing RFC 5646 locale'
13
+ spec.description = 'Parsing RFC 5646 locale'
14
+ spec.homepage = 'https://github.com/SPBTV/rfc5646'
15
+ spec.license = 'Apache 2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_runtime_dependency 'activesupport', '>= 3.0.0'
23
+ spec.add_runtime_dependency 'i18n', '~> 0.6'
24
+ spec.add_development_dependency 'bundler', '~> 1.10'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.3'
27
+ spec.add_development_dependency 'rubocop', '~> 0.34.2'
28
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rfc5646
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Artyom Bolshakov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: i18n
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.34.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.34.2
97
+ description: Parsing RFC 5646 locale
98
+ email:
99
+ - abolshakov@spbtv.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".travis.yml"
108
+ - CODE_OF_CONDUCT.md
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/setup
115
+ - config/locales/locales.en.yml
116
+ - lib/rfc5646.rb
117
+ - lib/rfc5646/engine.rb
118
+ - lib/rfc5646/locale.rb
119
+ - lib/rfc5646/version.rb
120
+ - rfc5646.gemspec
121
+ homepage: https://github.com/SPBTV/rfc5646
122
+ licenses:
123
+ - Apache 2.0
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.4.5
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Parsing RFC 5646 locale
145
+ test_files: []
146
+ has_rdoc: