linguini-codegen-ts 0.1.0-alpha.3

TypeScript code generator for Linguini localization projects
use linguini_cldr::{Operand, PluralRule, PluralRules, RelationOperator};

pub fn generate_plural_function(function_name: &str, rules: &PluralRules) -> String {
    let mut output = String::new();
    output.push_str(&format!(
        "export function {function_name}(value: number | string): string {{\n"
    ));
    output.push_str("  const operands = pluralOperands(value);\n");

    for category in rules
        .categories
        .iter()
        .filter(|category| category.category != "other")
    {
        output.push_str(&format!(
            "  if ({}) return \"{}\";\n",
            rule_expression(&category.rule),
            category.category
        ));
    }

    output.push_str("  return \"other\";\n");
    output.push_str("}\n\n");
    output.push_str(PLURAL_OPERANDS_HELPER);
    output
}

fn rule_expression(rule: &PluralRule) -> String {
    if rule.conditions.is_empty() {
        return "true".to_owned();
    }

    rule.conditions
        .iter()
        .map(|condition| {
            condition
                .relations
                .iter()
                .map(|relation| {
                    let value = operand_expression(relation.expression.operand);
                    let value = if let Some(modulo) = relation.expression.modulo {
                        format!("({value} % {modulo})")
                    } else {
                        value
                    };
                    relation_expression(&value, relation.operator, &relation.ranges.ranges)
                })
                .collect::<Vec<_>>()
                .join(" && ")
        })
        .map(|condition| format!("({condition})"))
        .collect::<Vec<_>>()
        .join(" || ")
}

fn relation_expression(
    value: &str,
    operator: RelationOperator,
    ranges: &[linguini_cldr::Range],
) -> String {
    let contains = ranges
        .iter()
        .map(|range| {
            if range.start == range.end {
                format!("{value} === {}", range.start)
            } else {
                format!("({value} >= {} && {value} <= {})", range.start, range.end)
            }
        })
        .collect::<Vec<_>>()
        .join(" || ");

    match operator {
        RelationOperator::Equal | RelationOperator::In | RelationOperator::Within => {
            format!("({contains})")
        }
        RelationOperator::NotEqual | RelationOperator::NotIn | RelationOperator::NotWithin => {
            format!("!({contains})")
        }
    }
}

fn operand_expression(operand: Operand) -> String {
    match operand {
        Operand::N => "operands.n",
        Operand::I => "operands.i",
        Operand::V => "operands.v",
        Operand::W => "operands.w",
        Operand::F => "operands.f",
        Operand::T => "operands.t",
        Operand::C => "operands.c",
        Operand::E => "operands.e",
    }
    .to_owned()
}

const PLURAL_OPERANDS_HELPER: &str = r#"function pluralOperands(value: number | string) {
  const source = String(value).replace(/^[+-]/, "");
  const [integer, fraction = ""] = source.split(".");
  const trimmedFraction = fraction.replace(/0+$/, "");

  return {
    n: Number(source),
    i: Number(integer),
    v: fraction.length,
    w: trimmedFraction.length,
    f: fraction === "" ? 0 : Number(fraction),
    t: trimmedFraction === "" ? 0 : Number(trimmedFraction),
    c: 0,
    e: 0,
  };
}
"#;

#[cfg(test)]
mod tests {
    use super::generate_plural_function;
    use linguini_cldr::load_plural_rules;

    const PLURALS: &str = r#"
    {
      "supplemental": {
        "plurals-type-cardinal": {
          "ru": {
            "pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11",
            "pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14",
            "pluralRule-count-many": "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14",
            "pluralRule-count-other": ""
          }
        }
      }
    }
    "#;

    #[test]
    fn generated_plural_function_snapshot_is_stable() {
        let rules = load_plural_rules(PLURALS, "ru").expect("rules");
        let output = generate_plural_function("pluralRu", &rules);

        assert_eq!(
            output,
            include_str!("../../../tests/fixtures/golden/snapshots/codegen-ts-plural-ru.ts")
        );
    }
}