lib/isodoc/i18n.rb



require "htmlentities"
require "twitter_cldr"
require_relative "i18n-yaml"
require_relative "date"
require_relative "l10n"
require_relative "liquid/liquid"
require "liquid"
require_relative "i18n/version"
require "base64"

module IsoDoc
  class I18n
    attr_accessor :labels

    def initialize(lang, script, locale: nil, i18nyaml: nil, i18nhash: nil)
      @lang = lang
      @script = script
      @locale = locale
      @cal = calendar_data
      @cal_en = TwitterCldr::Shared::Calendar.new(:en)
      @c = HTMLEntities.new
      init_labels(i18nyaml, i18nhash)
      liquid_init
    end

    def liquid_init
      ::IsoDoc::I18n::Liquid.set(self)
      ::Liquid::Template.register_filter(::IsoDoc::I18n::Liquid)
    end

    def calendar_data
      TwitterCldr::Shared::Calendar.new(tw_cldr_lang)
    rescue StandardError
      TwitterCldr::Shared::Calendar.new(:en)
    end

    def init_labels(i18nyaml, i18nhash)
      @labels = load_yaml(@lang, @script, i18nyaml, i18nhash)
      @labels["language"] = @lang
      @labels["script"] = @script
      @labels.each_key do |k|
        self.class.send(:define_method, k.downcase) { get[k] }
      end
    end

    # populate with variables, Liquid, inflections, ordinals/spellout
    def populate(keys, vars = {})
      ::Liquid::Template.parse(@labels.dig(*Array(keys)))
        .render(vars.merge("labels" => @labels))
    end

    def boolean_conj(list, conn)
      case list.size
      when 0 then ""
      when 1 then list.first
      when 2 then @labels["binary_#{conn}"].sub(/%1/, list[0])
        .sub(/%2/, list[1])
      else
        @labels["multiple_#{conn}"]
          .sub(/%1/, l10n(list[0..-2].join(enum_comma), @lang, @script))
          .sub(/%2/, list[-1])
      end
    end

    def enum_comma
      %w(Hans Hant).include?(@script) and return "、"
      ", "
    end

    def cleanup_entities(text, is_xml: true)
      if is_xml
        text.split(/([<>])/).each_slice(4).map do |a|
          a[0] = @c.decode(a[0])
          a
        end.join
      else
        @c.decode(text)
      end
    end

    # ord class is either SpelloutRules or OrdinalRules
    def inflect_ordinal(num, term, ord_class)
      lbl = if @labels["ordinal_keys"].nil? || @labels["ordinal_keys"].empty?
              @labels[ord_class]
            else @labels[ord_class][ordinal_key(term)]
            end
      tw_cldr_localize(num).to_rbnf_s(ord_class, lbl)
    rescue StandardError
      num.localize(@lang.to_sym).to_rbnf_s(ord_class, lbl)
    end

    def tw_cldr_localize(num)
      num.localize(tw_cldr_lang)
    rescue StandardError
      num.localize(:en)
    end

    INFLECTIONS = {
      number: "sg",
      case: "nom",
      gender: "m",
      person: "3rd",
      voice: "act",
      mood: "ind",
      tense: "pres",
    }.freeze

    INFLECTION_ORDER = %i(voice mood tense number case gender person).freeze

    def ordinal_key(term)
      @labels["ordinal_keys"].each_with_object([]) do |k, m|
        m << (term[k.to_s] || INFLECTIONS[k.to_sym])
      end.join(".")
    end

    def tw_cldr_lang
      if @lang == "zh" && @script == "Hans" then :"zh-cn"
      elsif @lang == "zh" && @script == "Hant" then :"zh-tw"
      else @lang.to_sym
      end
    end

    # can skip category if not present
    def inflect(word, options)
      i = @labels.dig("inflection", word) or return word
      i.is_a? String and return i
      INFLECTION_ORDER.each do |x|
        infl = options[x] || INFLECTIONS[x]
        i = i[infl] if i[infl]
        i.is_a? String and return i
      end
      word
    end
  end
end