Class: Rack::Acceptable::LanguageTag

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/acceptable/language_tag.rb

Overview

inspired by the ‘langtag’ gem (author: Martin Dürst) rubyforge.org/projects/langtag/ www.langtag.net/

Constant Summary collapse

GRANDFATHERED_TAGS =
{
  'art-lojban'  => 'jbo' ,
  'cel-gaulish' => nil   ,
  'en-gb-oed'   => nil   ,
  'i-ami'       => 'ami' ,
  'i-bnn'       => 'bnn' ,
  'i-default'   => nil   ,
  'i-enochian'  => nil   ,
  'i-hak'       => 'hak' ,
  'i-klingon'   => 'tlh' ,
  'i-lux'       => 'lb'  ,
  'i-mingo'     => nil   ,
  'i-navajo'    => 'nv'  ,
  'i-pwn'       => 'pwn' ,
  'i-tao'       => 'tao' ,
  'i-tay'       => 'tay' ,
  'i-tsu'       => 'tsu' ,
  'no-bok'      => 'nb'  ,
  'no-nyn'      => 'nn'  ,
  'sgn-be-fr'   => 'sfb' ,
  'sgn-be-nl'   => 'vgt' ,
  'sgn-ch-de'   => 'sgg' ,
  'zh-guoyu'    => 'cmn' ,
  'zh-hakka'    => 'hak' ,
  'zh-min'      => nil   ,
  'zh-min-nan'  => 'nan' ,
  'zh-xiang'    => 'hsn' 
}.freeze
LANGTAG_COMPOSITION_REGEX =
/^#{language}#{script}#{region}(?=#{variants}#{extensions}#{privateuse}$)/o.freeze
LANGTAG_INFO_REGEX =
/^#{language}#{script}#{region}(#{variants})#{extensions}#{privateuse}$/o.freeze
PRIVATEUSE_REGEX =
/^x(?:-[a-z\d]{1,8})+$/i.freeze
PRIVATEUSE =
'x'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*components) ⇒ LanguageTag

Returns a new instance of LanguageTag.



157
158
159
# File 'lib/rack/acceptable/language_tag.rb', line 157

def initialize(*components)
  @primary, @extlang, @script, @region, @variants, @extensions, @privateuse = *components
end

Instance Attribute Details

#extensionsObject

Returns the value of attribute extensions.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def extensions
  @extensions
end

#extlangObject

Returns the value of attribute extlang.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def extlang
  @extlang
end

#lengthObject (readonly)

Returns the number of subtags in self. Does not perform validation or recomposition.



174
175
176
# File 'lib/rack/acceptable/language_tag.rb', line 174

def length
  @length
end

#primaryObject

Returns the value of attribute primary.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def primary
  @primary
end

#privateuseObject

Returns the value of attribute privateuse.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def privateuse
  @privateuse
end

#regionObject

Returns the value of attribute region.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def region
  @region
end

#scriptObject

Returns the value of attribute script.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def script
  @script
end

#tagObject (readonly)

the most recent ‘build’ of tag



169
170
171
# File 'lib/rack/acceptable/language_tag.rb', line 169

def tag
  @tag
end

#variantsObject

Returns the value of attribute variants.



41
42
43
# File 'lib/rack/acceptable/language_tag.rb', line 41

def variants
  @variants
end

Class Method Details

.extract_language_info(langtag) ⇒ Object

Parameters

langtag<String>

The LangTag snippet.

Returns

Array or nil

It returns nil, when the snipped passed:

  • does not conform the Language-Tag ABNF (malformed)

  • represents a ‘grandfathered’ Language-Tag

  • starts with ‘x’ singleton (‘privateuse’).

Otherwise you’ll get an Array with:

  • primary subtag (as String, downcased),

  • extlang (as String, downcased) or nil,

  • script (as String, capitalized) or nil,

  • region (as String, upcased) or nil

  • downcased variants (Array) or nil.

Notes

In most cases, it’s quite enough. Take a look, for example, at ‘35-character recomendation’.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/rack/acceptable/language_tag.rb', line 105

def extract_language_info(langtag)
  tag = langtag.downcase
  return nil if GRANDFATHERED_TAGS.key?(langtag)
  return nil unless LANGTAG_INFO_REGEX === tag

  primary     = $1
  extlang     = nil
  script      = $2
  region      = $3
  variants    = $4.split(Utils::HYPHEN_SPLITTER)[1..-1]

  primary, *extlang = primary.split(Utils::HYPHEN_SPLITTER) if primary.include?(Const::HYPHEN)
  script.capitalize! if script
  region.upcase! if region

  [primary, extlang, script, region, variants]
end

.grandfathered?(tag) ⇒ Boolean

Checks if the String passed represents a ‘grandfathered’ Language-Tag. Works case-insensitively.

Returns:

  • (Boolean)


80
81
82
# File 'lib/rack/acceptable/language_tag.rb', line 80

def grandfathered?(tag)
  GRANDFATHERED_TAGS.key?(tag) || GRANDFATHERED_TAGS.key?(tag.downcase)
end

.parse(thing) ⇒ Object



123
124
125
126
127
# File 'lib/rack/acceptable/language_tag.rb', line 123

def parse(thing)
  return nil unless thing
  return thing if thing.kind_of?(self)
  self.new.recompose(thing)
end

.privateuse?(tag) ⇒ Boolean

Checks if the String passed represents a ‘privateuse’ Language-Tag. Works case-insensitively.

Returns:

  • (Boolean)


73
74
75
# File 'lib/rack/acceptable/language_tag.rb', line 73

def privateuse?(tag)
  PRIVATEUSE_REGEX === tag
end

Instance Method Details

#==(other) ⇒ Object



300
301
302
303
304
305
# File 'lib/rack/acceptable/language_tag.rb', line 300

def ==(other)
  return false unless other.kind_of?(self.class)
  compose
  other.compose
  @tag == other.tag || @tag.downcase == other.tag.downcase
end

#===(other) ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
# File 'lib/rack/acceptable/language_tag.rb', line 307

def ===(other)
  if other.kind_of?(self.class)
    s = other.compose
  elsif other.respond_to?(:to_str)
    s = other.to_str
  else
    return false
  end
  compose
  @tag == s || @tag.downcase == s.downcase
end

#composeObject

Builds the String, which represents self. Does not perform validation or recomposition.



164
165
166
167
# File 'lib/rack/acceptable/language_tag.rb', line 164

def compose
  @tag = to_a.join(Const::HYPHEN)
  @tag
end

#has_singleton?(key) ⇒ Boolean Also known as: extension?

Checks if self has a singleton passed. Works case-insensitively.

Returns:

  • (Boolean)


142
143
144
145
# File 'lib/rack/acceptable/language_tag.rb', line 142

def has_singleton?(key)
  return false unless @extensions
  @extensions.key?(key) || @extensions.key?(key.downcase)
end

#has_variant?(variant) ⇒ Boolean

Checks if self has a variant passed. Works case-insensitively.

Returns:

  • (Boolean)


134
135
136
137
138
# File 'lib/rack/acceptable/language_tag.rb', line 134

def has_variant?(variant)
  return false unless @variants
  v = variant.downcase
  @variants.any? { |var| v == var || v == var.downcase }
end

#matched_by_basic_range?(range) ⇒ Boolean Also known as: has_prefix?

Checks if the basic Language-Range passed matches self.

Example

tag = LanguageTag.parse('de-Latn-DE')
tag.matched_by_basic_range?('de-Latn-DE') #=> true
tag.matched_by_basic_range?('de-Latn') #=> true
tag.matched_by_basic_range?('*') #=> true
tag.matched_by_basic_range?('de-La') #=> false
tag.matched_by_basic_range?('de-de') #=> false
tag.matched_by_basic_range?('malformedlangtag') #=> false

Returns:

  • (Boolean)


283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/rack/acceptable/language_tag.rb', line 283

def matched_by_basic_range?(range)
  if range.kind_of?(self.class)
    s = range.recompose.tag
  elsif range.respond_to?(:to_str)
    return true if range.to_str == Const::WILDCARD
    s = self.class.parse(range).tag
  else
    return false
  end
  recompose
  @tag == s || @tag.index(s + Const::HYPHEN) == 0
rescue
  false
end

#matched_by_extended_range?(range) ⇒ Boolean

Checks if the extended Language-Range (in the shortest notation) passed matches self.

Returns:

  • (Boolean)


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/rack/acceptable/language_tag.rb', line 231

def matched_by_extended_range?(range)
  recompose
  subtags = @composition.split(Const::HYPHEN)
  subranges = range.downcase.split(Const::HYPHEN)

  subrange = subranges.shift
  subtag = subtags.shift

  while subrange
    if subrange == Const::WILDCARD
      subrange = subranges.shift
    elsif subtag == nil
      return false
    elsif subtag == subrange
      subtag = subtags.shift
      subrange = subranges.shift
    elsif subtag.size == 1
      return false
    else
      subtag = subtags.shift
    end
  end
  true
rescue
  false
end

#nicecasedObject



185
186
187
188
# File 'lib/rack/acceptable/language_tag.rb', line 185

def nicecased
  recompose   # we could not conveniently format malformed or invalid tags
  @nicecased  #.dup #uuuuugh
end

#recompose(thing = nil) ⇒ Object

Parameters

thing<String, optional>

The LangTag snippet

Returns

self

Raises

ArgumentError

The snippet passed:

  • does not conform the Language-Tag ABNF (malformed)

  • represents a ‘grandfathered’ Language-Tag

  • starts with ‘x’ singleton (‘privateuse’).

  • contains duplicate variants

  • contains duplicate singletons



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/rack/acceptable/language_tag.rb', line 350

def recompose(thing = nil)

  tag = nil
  compose

  if thing
    raise TypeError, "Can't convert #{thing.class} into String" unless thing.respond_to?(:to_str)
    return self if @tag == (tag = thing.to_str) || @tag.downcase == (tag = tag.downcase)
  else
    return self if @nicecased == (tag = @tag) || @composition == tag || @composition == (tag = tag.downcase)
  end

  if !GRANDFATHERED_TAGS.key?(tag) && LANGTAG_COMPOSITION_REGEX === tag

    @primary = $1
    @extlang    = nil
    @script     = $2
    @region     = $3
    components  = $'.split(Utils::HYPHEN_SPLITTER)
    components.shift

    @primary, *@extlang = @primary.split(Utils::HYPHEN_SPLITTER) if @primary.include?(Const::HYPHEN)

    @script.capitalize! if @script
    @region.upcase! if @region

    @extensions = nil
    @variants   = nil
    singleton   = nil

    while c = components.shift
      if c.size == 1
        break if c == PRIVATEUSE
        @extensions ||= {}
        if @extensions.key?(c)
          raise ArgumentError, "Invalid langtag (repeated singleton: #{c.inspect}): #{thing.inspect}"
        end
        singleton = c
        @extensions[singleton = c] = []
      elsif singleton
        @extensions[singleton] << c
      else
        @variants ||= []
        if @variants.include?(c)
          raise ArgumentError, "Invalid langtag (repeated variant: #{c.inspect}): #{thing.inspect}"
        end
        @variants << c
      end
    end

    @privateuse   = components.empty? ? nil : components
    @nicecased    = compose
    @composition  = @tag.downcase

  else
    raise ArgumentError, "Malformed, grandfathered or 'privateuse' Language-Tag: #{thing.inspect}"
  end

  self
end

#singletonsObject

Builds an ordered list of singletons.



150
151
152
153
154
155
# File 'lib/rack/acceptable/language_tag.rb', line 150

def singletons
  return nil unless @extensions
  keys = @extensions.keys
  keys.sort!
  keys
end

#to_aObject



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/rack/acceptable/language_tag.rb', line 190

def to_a
  ary = [@primary]
  ary.concat @extlang if @extlang
  ary << @script if @script
  ary << @region if @region
  ary.concat @variants if @variants
  singletons.each { |s| (ary << s).concat @extensions[s] } if @extensions
  (ary << PRIVATEUSE).concat @privateuse if @privateuse
  ary
rescue
  raise "LanguageTag has at least one malformed attribute: #{self.inspect}"
end

#valid?Boolean Also known as: langtag?

Validates self.

Notes

Validation is deferred by default, because the paranoid check & dup of everything is not a good way (in this case). So, you may create some tags, make them malformed/invalid, and still be able to compare and modify them. Only note, that things like ‘filtering’ and ‘lookup’ are not validation-free.

Returns:

  • (Boolean)


328
329
330
# File 'lib/rack/acceptable/language_tag.rb', line 328

def valid?
  !!recompose rescue false
end