diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 16ca594b1..d8b451a19 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -80,4 +80,133 @@ module I18next::Plurals "sk" => PluralForms::Special_Czech_Slovak, "sl" => PluralForms::Special_Slovenian, } + + # The array indices matches the PluralForms enum above + private NUMBERS = [ + [1, 2], # 1 + [1, 2], # 2 + [1], # 3 + [1, 2, 5], # 4 + [0, 1, 2, 3, 11, 100], # 5 + [1, 2, 5], # 6 + [1, 2, 5], # 7 + [1, 2, 3, 8], # 8 + [1, 2], # 9 (not used) + [1, 2, 3, 7, 11], # 10 + [1, 2, 3, 20], # 11 + [1, 2], # 12 + [0, 1], # 13 + [1, 2, 3, 4], # 14 + [1, 2, 10], # 15 + [1, 2, 0], # 16 + [1, 2], # 17 + [0, 1, 2], # 18 + [1, 2, 11, 20], # 19 + [1, 2, 20], # 20 + [5, 1, 2, 3], # 21 + [1, 2, 20, 21], # 22 + ] + + # "or" () + private NUMBERS_OR = [2, 1] + + # ----------------------------------- + # I18next plural resolver class + # ----------------------------------- + + class Resolver + @@forms : Hash(String, PluralForms) = init_rules() + @@version : UInt8 = 3 + + # Options + property simplify_plural_suffix : Bool = true + + # Suffixes + SUFFIXES_V1 = { + "", + "_plural_1", + "_plural_2", + "_plural_3", + "_plural_11", + "_plural_100", + } + SUFFIXES_V2 = {"_0", "_1", "_2", "_3", "_11", "_100"} + SUFFIXES_V3 = {"_0", "_1", "_2", "_3", "_4", "_5"} + + def initialize(version : UInt8 = 3) + # Sanity checks + # V4 isn't supported, as it requires a full CLDR database. + if version > 4 || version == 0 + raise "Invalid i18next version: v#{version}." + elsif version == 4 + # Logger.error("Unsupported i18next version: v4. Falling back to v3") + @@version = 3 + else + @@version = version + end + end + + def self.init_rules + # Look into sets + PLURAL_SETS.each do |form, langs| + langs.each { |lang| @@forms[lang] = form } + end + + # Add plurals from the "singles" set + @@forms.merge!(PLURAL_SINGLES) + end + + def get_plural_form(locale : String) : PluralForms + # Extract the ISO 639-1 or 639-2 code from an RFC 5646 + # language code, except for pt-BR which needs to be kept as-is. + if locale.starts_with?("pt-BR") + locale = "pt-BR" + else + locale = locale.split('-')[0] + end + + return @@forms[locale] if @@forms[locale]? + + # If nothing was found, then use the most common form, i.e + # one singular and one plural, as in english. Not perfect, + # but better than yielding an exception at the user. + return PluralForms::Single_not_one + end + + def get_suffix(locale : String, count : Int) : String + # Checked count must be absolute. In i18next, `rule.noAbs` is used to + # determine if comparison should be done on a signed or unsigned integer, + # but this variable is never set, resulting in the comparison always + # being done on absolute numbers. + return get_suffix_retrocompat(locale, count.abs) + end + + def get_suffix_retrocompat(locale : String, count : Int) : String + # Get plural form + plural_form = get_plural_form(locale) + rule_numbers = (locale == "or") ? NUMBERS_OR : NUMBERS[plural_form.to_i] + + # Languages with no plural have no suffix + return "" if plural_form.none? + + # Get the index and suffix for this number + # idx = Todo + suffix = rule_numbers[idx] + + # Simple plurals are handled differently in all versions (but v4) + if @simplify_plural_suffix && rule_numbers.size == 2 && rule_numbers[0] == 1 + return "_plural" if (suffix == 2) + return "" if (suffix == 1) + end + + # More complex plurals + # TODO: support `options.prepend` for v2 and v3 + # this.options.prepend && suffix.toString() ? this.options.prepend + suffix.toString() : suffix.toString() + case @version + when 1 then return SUFFIXES_V1[idx] + when 2 then return SUFFIXES_V2[idx] + else return SUFFIXES_V3[idx] + end + end + end end