rfc5646 0.1.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.
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: