├── VERSION ├── test ├── format │ ├── currency_test.rb │ ├── percent_test.rb │ ├── all.rb │ ├── decimal │ │ ├── fraction_test.rb │ │ ├── integer_test.rb │ │ └── number_test.rb │ ├── datetime_test.rb │ └── decimal_test.rb ├── all.rb ├── export │ ├── data │ │ ├── all.rb │ │ ├── base_test.rb │ │ ├── week_data_test.rb │ │ ├── windows_zones_test.rb │ │ ├── territories_containment_test.rb │ │ ├── locale_display_pattern_test.rb │ │ ├── delimiters_test.rb │ │ ├── metazones_test.rb │ │ ├── country_codes_test.rb │ │ ├── fields_test.rb │ │ ├── lists_test.rb │ │ ├── parent_locales_test.rb │ │ ├── subdivisions_test.rb │ │ ├── region_validity_test.rb │ │ ├── units_test.rb │ │ ├── territories_test.rb │ │ ├── currencies_test.rb │ │ ├── timezones_test.rb │ │ ├── numbers_test.rb │ │ └── languages_test.rb │ ├── code │ │ └── numbers_test.rb │ ├── element_test.rb │ ├── data_set_test.rb │ ├── file_based_data_set_test.rb │ ├── deep_validate_keys_test.rb │ └── data_file_test.rb ├── locale │ └── fallbacks_test.rb ├── test_helper.rb ├── core_ext │ ├── deep_stringify_test.rb │ └── deep_prune_test.rb ├── test_autotest.rb ├── draft_status_test.rb └── export_test.rb ├── docs ├── _config.yml └── index.md ├── .gitignore ├── .vscode └── extensions.json ├── cldr.thor ├── lib ├── cldr │ ├── locale.rb │ ├── ldml.rb │ ├── export │ │ ├── data │ │ │ ├── rbnf_root.rb │ │ │ ├── calendars.rb │ │ │ ├── layout.rb │ │ │ ├── languages.rb │ │ │ ├── territories.rb │ │ │ ├── likely_subtags.rb │ │ │ ├── locale_display_pattern.rb │ │ │ ├── subdivisions.rb │ │ │ ├── currency_digits_and_rounding.rb │ │ │ ├── week_data.rb │ │ │ ├── windows_zones.rb │ │ │ ├── country_codes.rb │ │ │ ├── plurals.rb │ │ │ ├── characters.rb │ │ │ ├── parent_locales.rb │ │ │ ├── delimiters.rb │ │ │ ├── territories_containment.rb │ │ │ ├── region_validity.rb │ │ │ ├── context_transforms.rb │ │ │ ├── numbering_systems.rb │ │ │ ├── variables.rb │ │ │ ├── region_currencies.rb │ │ │ ├── aliases.rb │ │ │ ├── plurals │ │ │ │ └── cldr_grammar.treetop │ │ │ ├── base.rb │ │ │ ├── currencies.rb │ │ │ ├── segments_root.rb │ │ │ ├── metazones.rb │ │ │ ├── lists.rb │ │ │ ├── rbnf.rb │ │ │ ├── transforms.rb │ │ │ ├── timezones.rb │ │ │ ├── plural_rules.rb │ │ │ ├── fields.rb │ │ │ ├── units.rb │ │ │ ├── calendars │ │ │ │ └── gregorian.rb │ │ │ └── numbers.rb │ │ ├── nil_data_set.rb │ │ ├── ruby.rb │ │ ├── code │ │ │ └── numbers.rb │ │ ├── data_set.rb │ │ ├── code.rb │ │ ├── element.rb │ │ ├── yaml.rb │ │ ├── file_based_data_set.rb │ │ ├── deep_validate_keys.rb │ │ ├── data.rb │ │ └── data_file.rb │ ├── format │ │ ├── currency.rb │ │ ├── percent.rb │ │ ├── decimal │ │ │ ├── base.rb │ │ │ ├── fraction.rb │ │ │ ├── integer.rb │ │ │ └── number.rb │ │ ├── datetime.rb │ │ ├── datetime │ │ │ └── base.rb │ │ ├── decimal.rb │ │ ├── time.rb │ │ └── date.rb │ ├── format.rb │ ├── locale │ │ └── fallbacks.rb │ ├── draft_status.rb │ ├── download.rb │ ├── ldml │ │ ├── attribute.rb │ │ └── attributes.rb │ ├── validate.rb │ ├── thor.rb │ └── export.rb ├── core_ext │ ├── string │ │ ├── camelize.rb │ │ └── underscore.rb │ └── hash │ │ ├── deep_merge.rb │ │ ├── symbolize_keys.rb │ │ ├── deep_prune.rb │ │ ├── deep_stringify.rb │ │ └── deep_sort.rb └── cldr.rb ├── .autotest ├── .rubocop.yml ├── Gemfile ├── .github ├── dependabot.yml ├── workflows │ ├── lint.yml │ └── test.yml └── pull_request_template.md ├── Rakefile ├── LICENSE ├── .rubocop_todo.yml ├── README.md ├── .git-blame-ignore-revs ├── Gemfile.lock ├── CHANGELOG.md └── ruby-cldr.gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | -------------------------------------------------------------------------------- /test/format/currency_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/format/percent_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /data/ 3 | /pkg/ 4 | .rvmrc 5 | /pkg/ 6 | .bundle/* 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "shopify.ruby-lsp", 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |filename| 4 | require File.expand_path(filename) 5 | end 6 | -------------------------------------------------------------------------------- /cldr.thor: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.dirname(__FILE__) + "/lib") 4 | 5 | require "rubygems" 6 | require "thor" 7 | require "cldr/thor" 8 | -------------------------------------------------------------------------------- /test/format/all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |filename| 4 | require File.expand_path(filename) 5 | end 6 | -------------------------------------------------------------------------------- /test/export/data/all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |filename| 4 | require File.expand_path(filename) 5 | end 6 | -------------------------------------------------------------------------------- /lib/cldr/locale.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Locale 6 | autoload :Fallbacks, "cldr/locale/fallbacks" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/core_ext/string/camelize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class String 4 | def camelize 5 | gsub(/(^.|_[a-zA-Z])/) { |m| m.sub("_", "").capitalize } 6 | end 7 | end unless String.new.respond_to?(:camelize) 8 | -------------------------------------------------------------------------------- /test/export/data/base_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestBase < Test::Unit::TestCase 7 | end 8 | -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | # require "autotest/restart" 2 | # require 'autotest/timestamp' 3 | require 'test/test_autotest' 4 | 5 | Autotest.add_hook :initialize do |config| 6 | config.add_mapping(%r(.*rb$), true) do |filename, _| 7 | tests_for(filename) 8 | end 9 | end -------------------------------------------------------------------------------- /lib/cldr/ldml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Ldml 5 | autoload :Attribute, "cldr/ldml/attribute" 6 | autoload :Attributes, "cldr/ldml/attributes" 7 | 8 | ATTRIBUTES = Attributes.new 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/core_ext/hash/deep_merge.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Hash 4 | def deep_merge(other) 5 | merger = proc { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : (v2 || v1) } 6 | merge(other, &merger) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/cldr/export/data/rbnf_root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class RbnfRoot < Rbnf 7 | def initialize 8 | super(:root) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/core_ext/hash/symbolize_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Hash 4 | def symbolize_keys 5 | each_with_object({}) do |(key, value), result| 6 | key = key.to_sym if key.respond_to?(:to_sym) 7 | result[key] = value 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | 4 | inherit_gem: 5 | rubocop-shopify: rubocop.yml 6 | 7 | AllCops: 8 | TargetRubyVersion: 3.2 9 | UseCache: true 10 | CacheRootDirectory: tmp/rubocop 11 | NewCops: enable 12 | 13 | Layout/LineLength: 14 | Enabled: false 15 | -------------------------------------------------------------------------------- /lib/cldr/format/currency.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Format 6 | class Currency < Decimal 7 | def apply(number, options = {}) 8 | super.gsub("¤", options[:currency] || "$") 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/cldr/format/percent.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Format 6 | class Percent < Decimal 7 | def apply(number, options = {}) 8 | super.gsub("¤", options[:percent_sign] || "%") 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/core_ext/string/underscore.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class String 4 | def underscore 5 | to_s.gsub("::", "/") 6 | .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') 7 | .gsub(/([a-z\d])([A-Z])/, '\1_\2') 8 | .tr("-", "_") 9 | .downcase 10 | end 11 | end unless String.new.respond_to?(:underscore) 12 | -------------------------------------------------------------------------------- /lib/core_ext/hash/deep_prune.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DeepPrune 4 | def deep_prune!(comparator = ->(v) { v.is_a?(Hash) && v.empty? }) 5 | delete_if do |_, v| 6 | v.deep_prune!(comparator) if v.is_a?(Hash) 7 | comparator.call(v) 8 | end 9 | end 10 | end 11 | 12 | Hash.send(:include, DeepPrune) 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "i18n" 6 | gem "nokogiri" 7 | gem "psych", ">= 4.0.0" 8 | gem "rubyzip" 9 | gem "thor" 10 | 11 | group :development do 12 | gem "debug" 13 | gem "jeweler" 14 | gem "pry-nav" 15 | gem "pry" 16 | gem "rubocop-shopify", require: false 17 | gem "ruby-lsp", require: false 18 | gem "test-unit" 19 | end 20 | -------------------------------------------------------------------------------- /test/export/data/week_data_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataWeekData < Test::Unit::TestCase 7 | test "first day data" do 8 | first_day = Cldr::Export::Data::WeekData.new[:first_day] 9 | assert_equal(["MV"], first_day["fri"]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/cldr/export/data/calendars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Calendars < Base 7 | autoload :Gregorian, "cldr/export/data/calendars/gregorian" 8 | 9 | def initialize(locale) 10 | super 11 | update(calendars: { gregorian: Gregorian.new(locale) }) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/export/code/numbers_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | # require File.dirname(__FILE__) + '/../../test_helper.rb' 5 | # 6 | # class TestExportCodeNumbers < Test::Unit::TestCase 7 | # define_method :"test: symbols" do 8 | # puts Cldr::Export::Code::Numbers.new(:de).build 9 | # symbols = Cldr::Data[:de]::Numbers.symbols 10 | # assert symbols.is_a?(Hash) && symbols.has_key?(:decimal) 11 | # end 12 | # end 13 | -------------------------------------------------------------------------------- /test/export/data/windows_zones_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | require "time" 6 | 7 | class TestCldrDataMetazones < Test::Unit::TestCase 8 | test "windows zone DE" do 9 | zones = Cldr::Export::Data::WindowsZones.new 10 | assert_equal(["Europe/Berlin", "Europe/Busingen"], zones["W. Europe Standard Time"]["DE"]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/cldr/export/nil_data_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | class NilDataSet 8 | class << self 9 | def [](locale) 10 | nil 11 | end 12 | 13 | def []=(locale, value) 14 | raise NotImplementedError, "tried to set a value on a NilDataSet" 15 | end 16 | 17 | def locales 18 | [] 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/export/data/territories_containment_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataTerritoriesContainment < Test::Unit::TestCase 7 | test "territories containment" do 8 | territories = Cldr::Export::Data::TerritoriesContainment.new[:territories] 9 | assert_equal(["BG", "BY", "CZ", "HU", "MD", "PL", "RO", "RU", "SK", "SU", "UA"], territories["151"][:contains]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## ruby-cldr 2 | 3 | `ruby-cldr` is a Ruby library for exporting data from Unicode Consortium's Common Locale Data Repository (CLDR). 4 | 5 | CLDR contains tons of high-quality locale data such as formatting rules for dates, times, numbers, currencies as well as language, country, calendar-specific names etc. 6 | 7 | For localizing applications in Ruby we obviously want to use this incredibly comprehensive and well-maintained resource. 8 | 9 | See the [README](https://github.com/ruby-i18n/ruby-cldr) for more information. 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /lib/cldr/export/ruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | class Ruby 6 | def export(locale, component, options = {}) 7 | data = Export.data(component, locale, options) 8 | data = data.to_ruby if data.respond_to?(:to_ruby) 9 | unless data.nil? || data.empty? 10 | path = Export.path(locale, component, "rb") 11 | Export.write(path, data) 12 | yield(component, locale, path) if block_given? 13 | data 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cldr/export/code/numbers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # module Cldr 4 | # module Export 5 | # module Code 6 | # end 7 | # end 8 | # end 9 | # 10 | # export code like this: 11 | # 12 | # module Cldr::Data::De 13 | # module Numbers 14 | # def self.symbols 15 | # end 16 | # 17 | # module Format 18 | # def self.currency 19 | # end 20 | # 21 | # def self.decimal 22 | # end 23 | # 24 | # def self.percent 25 | # end 26 | # 27 | # def self.scientific 28 | # end 29 | # end 30 | # end 31 | # end 32 | -------------------------------------------------------------------------------- /lib/cldr/format/decimal/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Decimal 6 | class Base 7 | def interpolate(string, value, orientation = :right) 8 | value = value.to_s 9 | length = value.length 10 | start = orientation == :left ? 0 : -length 11 | 12 | string = string.dup 13 | string = string.ljust(length, "#") if string.length < length 14 | string[start, length] = value 15 | string.gsub("#", "") 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cldr/export/data/layout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Layout < Base 7 | def initialize(locale) 8 | super 9 | update(layout: layout) 10 | end 11 | 12 | private 13 | 14 | def layout 15 | result = { orientation: {} } 16 | 17 | if (node = select_single("layout/orientation/characterOrder/text()")) 18 | result[:orientation][:character_order] = node.text 19 | end 20 | 21 | result 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/export/data/locale_display_pattern_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestLocaleDisplayPattern < Test::Unit::TestCase 7 | test "locale_display_pattern :de" do 8 | expected = { 9 | "locale_key_type_pattern" => "{0}: {1}", 10 | "locale_pattern" => "{0} ({1})", 11 | "locale_separator" => "{0}, {1}", 12 | } 13 | 14 | actual = Cldr::Export::Data::LocaleDisplayPattern.new(:de)[:locale_display_pattern] 15 | 16 | assert_equal expected, actual 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cldr/export/data/languages.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Languages < Base 7 | def initialize(locale) 8 | super 9 | update(languages: languages) 10 | end 11 | 12 | private 13 | 14 | def languages 15 | @languages ||= select("localeDisplayNames/languages/language").each_with_object({}) do |node, result| 16 | result[Cldr::Export.to_i18n(node.attribute("type").value)] = node.content unless alt?(node) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cldr/export/data/territories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Territories < Base 7 | def initialize(locale) 8 | super 9 | update(territories: territories) 10 | end 11 | 12 | private 13 | 14 | def territories 15 | @territories ||= select("localeDisplayNames/territories/territory").each_with_object({}) do |node, result| 16 | result[node.attribute("type").value.to_sym] = node.content unless alt?(node) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cldr/export/data/likely_subtags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class LikelySubtags < Base 7 | def initialize 8 | super(nil) 9 | update(subtags: subtags) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def subtags 16 | doc.xpath("//likelySubtag").each_with_object({}) do |subtag, ret| 17 | from = subtag.attribute("from").value 18 | to = subtag.attribute("to").value 19 | ret[from] = to 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/cldr/export/data/locale_display_pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class LocaleDisplayPattern < Base 7 | def initialize(locale) 8 | super 9 | update(locale_display_pattern: locale_display_pattern) 10 | end 11 | 12 | private 13 | 14 | def locale_display_pattern 15 | @locale_display_pattern ||= select("localeDisplayNames/localeDisplayPattern/*").each_with_object({}) do |node, result| 16 | result[node.name.underscore] = node.content 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/export/data/delimiters_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataDelimiters < Test::Unit::TestCase 7 | test "delimiters :de" do 8 | expected = { 9 | delimiters: { 10 | quotes: { 11 | default: { 12 | start: "„", 13 | end: "“", 14 | }, 15 | alternate: { 16 | start: "‚", 17 | end: "‘", 18 | }, 19 | }, 20 | }, 21 | } 22 | assert_equal expected, Cldr::Export::Data::Delimiters.new(:de) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/cldr/export/data/subdivisions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Subdivisions < Base 7 | def initialize(locale) 8 | super 9 | update(subdivisions: subdivisions) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def subdivisions 16 | @subdivisions ||= select("localeDisplayNames/subdivisions/subdivision").each_with_object({}) do |node, result| 17 | result[node.attribute("type").value.to_sym] = node.content unless alt?(node) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/cldr/format.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require "core_ext/string/camelize" 5 | 6 | module Cldr 7 | module Format 8 | autoload :Base, "cldr/format/base" 9 | autoload :Currency, "cldr/format/currency" 10 | autoload :Fraction, "cldr/format/fraction" 11 | autoload :Integer, "cldr/format/integer" 12 | autoload :Date, "cldr/format/date" 13 | autoload :Datetime, "cldr/format/datetime" 14 | autoload :Decimal, "cldr/format/decimal" 15 | autoload :Numeric, "cldr/format/numeric" 16 | autoload :Percent, "cldr/format/percent" 17 | autoload :Time, "cldr/format/time" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/export/data/metazones_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | require "date" 6 | 7 | class TestCldrDataMetazones < Test::Unit::TestCase 8 | test "metazone timezones" do 9 | timezones = Cldr::Export::Data::Metazones.new[:timezones] 10 | assert_equal({ "from" => DateTime.parse("1990-05-05T21:00:00+00:00"), "metazone" => "Europe_Eastern" }, timezones[:"Europe/Chisinau"].last) 11 | end 12 | 13 | test "metazone primaryzones" do 14 | primaryzones = Cldr::Export::Data::Metazones.new[:primaryzones] 15 | assert_equal("Europe/Berlin", primaryzones[:DE]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/export/data/country_codes_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataCountryCodes < Test::Unit::TestCase 7 | test "country codes" do 8 | expected = 9 | { 10 | AA: { 11 | "numeric" => "958", 12 | "alpha3" => "AAA", 13 | }, 14 | AC: { 15 | "alpha3" => "ASC", 16 | }, 17 | } 18 | country_codes = Cldr::Export::Data::CountryCodes.new 19 | assert_equal(country_codes[:country_codes][:AA], expected[:AA]) 20 | assert_equal(country_codes[:country_codes][:AC], expected[:AC]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/format/decimal/fraction_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 4 | 5 | class TestCldrDecimalFractionFormatWithInteger < Test::Unit::TestCase 6 | test "formats a fraction" do 7 | assert_equal ".45", Cldr::Format::Decimal::Fraction.new("###.##").apply("45") 8 | end 9 | 10 | test "pads zero digits on the right side" do 11 | assert_equal ".4500", Cldr::Format::Decimal::Fraction.new("###.0000#").apply("45") 12 | end 13 | 14 | test ":precision option overrides format precision" do 15 | assert_equal ".78901", Cldr::Format::Decimal::Fraction.new("###.##").apply("78901", precision: 5) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/cldr/export/data/currency_digits_and_rounding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class CurrencyDigitsAndRounding < Hash 9 | def initialize 10 | super 11 | 12 | Cldr::Export::Data::RAW_DATA[nil].xpath("//currencyData/fractions/info").each do |node| 13 | code = node.attr("iso4217") 14 | digits = node.attr("digits").to_i 15 | rounding = node.attr("rounding").to_i 16 | 17 | self[code.upcase.to_sym] = { digits: digits, rounding: rounding } 18 | end 19 | 20 | deep_sort! 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/cldr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core_ext/hash/symbolize_keys" 4 | 5 | module Cldr 6 | autoload :Download, "cldr/download" 7 | autoload :DraftStatus, "cldr/draft_status" 8 | autoload :Export, "cldr/export" 9 | autoload :Format, "cldr/format" 10 | autoload :Ldml, "cldr/ldml" 11 | autoload :Locale, "cldr/locale" 12 | autoload :Validate, "cldr/validate" 13 | autoload :ValidateUpstreamAssumptions, "cldr/validate_upstream_assumptions" 14 | 15 | class << self 16 | def fallbacks 17 | @@fallbacks ||= Cldr::Locale::Fallbacks.new 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cldr/export/data/week_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class WeekData < Base 7 | def initialize(*) 8 | super(nil) 9 | update(first_day: first_day) 10 | deep_sort! 11 | end 12 | 13 | def first_day 14 | @week_data ||= doc.xpath("supplementalData/weekData/firstDay").filter_map do |node| 15 | alt = node.attribute("alt") 16 | next if alt 17 | 18 | day = node.attribute("day").value 19 | territories = node.attribute("territories").value.split(" ") 20 | [day, territories] 21 | end.to_h 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/format/datetime_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | require "date" 6 | 7 | class TestCldrDatetimeFormat < Test::Unit::TestCase 8 | def setup 9 | super 10 | @locale = :de 11 | @calendar = Cldr::Export::Data::Calendars.new(@locale)[:calendars][:gregorian] 12 | end 13 | 14 | test "datetime pattern :de" do 15 | date = Cldr::Format::Date.new("dd.MM.yyyy", @calendar) 16 | time = Cldr::Format::Time.new("HH:mm", @calendar) 17 | result = Cldr::Format::Datetime.new("{{date}} {{time}}", date, time).apply(Time.new(2010, 1, 10, 13, 12, 11)) 18 | assert_equal "10.01.2010 13:12", result 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cldr/export/data/windows_zones.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | require "time" 5 | 6 | module Cldr 7 | module Export 8 | module Data 9 | class WindowsZones < Hash 10 | def initialize 11 | super 12 | 13 | Cldr::Export::Data::RAW_DATA[nil].xpath("//windowsZones/mapTimezones/mapZone").each_with_object(self) do |node, result| 14 | zone = node.attr("other").to_s 15 | territory = node.attr("territory") 16 | timezones = node.attr("type").split(" ") 17 | result[zone] ||= {} 18 | result[zone][territory] = timezones 19 | end 20 | 21 | deep_sort! 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/export/data/fields_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataFields < Test::Unit::TestCase 7 | test "Alias nodes are exported as paths to their targets" do 8 | data = Cldr::Export::Data::Fields.new(:root) 9 | path = data.dig(:fields, :day_narrow) 10 | assert_equal :"fields.day_short", path 11 | 12 | path = data.dig(*split_path_string(path)) 13 | assert_equal :"fields.day", path 14 | 15 | day = data.dig(*split_path_string(path)) 16 | assert_not_nil day 17 | end 18 | 19 | private 20 | 21 | def split_path_string(path) 22 | path.to_s.split(".").map(&:to_sym) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/cldr/locale/fallbacks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Locale 5 | class Fallbacks < Hash 6 | def [](locale) 7 | defined_parents = Cldr::Export::Data::ParentLocales.new 8 | 9 | ancestry = [locale] 10 | loop do 11 | if defined_parents[ancestry.last] 12 | ancestry << defined_parents[ancestry.last] 13 | elsif I18n::Locale::Tag.tag(ancestry.last).parents.any? 14 | ancestry << I18n::Locale::Tag.tag(ancestry.last).parents.first.to_sym 15 | else 16 | break 17 | end 18 | end 19 | ancestry << :root unless ancestry.last == :root 20 | store(locale, ancestry) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/export/data/lists_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataLists < Test::Unit::TestCase 7 | test "Alias nodes are exported as paths to their targets" do 8 | data = Cldr::Export::Data::Lists.new(:root) 9 | path = data.dig(:lists, :or_narrow) 10 | assert_equal :"lists.or_short", path 11 | 12 | path = data.dig(*split_path_string(path)) 13 | assert_equal :"lists.or", path 14 | 15 | pattern_or = data.dig(*split_path_string(path)) 16 | assert_not_nil pattern_or 17 | end 18 | 19 | private 20 | 21 | def split_path_string(path) 22 | path.to_s.split(".").map(&:to_sym) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/core_ext/hash/deep_stringify.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Hash 4 | def deep_stringify(stringify_keys: true, stringify_values: true) 5 | each_with_object({}) do |(key, value), result| 6 | value = value.deep_stringify(stringify_keys: stringify_keys, stringify_values: stringify_values) if value.is_a?(Hash) 7 | key = key.to_s if stringify_keys && key.is_a?(Symbol) 8 | value = value.to_s if stringify_values && value.is_a?(Symbol) 9 | result[key] = value 10 | end 11 | end 12 | 13 | def deep_stringify_keys 14 | deep_stringify(stringify_keys: true, stringify_values: false) 15 | end 16 | 17 | def deep_stringify_values 18 | deep_stringify(stringify_keys: false, stringify_values: true) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cldr/format/datetime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Datetime 6 | autoload :Base, "cldr/format/datetime/base" 7 | 8 | attr_reader :format, :date, :time 9 | 10 | def initialize(format, date, time) 11 | @format = format 12 | @date = date 13 | @time = time 14 | end 15 | 16 | def apply(datetime, options = {}) 17 | format.gsub(/(\{\{(date|time)\}\})/) do 18 | case Regexp.last_match(2) 19 | when "date" 20 | options[:date] || date.apply(datetime, options) 21 | when "time" 22 | options[:time] || time.apply(datetime, options) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/format/decimal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 4 | 5 | class TestCldrDecimalFormat < Test::Unit::TestCase 6 | test "single pattern, positive number" do 7 | assert_equal "123", Cldr::Format::Decimal.new("#").apply(123) 8 | end 9 | 10 | test "single pattern, negative number" do 11 | assert_equal "-123", Cldr::Format::Decimal.new("#").apply(-123) 12 | end 13 | 14 | test "positive/negative patterns, positive number" do 15 | assert_equal "123", Cldr::Format::Decimal.new("#;-#").apply(123) 16 | end 17 | 18 | test "positive/negative patterns, negative number" do 19 | assert_equal "-123", Cldr::Format::Decimal.new("#;-#").apply(-123) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/cldr/export/data/country_codes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class CountryCodes < Base 7 | def initialize 8 | super(nil) 9 | update(country_codes: country_codes) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def country_codes 16 | doc.xpath("//codeMappings/territoryCodes").each_with_object({}) do |node, hash| 17 | type = node.attribute("type").to_s.to_sym 18 | hash[type] = {} 19 | hash[type]["numeric"] = node[:numeric] if node[:numeric] 20 | hash[type]["alpha3"] = node[:alpha3] if node[:alpha3] 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/export/data/parent_locales_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require "yaml" 5 | 6 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 7 | 8 | class TestParentLocales < Test::Unit::TestCase 9 | test "ParentLocales uses symbols for keys and values" do 10 | data = Cldr::Export.data(:ParentLocales, {}) 11 | assert data.all? { |key, value| key.is_a?(Symbol) && value.is_a?(Symbol) }, "Not all keys and values are Symbols" 12 | end 13 | 14 | test "ParentLocales#to_h uses strings for keys and values" do 15 | data = Cldr::Export.data(:ParentLocales, {}).to_h 16 | assert data.all? { |key, value| key.is_a?(String) && value.is_a?(String) }, "Not all keys and values are Strings" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "rake" 5 | 6 | begin 7 | require "jeweler" 8 | Jeweler::Tasks.new do |gem| 9 | gem.name = "ruby-cldr" 10 | gem.summary = "Ruby library for exporting and using data from CLDR " 11 | gem.description = "Ruby library for exporting and using data from CLDR, see http://cldr.unicode.org" 12 | gem.email = "svenfuchs@artweb-design.de" 13 | gem.homepage = "http://github.com/ruby-i18n/ruby-cldr" 14 | gem.authors = ["Sven Fuchs"] 15 | gem.license = "MIT" 16 | gem.files = FileList["*.thor", "[A-Z]*", "{lib,test}/**/*"] 17 | end 18 | Jeweler::GemcutterTasks.new 19 | rescue LoadError 20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 21 | end 22 | -------------------------------------------------------------------------------- /lib/cldr/export/data/plurals.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class Plurals 9 | autoload :Grammar, "cldr/export/data/plurals/grammar" 10 | autoload :Parser, "cldr/export/data/plurals/grammar" 11 | autoload :Rules, "cldr/export/data/plurals/rules" 12 | autoload :Rule, "cldr/export/data/plurals/rules" 13 | autoload :Proposition, "cldr/export/data/plurals/rules" 14 | autoload :Expression, "cldr/export/data/plurals/rules" 15 | 16 | class << self 17 | def rules 18 | @@rules ||= Rules.read(Cldr::Export::Data::RAW_DATA[nil]) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/cldr/export/data/characters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Characters < Base 7 | def initialize(locale) 8 | super 9 | update(characters: { exemplars: exemplars }) 10 | end 11 | 12 | private 13 | 14 | def exemplars 15 | select("/ldml/characters/exemplarCharacters").map do |node| 16 | { 17 | # remove enclosing brackets 18 | characters: node.content[1..-2], 19 | type: type_from(node), 20 | } 21 | end 22 | end 23 | 24 | protected 25 | 26 | def type_from(node) 27 | node.attribute("type")&.value 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/cldr/export/data/parent_locales.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class ParentLocales < Hash 9 | def initialize 10 | super 11 | 12 | Cldr::Export::Data::RAW_DATA[nil].xpath("//parentLocales/parentLocale").each do |node| 13 | parent = Cldr::Export.to_i18n(node.attr("parent")) 14 | locales = node.attr("locales").split(" ").map { |locale| Cldr::Export.to_i18n(locale) } 15 | 16 | locales.each do |locale| 17 | self[locale] = parent 18 | end 19 | end 20 | 21 | deep_sort! 22 | end 23 | 24 | def to_h 25 | deep_stringify 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v2 18 | 19 | - name: Install libyaml 20 | run: sudo apt-get update -y && sudo apt-get install -y libyaml-dev 21 | 22 | - name: Set up Ruby 3.3 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | bundler-cache: true 26 | ruby-version: 3.3 27 | 28 | - name: Lint code 29 | run: bundle exec rubocop 30 | -------------------------------------------------------------------------------- /test/locale/fallbacks_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | 6 | class TestFallbacks < Test::Unit::TestCase 7 | test "fallbacks does basic hyphen chopping" do 8 | assert_equal [:root], Cldr.fallbacks[:root] 9 | assert_equal [:en, :root], Cldr.fallbacks[:en] 10 | assert_equal [:"fr-CA", :fr, :root], Cldr.fallbacks[:"fr-CA"] 11 | assert_equal [:"zh-Hant-HK", :"zh-Hant", :root], Cldr.fallbacks[:"zh-Hant-HK"] 12 | end 13 | 14 | test "fallbacks observe explicit parent overrides" do 15 | assert_equal [:"az-Arab", :root], Cldr.fallbacks[:"az-Arab"] 16 | assert_equal [:"en-CH", :"en-150", :"en-001", :en, :root], Cldr.fallbacks[:"en-CH"] 17 | assert_equal [:"zh-Hant", :root], Cldr.fallbacks[:"zh-Hant"] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/cldr/export/data_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "nil_data_set" 4 | 5 | module Cldr 6 | module Export 7 | class DataSet 8 | def initialize(parent: nil) 9 | @file_cache = {} 10 | @parent = parent || NilDataSet 11 | end 12 | 13 | def [](locale) 14 | file_cache[locale] ||= compute(locale) 15 | end 16 | 17 | def []=(locale, value) 18 | file_cache[locale] = value 19 | end 20 | 21 | def locales 22 | (file_cache.keys + locales_at_this_level + (parent&.locales || [])).uniq.sort 23 | end 24 | 25 | private 26 | 27 | attr_reader :file_cache, :parent 28 | 29 | def locales_at_this_level 30 | file_cache.keys 31 | end 32 | 33 | def compute(locale) 34 | parent[locale] 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cldr/export/data/delimiters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Delimiters < Base 7 | def initialize(locale) 8 | super 9 | update( 10 | delimiters: { 11 | quotes: { 12 | default: quotes("quotation"), 13 | alternate: quotes("alternateQuotation"), 14 | }, 15 | }, 16 | ) 17 | end 18 | 19 | private 20 | 21 | def quotes(type) 22 | start = select_single("delimiters/#{type}Start") 23 | end_ = select_single("delimiters/#{type}End") 24 | 25 | result = {} 26 | result[:start] = start.content if start 27 | result[:end] = end_.content if end_ 28 | result 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/cldr/export/data/territories_containment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class TerritoriesContainment < Base 7 | def initialize(*) 8 | super(nil) 9 | update(territories: territories) 10 | deep_sort! 11 | end 12 | 13 | def territories 14 | @territories ||= doc.xpath("supplementalData/territoryContainment/group").each_with_object( 15 | Hash.new { |h, k| h[k] = { contains: [] } }, 16 | ) do |territory, memo| 17 | territory_id = territory.attribute("type").value 18 | children = territory.attribute("contains").value.split(" ") 19 | 20 | memo[territory_id][:contains].concat(children) 21 | memo[territory_id][:contains].sort! 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/cldr/format/decimal/fraction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Decimal 6 | class Fraction < Base 7 | attr_reader :decimal, :precision 8 | 9 | def initialize(format, symbols = {}) 10 | super() 11 | 12 | @format = format ? format.split(".").pop : "" 13 | @decimal = symbols[:decimal] || "." 14 | @precision = @format.length 15 | end 16 | 17 | def apply(fraction, options = {}) 18 | precision = options[:precision] || self.precision 19 | if precision > 0 20 | decimal + interpolate(format(options), fraction, :left) 21 | else 22 | "" 23 | end 24 | end 25 | 26 | def format(options) 27 | options[:precision] ? "0" * options[:precision] : @format 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/cldr/draft_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module DraftStatus 5 | class Status 6 | include Comparable 7 | 8 | def initialize(name, value) 9 | @name = name 10 | @value = value 11 | end 12 | 13 | def <=>(other) 14 | @value <=> other.value 15 | end 16 | 17 | def to_s 18 | @name.to_s 19 | end 20 | 21 | protected 22 | 23 | attr_reader :value 24 | end 25 | 26 | ALL = [ 27 | :unconfirmed, 28 | :provisional, 29 | :contributed, 30 | :approved, 31 | ].map.with_index do |name, index| 32 | const_set(name.upcase, Status.new(name, index)) 33 | end.freeze 34 | 35 | ALL_BY_NAME = ALL.to_h { |status| [status.to_s, status] }.freeze 36 | private_constant :ALL_BY_NAME 37 | 38 | class << self 39 | def fetch(name) 40 | ALL_BY_NAME.fetch(name.to_s) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cldr/export/data/region_validity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class RegionValidity < Base 7 | def initialize 8 | super(nil) 9 | update(validity: { regions: regions }) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def regions 16 | doc.xpath("/supplementalData/idValidity/id[@type='region']").each_with_object({}) do |node, hash| 17 | type = node.attribute("idStatus").to_s.to_sym 18 | hash[type] = node.content.split(/\s+/).map(&:strip).reject(&:empty?).flat_map { |element| expand_range(*element.split("~")) } 19 | end 20 | end 21 | 22 | def expand_range(start, endd = nil) 23 | return [start] if endd.nil? 24 | 25 | prefix = start[0...-1] 26 | ((start[-1].ord)..(endd.ord)).map(&:chr).map { |c| "#{prefix}#{c}" } 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What are you trying to accomplish? 2 | 5 | 6 | ... 7 | 8 | ### What approach did you choose and why? 9 | 12 | 13 | ... 14 | 15 | ### What should reviewers focus on? 16 | 19 | 20 | ... 21 | 22 | ### The impact of these changes 23 | 26 | 27 | ... 28 | 29 | ### Testing 30 | 33 | 34 | ... 35 | 36 | ### Checklist 37 | 38 | * [ ] I have added a CHANGELOG entry for this change (or determined that it isn't needed) 39 | -------------------------------------------------------------------------------- /lib/cldr/export/data/context_transforms.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class ContextTransforms < Base 7 | def initialize(locale) 8 | super 9 | update(context_transforms: context_transforms) 10 | end 11 | 12 | private 13 | 14 | def context_transforms 15 | @context_transforms ||= select("contextTransforms/contextTransformUsage").each_with_object({}) do |usage_node, result| 16 | usage_type = usage_node.attribute("type").value.underscore.to_sym 17 | result[usage_type] = usage_node.xpath("contextTransform").each_with_object({}) do |transform_node, result| 18 | context_type = transform_node.attribute("type").value.underscore.to_sym 19 | transform_type = transform_node.content.underscore.gsub("firstword", "first_word") 20 | result[context_type] = transform_type 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) 4 | 5 | require "test/unit" 6 | require "cldr" 7 | require "debug" 8 | require "rubygems" 9 | 10 | module Test 11 | module Unit 12 | class TestCase 13 | class << self 14 | def test(name, &block) 15 | test_name = "test_#{name.gsub(/\s+/, "_")}".to_sym 16 | defined = begin 17 | instance_method(test_name) 18 | rescue 19 | false 20 | end 21 | raise "#{test_name} is already defined in #{self}" if defined 22 | 23 | if block_given? 24 | define_method(test_name, &block) 25 | else 26 | define_method(test_name) do 27 | flunk("No implementation provided for #{name}") 28 | end 29 | end 30 | end 31 | end 32 | 33 | def setup 34 | Cldr::Export.minimum_draft_status = Cldr::DraftStatus::CONTRIBUTED 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cldr/download.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | require "open-uri" 5 | require "zip" 6 | 7 | module Cldr 8 | module Download 9 | DEFAULT_SOURCE = "http://unicode.org/Public/cldr/%{version}/core.zip" 10 | DEFAULT_TARGET = "./vendor/cldr" 11 | DEFAULT_VERSION = 41 12 | 13 | class << self 14 | def download(source = DEFAULT_SOURCE, target = DEFAULT_TARGET, version = DEFAULT_VERSION, &block) 15 | source = format(source, version: version) 16 | target ||= File.expand_path(DEFAULT_TARGET) 17 | 18 | URI.parse(source).open do |tempfile| 19 | FileUtils.mkdir_p(target) 20 | Zip.on_exists_proc = true 21 | Zip::File.open(tempfile.path) do |file| 22 | file.each do |entry| 23 | path = File.join(target, entry.name) 24 | FileUtils.mkdir_p(File.dirname(path)) 25 | file.extract(entry, path) 26 | yield path if block_given? 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/export/data/subdivisions_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataSubdivisions < Test::Unit::TestCase 7 | test "subdivisions for Canada/Quebec in :ja" do 8 | subdivisions = Cldr::Export::Data::Subdivisions.new(:ja)[:subdivisions] 9 | 10 | assert_equal("ケベック州", subdivisions[:caqc]) 11 | end 12 | 13 | test "subdivisions empty for unsupported locale :zz" do 14 | data = Cldr::Export::Data::Subdivisions.new(:zz) 15 | 16 | assert_empty(data[:subdivisions]) 17 | end 18 | 19 | test "subdivisions locales are a subset of main locales" do 20 | root = File.expand_path("./vendor/cldr/common") 21 | 22 | main_locales = Dir["#{root}/main/*.xml"].map { |path| path =~ /([\w_-]+)\.xml/ && Regexp.last_match(1) } 23 | subdivisions_locales = Dir["#{root}/subdivisions/*.xml"].map { |path| path =~ /([\w_-]+)\.xml/ && Regexp.last_match(1) } 24 | 25 | assert_empty(subdivisions_locales - main_locales) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/cldr/export/data/numbering_systems.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Export 6 | module Data 7 | class NumberingSystems < Base 8 | def initialize(*args) 9 | super(nil) 10 | update(numbering_systems: numbering_systems) 11 | deep_sort! 12 | end 13 | 14 | private 15 | 16 | def numbering_systems 17 | doc.xpath("supplementalData/numberingSystems/numberingSystem").each_with_object({}) do |numbering, ret| 18 | system_name = numbering.attribute("id").value.to_sym 19 | type = numbering.attribute("type").value 20 | 21 | param = case type 22 | when "numeric" 23 | "digits" 24 | when "algorithmic" 25 | "rules" 26 | end 27 | 28 | ret[system_name] = { 29 | :type => type, 30 | param.to_sym => numbering.attribute(param).value, 31 | } 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cldr/export/data/variables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Variables < Base 7 | # only these variables will be exported 8 | VARIABLE_IDS = ["$grandfathered", "$language", "$territory", "$script", "$variant"] 9 | 10 | def initialize 11 | super(nil) 12 | update(variables: variables) 13 | end 14 | 15 | private 16 | 17 | def variables 18 | doc.xpath("//validity/variable").each_with_object({}) do |variable, ret| 19 | name = variable.attribute("id").value 20 | if VARIABLE_IDS.include?(name) 21 | ret[fix_var_name(name)] = split_value_list(variable.text) 22 | end 23 | end 24 | end 25 | 26 | def fix_var_name(var_name) 27 | # remove leading dollar sign 28 | var_name.sub(/\A\$/, "") 29 | end 30 | 31 | def split_value_list(value_list) 32 | value_list.strip.split(/[\s]+/) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cldr/format/datetime/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Datetime 6 | class Base 7 | attr_reader :calendar 8 | 9 | def initialize(format, calendar) 10 | @calendar = calendar 11 | compile(format) 12 | end 13 | 14 | protected 15 | 16 | def compile(format) 17 | (class << self; self; end).class_eval(<<-code) 18 | def apply(date, options = {}); #{compile_format(format)}; end 19 | code 20 | end 21 | 22 | # compile_format("EEEE, d. MMMM y") # => 23 | # '' + weekday(date, "EEEE", 4) + ', ' + day(date, "d", 1) + '. ' + 24 | # month(date, "MMMM", 4) + ' ' + year(date, "y", 1) + '' 25 | def compile_format(format) 26 | "'" + format.gsub(self.class.const_get(:PATTERN)) do |token| 27 | method = self.class.const_get(:METHODS)[token[0, 1]] 28 | "' + #{method}(date, #{token.inspect}, #{token.length}).to_s + '" 29 | end + "'" 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Sven Fuchs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/cldr/format/decimal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Decimal 6 | autoload :Base, "cldr/format/decimal/base" 7 | autoload :Fraction, "cldr/format/decimal/fraction" 8 | autoload :Integer, "cldr/format/decimal/integer" 9 | autoload :Number, "cldr/format/decimal/number" 10 | 11 | attr_reader :positive, :negative 12 | 13 | def initialize(format, symbols = {}) 14 | @positive, @negative = parse_format(format, symbols) 15 | end 16 | 17 | def apply(number, options = {}) 18 | number = Float(number) 19 | format = number.abs == number ? positive : negative 20 | format.apply(number, options) 21 | rescue TypeError, ArgumentError 22 | number 23 | end 24 | 25 | protected 26 | 27 | def parse_format(format, symbols) 28 | formats = format.split(symbols[:list] || ";") 29 | formats << "#{symbols[:minus] || "-"}#{format}" if formats.size == 1 30 | formats.map { |format| Number.new(format, symbols) } 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/cldr/export/data/region_currencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class RegionCurrencies < Base 7 | def initialize 8 | super(nil) 9 | update(region_currencies: currencies) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def currencies 16 | doc.xpath("//currencyData/region").each_with_object({}) do |region, ret| 17 | name = region.attribute("iso3166").value 18 | ret[name] = currency(region) 19 | end 20 | end 21 | 22 | def currency(node) 23 | (node / "currency").map do |currency| 24 | currency_code = currency.attribute("iso4217").value 25 | result = { "currency" => currency_code } 26 | 27 | if (from_node = currency.attribute("from")) 28 | result["from"] = from_node.value 29 | end 30 | 31 | if (to_node = currency.attribute("to")) 32 | result["to"] = to_node.value 33 | end 34 | 35 | result 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/cldr/ldml/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Ldml 5 | class Attribute 6 | class << self 7 | def parse(attlist, comments) 8 | element_name, attribute_name, type, mode = attlist.strip.split(/\s+/).map(&:to_sym) 9 | status = comments.find { |comment| comment == :VALUE || comment == :METADATA } || :DISTINGUISHING 10 | deprecated = comments.any? { |comment| comment == :DEPRECATED } 11 | Attribute.new(element_name, attribute_name, type, mode, status, deprecated) 12 | end 13 | end 14 | 15 | attr_reader :element_name, :attribute_name, :type, :mode 16 | 17 | def initialize(element_name, attribute_name, type, mode, status, deprecated) # rubocop:disable Metrics/ParameterLists 18 | @element_name = element_name 19 | @attribute_name = attribute_name 20 | @type = type 21 | @mode = mode 22 | @status = status 23 | @deprecated = deprecated 24 | end 25 | 26 | def distinguishing? 27 | @status == :DISTINGUISHING 28 | end 29 | 30 | def deprecated? 31 | @deprecated 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/cldr/export/data/aliases.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Aliases < Base 7 | # only these aliases will be exported 8 | ALIAS_TAGS = ["languageAlias", "territoryAlias"] 9 | 10 | def initialize 11 | super(nil) 12 | update(aliases: aliases) 13 | deep_sort! 14 | end 15 | 16 | private 17 | 18 | def aliases 19 | ALIAS_TAGS.each_with_object({}) do |alias_tag, ret| 20 | ret[alias_tag.sub("Alias", "")] = alias_for(alias_tag) 21 | end 22 | end 23 | 24 | def alias_for(alias_tag) 25 | doc.xpath("//alias/#{alias_tag}").each_with_object({}) do |alias_data, ret| 26 | next unless (replacement_attr = alias_data.attribute("replacement")) 27 | 28 | replacement = replacement_attr.value 29 | 30 | if replacement.include?(" ") 31 | replacement = replacement.split(" ") 32 | end 33 | 34 | type = alias_data.attribute("type").value 35 | ret[type] = replacement 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/export/data/region_validity_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataRegionValidity < Test::Unit::TestCase 7 | test "region validity" do 8 | region_validity = Cldr::Export::Data::RegionValidity.new[:validity][:regions] 9 | assert_include(region_validity[:regular], "AD") 10 | assert_equal(region_validity[:regular].size, 256) 11 | 12 | assert_include(region_validity[:special], "XB") 13 | assert_equal(region_validity[:special].size, 2) 14 | 15 | assert_include(region_validity[:macroregion], "002") 16 | assert_equal(region_validity[:macroregion].size, 35) 17 | 18 | assert_include(region_validity[:deprecated], "YU") 19 | assert_equal(region_validity[:deprecated].size, 12) 20 | 21 | assert_include(region_validity[:reserved], "QN") 22 | assert_equal(region_validity[:reserved].size, 13) 23 | 24 | assert_include(region_validity[:private_use], "XD") 25 | assert_equal(region_validity[:private_use].size, 23) 26 | 27 | assert_include(region_validity[:unknown], "ZZ") 28 | assert_equal(region_validity[:unknown].size, 1) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/cldr/export/code.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Code 6 | # autoload :Base, 'cldr/export/code/base' 7 | # autoload :Calendars, 'cldr/export/code/calendars' 8 | # autoload :Currencies, 'cldr/export/code/currencies' 9 | # autoload :Delimiters, 'cldr/export/code/delimiters' 10 | # autoload :Languages, 'cldr/export/code/languages' 11 | autoload :Numbers, "cldr/export/code/numbers" 12 | # autoload :Plurals, 'cldr/export/code/plurals' 13 | # autoload :Territories, 'cldr/export/code/territories' 14 | # autoload :Timezones, 'cldr/export/code/timezones' 15 | # autoload :Units, 'cldr/export/code/units' 16 | # 17 | # class << self 18 | # def dir 19 | # @dir ||= File.expand_path('./vendor/cldr/common') 20 | # end 21 | # 22 | # def dir=(dir) 23 | # @dir = dir 24 | # end 25 | # 26 | # def locales 27 | # Dir["#{dir}/main/*.xml"].map { |path| path =~ /([\w_-]+)\.xml/ && $1 } 28 | # end 29 | # 30 | # def components 31 | # self.constants.sort - [:Base, :Export] 32 | # end 33 | # end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cldr/export/element.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | class Element 6 | class << self 7 | def chain(node) 8 | "/" + (node.ancestors.reverse.drop(1) + [node]).map { |ancestor| formatted_name_with_attrs(ancestor) }.join("/") 9 | end 10 | 11 | def inheritance_chain(node) 12 | "/" + (node.ancestors.reverse.drop(1) + [node]).map { |ancestor| formatted_name_with_distinguishing_attrs(ancestor) }.join("/") 13 | end 14 | 15 | private 16 | 17 | def formatted_name_with_attrs(node) 18 | attrs = node.attributes.values.sort_by(&:name) 19 | "#{node.name}#{attrs.map { |attribute| "[@#{attribute.name}=\"#{attribute.value}\"]" }.join("")}" 20 | end 21 | 22 | def formatted_name_with_distinguishing_attrs(node) 23 | distinguishing = Cldr::Ldml::ATTRIBUTES.fetch(node.name.to_sym, {}).select { |_name, attribute| attribute.distinguishing? }.keys 24 | attrs = node.attributes.values.sort_by(&:name).select { |attribute| distinguishing.include?(attribute.name.to_sym) } 25 | "#{node.name}#{attrs.map { |attribute| "[@#{attribute.name}=\"#{attribute.value}\"]" }.join("")}" 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/cldr/export/data/plurals/cldr_grammar.treetop: -------------------------------------------------------------------------------- 1 | # http://unicode.org/draft/reports/tr35/tr35.html#Language_Plural_Rules 2 | # 3 | # condition = and_condition ('or' and_condition)* 4 | # and_condition = relation ('and' relation)* 5 | # relation = is_relation | in_relation | within_relation | 'n' 6 | # is_relation = expr 'is' ('not')? value 7 | # in_relation = expr ('not')? 'in' range 8 | # within_relation = expr ('not')? 'within' range 9 | # expr = 'n' ('mod' value)? 10 | # value = digit+ 11 | # digit = 0|1|2|3|4|5|6|7|8|9 12 | # range = value'..'value 13 | 14 | grammar CldrPluralGrammar 15 | rule or_condition 16 | and_condition (" or " and_condition)? 17 | end 18 | 19 | rule and_condition 20 | relation (" and " relation)? 21 | end 22 | 23 | rule relation 24 | is_relation / in_relation / within_relation / "n" 25 | end 26 | 27 | rule is_relation 28 | expr " is " "not "? value 29 | end 30 | 31 | rule in_relation 32 | expr " not"? " in " range 33 | end 34 | 35 | rule within_relation 36 | expr " not"? " within " range 37 | end 38 | 39 | rule expr 40 | "n" (" mod " value)? 41 | end 42 | 43 | rule range 44 | value ".." value 45 | end 46 | 47 | rule value 48 | [0-9]* 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/cldr/export/yaml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yaml" 4 | 5 | module Cldr 6 | module Export 7 | class Yaml 8 | def export(locale, component, options = {}) 9 | data = Export.data(component, locale, options).to_h 10 | data = format(data, locale, component) 11 | unless data.empty? 12 | path = Export.path(locale, component, "yml") 13 | Export.write(path, data.to_yaml) 14 | yield(component, locale, path) if block_given? 15 | data 16 | end 17 | end 18 | 19 | private 20 | 21 | UNSORTED_COMPONENTS = [:Calendars, :Delimiters, :Lists].freeze 22 | 23 | def format(data, locale, component) 24 | data.deep_prune! 25 | unless data.empty? 26 | data = data.deep_stringify_keys if data.respond_to?(:deep_stringify_keys) 27 | if data.respond_to?(:deep_sort) 28 | sorted_data = data.deep_sort 29 | raise "#{component} data for #{locale} is not sorted." unless sorted_data.to_s == data.to_s || UNSORTED_COMPONENTS.include?(component) 30 | end 31 | 32 | DeepValidateKeys.validate(data, component) 33 | 34 | data = { locale.to_s => data } unless locale.nil? 35 | end 36 | data 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | ruby-version: ["3.2", "3.3", "3.4"] 17 | cldr-version: [41] 18 | 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | 23 | - name: Install libyaml 24 | run: sudo apt-get update -y && sudo apt-get install -y libyaml-dev 25 | 26 | - name: Set up Ruby ${{ matrix.ruby-version }} 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | bundler-cache: true 30 | ruby-version: ${{ matrix.ruby-version }} 31 | 32 | - name: Download the version of CLDR to test against 33 | run: bundle exec thor cldr:download --version=${{ matrix.cldr-version }} 34 | 35 | - name: Run tests 36 | run: bundle exec ruby test/all.rb 37 | 38 | - name: Export the data 39 | run: bundle exec thor cldr:export 40 | 41 | - name: Validate the exported data 42 | run: bundle exec thor cldr:validate 43 | -------------------------------------------------------------------------------- /lib/cldr/ldml/attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Ldml 5 | COMMENT_RE = /^\s*\s*$/ 6 | ATTLIST_RE = /]*)>/ 7 | LDML_FILE = "vendor/cldr/common/dtd/ldml.dtd" 8 | 9 | class Attributes < Hash 10 | def initialize 11 | super 12 | attributes.group_by(&:element_name).transform_values { |values| values.to_h { |attribute| [attribute.attribute_name, attribute] } }.each do |key, value| 13 | self[key] = value 14 | end 15 | end 16 | 17 | private 18 | 19 | def attributes 20 | @attributes ||= begin 21 | acc = [] 22 | results = [] 23 | File.open(LDML_FILE) do |f| 24 | f.each_line do |line| 25 | if !acc.empty? && line =~ COMMENT_RE 26 | acc << Regexp.last_match(1).to_sym 27 | elsif !acc.empty? 28 | results << Cldr::Ldml::Attribute.parse(acc.first, acc[1..]) 29 | acc = [] 30 | end 31 | 32 | if line =~ ATTLIST_RE 33 | acc = [Regexp.last_match(1).strip] 34 | end 35 | end 36 | end 37 | results << Cldr::Ldml::Attribute.parse(acc.first, acc[1..]) unless acc.empty? 38 | 39 | results 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cldr/export/data/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core_ext/string/underscore" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class Base < Hash 9 | attr_reader :locale 10 | 11 | def initialize(locale) 12 | super() 13 | @locale = locale 14 | end 15 | 16 | protected 17 | 18 | def alt?(node) # TODO: Move this into DataFile 19 | !node.attribute("alt").nil? 20 | end 21 | 22 | def name(node) 23 | node.name.underscore 24 | end 25 | 26 | def count(node) 27 | node.attribute("count").value 28 | end 29 | 30 | def select(*sources) 31 | doc.xpath(xpath(sources)) 32 | end 33 | 34 | def select_single(*sources) 35 | results = doc.xpath(xpath(sources)) 36 | raise "#{locale || ""}: Expected 1 result for `#{sources}`, got #{results.size}" if results.size > 1 37 | 38 | results.first 39 | end 40 | 41 | def xpath(sources) 42 | path = sources.map { |source| source.respond_to?(:path) ? source.path : source }.join("/") 43 | path =~ %r{^/?/ldml} ? path : "//ldml/#{path}" 44 | end 45 | 46 | def doc 47 | Cldr::Export::Data::RAW_DATA[locale] 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/export/element_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | 6 | class TestElement < Test::Unit::TestCase 7 | setup do 8 | directory = File.expand_path("./vendor/cldr/common") 9 | @data_set = Cldr::Export::FileBasedDataSet.new(directory: directory) 10 | @en_file = @data_set[:en] 11 | @root_file = @data_set[:root] 12 | end 13 | 14 | test "Sorts the attributes in element chain by name" do 15 | node = @en_file.xpath("//language[@type='en_GB'][@alt='short']").first 16 | assert_equal("/ldml/localeDisplayNames/languages/language[@alt=\"short\"][@type=\"en_GB\"]", Cldr::Export::Element.chain(node)) 17 | end 18 | 19 | test "Inheritance element chain only includes distinguishing attributes" do 20 | node = @root_file.xpath("//calendar[@type='hebrew']/months/monthContext[@type='format']/monthWidth[@type='abbreviated']/alias").first 21 | assert_equal("/ldml/dates/calendars/calendar[@type=\"hebrew\"]/months/monthContext[@type=\"format\"]/monthWidth[@type=\"abbreviated\"]/alias[@path=\"../monthWidth[@type='wide']\"][@source=\"locale\"]", Cldr::Export::Element.chain(node)) 22 | assert_equal("/ldml/dates/calendars/calendar[@type=\"hebrew\"]/months/monthContext[@type=\"format\"]/monthWidth[@type=\"abbreviated\"]/alias", Cldr::Export::Element.inheritance_chain(node)) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/core_ext/deep_stringify_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../lib/core_ext/hash/deep_stringify")) 6 | 7 | class TestDeepPrune < Test::Unit::TestCase 8 | test "deep_stringify affects both keys and values" do 9 | input = { 10 | foo: :bar, 11 | parent: { 12 | nested: :baz, 13 | }, 14 | } 15 | expected = { 16 | "foo" => "bar", 17 | "parent" => { 18 | "nested" => "baz", 19 | }, 20 | } 21 | assert_equal expected, input.deep_stringify 22 | end 23 | 24 | test "deep_stringify_keys doesn't affect values" do 25 | input = { 26 | foo: :bar, 27 | parent: { 28 | nested: :baz, 29 | }, 30 | } 31 | expected = { 32 | "foo" => :bar, 33 | "parent" => { 34 | "nested" => :baz, 35 | }, 36 | } 37 | assert_equal expected, input.deep_stringify_keys 38 | end 39 | 40 | test "deep_stringify_values doesn't affect keys" do 41 | input = { 42 | foo: :bar, 43 | parent: { 44 | nested: :baz, 45 | }, 46 | } 47 | expected = { 48 | foo: "bar", 49 | parent: { 50 | nested: "baz", 51 | }, 52 | } 53 | assert_equal expected, input.deep_stringify_values 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/export/data_set_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | 6 | class TestDataSet < Test::Unit::TestCase 7 | class DummyDataSet < Cldr::Export::DataSet 8 | private 9 | 10 | def compute(locale) 11 | locale == :missing ? nil : parent[locale] * 2 12 | end 13 | 14 | def locales_at_this_level 15 | (parent&.locales || []).map { |l| (l.to_s * 2).to_sym } 16 | end 17 | end 18 | 19 | test "Uses provided values in preference to parent values" do 20 | parent_data_set = DummyDataSet.new 21 | data_set = DummyDataSet.new(parent: parent_data_set) 22 | 23 | parent_data_set[:de] = "de from parent" 24 | data_set[:de] = "de" 25 | 26 | assert_equal("de", data_set[:de]) 27 | end 28 | 29 | test "Computes based on parent values" do 30 | parent_data_set = DummyDataSet.new 31 | parent_data_set[:de] = "de" 32 | 33 | data_set = DummyDataSet.new(parent: parent_data_set) 34 | assert_equal("dede", data_set[:de]) 35 | end 36 | 37 | test "locales includes the locales of parents" do 38 | parent_data_set = DummyDataSet.new 39 | parent_data_set[:de] = "de" 40 | parent_data_set[:en] = "de" 41 | 42 | data_set = DummyDataSet.new(parent: parent_data_set) 43 | parent_data_set[:es] = "es" 44 | 45 | assert_equal([:de, :dede, :en, :enen, :es, :eses], data_set.locales) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_autotest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def tests_for(filename) 4 | pattern = case filename 5 | when /gregorian\.rb/ 6 | "test/export/data/calendars_test.rb" 7 | when %r(/base.rb) 8 | dir = filename.gsub("/base.rb", "") 9 | base = File.basename(dir) 10 | dir = File.dirname(dir).gsub(%(lib/cldr), "") 11 | "test/#{dir}/{#{base},#{base}/**/*}_test.rb" 12 | when %r(lib/cldr/.*\.rb) 13 | "test/" + filename.gsub("lib/cldr/", "").gsub(/\.rb$/, "_test.rb") 14 | when %r(^test/.*_test\.rb) 15 | filename 16 | end 17 | pattern ? Dir[pattern.gsub("//", "/")] : [] 18 | end 19 | 20 | if $PROGRAM_NAME == __FILE__ 21 | require "test/unit" 22 | 23 | class TestAutotestMatching < Test::Unit::TestCase 24 | define_method test_default_mapping_for_library_files do 25 | assert tests_for("lib/cldr/format/date.rb").all? { |file| file =~ /date_test.rb/ } 26 | assert tests_for("lib/cldr/format/decimal/fraction.rb").all? { |file| file =~ /fraction_test.rb/ } 27 | assert tests_for("lib/cldr/format/decimal/base.rb").all? { |file| file =~ /decimal/ } 28 | end 29 | 30 | define_method test_mapping_for_gregorian_rb do 31 | assert_equal ["test/export/data/calendars_test.rb"], tests_for("lib/cldr/calendars/gregorian.rb") 32 | end 33 | 34 | define_method test_default_mapping_for_test_files do 35 | assert_equal ["test/export_test.rb"], tests_for("test/export_test.rb") 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cldr/export/data/currencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Currencies < Base 7 | def initialize(locale) 8 | super 9 | update(currencies: currencies) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def currencies 16 | select("numbers/currencies/*").each_with_object({}) do |node, result| 17 | currency = currency(node) 18 | result[node.attribute("type").value.to_sym] = currency unless currency.empty? 19 | end 20 | end 21 | 22 | def currency(node) 23 | data = select(node, "displayName").each_with_object({}) do |node, result| 24 | if node.attribute("count") 25 | count = node.attribute("count").value.to_sym 26 | result[count] = node.content 27 | else 28 | result[:name] = node.content 29 | end 30 | end 31 | 32 | symbols = select(node, "symbol") 33 | narrow_symbol = symbols.select { |child_node| child_node.attribute("alt")&.value == "narrow" }.first 34 | data[:narrow_symbol] = narrow_symbol.content unless narrow_symbol.nil? 35 | 36 | symbol = symbols.select { |child_node| child_node.attribute("alt").nil? }.first 37 | data[:symbol] = symbol.content unless symbol.nil? 38 | 39 | data 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cldr/export/data/segments_root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class SegmentsRoot < Base 7 | def initialize 8 | super(:root) 9 | update(segments: segmentations) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def segmentations 16 | doc.xpath("ldml/segmentations/segmentation").each_with_object({}) do |seg, ret| 17 | type = seg.attribute("type").value.underscore.to_sym 18 | ret[type] = segmentation(seg) 19 | end 20 | end 21 | 22 | def segmentation(node) 23 | { 24 | variables: variables(node), 25 | rules: rules(node), 26 | } 27 | end 28 | 29 | def variables(node) 30 | (node / "variables" / "variable").map do |variable| 31 | { 32 | id: cast_value(variable.attribute("id").value), 33 | value: variable.text, 34 | } 35 | end 36 | end 37 | 38 | def rules(node) 39 | (node / "segmentRules" / "rule").map do |rule| 40 | { 41 | id: cast_value(rule.attribute("id").value), 42 | value: rule.text, 43 | } 44 | end 45 | end 46 | 47 | def cast_value(value) 48 | if value =~ /\A[\d]+\z/ 49 | value.to_i 50 | elsif value =~ /\A[\d.]+\z/ 51 | value.to_f 52 | else 53 | value 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/cldr/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "parser/current" 4 | 5 | module Cldr 6 | class Validate 7 | class << self 8 | def validate(target) 9 | errors = Dir.glob("#{target}/**/*.yml").reject { |path| path.match?(%r{/transforms/}) }.flat_map do |path| 10 | validate_yaml_file(path) 11 | end 12 | errors += Dir.glob("#{target}/**/*.rb").flat_map do |path| 13 | validate_ruby_file(path) 14 | end 15 | errors.each do |error| 16 | puts(error.message) 17 | end 18 | exit(errors.empty? ? 0 : 1) 19 | end 20 | 21 | private 22 | 23 | def validate_yaml_file(path) 24 | errors = [] 25 | contents = YAML.load_file(path, permitted_classes: [Cldr::Export::Data::Transforms, DateTime, Symbol, Time]) 26 | flattened = contents # TODO: flatten contents 27 | flattened.each do |key, value| 28 | errors << StandardError.new("`#{key}` in `#{path}` is not a string") unless key.is_a?(String) 29 | errors << StandardError.new("Value for `#{key}` in `#{path}` is nil") if value.nil? 30 | errors << StandardError.new("Value for `#{key}` in `#{path}` is empty") if value.empty? 31 | end 32 | errors 33 | end 34 | 35 | def validate_ruby_file(path) 36 | errors = [] 37 | begin 38 | Parser::CurrentRuby.parse(File.read(path)) 39 | rescue Parser::SyntaxError 40 | errors << StandardError.new("`#{path}` fails to parse as Ruby code") 41 | end 42 | errors 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/cldr/export/data/metazones.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | require "date" 5 | 6 | module Cldr 7 | module Export 8 | module Data 9 | class Metazones < Hash 10 | def initialize 11 | super 12 | 13 | data_file = Cldr::Export::Data::RAW_DATA[nil] 14 | 15 | self[:timezones] = data_file.xpath("//metaZones/metazoneInfo/timezone").each_with_object({}) do |node, result| 16 | timezone = node.attr("type").to_sym 17 | result[timezone] = metazone(node.xpath("usesMetazone")) 18 | result[timezone].sort_by! { |zone| [zone["from"] ? zone["from"] : DateTime.new, zone["to"] ? zone["to"] : DateTime.now] } 19 | end 20 | self[:primaryzones] = data_file.xpath("//primaryZones/primaryZone").each_with_object({}) do |node, result| 21 | territory = node.attr("iso3166").to_sym 22 | result[territory] = node.content 23 | end 24 | 25 | deep_sort! 26 | end 27 | 28 | protected 29 | 30 | def metazone(nodes) 31 | nodes.each_with_object([]) do |node, result| 32 | mzone = node.attr("mzone") 33 | from = node.attr("from") 34 | to = node.attr("to") 35 | data = { "metazone" => mzone } 36 | data["from"] = parse_date(from) if from 37 | data["to"] = parse_date(to) if to 38 | result << data 39 | end 40 | end 41 | 42 | def parse_date(date) 43 | DateTime.strptime(date + " UTC", "%F %R %Z") 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/cldr/export/file_based_data_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | class FileBasedDataSet < DataSet 8 | attr_reader :directory 9 | 10 | def initialize(directory: nil, parent: nil) 11 | super(parent: parent) 12 | @directory = directory 13 | end 14 | 15 | private 16 | 17 | attr_reader :file_cache 18 | 19 | def compute(locale) 20 | merge_paths(paths(locale)) 21 | end 22 | 23 | def locales_at_this_level 24 | Dir["#{directory}/main/*.xml"].map { |path| path =~ /([\w_-]+)\.xml/ && Regexp.last_match(1) }.map { |l| Cldr::Export.to_i18n(l) } 25 | end 26 | 27 | def paths_by_root 28 | @paths_by_root ||= Dir[File.join(directory, "**", "*.xml")].sort.group_by { |path| Nokogiri::XML(File.read(path)).root.name } 29 | end 30 | 31 | def paths(locale) 32 | if locale 33 | Dir[File.join(directory, "*", "#{Cldr::Export.from_i18n(locale)}.xml")].sort & paths_by_root["ldml"] 34 | else 35 | paths_by_root["supplementalData"] 36 | end 37 | end 38 | 39 | def merge_paths(paths_to_merge) 40 | return Cldr::Export::DataFile.new(Nokogiri::XML("")) if paths_to_merge.empty? 41 | 42 | first = Cldr::Export::DataFile.parse(File.read(paths_to_merge.first)) 43 | rest = paths_to_merge[1..] 44 | rest.reduce(first) do |result, path| 45 | parsed = Cldr::Export::DataFile.parse(File.read(path)) 46 | result.merge!(parsed) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --exclude-limit 100000` 3 | # on 2021-12-13 22:45:37 UTC using RuboCop version 1.23.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: Include. 12 | # Include: **/*.gemspec 13 | Gemspec/RequireMFA: 14 | Exclude: 15 | - 'ruby-cldr.gemspec' 16 | 17 | # Offense count: 1 18 | Lint/DuplicateMethods: 19 | Exclude: 20 | - 'lib/cldr/export/data/plurals/grammar.rb' 21 | 22 | # Offense count: 2 23 | # Configuration parameters: CountBlocks. 24 | Metrics/BlockNesting: 25 | Max: 4 26 | 27 | # Offense count: 1 28 | # Configuration parameters: Max, CountKeywordArgs. 29 | Metrics/ParameterLists: 30 | MaxOptionalParameters: 5 31 | 32 | # Offense count: 5 33 | Style/ClassVars: 34 | Exclude: 35 | - 'lib/cldr.rb' 36 | - 'lib/cldr/export.rb' 37 | - 'lib/cldr/export/data/base.rb' 38 | - 'lib/cldr/export/data/plurals.rb' 39 | 40 | # Offense count: 4 41 | # Cop supports --auto-correct. 42 | # Configuration parameters: AllowCoercion. 43 | Style/DateTime: 44 | Exclude: 45 | - 'lib/cldr/export/data/metazones.rb' 46 | - 'test/export/data/metazones_test.rb' 47 | 48 | # Offense count: 2 49 | # Cop supports --auto-correct. 50 | # Configuration parameters: EnforcedStyle. 51 | # SupportedStyles: empty, nil, both 52 | Style/EmptyElse: 53 | Exclude: 54 | - 'lib/cldr/format/date.rb' 55 | -------------------------------------------------------------------------------- /lib/cldr/format/decimal/integer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Decimal 6 | class Integer < Base 7 | attr_reader :format, :separator, :groups 8 | 9 | def initialize(format, symbols = {}) 10 | super() 11 | 12 | format = format.split(".")[0] 13 | @format = prepare_format(format, symbols) 14 | @groups = parse_groups(format) 15 | @separator = symbols[:group] || "," 16 | end 17 | 18 | def apply(number, options = {}) 19 | format_groups(interpolate(format, number.to_i)) 20 | end 21 | 22 | def format_groups(string) 23 | return string if groups.empty? 24 | 25 | tokens = [] 26 | tokens << chop_group(string, groups.first) 27 | tokens << chop_group(string, groups.last) while string.length > groups.last 28 | tokens << string 29 | tokens.compact.reverse.join(separator) 30 | end 31 | 32 | def parse_groups(format) 33 | return [] unless (index = format.rindex(",")) 34 | 35 | rest = format[0, index] 36 | widths = [format.length - index - 1] 37 | widths << rest.length - rest.rindex(",") - 1 if rest.rindex(",") 38 | widths.compact.uniq 39 | end 40 | 41 | def chop_group(string, size) 42 | string.slice!(-size, size) if string.length > size 43 | end 44 | 45 | def prepare_format(format, symbols) 46 | signs = symbols.values_at(:plus_sign, :minus_sign) 47 | format.tr(",", "").tr("+-", signs.join) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/cldr/format/decimal/number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Decimal 6 | class Number 7 | attr_reader :prefix, :suffix, :integer_format, :fraction_format, :symbols 8 | 9 | DEFAULT_SYMBOLS = { group: ",", decimal: ".", plus_sign: "+", minus_sign: "-" } 10 | FORMAT_PATTERN = /([^0#,\.]*)([0#,\.]+)([^0#,\.]*)$/ 11 | 12 | def initialize(format, symbols = {}) 13 | @symbols = DEFAULT_SYMBOLS.merge(symbols) 14 | @prefix, @suffix, @integer_format, @fraction_format = *parse_format(format, symbols) 15 | end 16 | 17 | def apply(number, options = {}) 18 | int, fraction = parse_number(number, options) 19 | 20 | result = integer_format.apply(int, options) 21 | result << fraction_format.apply(fraction, options) if fraction 22 | prefix + result + suffix 23 | end 24 | 25 | protected 26 | 27 | def parse_format(format, symbols = {}) 28 | format =~ FORMAT_PATTERN 29 | prefix, suffix, int, fraction = Regexp.last_match(1).to_s, Regexp.last_match(3).to_s, *Regexp.last_match(2).split(".") 30 | [prefix, suffix, Integer.new(int, symbols), Fraction.new(fraction, symbols)] 31 | end 32 | 33 | def parse_number(number, options = {}) 34 | precision = options[:precision] || fraction_format.precision 35 | number = round_to(number, precision) 36 | number.abs.to_s.split(".") 37 | end 38 | 39 | def round_to(number, precision) 40 | factor = 10**precision 41 | (number * factor).round.to_f / factor 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/cldr/export/data/lists.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Lists < Base 7 | def initialize(locale) 8 | super 9 | update(lists: lists) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def lists 16 | select("listPatterns/listPattern").each_with_object({}) do |list_pattern, list_pattern_ret| 17 | pattern_type = if (attribute = list_pattern.attribute("type")) 18 | attribute.value.underscore.to_sym 19 | else 20 | :default 21 | end 22 | list_pattern_ret[pattern_type] = list_pattern(list_pattern) 23 | end 24 | end 25 | 26 | def list_pattern(list_pattern) 27 | aliased = select_single(list_pattern, "alias") 28 | if aliased 29 | xpath_to_key(aliased.attribute("path").value) 30 | else 31 | get_pattern_parts(list_pattern) 32 | end 33 | end 34 | 35 | def get_pattern_parts(list_pattern) 36 | select(list_pattern, "listPatternPart").each_with_object({}) do |part, part_ret| 37 | part_ret[part.attribute("type").value.to_sym] = part.content 38 | end 39 | end 40 | 41 | def xpath_to_key(xpath) 42 | return :"lists.default" if xpath == "../listPattern" 43 | 44 | match = xpath.match(%r{^\.\./listPattern\[@type='([^']*)'\]$}) 45 | raise StandardError, "Didn't find expected data in alias path attribute: #{xpath}" unless match 46 | 47 | type = match[1].underscore 48 | :"lists.#{type}" 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/export/data/units_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataUnits < Test::Unit::TestCase 7 | test "units" do 8 | units = { 9 | day: { one: "{0} Tag", other: "{0} Tage" }, 10 | week: { one: "{0} Woche", other: "{0} Wochen" }, 11 | month: { one: "{0} Monat", other: "{0} Monate" }, 12 | year: { one: "{0} Jahr", other: "{0} Jahre" }, 13 | hour: { one: "{0} Stunde", other: "{0} Stunden" }, 14 | minute: { one: "{0} Minute", other: "{0} Minuten" }, 15 | second: { one: "{0} Sekunde", other: "{0} Sekunden" }, 16 | } 17 | data = Cldr::Export::Data::Units.new(:de)[:units][:unit_length][:long] 18 | 19 | assert_operator data.keys.count, :>=, 46 20 | assert_equal units[:day], data[:duration_day] 21 | assert_equal units[:week], data[:duration_week] 22 | assert_equal units[:month], data[:duration_month] 23 | assert_equal units[:year], data[:duration_year] 24 | assert_equal units[:hour], data[:duration_hour] 25 | assert_equal units[:minute], data[:duration_minute] 26 | assert_equal units[:second], data[:duration_second] 27 | end 28 | 29 | test "Alias nodes are exported as paths to their targets" do 30 | data = Cldr::Export::Data::Units.new(:root) 31 | path = data.dig(:units, :unit_length, :short, :duration_week_person) 32 | assert_equal :"units.unit_length.short.duration_week", path 33 | 34 | duration = data.dig(*split_path_string(path)) 35 | assert_not_nil duration 36 | end 37 | 38 | private 39 | 40 | def split_path_string(path) 41 | path.to_s.split(".").map(&:to_sym) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/cldr/export/data/rbnf.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Export 6 | module Data 7 | class Rbnf < Base 8 | def initialize(*args) 9 | super 10 | update(rbnf: { grouping: rule_groups }) 11 | end 12 | 13 | private 14 | 15 | def rule_groups 16 | grouping_nodes = select("rbnf/rulesetGrouping") 17 | return {} if grouping_nodes.empty? 18 | 19 | grouping_nodes.map do |grouping_node| 20 | { 21 | type: grouping_node.attribute("type").value, 22 | ruleset: (grouping_node / "ruleset").map do |ruleset_node| 23 | rule_set(ruleset_node) 24 | end, 25 | } 26 | end 27 | end 28 | 29 | def rule_set(ruleset_node) 30 | attrs = { 31 | type: ruleset_node.attribute("type").value, 32 | rules: (ruleset_node / "rbnfrule").map do |rule_node| 33 | radix = if (radix_attr = rule_node.attribute("radix")) 34 | cast_value(radix_attr.value) 35 | end 36 | 37 | attrs = { 38 | value: cast_value(rule_node.attribute("value").value), 39 | rule: fix_rule(rule_node.text), 40 | } 41 | 42 | attrs[:radix] = radix if radix 43 | attrs 44 | end, 45 | } 46 | 47 | access = ruleset_node.attribute("access") 48 | attrs[:access] = access.value if access 49 | attrs 50 | end 51 | 52 | def cast_value(val) 53 | if val =~ /\A[\d]+\z/ 54 | val.to_i 55 | else 56 | val 57 | end 58 | end 59 | 60 | def fix_rule(rule) 61 | rule.gsub(/\A'/, "").gsub("←", "<").gsub("→", ">") 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/core_ext/deep_prune_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../lib/core_ext/hash/deep_prune")) 6 | 7 | class TestDeepPrune < Test::Unit::TestCase 8 | test "deep_prune! returns empty Hash for empty Hash" do 9 | assert_equal({}, {}.deep_prune!) 10 | end 11 | 12 | test "deep_prune! with default comparator calls deep_prune! recursively and removes empty hashes" do 13 | hash = { 14 | "nil_value" => nil, 15 | "empty_hash" => {}, 16 | "nested_empty_hash" => { 17 | "foo" => {}, 18 | }, 19 | "nested_non_empty_hash" => { 20 | "bar" => "baz", 21 | }, 22 | "empty_string" => "", 23 | "empty_array" => [], 24 | "integer" => 1, 25 | } 26 | 27 | expected = { 28 | "nil_value" => nil, 29 | "nested_non_empty_hash" => { 30 | "bar" => "baz", 31 | }, 32 | "empty_string" => "", 33 | "empty_array" => [], 34 | "integer" => 1, 35 | } 36 | assert_equal(expected, hash.deep_prune!) 37 | end 38 | 39 | test "deep_prune! can be called with other comparators to removes blank items" do 40 | blank = ->(v) { v.respond_to?(:empty?) ? !!v.empty? : !v } # Rails' Object#blank? 41 | 42 | hash = { 43 | "nil_value" => nil, 44 | "empty_hash" => {}, 45 | "empty_string" => "", 46 | "empty_array" => [], 47 | "integer" => 1, 48 | "nested_empty_hash" => { 49 | "foo" => {}, 50 | "nil_empty_value" => nil, 51 | "nested_empty_string" => "", 52 | "nested_empty_array" => [], 53 | }, 54 | "nested_non_empty_hash" => { 55 | "bar" => "baz", 56 | }, 57 | 58 | } 59 | 60 | expected = { 61 | "integer" => 1, 62 | "nested_non_empty_hash" => { 63 | "bar" => "baz", 64 | }, 65 | } 66 | assert_equal(expected, hash.deep_prune!(blank)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/draft_status_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) 5 | 6 | class TestExport < Test::Unit::TestCase 7 | test "statuses can be compared" do 8 | assert(Cldr::DraftStatus::UNCONFIRMED < Cldr::DraftStatus::PROVISIONAL) 9 | assert(Cldr::DraftStatus::PROVISIONAL < Cldr::DraftStatus::CONTRIBUTED) 10 | assert(Cldr::DraftStatus::CONTRIBUTED < Cldr::DraftStatus::APPROVED) 11 | end 12 | 13 | test "statuses can be looked up by name" do 14 | assert_equal(Cldr::DraftStatus::UNCONFIRMED, Cldr::DraftStatus.fetch("unconfirmed")) 15 | assert_equal(Cldr::DraftStatus::PROVISIONAL, Cldr::DraftStatus.fetch("provisional")) 16 | assert_equal(Cldr::DraftStatus::CONTRIBUTED, Cldr::DraftStatus.fetch("contributed")) 17 | assert_equal(Cldr::DraftStatus::APPROVED, Cldr::DraftStatus.fetch("approved")) 18 | end 19 | 20 | test "statuses can be looked up by symbolic name" do 21 | assert_equal(Cldr::DraftStatus::UNCONFIRMED, Cldr::DraftStatus.fetch(:unconfirmed)) 22 | assert_equal(Cldr::DraftStatus::PROVISIONAL, Cldr::DraftStatus.fetch(:provisional)) 23 | assert_equal(Cldr::DraftStatus::CONTRIBUTED, Cldr::DraftStatus.fetch(:contributed)) 24 | assert_equal(Cldr::DraftStatus::APPROVED, Cldr::DraftStatus.fetch(:approved)) 25 | end 26 | 27 | test "statuses can be looked up by themselves" do 28 | assert_equal(Cldr::DraftStatus::UNCONFIRMED, Cldr::DraftStatus.fetch(Cldr::DraftStatus::UNCONFIRMED)) 29 | assert_equal(Cldr::DraftStatus::PROVISIONAL, Cldr::DraftStatus.fetch(Cldr::DraftStatus::PROVISIONAL)) 30 | assert_equal(Cldr::DraftStatus::CONTRIBUTED, Cldr::DraftStatus.fetch(Cldr::DraftStatus::CONTRIBUTED)) 31 | assert_equal(Cldr::DraftStatus::APPROVED, Cldr::DraftStatus.fetch(Cldr::DraftStatus::APPROVED)) 32 | end 33 | 34 | test "invalid statuses are not found" do 35 | assert_raises(KeyError) { Cldr::DraftStatus.fetch(:invalid) } 36 | end 37 | 38 | test "statuses are their own class" do 39 | Cldr::DraftStatus::ALL.each do |status| 40 | assert_instance_of(Cldr::DraftStatus::Status, status) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/cldr/export/data/transforms.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Cldr 5 | module Export 6 | module Data 7 | class Transforms < Base 8 | attr_reader :transform_file 9 | 10 | def initialize(transform_file) 11 | super(nil) # no locale 12 | @transform_file = transform_file 13 | update(transforms: transforms) 14 | end 15 | 16 | protected 17 | 18 | def doc 19 | Cldr::Export::DataFile.parse(File.read(transform_file)) 20 | end 21 | 22 | private 23 | 24 | def transforms 25 | doc.xpath("supplementalData/transforms/transform").map do |transform_node| 26 | { 27 | source: transform_node.attribute("source").value, 28 | target: transform_node.attribute("target").value, 29 | variant: get_variant(transform_node), 30 | direction: transform_node.attribute("direction").value, 31 | rules: rules(transform_node), 32 | } 33 | end 34 | end 35 | 36 | def get_variant(node) 37 | node.attribute("variant")&.value 38 | end 39 | 40 | def rules(transform_node) 41 | rules = fix_rule_wrapping( 42 | doc.xpath("#{transform_node.path}/tRule").flat_map do |rule_node| 43 | fix_rule(rule_node.content).split("\n").map(&:strip) 44 | end, 45 | ) 46 | 47 | rules.reject do |rule| 48 | rule.strip.empty? || rule.strip.start_with?("#") 49 | end 50 | end 51 | 52 | def fix_rule_wrapping(rules) 53 | wrap = false 54 | 55 | rules.each_with_object([]) do |rule, ret| 56 | if wrap 57 | ret.last.sub!(/\\\z/, rule) 58 | else 59 | ret << rule 60 | end 61 | 62 | wrap = rule.end_with?("\\") 63 | end 64 | end 65 | 66 | def fix_rule(rule) 67 | rule 68 | .gsub("←", "<") 69 | .gsub("→", ">") 70 | .gsub("↔", "<>") 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/cldr/export/data/timezones.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # I probably don't really get timezones. 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class Timezones < Base 9 | def initialize(locale) 10 | super 11 | 12 | update( 13 | formats: formats, 14 | metazones: metazones, 15 | timezones: timezones, 16 | ) 17 | deep_sort! 18 | end 19 | 20 | private 21 | 22 | def formats 23 | @formats ||= select("dates/timeZoneNames/*").each_with_object({}) do |format, result| 24 | if format.name.end_with?("Format") 25 | underscored_name = format.name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase 26 | result[underscored_name] = format.text 27 | end 28 | end 29 | end 30 | 31 | def timezones 32 | @timezones ||= select("dates/timeZoneNames/zone").each_with_object({}) do |zone, result| 33 | type = zone.attr("type").to_sym 34 | result[type] = {} 35 | city = select_single(zone, "exemplarCity[not(@alt)]") 36 | result[type][:city] = city.content if city 37 | long = nodes_to_hash(zone.xpath("long/*")) 38 | result[type][:long] = long unless long.empty? 39 | short = nodes_to_hash(zone.xpath("short/*")) 40 | result[type][:short] = short unless short.empty? 41 | end 42 | end 43 | 44 | def metazones 45 | @metazones ||= select("dates/timeZoneNames/metazone").each_with_object({}) do |zone, result| 46 | type = zone.attr("type").to_sym 47 | result[type] = {} 48 | long = nodes_to_hash(zone.xpath("long/*")) 49 | result[type][:long] = long unless long.empty? 50 | short = nodes_to_hash(zone.xpath("short/*")) 51 | result[type][:short] = short unless short.empty? 52 | end 53 | end 54 | 55 | protected 56 | 57 | def nodes_to_hash(nodes) 58 | nodes.each_with_object({}) do |node, result| 59 | result[node.name.to_sym] = node.content 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/cldr/export/data/plural_rules.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | class PluralRules < Hash 9 | attr_reader :locale 10 | 11 | def initialize(locale) 12 | super() 13 | 14 | find_rules(locale).each_pair do |rule_type, rule_data| 15 | self[rule_type.to_sym] = rule_data.each_with_object({}) do |rule, ret| 16 | ret[rule.attributes["count"].text] = rule.text 17 | end 18 | end 19 | 20 | deep_sort! 21 | end 22 | 23 | private 24 | 25 | def sources 26 | @sources ||= ["plurals", "ordinals"].each_with_object({}) do |source_name, ret| 27 | ret[source_name] = Cldr::Export::DataFile.parse(File.read("#{Cldr::Export::Data::RAW_DATA.directory}/supplemental/#{source_name}.xml")) 28 | end 29 | end 30 | 31 | def find_rules(locale) 32 | locale = locale.to_s 33 | 34 | sources.each_with_object({}) do |(_file, source), ret| 35 | # try to find exact match, then fall back 36 | node = find_rules_for_exact_locale(locale, source) || 37 | find_rules_for_exact_locale(base_locale(locale), source) || 38 | find_rules_for_base_locale(locale, source) || 39 | find_rules_for_base_locale(base_locale(locale), source) 40 | 41 | if node 42 | name = (source / "plurals").first.attributes["type"].value 43 | ret[name] = node / "pluralRule" 44 | end 45 | end 46 | end 47 | 48 | def find_rules_for_exact_locale(locale, source) 49 | (source / "plurals/pluralRules").find do |node| 50 | node.attributes["locales"].text 51 | .split(" ").map(&:downcase) 52 | .include?(locale.downcase) 53 | end 54 | end 55 | 56 | def find_rules_for_base_locale(locale, source) 57 | (source / "plurals/pluralRules").find do |node| 58 | node.attributes["locales"].text 59 | .split(" ").map { |l| base_locale(l) } 60 | .include?(locale.downcase) 61 | end 62 | end 63 | 64 | def base_locale(locale) 65 | locale.split(/[_-]/).first 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/cldr/format/time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Time < Datetime::Base 6 | PATTERN = /a{1}|h{1,2}|H{1,2}|K{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|z{1,4}|Z{1,4}/ 7 | METHODS = { # ignoring u, l, g, j, A 8 | "a" => :period, 9 | "h" => :hour, 10 | "H" => :hour, 11 | "K" => :hour, 12 | "k" => :hour, 13 | "m" => :minute, 14 | "s" => :second, 15 | "S" => :second_fraction, 16 | "z" => :timezone, 17 | "Z" => :timezone, 18 | "v" => :timezone_generic_non_location, 19 | "V" => :timezone_metazone, 20 | } 21 | 22 | def period(time, pattern, length) 23 | calendar[:periods][:format][:wide][time.strftime("%p").downcase.to_sym] 24 | end 25 | 26 | def hour(time, pattern, length) 27 | hour = time.hour 28 | hour = case pattern[0, 1] 29 | when "h" # [1-12] 30 | if hour > 12 31 | hour - 12 32 | else 33 | (hour == 0 ? 12 : hour) 34 | end 35 | when "H" # [0-23] 36 | hour 37 | when "K" # [0-11] 38 | hour > 11 ? hour - 12 : hour 39 | when "k" # [1-24] 40 | hour == 0 ? 24 : hour 41 | end 42 | length == 1 ? hour.to_s : hour.to_s.rjust(length, "0") 43 | end 44 | 45 | def minute(time, pattern, length) 46 | length == 1 ? time.min.to_s : time.min.to_s.rjust(length, "0") 47 | end 48 | 49 | def second(time, pattern, length) 50 | length == 1 ? time.sec.to_s : time.sec.to_s.rjust(length, "0") 51 | end 52 | 53 | def second_fraction(time, pattern, length) 54 | raise "can not use the S format with more than 6 digits" if length > 6 55 | 56 | (time.usec.to_f / 10**(6 - length)).round.to_s.rjust(length, "0") 57 | end 58 | 59 | def timezone(time, pattern, length) 60 | case length 61 | when 1..3 62 | time.zone 63 | else 64 | raise NotImplementedError, 'not yet implemented (requires timezone translation data")' 65 | end 66 | end 67 | 68 | def timezone_generic_non_location(time, pattern, length) 69 | raise NotImplementedError, 'not yet implemented (requires timezone translation data")' 70 | end 71 | 72 | def round_to(number, precision) 73 | factor = 10**precision 74 | (number * factor).round.to_f / factor 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/export/file_based_data_set_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | 6 | class TestFileBasedDataSet < Test::Unit::TestCase 7 | test "#paths finds all the language-dependent data files" do 8 | directory = File.expand_path("./vendor/cldr/common") 9 | data_set = Cldr::Export::FileBasedDataSet.new(directory: directory) 10 | 11 | expected = [ 12 | "annotations/af.xml", 13 | "annotationsDerived/af.xml", 14 | "casing/af.xml", 15 | "collation/af.xml", 16 | "main/af.xml", 17 | "rbnf/af.xml", 18 | "subdivisions/af.xml", 19 | ].map { |f| File.join(directory, f) } 20 | assert_equal expected, data_set.send(:paths, :af) 21 | end 22 | 23 | test "#paths finds all the supplemental data files" do 24 | directory = File.expand_path("./vendor/cldr/common") 25 | data_set = Cldr::Export::FileBasedDataSet.new(directory: directory) 26 | 27 | expected_non_transform_files = [ 28 | "supplemental-temp/coverageLevels2.xml", 29 | "supplemental/attributeValueValidity.xml", 30 | "supplemental/characters.xml", 31 | "supplemental/coverageLevels.xml", 32 | "supplemental/dayPeriods.xml", 33 | "supplemental/genderList.xml", 34 | "supplemental/grammaticalFeatures.xml", 35 | "supplemental/languageGroup.xml", 36 | "supplemental/languageInfo.xml", 37 | "supplemental/likelySubtags.xml", 38 | "supplemental/metaZones.xml", 39 | "supplemental/numberingSystems.xml", 40 | "supplemental/ordinals.xml", 41 | "supplemental/pluralRanges.xml", 42 | "supplemental/plurals.xml", 43 | "supplemental/rgScope.xml", 44 | "supplemental/subdivisions.xml", 45 | "supplemental/supplementalData.xml", 46 | "supplemental/supplementalMetadata.xml", 47 | "supplemental/units.xml", 48 | "supplemental/windowsZones.xml", 49 | "validity/currency.xml", 50 | "validity/language.xml", 51 | "validity/region.xml", 52 | "validity/script.xml", 53 | "validity/subdivision.xml", 54 | "validity/unit.xml", 55 | "validity/variant.xml", 56 | ].map { |f| File.join(directory, f) } 57 | 58 | supplemental_data_paths = data_set.send(:paths, nil) 59 | 60 | assert_equal expected_non_transform_files, supplemental_data_paths.reject { |p| p.include?("transforms/") } 61 | assert_not_empty supplemental_data_paths.select { |p| p.include?("transforms/") } 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/cldr/export/data/fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Fields < Base 7 | def initialize(locale) 8 | super 9 | update(fields: fields) 10 | deep_sort! 11 | end 12 | 13 | private 14 | 15 | def fields 16 | select("dates/fields/field").each_with_object({}) do |field_node, ret| 17 | type = field_node.attribute("type").value.underscore.to_sym 18 | ret[type] = field(field_node) 19 | end 20 | end 21 | 22 | def field(field_node) 23 | aliased = select_single(field_node, "alias") 24 | return xpath_to_key(aliased.attribute("path").value) if aliased 25 | 26 | result = {} 27 | 28 | unless (display_name = (field_node / "displayName").text).empty? 29 | result[:display_name] = display_name 30 | end 31 | 32 | unless (forms = relative_forms(field_node)).empty? 33 | result[:relative] = forms 34 | end 35 | 36 | unless (forms = relative_time_forms(field_node)).empty? 37 | result[:relative_time] = forms 38 | end 39 | 40 | result 41 | end 42 | 43 | def relative_forms(field_node) 44 | (field_node / "relative").each_with_object({}) do |relative_node, ret| 45 | type = relative_node.attribute("type").value.to_i 46 | ret[type] = relative_node.text 47 | end 48 | end 49 | 50 | def relative_time_forms(field_node) 51 | (field_node / "relativeTime").each_with_object({}) do |relative_time_node, ret| 52 | type = relative_time_node.attribute("type").value 53 | ret[type] = relative_time_patterns(relative_time_node) 54 | end 55 | end 56 | 57 | def relative_time_patterns(relative_time_node) 58 | (relative_time_node / "relativeTimePattern").each_with_object({}) do |relative_time_pattern_node, ret| 59 | count = relative_time_pattern_node.attribute("count").value 60 | ret[count] = relative_time_pattern_node.text 61 | end 62 | end 63 | 64 | def xpath_to_key(xpath) 65 | match = xpath.match(%r{^\.\./field\[@type='([^']*)'\]$}) 66 | raise StandardError, "Didn't find expected data in alias path attribute: #{xpath}" unless match 67 | 68 | type = match[1].underscore 69 | :"fields.#{type}" 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/export/deep_validate_keys_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) 5 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../lib/cldr/export/deep_validate_keys")) 6 | 7 | class TestDeepValidateKeys < Test::Unit::TestCase 8 | test "#paths_match? with empty pattern matches only empty key" do 9 | assert DeepValidateKeys.send(:"paths_match?", [], []) 10 | refute DeepValidateKeys.send(:"paths_match?", [], ["foo", "bar", "baz", "qux"]) 11 | end 12 | 13 | test "#paths_match? matches with exact match" do 14 | assert DeepValidateKeys.send(:"paths_match?", ["foo", "bar"], ["foo", "bar"]) 15 | 16 | refute DeepValidateKeys.send(:"paths_match?", ["foo"], ["foo", "bar", "baz", "qux"]) 17 | refute DeepValidateKeys.send(:"paths_match?", ["foo"], ["bar", "baz", "qux"]) 18 | refute DeepValidateKeys.send(:"paths_match?", ["foo", "bar"], ["foo"]) 19 | refute DeepValidateKeys.send(:"paths_match?", ["foo", "baz"], ["foo", "bar", "baz", "qux"]) 20 | end 21 | 22 | test "#paths_match? with . matches single element" do 23 | assert DeepValidateKeys.send(:"paths_match?", ["foo", ".", "baz"], ["foo", "bar", "baz"]) 24 | assert DeepValidateKeys.send(:"paths_match?", ["foo", ".", ".", "qux"], ["foo", "bar", "baz", "qux"]) 25 | assert DeepValidateKeys.send(:"paths_match?", ["."], ["foo"]) 26 | assert DeepValidateKeys.send(:"paths_match?", [".", "bar"], ["foo", "bar"]) 27 | 28 | refute DeepValidateKeys.send(:"paths_match?", ["."], []) 29 | end 30 | 31 | test "#paths_match? with trailing * matches anything after the star" do 32 | assert DeepValidateKeys.send(:"paths_match?", ["foo", "*"], ["foo", "bar", "baz", "qux"]) 33 | assert DeepValidateKeys.send(:"paths_match?", ["*"], []) 34 | assert DeepValidateKeys.send(:"paths_match?", ["*"], ["foo", "bar", "baz", "qux"]) 35 | end 36 | 37 | test "#paths_match? with * greedy matches up to the last match of the next element" do 38 | assert DeepValidateKeys.send(:"paths_match?", ["foo", "*", "baz", "quxx"], ["foo", "bar", "baz", "qux", "baz", "quxx"]) 39 | 40 | refute DeepValidateKeys.send(:"paths_match?", ["foo", "*", "baz", "quxx"], ["foo", "bar", "baz", "qux"]) 41 | end 42 | 43 | test "#paths_match? raise when given a pattern with multiple *" do 44 | exc = assert_raises(NotImplementedError) do 45 | DeepValidateKeys.send(:"paths_match?", ["foo", "*", "baz", "*"], ["foo", "bar", "baz", "qux", "baz", "quxx"]) 46 | end 47 | assert_equal "Multiple * in pattern is unsupported", exc.message 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/cldr/export/data/units.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Units < Base 7 | def initialize(locale) 8 | super 9 | update( 10 | units: { 11 | duration_unit: duration_unit, 12 | unit_length: unit_length, 13 | }, 14 | ) 15 | deep_sort! 16 | end 17 | 18 | private 19 | 20 | def unit_length 21 | select("units/unitLength").each_with_object({}) do |node, result| 22 | result[node.attribute("type").value.underscore.to_sym] = units(node) 23 | end 24 | end 25 | 26 | def units(node) 27 | aliased = select_single(node, "alias") 28 | return units_xpath_to_key(aliased.attribute("path").value) if aliased 29 | 30 | node.xpath("unit").each_with_object({}) do |node, result| 31 | result[node.attribute("type").value.underscore.to_sym] = unit(node) 32 | end 33 | end 34 | 35 | def unit(node) 36 | aliased = select_single(node, "alias") 37 | return unit_xpath_to_key(aliased.attribute("path").value) if aliased 38 | 39 | node.xpath("unitPattern").each_with_object({}) do |node, result| 40 | # Ignore cases for now. We don't have a way to expose them yet. 41 | # TODO: https://github.com/ruby-i18n/ruby-cldr/issues/67 42 | next result if node.attribute("case") 43 | 44 | count = node.attribute("count") ? node.attribute("count").value.to_sym : :one 45 | result[count] = node.content 46 | end 47 | end 48 | 49 | def duration_unit 50 | select("units/durationUnit").each_with_object({}) do |node, result| 51 | result[node.attribute("type").value.underscore.to_sym] = node.xpath("durationUnitPattern").first.content 52 | end 53 | end 54 | 55 | def units_xpath_to_key(xpath) 56 | match = xpath.match(%r{^\.\./unitLength\[@type='([^']*)'\]$}) 57 | raise StandardError, "Didn't find expected data in alias path attribute: #{xpath}" unless match 58 | 59 | type = match[1].underscore 60 | :"units.unit_length.#{type}" 61 | end 62 | 63 | def unit_xpath_to_key(xpath) 64 | match = xpath.match(%r{^\.\./unit\[@type='([^']*)'\]$}) 65 | raise StandardError, "Didn't find expected data in alias path attribute: #{xpath}" unless match 66 | 67 | type = match[1].underscore 68 | :"units.unit_length.short.#{type}" 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/export_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require "yaml" 5 | require "fileutils" 6 | 7 | require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) 8 | 9 | class TestExport < Test::Unit::TestCase 10 | def setup 11 | Cldr::Export.base_path = tmp_dir 12 | Cldr::Export.minimum_draft_status = Cldr::DraftStatus::CONTRIBUTED 13 | begin 14 | FileUtils.mkdir_p(tmp_dir) 15 | rescue 16 | nil 17 | end 18 | end 19 | 20 | def teardown 21 | FileUtils.rm_r(tmp_dir) 22 | end 23 | 24 | def tmp_dir 25 | File.expand_path(File.dirname(__FILE__) + "/tmp") 26 | end 27 | 28 | test "passing the merge option generates and merge data for all fallback locales" do 29 | data = Cldr::Export.data(:Numbers, :"de-AT") 30 | assert !data[:numbers][:latn][:formats][:nan] 31 | 32 | data = Cldr::Export.data(:Numbers, :"de-AT", merge: true) 33 | assert_equal "NaN", data[:numbers][:latn][:symbols][:nan] 34 | end 35 | 36 | test "passing the merge option generates and merges Plurals data from fallback locales" do 37 | data = Cldr::Export.data(:Plurals, :"af-NA") 38 | assert_nil(data) 39 | 40 | data = Cldr::Export.data(:Plurals, :"af-NA", merge: true) 41 | assert_equal([:"af-NA"], data.keys) 42 | end 43 | 44 | test "the merge option respects parentLocales" do 45 | data = Cldr::Export.data(:Calendars, :"en-GB", merge: true) 46 | assert_equal "dd/MM/y", data[:calendars][:gregorian][:additional_formats]["yMd"] 47 | end 48 | 49 | test "exports data to files" do 50 | Cldr::Export.export(locales: [:de], components: [:Calendars]) 51 | assert File.exist?(Cldr::Export.path("de", "calendars", "yml")) 52 | end 53 | 54 | test "exported data starts with the locale at top level" do 55 | Cldr::Export.export(locales: [:de], components: [:Calendars]) 56 | data = {} 57 | File.open(Cldr::Export.path("de", "calendars", "yml")) do |f| 58 | data = YAML.load(f) 59 | end 60 | assert data["de"] 61 | end 62 | 63 | test "#locales_to_merge does not fall back to English (unless the locale is English based)" do 64 | assert_equal [:ko, :root], Cldr::Export.locales_to_merge(:ko, "numbers", merge: true) 65 | assert_equal [:"pt-BR", :pt, :root], Cldr::Export.locales_to_merge(:"pt-BR", "numbers", merge: true) 66 | assert_equal [:"en-GB", :"en-001", :en, :root], Cldr::Export.locales_to_merge(:"en-GB", "numbers", merge: true) 67 | end 68 | 69 | test "#locales_to_merge does not fall back if :merge option is false" do 70 | assert_equal [:"pt-BR"], Cldr::Export.locales_to_merge(:"pt-BR", "numbers", merge: false) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/cldr/export/deep_validate_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeepValidateKeys 4 | class << self 5 | # Keys must be in snake_case, unless specifically exempted. 6 | def validate(hash, component, key_path = []) 7 | hash.each do |key, value| 8 | full_path = key_path + [key] 9 | raise ArgumentError, "Invalid key: #{full_path} in #{component}" unless key.to_s == key.to_s.underscore || 10 | SNAKE_CASE_EXCEPTIONS.fetch(component, []).any? { |exception| paths_match?(exception, full_path) } 11 | 12 | validate(value, component, full_path) if value.is_a?(Hash) 13 | end 14 | end 15 | 16 | private 17 | 18 | SNAKE_CASE_EXCEPTIONS = { 19 | Aliases: [ 20 | ["aliases", "language", "."], 21 | ["aliases", "territory", "."], 22 | ], 23 | Calendars: [["calendars", "gregorian", "additional_formats", "."]], 24 | Currencies: [["currencies", "."]], 25 | CountryCodes: [["country_codes", "."]], 26 | CurrencyDigitsAndRounding: [["."]], 27 | Fields: [["*", "relative", "."]], 28 | Languages: [["languages", "."]], 29 | LikelySubtags: [["subtags", "."]], 30 | Metazones: [ 31 | ["primaryzones", "."], 32 | ["timezones", "."], 33 | ], 34 | RegionCurrencies: [["region_currencies", "."]], 35 | ParentLocales: [["."]], 36 | Subdivisions: [["subdivisions", "."]], 37 | Territories: [["territories", "."]], 38 | TerritoriesContainment: [["territories", "."]], 39 | Timezones: [ 40 | ["metazones", "."], 41 | ["timezones", "."], 42 | ], 43 | WindowsZones: [ 44 | ["."], 45 | [".", "."], 46 | ], 47 | } 48 | 49 | def paths_match?(pattern, key) 50 | raise NotImplementedError, "Multiple * in pattern is unsupported" if pattern.count { |element| element == "*" } > 1 51 | 52 | pattern_index = 0 53 | key_index = 0 54 | 55 | while pattern_index < pattern.length 56 | if pattern[pattern_index] == "*" 57 | pattern_index += 1 58 | return true if pattern_index == pattern.length # "*" at the end of a pattern matches everything 59 | 60 | last_match = key.rindex(pattern[pattern_index]) # "*" is greedy, so we need to find the last match 61 | return false if last_match.nil? 62 | 63 | key_index = last_match 64 | elsif !(pattern[pattern_index] == key[key_index] || (pattern[pattern_index] == "." && key_index < key.length)) 65 | return false 66 | else 67 | pattern_index += 1 68 | key_index += 1 69 | end 70 | end 71 | 72 | return false unless key_index == key.length 73 | 74 | true 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/export/data/territories_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataTerritories < Test::Unit::TestCase 7 | test "territories :de" do 8 | # rubocop:disable Layout/MultilineArrayLineBreaks 9 | codes = [ # rubocop:disable Metrics/CollectionLiteralLength 10 | :"001", :"002", :"003", :"005", :"009", :"011", :"013", :"014", 11 | :"015", :"017", :"018", :"019", :"021", :"029", :"030", :"034", 12 | :"035", :"039", :"053", :"054", :"057", :"061", :"142", :"143", 13 | :"145", :"150", :"151", :"154", :"155", :"202", :"419", :AC, :AD, 14 | :AE, :AF, :AG, :AI, :AL, :AM, :AO, :AQ, :AR, :AS, :AT, :AU, :AW, 15 | :AX, :AZ, :BA, :BB, :BD, :BE, :BF, :BG, :BH, :BI, :BJ, :BL, :BM, 16 | :BN, :BO, :BQ, :BR, :BS, :BT, :BV, :BW, :BY, :BZ, :CA, :CC, :CD, 17 | :CF, :CG, :CH, :CI, :CK, :CL, :CM, :CN, :CO, :CP, :CR, :CU, :CV, 18 | :CW, :CX, :CY, :CZ, :DE, :DG, :DJ, :DK, :DM, :DO, :DZ, :EA, :EC, 19 | :EE, :EG, :EH, :ER, :ES, :ET, :EU, :FI, :FJ, :FK, :FM, :FO, :FR, 20 | :GA, :GB, :GD, :GE, :GF, :GG, :GH, :GI, :GL, :GM, :GN, :GP, :GQ, 21 | :GR, :GS, :GT, :GU, :GW, :GY, :HK, :HM, :HN, :HR, :HT, :HU, :IC, 22 | :ID, :IE, :IL, :IM, :IN, :IO, :IQ, :IR, :IS, :IT, :JE, :JM, :JO, 23 | :JP, :KE, :KG, :KH, :KI, :KM, :KN, :KP, :KR, :KW, :KY, :KZ, :LA, 24 | :LB, :LC, :LI, :LK, :LR, :LS, :LT, :LU, :LV, :LY, :MA, :MC, :MD, 25 | :ME, :MF, :MG, :MH, :MK, :ML, :MM, :MN, :MO, :MP, :MQ, :MR, :MS, 26 | :MT, :MU, :MV, :MW, :MX, :MY, :MZ, :NA, :NC, :NE, :NF, :NG, :NI, 27 | :NL, :NO, :NP, :NR, :NU, :NZ, :OM, :PA, :PE, :PF, :PG, :PH, :PK, 28 | :PL, :PM, :PN, :PR, :PS, :PT, :PW, :PY, :QA, :QO, :RE, :RO, :RS, 29 | :RU, :RW, :SA, :SB, :SC, :SD, :SE, :SG, :SH, :SI, :SJ, :SK, :SL, 30 | :SM, :SN, :SO, :SR, :SS, :ST, :SV, :SX, :SY, :SZ, :TA, :TC, :TD, 31 | :TF, :TG, :TH, :TJ, :TK, :TL, :TM, :TN, :TO, :TR, :TT, :TV, :TW, 32 | :TZ, :UA, :UG, :UN, :UM, :US, :UY, :UZ, :VA, :VC, :VE, :VG, :VI, 33 | :VN, :VU, :WF, :WS, :XA, :XB, :XK, :YE, :YT, :ZA, :ZM, :ZW, :ZZ, 34 | :EZ, 35 | ] 36 | # rubocop:enable Layout/MultilineArrayLineBreaks 37 | 38 | territories = Cldr::Export::Data::Territories.new(:de)[:territories] 39 | assert_empty codes - territories.keys, "Unexpected missing territories" 40 | assert_empty territories.keys - codes, "Unexpected extra territories" 41 | assert_equal("Deutschland", territories[:DE]) 42 | end 43 | 44 | test "territories does not overwrite long form with the short one" do 45 | territories = Cldr::Export::Data::Territories.new(:en)[:territories] 46 | 47 | assert_equal "United States", territories[:US] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/format/decimal/integer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 4 | 5 | class TestCldrDecimalIntegerFormatWithInteger < Test::Unit::TestCase 6 | test "interpolates a number" do 7 | assert_equal "123", Cldr::Format::Decimal::Integer.new("###").apply(123) 8 | end 9 | 10 | test "interpolates a number on the right side" do 11 | assert_equal "0123", Cldr::Format::Decimal::Integer.new("0###").apply(123) 12 | end 13 | 14 | test "strips optional digits" do 15 | assert_equal "123", Cldr::Format::Decimal::Integer.new("######").apply(123) 16 | end 17 | 18 | test "single group" do 19 | assert_equal "1,23", Cldr::Format::Decimal::Integer.new("#,##").apply(123) 20 | end 21 | 22 | test "multiple groups with a primary group size" do 23 | assert_equal "1,23,45,67,89", Cldr::Format::Decimal::Integer.new("#,##").apply(123456789) 24 | end 25 | 26 | test "multiple groups with a primary and secondary group size" do 27 | assert_equal "12,34,56,789", Cldr::Format::Decimal::Integer.new("#,##,##0").apply(123456789) 28 | end 29 | 30 | test "does not group when no digits left of the grouping position" do 31 | assert_equal "123", Cldr::Format::Decimal::Integer.new("#,###").apply(123) 32 | end 33 | 34 | test "does not include a fraction for a float" do 35 | assert_equal "123", Cldr::Format::Decimal::Integer.new("###").apply(123.45) 36 | end 37 | 38 | test "does not round when given a float" do 39 | assert_equal "123", Cldr::Format::Decimal::Integer.new("###").apply(123.55) 40 | end 41 | 42 | test "does not round with :precision => 1" do 43 | assert_equal "123", Cldr::Format::Decimal::Integer.new("###").apply(123.55, precision: 1) 44 | end 45 | 46 | test "ignores a fraction part given in the format string" do 47 | assert_equal "1,234", Cldr::Format::Decimal::Integer.new("#,##0.##").apply(1234.567) 48 | end 49 | 50 | test "cldr example #,##0.## => 1 234" do 51 | assert_equal "1,234", Cldr::Format::Decimal::Integer.new("#,##0.##").apply(1234.567) 52 | end 53 | 54 | test "cldr example #,##0.### => 1 234" do 55 | assert_equal "1 234", Cldr::Format::Decimal::Integer.new("#,##0.###", group: " ").apply(1234.567) 56 | end 57 | 58 | test "cldr example ###0.##### => 1234" do 59 | assert_equal "1234", Cldr::Format::Decimal::Integer.new("###0.#####", group: " ").apply(1234.567) 60 | end 61 | 62 | test "cldr example ###0.0000# => 1234" do 63 | assert_equal "1234", Cldr::Format::Decimal::Integer.new("###0.0000#", group: " ").apply(1234.567) 64 | end 65 | 66 | test "cldr example 00000.0000 => 01234" do 67 | assert_equal "01234", Cldr::Format::Decimal::Integer.new("00000.0000", group: " ").apply(1234.567) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/core_ext/hash/deep_sort.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copied from https://github.com/mcrossen/deepsort/blob/786fe3dd35980f028c0842797d25b27e53cd95f8/lib/deepsort.rb 4 | # MIT licensed 5 | 6 | # ----- 7 | # 8 | # MIT License 9 | # 10 | # Copyright (c) 2016 Mark Crossen 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documentation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furnished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in all 20 | # copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | # SOFTWARE. 29 | # 30 | # ----- 31 | 32 | module DeepSort 33 | module DeepSortHash 34 | def deep_sort(options = {}) 35 | deep_sort_by(options) { |obj| obj } 36 | end 37 | 38 | def deep_sort!(options = {}) 39 | deep_sort_by!(options) { |obj| obj } 40 | end 41 | 42 | def deep_sort_by(options = {}, &block) 43 | hash = map do |key, value| 44 | [ 45 | if key.respond_to?(:deep_sort_by) 46 | key.deep_sort_by(options, &block) 47 | else 48 | key 49 | end, 50 | 51 | if value.respond_to?(:deep_sort_by) 52 | value.deep_sort_by(options, &block) 53 | else 54 | value 55 | end, 56 | ] 57 | end 58 | 59 | Hash[options[:hash] == false ? hash : hash.sort_by(&block)] 60 | end 61 | 62 | def deep_sort_by!(options = {}, &block) 63 | hash = map do |key, value| 64 | [ 65 | if key.respond_to?(:deep_sort_by!) 66 | key.deep_sort_by!(options, &block) 67 | else 68 | key 69 | end, 70 | 71 | if value.respond_to?(:deep_sort_by!) 72 | value.deep_sort_by!(options, &block) 73 | else 74 | value 75 | end, 76 | ] 77 | end 78 | replace(Hash[options[:hash] == false ? hash : hash.sort_by!(&block)]) 79 | end 80 | 81 | # comparison for hashes is ill-defined. this performs array or string comparison if the normal comparison fails. 82 | def <=>(other) 83 | super || to_a <=> other.to_a || to_s <=> other.to_s 84 | end 85 | end 86 | end 87 | 88 | Hash.send(:include, DeepSort::DeepSortHash) 89 | -------------------------------------------------------------------------------- /lib/cldr/export/data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core_ext/string/camelize" 4 | 5 | module Cldr 6 | module Export 7 | module Data 8 | autoload :Aliases, "cldr/export/data/aliases" 9 | autoload :Base, "cldr/export/data/base" 10 | autoload :Calendars, "cldr/export/data/calendars" 11 | autoload :Characters, "cldr/export/data/characters" 12 | autoload :ContextTransforms, "cldr/export/data/context_transforms" 13 | autoload :CountryCodes, "cldr/export/data/country_codes" 14 | autoload :Currencies, "cldr/export/data/currencies" 15 | autoload :CurrencyDigitsAndRounding, "cldr/export/data/currency_digits_and_rounding" 16 | autoload :Delimiters, "cldr/export/data/delimiters" 17 | autoload :Fields, "cldr/export/data/fields" 18 | autoload :Languages, "cldr/export/data/languages" 19 | autoload :Layout, "cldr/export/data/layout" 20 | autoload :LikelySubtags, "cldr/export/data/likely_subtags" 21 | autoload :Lists, "cldr/export/data/lists" 22 | autoload :LocaleDisplayPattern, "cldr/export/data/locale_display_pattern" 23 | autoload :Metazones, "cldr/export/data/metazones" 24 | autoload :NumberingSystems, "cldr/export/data/numbering_systems" 25 | autoload :Numbers, "cldr/export/data/numbers" 26 | autoload :ParentLocales, "cldr/export/data/parent_locales" 27 | autoload :Plurals, "cldr/export/data/plurals" 28 | autoload :PluralRules, "cldr/export/data/plural_rules" 29 | autoload :Rbnf, "cldr/export/data/rbnf" 30 | autoload :RbnfRoot, "cldr/export/data/rbnf_root" 31 | autoload :RegionCurrencies, "cldr/export/data/region_currencies" 32 | autoload :RegionValidity, "cldr/export/data/region_validity" 33 | autoload :SegmentsRoot, "cldr/export/data/segments_root" 34 | autoload :Subdivisions, "cldr/export/data/subdivisions" 35 | autoload :Territories, "cldr/export/data/territories" 36 | autoload :TerritoriesContainment, "cldr/export/data/territories_containment" 37 | autoload :Timezones, "cldr/export/data/timezones" 38 | autoload :Units, "cldr/export/data/units" 39 | autoload :Variables, "cldr/export/data/variables" 40 | autoload :WeekData, "cldr/export/data/week_data" 41 | autoload :WindowsZones, "cldr/export/data/windows_zones" 42 | autoload :Transforms, "cldr/export/data/transforms" 43 | 44 | RAW_DATA = Cldr::Export::FileBasedDataSet.new(directory: File.expand_path("./vendor/cldr/common")) 45 | 46 | class << self 47 | def components 48 | constants.sort - [:Base, :Export, :RAW_DATA] 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/export/data/currencies_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrCurrencies < Test::Unit::TestCase 7 | test "currencies :de" do 8 | # rubocop:disable Layout/MultilineArrayLineBreaks 9 | codes = [ # rubocop:disable Metrics/CollectionLiteralLength 10 | :ADP, :AED, :AFA, :AFN, :ALL, :AMD, :ANG, :AOA, :AOK, :AON, :AOR, 11 | :ARA, :ARP, :ARS, :ATS, :AUD, :AWG, :AZM, :AZN, :BAD, :BAM, :BBD, 12 | :BDT, :BEC, :BEF, :BEL, :BGL, :BGN, :BHD, :BIF, :BMD, :BND, :BOB, 13 | :BOP, :BOV, :BRB, :BRC, :BRE, :BRL, :BRN, :BRR, :BRZ, :BSD, :BTN, 14 | :BUK, :BWP, :BYB, :BYR, :BZD, :CAD, :CDF, :CHE, :CHF, :CHW, :CLF, 15 | :CLP, :CNY, :COP, :CRC, :CSD, :CSK, :CUC, :CUP, :CVE, :CYP, :CZK, 16 | :DDM, :DEM, :DJF, :DKK, :DOP, :DZD, :ECS, :ECV, :EEK, :EGP, :ERN, 17 | :ESA, :ESB, :ESP, :ETB, :EUR, :FIM, :FJD, :FKP, :FRF, :GBP, :GEK, 18 | :GEL, :GHC, :GHS, :GIP, :GMD, :GNF, :GNS, :GQE, :GRD, :GTQ, :GWE, 19 | :GWP, :GYD, :HKD, :HNL, :HRD, :HRK, :HTG, :HUF, :IDR, :IEP, :ILP, 20 | :ILS, :INR, :IQD, :IRR, :ISK, :ITL, :JMD, :JOD, :JPY, :KES, :KGS, 21 | :KHR, :KMF, :KPW, :KRW, :KWD, :KYD, :KZT, :LAK, :LBP, :LKR, :LRD, 22 | :LSL, :LTL, :LTT, :LUC, :LUF, :LUL, :LVL, :LVR, :LYD, :MAD, :MAF, 23 | :MDL, :MGA, :MGF, :MKD, :MLF, :MMK, :MNT, :MOP, :MRO, :MTL, :MTP, 24 | :MUR, :MVR, :MWK, :MXN, :MXP, :MXV, :MYR, :MZE, :MZM, :MZN, :NAD, 25 | :NGN, :NIC, :NIO, :NLG, :NOK, :NPR, :NZD, :OMR, :PAB, :PEI, :PEN, 26 | :PES, :PGK, :PHP, :PKR, :PLN, :PLZ, :PTE, :PYG, :QAR, :RHD, :ROL, 27 | :RON, :RSD, :RUB, :RUR, :RWF, :SAR, :SBD, :SCR, :SDD, :SDG, :SDP, 28 | :SEK, :SGD, :SHP, :SIT, :SKK, :SLL, :SOS, :SRD, :SRG, :SSP, :STD, 29 | :SUR, :SVC, :SYP, :SZL, :THB, :TJR, :TJS, :TMM, :TMT, :TND, :TOP, 30 | :TPE, :TRL, :TRY, :TTD, :TWD, :TZS, :UAH, :UAK, :UGS, :UGX, :USD, 31 | :USN, :USS, :UYP, :UYU, :UZS, :VEB, :VEF, :VND, :VUV, :WST, :XAF, 32 | :XAG, :XAU, :XBA, :XBB, :XBC, :XBD, :XCD, :XDR, :XEU, :XFO, :XFU, 33 | :XOF, :XPD, :XPF, :XPT, :XRE, :XTS, :XXX, :YDD, :YER, :YUD, :YUM, 34 | :YUN, :ZAL, :ZAR, :ZMK, :ZMW, :ZRN, :ZRZ, :ZWD, :ZWL, :ZWR, :ALK, 35 | :ARL, :ARM, :BAN, :BGM, :BGO, :BOL, :CLE, :CNX, :COU, :ILR, :ISJ, 36 | :KRH, :KRO, :MCF, :MDC, :MKN, :MVP, :UYI, :VNN, :XSU, :XUA, :YUR, 37 | :BYN, :CNH, :MRU, :STN, :VES, 38 | ] 39 | # rubocop:enable Layout/MultilineArrayLineBreaks 40 | 41 | currencies = Cldr::Export::Data::Currencies.new(:de)[:currencies] 42 | assert_empty codes - currencies.keys, "Unexpected missing currencies" 43 | assert_empty currencies.keys - codes, "Unexpected extra currencies" 44 | assert_equal({ name: "Euro", "narrow_symbol": "€", one: "Euro", other: "Euro", symbol: "€" }, currencies[:EUR]) 45 | end 46 | 47 | test "currencies populates symbol-narrow when narrow symbol is not equal to the regular symbol" do 48 | currencies = Cldr::Export::Data::Currencies.new(:root)[:currencies] 49 | assert_equal({ symbol: "US$", "narrow_symbol": "$" }, currencies[:USD]) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby library for exporting data from CLDR 2 | 3 | [![Tests](https://github.com/ruby-i18n/ruby-cldr/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-i18n/ruby-cldr/actions/workflows/test.yml) 4 | 5 | The Unicode Consortium's [Common Locale Data Repository (CLDR)](https://cldr.unicode.org/) contains tons of high-quality locale data such as formatting rules for dates, times, numbers, currencies as well as language, country, calendar-specific names etc. 6 | 7 | For localizing applications in Ruby we obviously want to use this incredibly comprehensive and well-maintained resource. 8 | 9 | `ruby-cldr` exports the [XML-serialized CLDR data](https://github.com/unicode-org/cldr/releases) as YAML and Ruby files, for consumption in an [`I18n`](https://github.com/ruby-i18n/i18n) context. 10 | 11 | ## WIP status 12 | 13 | `ruby-cldr` is a work in progress towards a complete and accurate serialization of the CLDR data as Ruby + YAML files. 14 | 15 | There are still a number of issues that need to be addressed before it can be considered production-ready. 16 | 17 | ## Requirements 18 | 19 | * Ruby 3.2+ 20 | * [Thor](http://whatisthor.com/) 21 | 22 | ## Installation 23 | 24 | ``` 25 | gem install bundler 26 | bundle install 27 | 28 | thor cldr:download 29 | ``` 30 | 31 | ## Export 32 | 33 | By default, the `thor cldr:export` command will export all known components from all locales to the target directory: 34 | 35 | ``` 36 | thor cldr:export 37 | ``` 38 | 39 | ### Locales, components, and target directory 40 | 41 | You can also optionally specify locales and/or components to export as well as the target directory: 42 | 43 | ```bash 44 | # Export the `Numbers` and `Plurals` components for the locales `de`, `fr-FR` and `en-ZA` to the `./data` target directory 45 | 46 | thor cldr:export --locales de fr-FR en-ZA --components Numbers Plurals --target=./data 47 | 48 | ``` 49 | 50 | ### Draft status 51 | 52 | CLDR defines a hierarchy of four [draft statuses](http://www.unicode.org/reports/tr35/#Attribute_draft), used to indicate how confident they are in the data: `unconfirmed` < `provisional` < `contributed` < `approved`. 53 | 54 | By default, `ruby-cldr` only exports data with a minimum draft status of `contributed` (i.e., `contributed` or `approved`). This is the same threshold that is used by the Unicode Consortium's [International Components for Unicode (ICU)](https://icu.unicode.org/). 55 | 56 | Set the `--draft-status=` parameter to specify the minimum draft status the data needs in order to be exported: 57 | 58 | ```bash 59 | # Export any data with a minimum draft status of `provisional` 60 | # (i.e., `provisional`, `contributed` or `approved`)). 61 | 62 | thor cldr:export --draft-status=provisional 63 | ``` 64 | 65 | ## Tests 66 | 67 | ``` 68 | bundle exec ruby test/all.rb 69 | ``` 70 | 71 | ## Resources 72 | 73 | * [`unicode-org/cldr`](https://github.com/unicode-org/cldr), the official upstream source of CLDR data 74 | * [`unicode-org/cldr-json`](https://github.com/unicode-org/cldr-json/), a JSON serialization of the CLDR data 75 | * [CLDR Markup specification](http://www.unicode.org/reports/tr35/) 76 | * [Plural Rules table](https://unicode-org.github.io/cldr-staging/charts/41/supplemental/language_plural_rules.html) 77 | -------------------------------------------------------------------------------- /test/format/decimal/number_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 4 | 5 | class TestCldrDecimalNumberFormat < Test::Unit::TestCase 6 | test "interpolates a number" do 7 | assert_equal "123", Cldr::Format::Decimal::Number.new("###").apply(123) 8 | end 9 | 10 | test "interpolates a number on the right side" do 11 | assert_equal "0123", Cldr::Format::Decimal::Number.new("0###").apply(123) 12 | end 13 | 14 | test "strips optional digits" do 15 | assert_equal "123", Cldr::Format::Decimal::Number.new("######").apply(123) 16 | end 17 | 18 | test "single group" do 19 | assert_equal "1,23", Cldr::Format::Decimal::Number.new("#,##").apply(123) 20 | end 21 | 22 | test "multiple groups with a primary group size" do 23 | assert_equal "1,23,45,67,89", Cldr::Format::Decimal::Number.new("#,##").apply(123456789) 24 | end 25 | 26 | test "multiple groups with a primary and secondary group size" do 27 | assert_equal "12,34,56,789", Cldr::Format::Decimal::Number.new("#,##,##0").apply(123456789) 28 | end 29 | 30 | test "does not group when no digits left of the grouping position" do 31 | assert_equal "123", Cldr::Format::Decimal::Number.new("#,###").apply(123) 32 | end 33 | 34 | test "interpolates a fraction when defined by the format" do 35 | assert_equal "123.45", Cldr::Format::Decimal::Number.new("###.##").apply(123.45) 36 | end 37 | 38 | test "interpolates a fraction when not defined by the format but :precision given" do 39 | assert_equal "123.45", Cldr::Format::Decimal::Number.new("###").apply(123.45, precision: 2) 40 | end 41 | 42 | test "rounds a fraction" do 43 | assert_equal "123.46", Cldr::Format::Decimal::Number.new("###.##").apply(123.456) 44 | end 45 | 46 | test "interpolates fraction on the left side" do 47 | assert_equal "123.4500", Cldr::Format::Decimal::Number.new("###.0000#").apply(123.45) 48 | end 49 | 50 | test "rounds with precision => 0" do 51 | assert_equal "124", Cldr::Format::Decimal::Number.new("###.##").apply(123.55, precision: 0) 52 | end 53 | 54 | test "rounds with precision => 1" do 55 | assert_equal "124", Cldr::Format::Decimal::Number.new("###.##").apply(123.55, precision: 0) 56 | end 57 | 58 | test "cldr example #,##0.## => 1 234,57" do 59 | assert_equal "1 234,57", Cldr::Format::Decimal::Number.new("#,##0.##", decimal: ",", group: " ").apply(1234.567) 60 | end 61 | 62 | test "cldr example #,##0.### => 1 234,567" do 63 | assert_equal "1 234,567", Cldr::Format::Decimal::Number.new("#,##0.###", decimal: ",", group: " ").apply(1234.567) 64 | end 65 | 66 | test "cldr example ###0.##### => 1234,567" do 67 | assert_equal "1234,567", Cldr::Format::Decimal::Number.new("###0.#####", decimal: ",", group: " ").apply(1234.567) 68 | end 69 | 70 | test "cldr example ###0.0000# => 1234,5670" do 71 | assert_equal "1234,5670", Cldr::Format::Decimal::Number.new("###0.0000#", decimal: ",", group: " ").apply(1234.567) 72 | end 73 | 74 | test "cldr example 00000.0000 => 01234,5670" do 75 | assert_equal "01234,5670", Cldr::Format::Decimal::Number.new("00000.0000", decimal: ",", group: " ").apply(1234.567) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # RuboCop commits 2 | 3 | ## 2023-01-04: Correct RuboCop indentation offenses 4 | 4707a6b7e0e2d71a162db87e8c040aabad5bbf3a 5 | 6 | ## 2022-09-13: Autocorrect `Style/MagicCommentFormat` offenses 7 | 35bf9c64468f6830a8309717dc6ba1586f1043f5 8 | 9 | ## 2022-09-13: Autocorrect `Style/TrailingCommaInArguments` offenses 10 | 87a64b008a299a43d061386732eec027c04a19f7 11 | 12 | ## 2022-08-08: Fix `Style/ClassMethodsDefinitions` violations 13 | c2ed42ee3fbb1c81f21750b7d44afc704ea95818 14 | 15 | ## 2022-06-14: Autocorrect `Layout/EmptyLineAfterGuardClause` 16 | d96b73ffcd169b0b5e2c6c1650d261e0a18ffbe4 17 | 18 | ## 2022-04-20: Apply `rubocop -a --only=Layout/EmptyLineAfterGuardClause` 19 | 0e06046e050615266634885d6bcfb142e6c5e0b9 20 | 21 | ## 2021-12-13: Autocorrect `Style/SymbolProc` 22 | 683b4a989a47b92a36e5cd0b34f0c4adf5dc583b 23 | 24 | ## 2021-12-13: Autocorrect `Style/PreferredHashMethods` 25 | 1ac7f16311ebff16e9e9339255313b647bb32cb3 26 | 27 | ## 2021-12-13: Autocorrect `Style/ClassAndModuleChildren` 28 | 9c4012da5c56b6d78c15715b4e977d356ccdd9ea 29 | 30 | ## 2021-12-13: Autocorrect `Style/AndOr` 31 | 8755ec5374831e1e16e37710982d313e233e6f2b 32 | 33 | ## 2021-12-13: Autocorrect `Style/ZeroLengthPredicate` 34 | 5e9f3aee1121e692acb0373b0f67b3d8541cd0b3 35 | 36 | ## 2021-12-13: Autocorrect `Style/SpecialGlobalVars` 37 | 96402d986d652cd5ddea01c1ffc7ddab4386bb27 38 | 39 | ## 2021-12-13: Autocorrect `Style/WordArray` 40 | a42b72dcbae983e1b2f55e7802833110850ac3ce 41 | 42 | ## 2021-12-13: Autocorrect `Style/RedundantPercentQ` 43 | 6e954e9098acadd40d4d7282296148138451c34a 44 | 45 | ## 2021-12-13: Autocorrect `Layout/EmptyLinesAroundClassBody` 46 | 316f27b7140ac0da9a7c83262b4905875a1ee67b 47 | 48 | ## 2021-12-13: Autocorrect `Layout/IndentationWidth` 49 | 615c0c3b9e51f8715cddd7cc8df1e75138eba40f 50 | 51 | ## 2021-12-13: Autocorrect `Style/PerlBackrefs` 52 | 2e31dfa62f8c2728a72e7241ed1221a3c9134a6d 53 | 54 | ## 2021-12-13: Autocorrect `Layout/SpaceInsideParens` 55 | 96554d14f882456076c7af97b22ad0cea4d5a23b 56 | 57 | ## 2021-12-13: Autocorrect `Layout/SpaceAroundOperators` 58 | d586968abdee6ca9a94a5ba7242eac021beb73df 59 | 60 | ## 2021-12-13: Autocorrect `Style/QuotedSymbols` 61 | 000d48cafe0c2bdaef6be197f854e589c50c01b7 62 | 63 | ## 2021-12-13: Autocorrect `Layout/ExtraSpacing` 64 | 90e57b3b86df53886e3922f7cc8cbe3b637820ca 65 | 66 | ## 2021-12-13: Autocorrect `Style/TrailingCommaInHashLiteral` 67 | 75a1aa7be00f15d5dfc247034c8da7c2e7e81276 68 | 69 | ## 2021-12-13: Autocorrect `Layout/SpaceAfterComma` 70 | 87fc0afd697a66febaecd6c69a5f96eec098ca17 71 | 72 | ## 2021-12-13: Autocorrect `Style/EachWithObject` 73 | 6ca15328f0352c046128d6e167054f0944f4afaf 74 | 75 | ## 2021-12-13: Autocorrect `Layout/TrailingEmptyLines` 76 | 7ba858d7c40ffb22084bf932f087139c20beba35 77 | 78 | ## 2021-12-13: Autocorrect `Layout/TrailingWhitespace` 79 | 76e69ab0ea9702dc60c82564bca62eda03ee646f 80 | 81 | ## 2021-12-13: Autocorrect `Layout/HashAlignment` 82 | 229e6ff8209850381a121b8e0eff137e4da081e5 83 | 84 | ## 2021-12-13: Autocorrect `Style/MethodCallWithArgsParentheses` 85 | a057d7361f34d3dea78e254dc866f937d23dbe89 86 | 87 | ## 2021-12-13: Autocorrect `Style/FrozenStringLiteralComment` 88 | c839d296b7f9e0bd0fbefe1dae548f6003ca0840 89 | 90 | ## 2021-12-13: Autocorrect `Layout/ArrayAlignment` 91 | d3ef77ac5e0ed2778a277b97465692ab7b18a534 92 | 93 | ## 2021-12-13: Autocorrect `Style/HashSyntax` 94 | 2f1e4442b6cfd0f6819a3852b4852594b3951431 95 | 96 | ## 2021-12-13: Autocorrect `Style/StringLiterals` 97 | 3905dbee72b1f2f4c6ce4f2895aae1c07668b694 98 | -------------------------------------------------------------------------------- /lib/cldr/export/data_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "nokogiri" 4 | 5 | module Cldr 6 | module Export 7 | class DataFile 8 | class << self 9 | def parse(string, minimum_draft_status: nil) 10 | doc = Nokogiri::XML(string) do |config| 11 | config.strict.noblanks 12 | end 13 | DataFile.new(doc, minimum_draft_status: minimum_draft_status) 14 | end 15 | 16 | def filter_by_draft(doc, minimum_draft_status) 17 | doc.traverse do |child| 18 | next unless child.text? 19 | 20 | draft_status = child.parent.attribute("draft").nil? ? Cldr::DraftStatus::APPROVED : Cldr::DraftStatus.fetch(child.parent.attribute("draft")) 21 | if draft_status < minimum_draft_status 22 | ancestors = child.ancestors 23 | child.remove 24 | # Remove the ancestors that are now empty 25 | ancestors.each do |ancestor| 26 | ancestor.remove if ancestor.children.empty? 27 | end 28 | end 29 | end 30 | doc 31 | end 32 | end 33 | 34 | attr_reader :doc, :minimum_draft_status 35 | 36 | def initialize(doc, minimum_draft_status: nil) 37 | @minimum_draft_status = minimum_draft_status || Cldr::Export.minimum_draft_status 38 | @doc = Cldr::Export::DataFile.filter_by_draft(doc, @minimum_draft_status) 39 | end 40 | 41 | def traverse(&block) 42 | @doc.traverse(&block) 43 | end 44 | 45 | def xpath(path) 46 | @doc.xpath(path) 47 | end 48 | 49 | def /(*args) 50 | @doc./(*args) 51 | end 52 | 53 | def locale 54 | language = @doc.xpath("//ldml/identity/language").first&.attribute("type")&.value 55 | territory = @doc.xpath("//ldml/identity/territory").first&.attribute("type")&.value 56 | elements = [language, territory].compact 57 | elements.empty? ? nil : elements.join("-").to_sym 58 | end 59 | 60 | def merge(other) 61 | verify_merging_requirements(other) 62 | Cldr::Export::DataFile.new(merge_docs!(@doc.dup, other.doc), minimum_draft_status: minimum_draft_status) 63 | end 64 | 65 | def merge!(other) 66 | verify_merging_requirements(other) 67 | merge_docs!(@doc, other.doc) 68 | self 69 | end 70 | 71 | private 72 | 73 | def verify_merging_requirements(other) 74 | raise StandardError, "Cannot merge data file with more permissive draft status" if other.minimum_draft_status < minimum_draft_status 75 | raise StandardError, "Cannot merge data file from different locales" if other.locale != locale 76 | end 77 | 78 | def merge_docs!(doc, other) 79 | # Some parts (`ldml`, `ldmlBCP47` amd `supplementalData`) of CLDR data require that you merge all the 80 | # files with the same root element before doing lookups. 81 | # Ref: https://www.unicode.org/reports/tr35/tr35.html#XML_Format 82 | # 83 | # Note that it technically is no longer compliant with the CLDR `ldml.dtd`, since: 84 | # * it has repeated elements 85 | # * the elements no longer refer to the filename 86 | # 87 | # However, this is not an issue, since #xpath will find all of the matches from each of the repeated elements, 88 | # and the elements are not important to us / make no sense when combined together. 89 | other.root.children.each do |child| 90 | doc.root.add_child(child.dup) 91 | end 92 | doc 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/cldr/thor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "thor" 4 | require "cldr" 5 | require "cldr/download" 6 | require "cldr/validate" 7 | require "cldr/validate_upstream_assumptions" 8 | 9 | module Cldr 10 | class Thor < ::Thor 11 | namespace "cldr" 12 | 13 | desc "download [--version=#{Cldr::Download::DEFAULT_VERSION}] [--target=#{Cldr::Download::DEFAULT_TARGET}] [--source=#{format(Cldr::Download::DEFAULT_SOURCE, version: Cldr::Download::DEFAULT_VERSION)}]", "Download and extract CLDR data" 14 | option :version, 15 | aliases: [:v], 16 | type: :numeric, 17 | default: Cldr::Download::DEFAULT_VERSION, 18 | banner: Cldr::Download::DEFAULT_VERSION, 19 | desc: "Release version of the CLDR data to use" 20 | option :target, 21 | aliases: [:t], 22 | type: :string, 23 | default: Cldr::Download::DEFAULT_TARGET, 24 | banner: Cldr::Download::DEFAULT_TARGET, 25 | desc: "Where on the filesystem to extract the downloaded data" 26 | option :source, 27 | aliases: [:s], 28 | type: :string, 29 | default: Cldr::Download::DEFAULT_SOURCE, 30 | banner: Cldr::Download::DEFAULT_SOURCE, 31 | desc: "Override the location of the CLDR zip to download. Overrides --version" 32 | def download 33 | Cldr::Download.download(options["source"], options["target"], options["version"]) { putc(".") } 34 | end 35 | 36 | DEFAULT_MINIMUM_DRAFT_STATUS = Cldr::DraftStatus::CONTRIBUTED 37 | 38 | desc "export [--locales=de fr-FR en-ZA] [--components=Numbers Plurals] [--target=#{Cldr::Export::DEFAULT_TARGET}] [--merge/--no-merge]", 39 | "Export CLDR data by locales and components to target dir" 40 | option :locales, aliases: [:l], type: :array, banner: "de fr-FR en-ZA", enum: Cldr::Export::Data::RAW_DATA.locales.map(&:to_s) 41 | option :components, aliases: [:c], type: :array, banner: "Numbers Plurals", enum: Cldr::Export::Data.components.map(&:to_s) 42 | option :target, aliases: [:t], type: :string, default: Cldr::Export::DEFAULT_TARGET, banner: Cldr::Export::DEFAULT_TARGET 43 | option :draft_status, 44 | aliases: [:d], 45 | type: :string, 46 | enum: Cldr::DraftStatus::ALL.map(&:to_s), 47 | default: DEFAULT_MINIMUM_DRAFT_STATUS.to_s, 48 | banner: DEFAULT_MINIMUM_DRAFT_STATUS.to_s, 49 | desc: "The minimum draft status to include in the export" 50 | option :merge, aliases: [:m], type: :boolean, default: false 51 | def export 52 | $stdout.sync 53 | 54 | formatted_options = options.dup.symbolize_keys 55 | 56 | if formatted_options.key?(:locales) 57 | formatted_options[:locales] = formatted_options[:locales].map(&:to_sym) 58 | end 59 | if formatted_options.key?(:components) 60 | formatted_options[:components] = formatted_options[:components].map(&:to_sym) 61 | end 62 | 63 | if formatted_options.key?(:draft_status) 64 | formatted_options[:minimum_draft_status] = Cldr::DraftStatus.fetch(formatted_options[:draft_status]) 65 | formatted_options.delete(:draft_status) 66 | end 67 | 68 | Cldr::Export.export(formatted_options) { putc(".") } 69 | puts 70 | end 71 | 72 | # TODO: flatten task, e.g. flatten all plural locale files into one big file 73 | 74 | desc "validate_upstream", "Verify our assumptions about the CLDR data are correct." 75 | def validate_upstream 76 | Cldr::ValidateUpstreamAssumptions.validate 77 | end 78 | 79 | desc "validate", "Run QA checks against the output data" 80 | option :target, 81 | aliases: [:t], 82 | type: :string, 83 | default: Cldr::Export::DEFAULT_TARGET, 84 | banner: Cldr::Export::DEFAULT_TARGET, 85 | desc: "Where on the filesystem the extracted data to validate is" 86 | def validate 87 | Cldr::Validate.validate(options["target"]) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/export/data/timezones_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataTimezones < Test::Unit::TestCase 7 | test "timezones :de" do 8 | # rubocop:disable Layout/MultilineArrayLineBreaks 9 | codes_subset = [ 10 | :"Etc/Unknown", :"Europe/Tirane", :"Asia/Yerevan", :"Antarctica/Vostok", 11 | :"Antarctica/DumontDUrville", :"Europe/Vienna", :"Europe/Brussels", 12 | :"Africa/Ouagadougou", :"Africa/Porto-Novo", :"America/St_Barthelemy", 13 | :"Atlantic/Bermuda", :"America/Sao_Paulo", :"America/Coral_Harbour", 14 | :"America/St_Johns", :"Europe/Zurich", :"Pacific/Easter", :"America/Bogota", :"America/Havana", 15 | :"Atlantic/Cape_Verde", :"America/Curacao", :"Indian/Christmas", 16 | :"Asia/Nicosia", :"Europe/Prague", :"Europe/Busingen", 17 | :"Africa/Djibouti", :"Europe/Copenhagen", :"Africa/Algiers", 18 | :"Africa/Cairo", :"Africa/El_Aaiun", :"Africa/Asmera", 19 | :"Atlantic/Canary", :"Africa/Addis_Ababa", :"Pacific/Fiji", 20 | :"Pacific/Truk", :"Pacific/Ponape", :"Atlantic/Faeroe", 21 | :"Europe/London", :"Asia/Tbilisi", :"Africa/Accra", :"America/Godthab", 22 | :"America/Scoresbysund", :"Europe/Athens", :"Atlantic/South_Georgia", 23 | :"Asia/Hong_Kong", :"Asia/Jayapura", :"Europe/Dublin", :"Asia/Calcutta", 24 | :"Asia/Baghdad", :"Asia/Tehran", :"Atlantic/Reykjavik", :"Europe/Rome", 25 | :"America/Jamaica", :"Asia/Tokyo", :"Asia/Bishkek", :"Indian/Comoro", 26 | :"America/St_Kitts", :"Asia/Pyongyang", :"America/Cayman", 27 | :"Asia/Aqtobe", :"America/St_Lucia", :"Europe/Vilnius", 28 | :"Europe/Luxembourg", :"Africa/Tripoli", :"Europe/Chisinau", 29 | :"Asia/Macau", :"Indian/Maldives", :"America/Mexico_City", 30 | :"Asia/Katmandu", :"Asia/Muscat", :"Europe/Warsaw", :"Atlantic/Azores", 31 | :"Europe/Lisbon", :"America/Asuncion", :"Asia/Qatar", :"Indian/Reunion", 32 | :"Europe/Bucharest", :"Europe/Belgrade", :"Europe/Moscow", 33 | :"Europe/Volgograd", :"Asia/Yekaterinburg", :"Asia/Novosibirsk", 34 | :"Asia/Novokuznetsk", :"Asia/Krasnoyarsk", :"Asia/Yakutsk", 35 | :"Asia/Vladivostok", :"Asia/Sakhalin", :"Asia/Kamchatka", 36 | :"Asia/Riyadh", :"Africa/Khartoum", :"Asia/Singapore", 37 | :"Atlantic/St_Helena", :"Africa/Mogadishu", :"Africa/Sao_Tome", 38 | :"America/El_Salvador", :"America/Lower_Princes", :"Asia/Damascus", 39 | :"Africa/Lome", :"Asia/Dushanbe", :"America/Port_of_Spain", 40 | :"Asia/Taipei", :"Africa/Dar_es_Salaam", :"Europe/Uzhgorod", 41 | :"Europe/Kiev", :"Europe/Zaporozhye", :"America/North_Dakota/Beulah", 42 | :"America/North_Dakota/New_Salem", :"America/North_Dakota/Center", 43 | :"America/Indiana/Vincennes", :"America/Indiana/Petersburg", 44 | :"America/Indiana/Tell_City", :"America/Indiana/Knox", 45 | :"America/Indiana/Winamac", :"America/Indiana/Marengo", 46 | :"America/Indiana/Vevay", :"America/Kentucky/Monticello", 47 | :"Asia/Tashkent", :"Europe/Vatican", :"America/St_Vincent", 48 | :"America/St_Thomas", :"Asia/Saigon", :"America/Santa_Isabel", 49 | ] 50 | # rubocop:enable Layout/MultilineArrayLineBreaks 51 | 52 | timezones = Cldr::Export::Data::Timezones.new(:de)[:timezones] 53 | assert_empty codes_subset - timezones.keys, "Could not find some timezones" 54 | assert_equal({ city: "Wien" }, timezones[:"Europe/Vienna"]) 55 | end 56 | 57 | test "timezone daylight" do 58 | london = Cldr::Export::Data::Timezones.new(:de)[:timezones][:"Europe/London"] 59 | assert_equal({ city: "London", long: { daylight: "Britische Sommerzeit" } }, london) 60 | end 61 | 62 | test "metazone :de Europe_Western" do 63 | europe_western = Cldr::Export::Data::Timezones.new(:de)[:metazones][:Europe_Western] 64 | long = { generic: "Westeuropäische Zeit", standard: "Westeuropäische Normalzeit", daylight: "Westeuropäische Sommerzeit" } 65 | short = { generic: "WEZ", standard: "WEZ", daylight: "WESZ" } 66 | assert_equal({ long: long, short: short }, europe_western) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | ast (2.4.3) 6 | base64 (0.2.0) 7 | bigdecimal (3.1.8) 8 | builder (3.3.0) 9 | coderay (1.1.3) 10 | concurrent-ruby (1.3.5) 11 | date (3.5.1) 12 | debug (1.11.0) 13 | irb (~> 1.10) 14 | reline (>= 0.3.8) 15 | descendants_tracker (0.0.4) 16 | thread_safe (~> 0.3, >= 0.3.1) 17 | erb (5.1.3) 18 | faraday (0.9.2) 19 | multipart-post (>= 1.2, < 3) 20 | git (1.11.0) 21 | rchardet (~> 1.8) 22 | github_api (0.16.0) 23 | addressable (~> 2.4.0) 24 | descendants_tracker (~> 0.0.4) 25 | faraday (~> 0.8, < 0.10) 26 | hashie (>= 3.4) 27 | mime-types (>= 1.16, < 3.0) 28 | oauth2 (~> 1.0) 29 | hashie (5.0.0) 30 | highline (3.1.1) 31 | reline 32 | i18n (1.14.7) 33 | concurrent-ruby (~> 1.0) 34 | io-console (0.8.1) 35 | irb (1.15.3) 36 | pp (>= 0.6.0) 37 | rdoc (>= 4.0.0) 38 | reline (>= 0.4.2) 39 | jeweler (2.3.9) 40 | builder 41 | bundler 42 | git (>= 1.2.5) 43 | github_api (~> 0.16.0) 44 | highline (>= 1.6.15) 45 | nokogiri (>= 1.5.10) 46 | psych 47 | rake 48 | rdoc 49 | semver2 50 | json (2.16.0) 51 | jwt (2.9.3) 52 | base64 53 | language_server-protocol (3.17.0.5) 54 | lint_roller (1.1.0) 55 | logger (1.7.0) 56 | method_source (1.1.0) 57 | mime-types (2.99.3) 58 | mini_portile2 (2.8.9) 59 | multi_json (1.15.0) 60 | multi_xml (0.7.1) 61 | bigdecimal (~> 3.1) 62 | multipart-post (2.4.1) 63 | nokogiri (1.18.10) 64 | mini_portile2 (~> 2.8.2) 65 | racc (~> 1.4) 66 | nokogiri (1.18.10-aarch64-linux-gnu) 67 | racc (~> 1.4) 68 | nokogiri (1.18.10-arm-linux-gnu) 69 | racc (~> 1.4) 70 | nokogiri (1.18.10-arm64-darwin) 71 | racc (~> 1.4) 72 | nokogiri (1.18.10-x86_64-darwin) 73 | racc (~> 1.4) 74 | nokogiri (1.18.10-x86_64-linux-gnu) 75 | racc (~> 1.4) 76 | oauth2 (1.4.8) 77 | faraday (>= 0.8, < 3.0) 78 | jwt (>= 1.0, < 3.0) 79 | multi_json (~> 1.3) 80 | multi_xml (~> 0.5) 81 | rack (>= 1.2, < 3) 82 | parallel (1.27.0) 83 | parser (3.3.10.0) 84 | ast (~> 2.4.1) 85 | racc 86 | power_assert (3.0.1) 87 | pp (0.6.3) 88 | prettyprint 89 | prettyprint (0.2.0) 90 | prism (1.6.0) 91 | pry (0.14.2) 92 | coderay (~> 1.1) 93 | method_source (~> 1.0) 94 | pry-nav (1.0.0) 95 | pry (>= 0.9.10, < 0.15) 96 | psych (5.3.1) 97 | date 98 | stringio 99 | racc (1.8.1) 100 | rack (2.2.20) 101 | rainbow (3.1.1) 102 | rake (13.2.1) 103 | rbs (3.9.5) 104 | logger 105 | rchardet (1.8.0) 106 | rdoc (6.15.1) 107 | erb 108 | psych (>= 4.0.0) 109 | tsort 110 | regexp_parser (2.11.3) 111 | reline (0.6.2) 112 | io-console (~> 0.5) 113 | rubocop (1.81.7) 114 | json (~> 2.3) 115 | language_server-protocol (~> 3.17.0.2) 116 | lint_roller (~> 1.1.0) 117 | parallel (~> 1.10) 118 | parser (>= 3.3.0.2) 119 | rainbow (>= 2.2.2, < 4.0) 120 | regexp_parser (>= 2.9.3, < 3.0) 121 | rubocop-ast (>= 1.47.1, < 2.0) 122 | ruby-progressbar (~> 1.7) 123 | unicode-display_width (>= 2.4.0, < 4.0) 124 | rubocop-ast (1.48.0) 125 | parser (>= 3.3.7.2) 126 | prism (~> 1.4) 127 | rubocop-shopify (2.18.0) 128 | rubocop (~> 1.62) 129 | ruby-lsp (0.26.4) 130 | language_server-protocol (~> 3.17.0) 131 | prism (>= 1.2, < 2.0) 132 | rbs (>= 3, < 5) 133 | ruby-progressbar (1.13.0) 134 | rubyzip (3.2.2) 135 | semver2 (3.4.2) 136 | stringio (3.2.0) 137 | test-unit (3.7.3) 138 | power_assert 139 | thor (1.4.0) 140 | thread_safe (0.3.6) 141 | tsort (0.2.0) 142 | unicode-display_width (3.2.0) 143 | unicode-emoji (~> 4.1) 144 | unicode-emoji (4.1.0) 145 | 146 | PLATFORMS 147 | aarch64-linux 148 | arm-linux 149 | arm64-darwin 150 | x86-linux 151 | x86_64-darwin 152 | x86_64-linux 153 | 154 | DEPENDENCIES 155 | debug 156 | i18n 157 | jeweler 158 | nokogiri 159 | pry 160 | pry-nav 161 | psych (>= 4.0.0) 162 | rubocop-shopify 163 | ruby-lsp 164 | rubyzip 165 | test-unit 166 | thor 167 | 168 | BUNDLED WITH 169 | 2.5.23 170 | -------------------------------------------------------------------------------- /test/export/data/numbers_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataNumbers < Test::Unit::TestCase 7 | test "number symbols :de" do 8 | expected = { 9 | approximately_sign: "≈", 10 | decimal: ",", 11 | exponential: "E", 12 | group: ".", 13 | infinity: "∞", 14 | list: ";", 15 | minus_sign: "-", 16 | nan: "NaN", 17 | per_mille: "‰", 18 | percent_sign: "%", 19 | plus_sign: "+", 20 | superscripting_exponent: "·", 21 | time_separator: ":", 22 | } 23 | assert_equal expected, Cldr::Export::Data::Numbers.new(:de)[:numbers][:latn][:symbols] 24 | end 25 | 26 | test "number formats :de" do 27 | expected = { 28 | currency: { 29 | patterns: { 30 | default: { 31 | accounting: "#,##0.00 ¤", 32 | standard: "#,##0.00 ¤", 33 | }, 34 | short: { 35 | standard: { 36 | "1000": { one: "0", other: "0" }, 37 | "10000": { one: "0", other: "0" }, 38 | "100000": { one: "0", other: "0" }, 39 | "1000000": { one: "0 Mio'.' ¤", other: "0 Mio'.' ¤" }, 40 | "10000000": { one: "00 Mio'.' ¤", other: "00 Mio'.' ¤" }, 41 | "100000000": { one: "000 Mio'.' ¤", other: "000 Mio'.' ¤" }, 42 | "1000000000": { one: "0 Mrd'.' ¤", other: "0 Mrd'.' ¤" }, 43 | "10000000000": { one: "00 Mrd'.' ¤", other: "00 Mrd'.' ¤" }, 44 | "100000000000": { one: "000 Mrd'.' ¤", other: "000 Mrd'.' ¤" }, 45 | "1000000000000": { one: "0 Bio'.' ¤", other: "0 Bio'.' ¤" }, 46 | "10000000000000": { one: "00 Bio'.' ¤", other: "00 Bio'.' ¤" }, 47 | "100000000000000": { one: "000 Bio'.' ¤", other: "000 Bio'.' ¤" }, 48 | }, 49 | }, 50 | }, 51 | unit: { one: "{0} {1}", other: "{0} {1}" }, 52 | }, 53 | decimal: { 54 | patterns: { 55 | default: { 56 | standard: "#,##0.###", 57 | }, 58 | long: { 59 | standard: { 60 | "1000": { one: "0 Tausend", other: "0 Tausend" }, 61 | "10000": { one: "00 Tausend", other: "00 Tausend" }, 62 | "100000": { one: "000 Tausend", other: "000 Tausend" }, 63 | "1000000": { one: "0 Million", other: "0 Millionen" }, 64 | "10000000": { one: "00 Millionen", other: "00 Millionen" }, 65 | "100000000": { one: "000 Millionen", other: "000 Millionen" }, 66 | "1000000000": { one: "0 Milliarde", other: "0 Milliarden" }, 67 | "10000000000": { one: "00 Milliarden", other: "00 Milliarden" }, 68 | "100000000000": { one: "000 Milliarden", other: "000 Milliarden" }, 69 | "1000000000000": { one: "0 Billion", other: "0 Billionen" }, 70 | "10000000000000": { one: "00 Billionen", other: "00 Billionen" }, 71 | "100000000000000": { one: "000 Billionen", other: "000 Billionen" }, 72 | }, 73 | }, 74 | short: { 75 | standard: { 76 | "1000": { one: "0", other: "0" }, 77 | "10000": { one: "0", other: "0" }, 78 | "100000": { one: "0", other: "0" }, 79 | "1000000": { one: "0 Mio'.'", other: "0 Mio'.'" }, 80 | "10000000": { one: "00 Mio'.'", other: "00 Mio'.'" }, 81 | "100000000": { one: "000 Mio'.'", other: "000 Mio'.'" }, 82 | "1000000000": { one: "0 Mrd'.'", other: "0 Mrd'.'" }, 83 | "10000000000": { one: "00 Mrd'.'", other: "00 Mrd'.'" }, 84 | "100000000000": { one: "000 Mrd'.'", other: "000 Mrd'.'" }, 85 | "1000000000000": { one: "0 Bio'.'", other: "0 Bio'.'" }, 86 | "10000000000000": { one: "00 Bio'.'", other: "00 Bio'.'" }, 87 | "100000000000000": { one: "000 Bio'.'", other: "000 Bio'.'" }, 88 | }, 89 | }, 90 | }, 91 | }, 92 | percent: { patterns: { default: { standard: "#,##0 %" } } }, 93 | scientific: { patterns: { default: { standard: "#E0" } } }, 94 | } 95 | assert_equal expected, Cldr::Export::Data::Numbers.new(:de)[:numbers][:latn][:formats] 96 | end 97 | 98 | test "redirects in root locale" do 99 | assert_equal :"numbers.latn.formats.decimal.patterns.short", 100 | Cldr::Export::Data::Numbers.new(:root)[:numbers][:latn][:formats][:decimal][:patterns][:long] 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/export/data/languages_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__) + "/../../test_helper")) 5 | 6 | class TestCldrDataLanguages < Test::Unit::TestCase 7 | test "languages :de" do 8 | # rubocop:disable Layout/MultilineArrayLineBreaks 9 | codes = [ # rubocop:disable Metrics/CollectionLiteralLength 10 | :aa, :ab, :ace, :ach, :ada, :ady, :ae, :aeb, :af, 11 | :afh, :agq, :ain, :ak, :akk, :akz, :ale, :aln, :alt, :am, 12 | :an, :ang, :anp, :ar, :"ar-001", :arc, :arn, :aro, :arp, 13 | :arq, :ars, :arw, :ary, :arz, :as, :asa, :ase, :ast, :av, 14 | :avk, :awa, :ay, :az, :ba, :bal, :ban, :bar, :bas, :bax, 15 | :bbc, :bbj, :be, :bej, :bem, :bew, :bez, :bfd, :bfq, :bg, 16 | :bgn, :bho, :bi, :bik, :bin, :bjn, :bkm, :bla, :bm, :bn, 17 | :bo, :bpy, :bqi, :br, :bra, :brh, :brx, :bs, :bss, :bua, 18 | :bug, :bum, :byn, :byv, :ca, :cad, :car, :cay, :cch, :ccp, :ce, 19 | :ceb, :cgg, :ch, :chb, :chg, :chk, :chm, :chn, :cho, :chp, 20 | :chr, :chy, :ckb, :co, :cop, :cps, :cr, :crh, :crs, :cs, 21 | :csb, :cu, :cv, :cy, :da, :dak, :dar, :dav, :de, :"de-AT", 22 | :"de-CH", :del, :den, :dgr, :din, :dje, :doi, :dsb, :dtp, 23 | :dua, :dum, :dv, :dyo, :dyu, :dz, :dzg, :ebu, :ee, :efi, 24 | :egl, :egy, :eka, :el, :elx, :en, :enm, 25 | :eo, :es, :esu, :et, :eu, :ewo, :ext, :fa, :"fa-AF", :fan, :fat, :ff, 26 | :fi, :fil, :fit, :fj, :fo, :fon, :fr, :frc, :frm, :fro, 27 | :frp, :frr, :frs, :fur, :fy, :ga, :gaa, :gag, :gan, :gay, 28 | :gba, :gbz, :gd, :gez, :gil, :gl, :glk, :gmh, :gn, :goh, 29 | :gom, :gon, :gor, :got, :grb, :grc, :gsw, :gu, :guc, :gur, 30 | :guz, :gv, :gwi, :ha, :hai, :hak, :haw, :he, :hi, :hif, 31 | :hil, :hit, :hmn, :ho, :hr, :hsb, :hsn, :ht, :hu, :hup, 32 | :hy, :hz, :ia, :iba, :ibb, :id, :ie, :ig, :ii, :ik, :ilo, 33 | :inh, :io, :is, :it, :iu, :izh, :ja, :jam, :jbo, :jgo, 34 | :jmc, :jpr, :jrb, :jut, :jv, :ka, :kaa, :kab, :kac, 35 | :kaj, :kam, :kaw, :kbd, :kbl, :kcg, :kde, :kea, :ken, 36 | :kfo, :kg, :kgp, :kha, :kho, :khq, :khw, :ki, :kiu, :kj, 37 | :kk, :kkj, :kl, :kln, :km, :kmb, :kn, :ko, :koi, :kok, 38 | :kos, :kpe, :kr, :krc, :kri, :krj, :krl, :kru, :ks, :ksb, 39 | :ksf, :ksh, :ku, :kum, :kut, :kv, :kw, :ky, :la, :lad, 40 | :lag, :lah, :lam, :lb, :lez, :lfn, :lg, :li, :lij, :liv, 41 | :lkt, :lmo, :ln, :lo, :lol, :lou, :loz, :lrc, :lt, :ltg, 42 | :lu, :lua, :lui, :lun, :luo, :lus, :luy, :lv, :lzh, :lzz, 43 | :mad, :maf, :mag, :mai, :mak, :man, :mas, :mde, :mdf, :mdr, 44 | :men, :mer, :mfe, :mg, :mga, :mgh, :mgo, :mh, :mi, :mic, 45 | :min, :mk, :ml, :mn, :mnc, :mni, :moh, :mos, :mr, :mrj, :ms, 46 | :mt, :mua, :mul, :mus, :mwl, :mwr, :mwv, :my, :mye, :myv, 47 | :mzn, :na, :nan, :nap, :naq, :nb, :nd, :nds, :"nds-NL", :ne, 48 | :new, :ng, :nia, :niu, :njo, :nl, :"nl-BE", :nmg, :nn, :nnh, 49 | :no, :nog, :non, :nov, :nqo, :nr, :nso, :nus, :nv, :nwc, :ny, 50 | :nym, :nyn, :nyo, :nzi, :oc, :oj, :om, :or, :os, :osa, :ota, 51 | :pa, :pag, :pal, :pam, :pap, :pau, :pcd, :pcm, :pdc, :pdt, 52 | :peo, :pfl, :phn, :pi, :pl, :pms, :pnt, :pon, :prg, :pro, 53 | :ps, :pt, :qu, :quc, :qug, :raj, :rap, :rar, :rgn, :rhg, :rif, :rm, 54 | :rn, :ro, :"ro-MD", :rof, :rom, :rtm, :ru, :rue, :rug, 55 | :rup, :rw, :rwk, :sa, :sad, :sah, :sam, :saq, :sas, :sat, :saz, 56 | :sba, :sbp, :sc, :scn, :sco, :sd, :sdc, :sdh, :se, :see, :seh, 57 | :sei, :sel, :ses, :sg, :sga, :sgs, :sh, :shi, :shn, :shu, :si, 58 | :sid, :sk, :sl, :sli, :sly, :sm, :sma, :smj, :smn, :sms, :sn, 59 | :snk, :so, :sog, :sq, :sr, :srn, :srr, :ss, :ssy, :st, :stq, 60 | :su, :suk, :sus, :sux, :sv, :sw, :"sw-CD", :swb, :syc, :syr, 61 | :szl, :ta, :tcy, :te, :tem, :teo, :ter, :tet, :tg, :th, :ti, 62 | :tig, :tiv, :tk, :tkl, :tkr, :tl, :tlh, :tli, :tly, :tmh, 63 | :tn, :to, :tog, :tpi, :tr, :tru, :trv, :ts, :tsd, :tsi, :tt, 64 | :ttt, :tum, :tvl, :tw, :twq, :ty, :tyv, :tzm, :udm, :ug, :uga, 65 | :uk, :umb, :und, :ur, :uz, :vai, :ve, :vec, :vep, :vi, :vls, 66 | :vmf, :vo, :vot, :vro, :vun, :wa, :wae, :wal, :war, :was, 67 | :wbp, :wo, :wuu, :xal, :xh, :xmf, :xog, :yao, :yap, :yav, 68 | :ybb, :yi, :yo, :yrl, :yue, :za, :zap, :zbl, :zea, :zen, 69 | :zgh, :zh, :"zh-Hans", :"zh-Hant", :zu, :zun, :zxx, :zza, 70 | ] 71 | # rubocop:enable Layout/MultilineArrayLineBreaks 72 | 73 | languages = Cldr::Export::Data::Languages.new(:de)[:languages] 74 | 75 | assert_empty codes - languages.keys, "Unexpected missing languages" 76 | assert_empty languages.keys - codes, "Unexpected extra languages" 77 | assert_equal("Deutsch", languages[:de]) 78 | end 79 | 80 | test "languages does not overwrite long form with the short one" do 81 | languages = Cldr::Export::Data::Languages.new(:en)[:languages] 82 | 83 | assert_equal "American English", languages[:"en-US"] 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/cldr/format/date.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Format 5 | class Date < Datetime::Base 6 | PATTERN = /G{1,5}|y+|Y+|Q{1,4}|q{1,5}|M{1,5}|L{1,5}|d{1,2}|F{1}|E{1,5}|e{1,5}|c{1,5}/ 7 | METHODS = { # ignoring u, l, g, j, A 8 | "G" => :era, 9 | "y" => :year, 10 | "Y" => :year_of_week_of_year, 11 | "Q" => :quarter, 12 | "q" => :quarter_stand_alone, 13 | "M" => :month, 14 | "L" => :month_stand_alone, 15 | "w" => :week_of_year, 16 | "W" => :week_of_month, 17 | "d" => :day, 18 | "D" => :day_of_month, 19 | "F" => :day_of_week_in_month, 20 | "E" => :weekday, 21 | "e" => :weekday_local, 22 | "c" => :weekday_local_stand_alone, 23 | } 24 | 25 | def era(date, pattern, length) 26 | raise NotImplementedError, "not implemented" 27 | end 28 | 29 | def year(date, pattern, length) 30 | year = date.year.to_s 31 | year = year.length == 1 ? year : year[-2, 2] if length == 2 32 | year = year.rjust(length, "0") if length > 1 33 | year 34 | end 35 | 36 | def year_of_week_of_year(date, pattern, length) 37 | raise NotImplementedError, "not implemented" 38 | end 39 | 40 | def day_of_week_in_month(date, pattern, length) # e.g. 2nd Wed in July 41 | raise NotImplementedError, "not implemented" 42 | end 43 | 44 | def quarter(date, pattern, length) 45 | quarter = (date.month.to_i - 1) / 3 + 1 46 | case length 47 | when 1 48 | quarter.to_s 49 | when 2 50 | quarter.to_s.rjust(length, "0") 51 | when 3 52 | calendar[:quarters][:format][:abbreviated][quarter] 53 | when 4 54 | calendar[:quarters][:format][:wide][quarter] 55 | end 56 | end 57 | 58 | def quarter_stand_alone(date, pattern, length) 59 | quarter = (date.month.to_i - 1) / 3 + 1 60 | case length 61 | when 1 62 | quarter.to_s 63 | when 2 64 | quarter.to_s.rjust(length, "0") 65 | when 3 66 | raise NotImplementedError, 'not yet implemented (requires cldr\'s "multiple inheritance")' 67 | # calendar[:quarters][:stand_alone][:abbreviated][key] 68 | when 4 69 | raise NotImplementedError, 'not yet implemented (requires cldr\'s "multiple inheritance")' 70 | # calendar[:quarters][:stand_alone][:wide][key] 71 | when 5 72 | calendar[:quarters][:stand_alone][:narrow][quarter] 73 | end 74 | end 75 | 76 | def month(date, pattern, length) 77 | case length 78 | when 1 79 | date.month.to_s 80 | when 2 81 | date.month.to_s.rjust(length, "0") 82 | when 3 83 | calendar[:months][:format][:abbreviated][date.month] 84 | when 4 85 | calendar[:months][:format][:wide][date.month] 86 | when 5 87 | raise NotImplementedError, 'not yet implemented (requires cldr\'s "multiple inheritance")' 88 | # calendar[:months][:format][:narrow][date.month] 89 | else 90 | # raise unknown date format 91 | end 92 | end 93 | 94 | def month_stand_alone(date, pattern, length) 95 | case length 96 | when 1 97 | date.month.to_s 98 | when 2 99 | date.month.to_s.rjust(length, "0") 100 | when 3 101 | raise NotImplementedError, 'not yet implemented (requires cldr\'s "multiple inheritance")' 102 | # calendar[:months][:stand_alone][:abbreviated][date.month] 103 | when 4 104 | raise NotImplementedError, 'not yet implemented (requires cldr\'s "multiple inheritance")' 105 | # calendar[:months][:stand_alone][:wide][date.month] 106 | when 5 107 | calendar[:months][:stand_alone][:narrow][date.month] 108 | else 109 | # raise unknown date format 110 | end 111 | end 112 | 113 | def day(date, pattern, length) 114 | case length 115 | when 1 116 | date.day.to_s 117 | when 2 118 | date.day.to_s.rjust(length, "0") 119 | end 120 | end 121 | 122 | WEEKDAY_KEYS = [:sun, :mon, :tue, :wed, :thu, :fri, :sat] 123 | 124 | def weekday(date, pattern, length) 125 | key = WEEKDAY_KEYS[date.wday] 126 | case length 127 | when 1..3 128 | calendar[:days][:format][:abbreviated][key] 129 | when 4 130 | calendar[:days][:format][:wide][key] 131 | when 5 132 | calendar[:days][:stand_alone][:narrow][key] 133 | end 134 | end 135 | 136 | def weekday_local(date, pattern, length) 137 | # "Like E except adds a numeric value depending on the local starting day of the week" 138 | raise NotImplementedError, "not implemented (need to defer a country to lookup the local first day of week from weekdata)" 139 | end 140 | 141 | def weekday_local_stand_alone(date, pattern, length) 142 | raise NotImplementedError, "not implemented (need to defer a country to lookup the local first day of week from weekdata)" 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## How do I make a good changelog? 7 | ### Guiding Principles 8 | - Changelogs are for humans, not machines. 9 | - There should be an entry for every single version. 10 | - The same types of changes should be grouped. 11 | - Versions and sections should be linkable. 12 | - The latest version comes first. 13 | - The release date of each version is displayed. 14 | - Mention whether you follow Semantic Versioning. 15 | 16 | ### Types of changes 17 | - Added for new features. 18 | - Changed for changes in existing functionality. 19 | - Deprecated for soon-to-be removed features. 20 | - Removed for now removed features. 21 | - Fixed for any bug fixes. 22 | - Security in case of vulnerabilities. 23 | 24 | ## [Unreleased] 25 | 26 | - Only export plurals.rb for those that have plurals data, [#71](https://github.com/ruby-i18n/ruby-cldr/pull/71) 27 | - Only export plural keys for currencies that have pluralization data, [#80](https://github.com/ruby-i18n/ruby-cldr/pull/80) 28 | - Sort the exported data by key, [#82](https://github.com/ruby-i18n/ruby-cldr/pull/82) 29 | - Prune empty hashes / files before outputting, [#86](https://github.com/ruby-i18n/ruby-cldr/pull/86) 30 | - Re-add the `ParentLocales` component, this time as a shared component, [#91](https://github.com/ruby-i18n/ruby-cldr/pull/91) 31 | - Changed the keys and values of `ParentLocales` component to be symbols, [#101](https://github.com/ruby-i18n/ruby-cldr/pull/101) 32 | - Fixed bug with fallbacks for locales that had more than two segments, [#101](https://github.com/ruby-i18n/ruby-cldr/pull/101) 33 | - Merge all the related data files before doing lookups, [#98](https://github.com/ruby-i18n/ruby-cldr/pull/98) 34 | - Standardize component names for the `thor cldr:export` command (and internally in the codebase), [#121](https://github.com/ruby-i18n/ruby-cldr/pull/121) 35 | - Standardize locale names for the `thor cldr:export` command (and internally in the codebase), [#121](https://github.com/ruby-i18n/ruby-cldr/pull/121) 36 | - Output `plurals.rb` with the `ruby-cldr` style locale codes (only affects `pt-PT` in CLDR v34), [#121](https://github.com/ruby-i18n/ruby-cldr/pull/121) 37 | - Export data at with a consistent minimum draft status, [#124](https://github.com/ruby-i18n/ruby-cldr/pull/124) 38 | - Add `--draft-status` flag for specifying the minimum draft status for data to be exported, [#124](https://github.com/ruby-i18n/ruby-cldr/pull/124) 39 | - Export locale-specific data files into `locales` subdirectory, [#135](https://github.com/ruby-i18n/ruby-cldr/pull/135) 40 | - Inherit currency symbol from ancestor locale instead of using other versions, [#137](https://github.com/ruby-i18n/ruby-cldr/pull/137) 41 | - Export region validity data, [#179](https://github.com/ruby-i18n/ruby-cldr/pull/179) 42 | - `Layout` component no longer exports files unless they contain data, [#183](https://github.com/ruby-i18n/ruby-cldr/pull/183) 43 | - Sort the data at the component level, allowing components to specify their own sort orders, [#200](https://github.com/ruby-i18n/ruby-cldr/pull/200) 44 | - Export `` data, [#206](https://github.com/ruby-i18n/ruby-cldr/pull/206) 45 | - `Numbers` component now outputs data from all number systems, [#189](https://github.com/ruby-i18n/ruby-cldr/pull/189) 46 | - Use `snake_case` for key names unless they are an external identifier, [#207](https://github.com/ruby-i18n/ruby-cldr/pull/207) 47 | - Add `WeekData` component, [#229](https://github.com/ruby-i18n/ruby-cldr/pull/229) 48 | - Drop support for Ruby 2, [#265](https://github.com/ruby-i18n/ruby-cldr/pull/265) 49 | - Drop support for Ruby 3.0, [#268](https://github.com/ruby-i18n/ruby-cldr/pull/268) 50 | 51 | --- 52 | 53 | ## [0.5.0] - 2020-11-20 54 | 55 | - Added a changelog, [#49](https://github.com/ruby-i18n/ruby-cldr/pull/49) 56 | - Added Travis CI for testing, [#48](https://github.com/ruby-i18n/ruby-cldr/pull/48) 57 | - Added root fallback to en language, [#47](https://github.com/ruby-i18n/ruby-cldr/pull/47) 58 | - Added subdivisions to the list of exportable components, [#46](https://github.com/ruby-i18n/ruby-cldr/pull/46) 59 | - Added country codes as an exportable component, [#61](https://github.com/ruby-i18n/ruby-cldr/pull/61) 60 | - Added narrow symbols to exported currency data, [#64](https://github.com/ruby-i18n/ruby-cldr/pull/64) 61 | 62 | ## [0.4.0] - 2020-09-01 63 | 64 | - Support pluralization codes with missing spaces [#53](https://github.com/ruby-i18n/ruby-cldr/pull/53) 65 | - Add in functionality to export country codes [#61](https://github.com/ruby-i18n/ruby-cldr/pull/61) 66 | 67 | ## [0.3.0] - 2019-06-16 68 | 69 | - Export currency names [#51](https://github.com/ruby-i18n/ruby-cldr/pull/51) 70 | - Bring back root fallback for english [#47](https://github.com/ruby-i18n/ruby-cldr/pull/47) 71 | - Export subdivisions [#46](https://github.com/ruby-i18n/ruby-cldr/pull/46) 72 | 73 | ## [0.2.0] - 2019-03-26 74 | 75 | - Updated to CLDR 34 [#43](https://github.com/ruby-i18n/ruby-cldr/pull/43) 76 | - Lots of [other changes](https://github.com/ruby-i18n/ruby-cldr/compare/v0.1.1...v0.2.0) 77 | 78 | [Unreleased]: https://github.com/ruby-i18n/ruby-cldr/compare/v0.5.0...HEAD 79 | -------------------------------------------------------------------------------- /ruby-cldr.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # Generated by jeweler 3 | # DO NOT EDIT THIS FILE DIRECTLY 4 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 5 | # frozen_string_literal: true 6 | 7 | # stub: ruby-cldr 0.5.0 ruby lib 8 | 9 | Gem::Specification.new do |s| 10 | s.name = "ruby-cldr" 11 | s.version = "0.5.0" 12 | 13 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to?(:required_rubygems_version=) 14 | s.require_paths = ["lib"] 15 | s.authors = ["Sven Fuchs"] 16 | s.description = "Ruby library for exporting and using data from CLDR, see http://cldr.unicode.org" 17 | s.email = "svenfuchs@artweb-design.de" 18 | s.extra_rdoc_files = [ 19 | "CHANGELOG.md", 20 | "LICENSE", 21 | "README.md", 22 | ] 23 | s.files = [ 24 | "CHANGELOG.md", 25 | "Gemfile", 26 | "Gemfile.lock", 27 | "LICENSE", 28 | "README.md", 29 | "Rakefile", 30 | "VERSION", 31 | "cldr.thor", 32 | "lib/cldr.rb", 33 | "lib/cldr/data.rb", 34 | "lib/cldr/download.rb", 35 | "lib/cldr/draft_status.rb", 36 | "lib/cldr/export.rb", 37 | "lib/cldr/export/code.rb", 38 | "lib/cldr/export/code/numbers.rb", 39 | "lib/cldr/export/data.rb", 40 | "lib/cldr/export/data/aliases.rb", 41 | "lib/cldr/export/data/base.rb", 42 | "lib/cldr/export/data/calendars.rb", 43 | "lib/cldr/export/data/calendars/gregorian.rb", 44 | "lib/cldr/export/data/characters.rb", 45 | "lib/cldr/export/data/country_codes.rb", 46 | "lib/cldr/export/data/currencies.rb", 47 | "lib/cldr/export/data/currency_digits_and_rounding.rb", 48 | "lib/cldr/export/data/delimiters.rb", 49 | "lib/cldr/export/data/fields.rb", 50 | "lib/cldr/export/data/languages.rb", 51 | "lib/cldr/export/data/layout.rb", 52 | "lib/cldr/export/data/likely_subtags.rb", 53 | "lib/cldr/export/data/lists.rb", 54 | "lib/cldr/export/data/metazones.rb", 55 | "lib/cldr/export/data/numbering_systems.rb", 56 | "lib/cldr/export/data/numbers.rb", 57 | "lib/cldr/export/data/parent_locales.rb", 58 | "lib/cldr/export/data/plural_rules.rb", 59 | "lib/cldr/export/data/plurals.rb", 60 | "lib/cldr/export/data/plurals/cldr_grammar.treetop", 61 | "lib/cldr/export/data/plurals/grammar.rb", 62 | "lib/cldr/export/data/plurals/rules.rb", 63 | "lib/cldr/export/data/rbnf.rb", 64 | "lib/cldr/export/data/rbnf_root.rb", 65 | "lib/cldr/export/data/region_currencies.rb", 66 | "lib/cldr/export/data/segments_root.rb", 67 | "lib/cldr/export/data/subdivisions.rb", 68 | "lib/cldr/export/data/territories.rb", 69 | "lib/cldr/export/data/territories_containment.rb", 70 | "lib/cldr/export/data/timezones.rb", 71 | "lib/cldr/export/data/transforms.rb", 72 | "lib/cldr/export/data/units.rb", 73 | "lib/cldr/export/data/variables.rb", 74 | "lib/cldr/export/data/windows_zones.rb", 75 | "lib/cldr/export/data_file.rb", 76 | "lib/cldr/export/deep_validate_keys.rb", 77 | "lib/cldr/export/ruby.rb", 78 | "lib/cldr/export/yaml.rb", 79 | "lib/cldr/format.rb", 80 | "lib/cldr/format/currency.rb", 81 | "lib/cldr/format/date.rb", 82 | "lib/cldr/format/datetime.rb", 83 | "lib/cldr/format/datetime/base.rb", 84 | "lib/cldr/format/decimal.rb", 85 | "lib/cldr/format/decimal/base.rb", 86 | "lib/cldr/format/decimal/fraction.rb", 87 | "lib/cldr/format/decimal/integer.rb", 88 | "lib/cldr/format/decimal/number.rb", 89 | "lib/cldr/format/percent.rb", 90 | "lib/cldr/format/time.rb", 91 | "lib/cldr/locale.rb", 92 | "lib/cldr/locale/fallbacks.rb", 93 | "lib/cldr/thor.rb", 94 | "lib/cldr/validate.rb", 95 | "lib/core_ext/hash/deep_merge.rb", 96 | "lib/core_ext/hash/deep_prune.rb", 97 | "lib/core_ext/hash/deep_sort.rb", 98 | "lib/core_ext/hash/deep_stringify.rb", 99 | "lib/core_ext/hash/symbolize_keys.rb", 100 | "lib/core_ext/string/camelize.rb", 101 | "lib/core_ext/string/underscore.rb", 102 | "test/all.rb", 103 | "test/core_ext/deep_prune_test.rb", 104 | "test/core_ext/deep_stringify_test.rb", 105 | "test/draft_status_test.rb", 106 | "test/export/code/numbers_test.rb", 107 | "test/export/data/all.rb", 108 | "test/export/data/base_test.rb", 109 | "test/export/data/calendars_test.rb", 110 | "test/export/data/country_codes_test.rb", 111 | "test/export/data/currencies_test.rb", 112 | "test/export/data/delimiters_test.rb", 113 | "test/export/data/languages_test.rb", 114 | "test/export/data/metazones_test.rb", 115 | "test/export/data/numbers_test.rb", 116 | "test/export/data/parent_locales_test.rb", 117 | "test/export/data/plurals_test.rb", 118 | "test/export/data/subdivisions_test.rb", 119 | "test/export/data/territories_containment_test.rb", 120 | "test/export/data/territories_test.rb", 121 | "test/export/data/timezones_test.rb", 122 | "test/export/data/units_test.rb", 123 | "test/export/data/windows_zones_test.rb", 124 | "test/export/data_file_test.rb", 125 | "test/export/yaml_test.rb", 126 | "test/export_test.rb", 127 | "test/format/all.rb", 128 | "test/format/currency_test.rb", 129 | "test/format/date_test.rb", 130 | "test/format/datetime_test.rb", 131 | "test/format/decimal/fraction_test.rb", 132 | "test/format/decimal/integer_test.rb", 133 | "test/format/decimal/number_test.rb", 134 | "test/format/decimal_test.rb", 135 | "test/format/percent_test.rb", 136 | "test/format/time_test.rb", 137 | "test/locale/fallbacks_test.rb", 138 | "test/test_autotest.rb", 139 | "test/test_helper.rb", 140 | ] 141 | s.homepage = "http://github.com/ruby-i18n/ruby-cldr" 142 | s.licenses = ["MIT"] 143 | s.summary = "Ruby library for exporting and using data from CLDR" 144 | 145 | s.add_dependency("i18n", [">= 0"]) 146 | s.add_dependency("nokogiri", [">= 0"]) 147 | s.add_dependency("psych", [">= 4.0.0"]) 148 | s.add_dependency("rubyzip", [">= 0"]) 149 | s.add_dependency("thor", [">= 1.3.0"]) 150 | s.add_development_dependency("jeweler", [">= 0"]) 151 | s.add_development_dependency("pry", [">= 0"]) 152 | s.add_development_dependency("pry-nav", [">= 0"]) 153 | s.add_development_dependency("rubocop-shopify", [">= 0"]) 154 | s.add_development_dependency("ruby-lsp", [">= 0"]) 155 | s.add_development_dependency("test-unit", [">= 0"]) 156 | end 157 | -------------------------------------------------------------------------------- /lib/cldr/export/data/calendars/gregorian.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Calendars 7 | class Gregorian < Base 8 | def initialize(locale) 9 | super 10 | update( 11 | additional_formats: additional_formats, 12 | days: contexts("day"), 13 | eras: eras.deep_sort, 14 | fields: fields.deep_sort, 15 | formats: { 16 | date: formats("date"), 17 | datetime: formats("dateTime"), 18 | time: formats("time"), 19 | }, 20 | months: contexts("month"), 21 | periods: contexts("dayPeriod", group: "alt").deep_sort, 22 | quarters: contexts("quarter"), 23 | ) 24 | end 25 | 26 | def calendar 27 | @calendar ||= select_single('dates/calendars/calendar[@type="gregorian"]') 28 | end 29 | 30 | def contexts(kind, options = {}) 31 | select(calendar, "#{kind}s/#{kind}Context").each_with_object({}) do |node, result| 32 | context = node.attribute("type").value.underscore.to_sym 33 | result[context] = widths(node, kind, context, options) 34 | end 35 | end 36 | 37 | def widths(node, kind, context, options = {}) 38 | select(node, "#{kind}Width").each_with_object({}) do |node, result| 39 | width = node.attribute("type").value.to_sym 40 | result[width] = elements(node, kind, context, width, options) 41 | end 42 | end 43 | 44 | def elements(node, kind, context, width, options = {}) 45 | aliased = select_single(node, "alias") 46 | 47 | if aliased 48 | xpath_to_key(aliased.attribute("path").value, kind, context, width) 49 | else 50 | select(node, kind).each_with_object({}) do |node, result| 51 | key = node.attribute("type").value 52 | key = key =~ /^\d*$/ ? key.to_i : key.underscore.to_sym 53 | 54 | if options[:group] && (found_group = node.attribute(options[:group])) 55 | result[found_group.value.to_sym] ||= {} 56 | result[found_group.value.to_sym][key] = node.content 57 | else 58 | result[key] = node.content 59 | end 60 | end 61 | end 62 | end 63 | 64 | def xpath_to_key(xpath, kind, context, width) 65 | kind = (xpath =~ %r(/([^\/]*)Width) && Regexp.last_match(1)) || kind 66 | context = (xpath =~ %r(Context\[@type='([^\/]*)'\]) && Regexp.last_match(1))&.underscore || context 67 | width = (xpath =~ %r(Width\[@type='([^\/]*)'\]) && Regexp.last_match(1)) || width 68 | :"calendars.gregorian.#{kind}s.#{context}.#{width}" 69 | end 70 | 71 | def eras 72 | if calendar 73 | base_path = calendar.path.sub(%r{^/ldml/}, "") + "/eras" 74 | keys = select("#{base_path}/*").map(&:name) 75 | 76 | keys.each_with_object({}) do |name, result| 77 | path = "#{base_path}/#{name}/*" 78 | key = name.gsub("era", "").gsub(/s$/, "").downcase.to_sym 79 | 80 | key_result = select(path).each_with_object({}) do |node, ret| 81 | if node.name == "alias" 82 | target = (node.attribute("path").value.match(%r{/([^\/]+)$}) && Regexp.last_match(1)).gsub("era", "").gsub(/s$/, "").downcase 83 | break :"calendars.gregorian.eras.#{target}" 84 | end 85 | 86 | type = node.attribute("type").value.to_i 87 | ret[type] = node.content 88 | end 89 | result[key] = key_result unless key_result.empty? 90 | end 91 | else 92 | {} 93 | end 94 | end 95 | 96 | def extract(path, lambdas) 97 | nodes = select(path) 98 | nodes.each_with_object({}) do |node, ret| 99 | key = lambdas[:key].call(node) 100 | ret[key] = lambdas[:value].call(node) 101 | end 102 | end 103 | 104 | def formats(type) 105 | formats = select(calendar, "#{type}Formats/#{type}FormatLength").each_with_object({}) do |node, result| 106 | key = node.attribute("type").value.underscore.to_sym 107 | result[key] = pattern(node, type) 108 | end 109 | if (default = default_format(type)) 110 | formats = default.merge(formats) 111 | end 112 | formats 113 | end 114 | 115 | def additional_formats 116 | select(calendar, "dateTimeFormats/availableFormats/dateFormatItem").each_with_object({}) do |node, result| 117 | key = node.attribute("id").value 118 | result[key] = node.content 119 | end 120 | end 121 | 122 | def default_format(type) 123 | if (node = select_single(calendar, "#{type}Formats/default")) 124 | key = node.attribute("choice").value.to_sym 125 | { default: :"calendars.gregorian.formats.#{type.downcase}.#{key}" } 126 | end 127 | end 128 | 129 | def pattern(node, type) 130 | select(node, "#{type}Format/pattern").each_with_object({}) do |node, result| 131 | pattern = node.content 132 | pattern = pattern.gsub("{0}", "{{time}}").gsub("{1}", "{{date}}") if type == "dateTime" 133 | result[:pattern] = pattern 134 | end 135 | end 136 | 137 | # NOTE: As of CLDR 23, this data moved from inside each "calendar" tag to under its parent, the "dates" tag. 138 | # That probably means this `fields` method should be moved up to the parent as well. 139 | def fields 140 | select("dates/fields/field").each_with_object({}) do |node, result| 141 | key = node.attribute("type").value.underscore.gsub("dayperiod", "day_period").to_sym 142 | name = node.xpath("displayName").first 143 | result[key] = name.content if name 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/cldr/export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "i18n" 4 | require "fileutils" 5 | require "i18n/locale/tag" 6 | require "cldr/export/deep_validate_keys" 7 | require "core_ext/string/camelize" 8 | require "core_ext/string/underscore" 9 | require "core_ext/hash/deep_stringify" 10 | require "core_ext/hash/deep_merge" 11 | require "core_ext/hash/deep_prune" 12 | require "core_ext/hash/deep_sort" 13 | 14 | module Cldr 15 | module Export 16 | autoload :Code, "cldr/export/code" 17 | autoload :Data, "cldr/export/data" 18 | autoload :DataFile, "cldr/export/data_file" 19 | autoload :DataSet, "cldr/export/data_set" 20 | autoload :Element, "cldr/export/element" 21 | autoload :FileBasedDataSet, "cldr/export/file_based_data_set" 22 | autoload :Ruby, "cldr/export/ruby" 23 | autoload :Yaml, "cldr/export/yaml" 24 | 25 | SHARED_COMPONENTS = [ 26 | :Aliases, 27 | :CountryCodes, 28 | :CurrencyDigitsAndRounding, 29 | :LikelySubtags, 30 | :Metazones, 31 | :NumberingSystems, 32 | :ParentLocales, 33 | :RbnfRoot, 34 | :RegionCurrencies, 35 | :RegionValidity, 36 | :SegmentsRoot, 37 | :TerritoriesContainment, 38 | :Transforms, 39 | :Variables, 40 | :WeekData, 41 | :WindowsZones, 42 | ].freeze 43 | 44 | DEFAULT_TARGET = "./data" 45 | 46 | class << self 47 | def base_path 48 | @@base_path ||= File.expand_path(DEFAULT_TARGET) 49 | end 50 | 51 | def base_path=(base_path) 52 | @@base_path = File.expand_path(base_path) 53 | end 54 | 55 | def minimum_draft_status 56 | raise StandardError, "minimum_draft_status is not yet set." unless defined?(@@minimum_draft_status) 57 | 58 | @@minimum_draft_status 59 | end 60 | 61 | def minimum_draft_status=(draft_status) 62 | @@minimum_draft_status = draft_status 63 | end 64 | 65 | def export(options = {}, &block) 66 | locales = options[:locales] || Data::RAW_DATA.locales 67 | components = options[:components] || Data.components 68 | self.minimum_draft_status = options[:minimum_draft_status] if options[:minimum_draft_status] 69 | self.base_path = options[:target] if options[:target] 70 | 71 | shared_components, locale_components = components.partition do |component| 72 | shared_component?(component) 73 | end 74 | 75 | shared_components.each do |component| 76 | case component 77 | when :Transforms 78 | Dir.glob("#{Cldr::Export::Data::RAW_DATA.directory}/transforms/**.xml").each do |transform_file| 79 | data = Data::Transforms.new(transform_file) 80 | source = data[:transforms].first[:source] 81 | target = data[:transforms].first[:target] 82 | variant = data[:transforms].first[:variant] 83 | file_name = [source, target, variant].compact.join("-") 84 | output_path = File.join(base_path, "transforms", "#{file_name}.yml") 85 | write(output_path, data.to_yaml) 86 | yield component, nil, output_path if block_given? 87 | end 88 | else 89 | ex = exporter(component, options[:format]) 90 | ex.export(nil, component, options, &block) 91 | end 92 | end 93 | 94 | locales.each do |locale| 95 | locale_components.each do |component| 96 | exporter(component, options[:format]).export(locale, component, options, &block) 97 | end 98 | end 99 | end 100 | 101 | def exporter(component, format) 102 | name = if format 103 | format 104 | else 105 | component == :Plurals ? "ruby" : "yaml" 106 | end 107 | const_get(name.to_s.camelize).new 108 | end 109 | 110 | def data(component, locale, options = {}) 111 | case component 112 | when :Plurals 113 | plural_data(component, locale, options) 114 | else 115 | if shared_component?(component) 116 | shared_data(component, options) 117 | else 118 | locale_based_data(component, locale, options) 119 | end 120 | end 121 | end 122 | 123 | def locale_based_data(component, locale, options = {}) 124 | locales_to_merge(locale, component, options).inject({}) do |result, locale| 125 | data = Data.const_get(component.to_s).new(locale) 126 | if data 127 | data.is_a?(Hash) ? data.deep_merge(result).deep_sort : data 128 | else 129 | result 130 | end 131 | end 132 | end 133 | 134 | def plural_data(component, locale, options = {}) 135 | result = locales_to_merge(locale, component, options).lazy.map do |ancestor| 136 | Data::Plurals.rules.slice(ancestor) 137 | end.reject(&:empty?).first 138 | 139 | if result && result.keys != [locale] 140 | result[locale] = result.delete(result.keys.first) 141 | end 142 | 143 | result 144 | end 145 | 146 | def shared_data(component, options = {}) 147 | case component 148 | when :Transforms 149 | # do nothing, this has to be handled separately 150 | else 151 | Data.const_get(component.to_s).new 152 | end 153 | end 154 | 155 | def to_i18n(locale) 156 | locale.to_s.gsub("_", "-").to_sym 157 | end 158 | 159 | def from_i18n(locale) 160 | locale.to_s.gsub("-", "_").to_sym 161 | end 162 | 163 | def locales_to_merge(locale, component, options) 164 | if options[:merge] 165 | locales = Cldr.fallbacks[locale] 166 | locales.reject! { |locale| locale == :root } unless should_merge_root?(component) 167 | locales 168 | else 169 | [locale] 170 | end 171 | end 172 | 173 | def write(path, data) 174 | FileUtils.rm_rf(path) 175 | FileUtils.mkdir_p(File.dirname(path)) 176 | File.write(path, data) 177 | end 178 | 179 | def path(locale, component, extension) 180 | path = [Export.base_path] 181 | unless shared_component?(component) 182 | path << "locales" 183 | path << locale.to_s 184 | end 185 | path << "#{component.to_s.underscore}.#{extension}" 186 | File.join(*path) 187 | end 188 | 189 | def shared_component?(component) 190 | SHARED_COMPONENTS.include?(component) 191 | end 192 | 193 | def should_merge_root?(component) 194 | return false if [:Rbnf, :Fields].include?(component) 195 | 196 | true 197 | end 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/cldr/export/data/numbers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cldr 4 | module Export 5 | module Data 6 | class Numbers < Base 7 | def initialize(locale) 8 | super 9 | update( 10 | numbers: number_systems, 11 | ) 12 | deep_sort! 13 | end 14 | 15 | private 16 | 17 | FORMAT_TYPES = ["currency", "decimal", "percent", "scientific"].freeze 18 | 19 | def number_systems 20 | number_systems = select("/descendant::*[attribute::numberSystem]").map { |node| node["numberSystem"] }.uniq.map(&:to_sym) 21 | number_systems.to_h do |number_system| 22 | children = { 23 | formats: FORMAT_TYPES.to_h do |type| 24 | results = { patterns: format(number_system, type) } 25 | results.merge!({ unit: unit(number_system) }) if type == "currency" 26 | [type.to_sym, results] 27 | end, 28 | symbols: symbols(number_system), 29 | } 30 | [number_system, children] 31 | end 32 | end 33 | 34 | def symbols(number_system) 35 | number_system_node = select_single("numbers/symbols[@numberSystem=\"#{number_system}\"]") 36 | 37 | aliased = select_single(number_system_node, "alias") 38 | if aliased 39 | return xpath_to_symbols_alias(aliased["path"]) 40 | end 41 | 42 | select("numbers/symbols[@numberSystem=\"#{number_system}\"]/*").each_with_object({}) do |node, result| 43 | result[name(node).to_sym] = node.content 44 | end 45 | end 46 | 47 | def format(number_system, type) 48 | number_system_node = select_single("numbers/#{type}Formats[@numberSystem=\"#{number_system}\"]") 49 | return {} unless number_system_node 50 | 51 | aliased = select_single(number_system_node, "alias") 52 | if aliased 53 | return xpath_to_format_alias(aliased["path"], type) 54 | end 55 | 56 | result = select("numbers/#{type}Formats[@numberSystem=\"#{number_system}\"]/#{type}FormatLength").each_with_object({}) do |format_length_node, format_result| 57 | format_length_key = format_length_node["type"]&.to_sym || default_format_length_type 58 | 59 | aliased = select_single(format_length_node, "alias") 60 | if aliased 61 | format_result[format_length_key] = xpath_to_format_length_alias(aliased["path"], number_system, type) 62 | next 63 | end 64 | 65 | format_result[format_length_key] = if format_length_key == default_format_length_type 66 | parse_default_format_length_node(number_system, format_length_node, type) 67 | else 68 | parse_format_length_node(format_length_node, type) 69 | end 70 | end 71 | 72 | result 73 | end 74 | 75 | def parse_default_format_length_node(number_system, format_length_node, type) 76 | result = {} 77 | select(format_length_node, "#{type}Format").each do |format_node| 78 | format_key = format_node["type"]&.to_sym || default_format_type 79 | 80 | if (aliased = select_single(format_node, "alias")) 81 | result[format_key] = xpath_to_default_format_length_node_alias(aliased["path"], number_system, default_format_length_type) 82 | else 83 | pattern_node = select_single(format_node, "pattern[not(@alt)]") # https://github.com/ruby-i18n/ruby-cldr/issues/125 84 | next unless pattern_node 85 | 86 | result[format_key] = pattern_node.content 87 | end 88 | end 89 | result 90 | end 91 | 92 | def parse_format_length_node(format_length_node, type) 93 | result = {} 94 | select(format_length_node, "#{type}Format").each do |format_node| 95 | format_key = format_node["type"]&.to_sym || default_format_type 96 | 97 | result[format_key] ||= select(format_node, "pattern").each_with_object({}) do |pattern_node, pattern_result| 98 | pattern_key = pattern_node["type"]&.to_sym || default_pattern_type 99 | pattern_count = pattern_node["count"]&.to_sym 100 | 101 | if pattern_count 102 | if pattern_result[pattern_key].nil? 103 | pattern_result[pattern_key] ||= {} 104 | elsif !pattern_result[pattern_key].is_a?(Hash) 105 | raise "can't parse patterns with and without 'count' attribute in the same section" 106 | end 107 | 108 | pattern_result[pattern_key][pattern_count] = pattern_node.content 109 | else 110 | pattern_result[pattern_key] = pattern_node.content 111 | end 112 | end 113 | end 114 | result 115 | end 116 | 117 | def default_format_length_type 118 | # TODO: It would be better is this were one of the valid values for the type attribute 119 | # 120 | # But I haven't been able to figure out what the default is. 121 | @default_format_length_type ||= :default 122 | end 123 | 124 | def default_format_type 125 | @default_format_type ||= begin 126 | # Verify that the default format type has not changed / is the same for all the types 127 | ldml_dtd_file = File.read("vendor/cldr/common/dtd/ldml.dtd") 128 | FORMAT_TYPES.each do |type| 129 | next if ldml_dtd_file.include?("") 130 | 131 | raise "The default type for #{type}Format has changed. Some code will need to be updated." 132 | end 133 | :standard 134 | end 135 | end 136 | 137 | def default_pattern_type 138 | @default_pattern_type ||= begin 139 | ldml_dtd_file = File.read("vendor/cldr/common/dtd/ldml.dtd") 140 | ldml_dtd_file.match("")[1] 141 | end.to_sym 142 | end 143 | 144 | def xpath_to_default_format_length_node_alias(xpath, number_system, format_length_key) 145 | match = xpath.match(%r{\.\./currencyFormat\[@type='(\w+)+'\]}) 146 | raise "Alias doesn't match expected pattern: #{xpath}" unless match 147 | 148 | target_type = match[1] 149 | :"numbers.#{number_system}.formats.currency.patterns.#{format_length_key}.#{target_type}" 150 | end 151 | 152 | def xpath_to_format_length_alias(xpath, number_system, type) 153 | match = xpath.match(%r{\.\./#{type}FormatLength\[@type='(\w+)'\]}) 154 | raise "Alias doesn't match expected pattern: #{xpath}" unless match 155 | 156 | length = match[1] 157 | :"numbers.#{number_system}.formats.#{type}.patterns.#{length}" 158 | end 159 | 160 | def xpath_to_symbols_alias(xpath) 161 | match = xpath.match(%r{\.\./symbols\[@numberSystem='(\w+)'\]}) 162 | raise "Alias doesn't match expected pattern: #{xpath}" unless match 163 | 164 | target_number_system = match[1] 165 | :"numbers.#{target_number_system}.symbols" 166 | end 167 | 168 | def xpath_to_format_alias(xpath, type) 169 | match = xpath.match(%r{\.\./#{type}Formats\[@numberSystem='(\w+)'\]}) 170 | raise "Alias doesn't match expected pattern: #{xpath}" unless match 171 | 172 | target_number_system = match[1] 173 | :"numbers.#{target_number_system}.formats.#{type}" 174 | end 175 | 176 | def unit(number_system) 177 | select("numbers/currencyFormats[@numberSystem=\"#{number_system}\"]/unitPattern").each_with_object({}) do |node, result| 178 | count = node["count"].to_sym 179 | result[count] = node.content 180 | end 181 | end 182 | end 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/export/data_file_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.join(File.expand_path(File.dirname(__FILE__)), "../test_helper") 4 | 5 | class TestDataFile < Test::Unit::TestCase 6 | def cldr_data 7 | File.read("#{Cldr::Export::Data::RAW_DATA.directory}/main/de.xml") 8 | end 9 | 10 | test "merging" do 11 | first_contents = <<~XML_CONTENTS 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Q 21 | 22 | in {0} Q 23 | in {0} Q 24 | 25 | 26 | vor {0} Q 27 | vor {0} Q 28 | 29 | 30 | 31 | 32 | XML_CONTENTS 33 | 34 | first_parsed = Cldr::Export::DataFile.parse(first_contents) 35 | 36 | second_contents = <<~XML_CONTENTS 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Port. 49 | Alt. 50 | Di. 51 | Ges. 52 | 53 | 54 | 55 | 56 | XML_CONTENTS 57 | 58 | second_parsed = Cldr::Export::DataFile.parse(second_contents) 59 | 60 | merged = first_parsed.merge(second_parsed) 61 | 62 | # Inputs are unchanged 63 | assert_equal(first_contents, first_parsed.doc.to_xml) 64 | assert_equal(second_contents, second_parsed.doc.to_xml) 65 | 66 | merged_with_mutation = first_parsed.merge!(second_parsed) 67 | 68 | expected = <<~XML_CONTENTS 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Q 78 | 79 | in {0} Q 80 | in {0} Q 81 | 82 | 83 | vor {0} Q 84 | vor {0} Q 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Port. 97 | Alt. 98 | Di. 99 | Ges. 100 | 101 | 102 | 103 | 104 | XML_CONTENTS 105 | 106 | assert_instance_of(Cldr::Export::DataFile, merged) 107 | assert_equal(expected, merged.doc.to_xml) 108 | assert_instance_of(Cldr::Export::DataFile, merged_with_mutation) 109 | assert_equal(expected, merged_with_mutation.doc.to_xml) 110 | end 111 | 112 | test "locale parsing" do 113 | xml_contents = <<~XML_CONTENTS 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | XML_CONTENTS 122 | 123 | parsed = Cldr::Export::DataFile.parse(xml_contents) 124 | 125 | assert_equal(:de, parsed.locale) 126 | 127 | xml_contents = <<~XML_CONTENTS 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | XML_CONTENTS 137 | 138 | parsed = Cldr::Export::DataFile.parse(xml_contents) 139 | 140 | assert_equal(:"de-CH", parsed.locale) 141 | end 142 | 143 | test "locale parsing returns nil when missing" do 144 | xml_contents = <<~XML_CONTENTS 145 | 146 | 147 | 148 | 149 | 150 | 151 | XML_CONTENTS 152 | 153 | parsed = Cldr::Export::DataFile.parse(xml_contents) 154 | 155 | assert_nil(parsed.locale) 156 | end 157 | end 158 | 159 | class TestDataFileDraftStatusFilter < Test::Unit::TestCase 160 | def setup; end # We don't want the default behaviour for these. 161 | 162 | def cldr_data 163 | File.read("#{Cldr::Export::Data::RAW_DATA.directory}/main/de.xml") 164 | end 165 | 166 | test "filters file by draft status" do 167 | unconfirmed_count = pairs(Cldr::Export::DataFile.parse(cldr_data, minimum_draft_status: Cldr::DraftStatus::UNCONFIRMED)).count 168 | provisional_count = pairs(Cldr::Export::DataFile.parse(cldr_data, minimum_draft_status: Cldr::DraftStatus::PROVISIONAL)).count 169 | contributed_count = pairs(Cldr::Export::DataFile.parse(cldr_data, minimum_draft_status: Cldr::DraftStatus::CONTRIBUTED)).count 170 | approved_count = pairs(Cldr::Export::DataFile.parse(cldr_data, minimum_draft_status: Cldr::DraftStatus::APPROVED)).count 171 | 172 | assert(unconfirmed_count >= provisional_count, "Found #{unconfirmed_count} unconfirmed pairs, and #{provisional_count} provisional pairs") 173 | assert(provisional_count >= contributed_count, "Found #{provisional_count} provisional pairs, and #{contributed_count} contributed pairs") 174 | assert(contributed_count >= approved_count, "Found #{contributed_count} contributed pairs, and #{approved_count} approved pairs") 175 | end 176 | 177 | test "removes draft pairs and empty ancestors" do 178 | xml_contents = <<~XML_CONTENTS 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | letztes Quartal 188 | dieses Quartal 189 | nächstes Quartal 190 | 191 | in {0} Q 192 | in {0} Q 193 | 194 | 195 | vor {0} Q 196 | vor {0} Q 197 | 198 | 199 | 200 | 201 | XML_CONTENTS 202 | 203 | parsed = Cldr::Export::DataFile.parse(xml_contents, minimum_draft_status: Cldr::DraftStatus::APPROVED) 204 | 205 | expected = <<~XML_CONTENTS 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | XML_CONTENTS 214 | assert_equal(expected, parsed.doc.to_xml) 215 | end 216 | 217 | private 218 | 219 | def pairs(doc) 220 | return to_enum(:pairs, doc) unless block_given? 221 | 222 | doc.traverse do |node| 223 | next unless node.text? 224 | 225 | yield node.path, node.text 226 | end 227 | end 228 | end 229 | --------------------------------------------------------------------------------