linguini-cli 0.1.0-alpha.3

Command-line interface for Linguini localization projects
use std::collections::BTreeMap;

use linguini_cldr::built_in_plural_rules;
use linguini_ir::{
    IrBranch, IrExpression, IrForm, IrFormEntry, IrFunction, IrFunctionBranch,
    IrFunctionBranchValue, IrModule, IrText, IrTextPart, IrValue,
};

use super::SampleValue;

pub(super) struct Renderer<'a> {
    schema: &'a IrModule,
    module: &'a IrModule,
    locale: &'a str,
}

impl<'a> Renderer<'a> {
    pub(super) fn new(schema: &'a IrModule, module: &'a IrModule, locale: &'a str) -> Self {
        Self {
            schema,
            module,
            locale,
        }
    }

    pub(super) fn render_message(
        &self,
        name: &str,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let Some(message) = self
            .module
            .messages
            .iter()
            .find(|message| message.name == name)
        else {
            return String::new();
        };
        let Some(body) = &message.body else {
            return String::new();
        };
        self.render_text(body, &self.context(name), inputs)
    }

    fn render_text(
        &self,
        text: &IrText,
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        text.parts
            .iter()
            .map(|part| match part {
                IrTextPart::Text(value) => value.clone(),
                IrTextPart::Placeholder(expression) => {
                    self.eval_expression(expression, context, inputs)
                }
            })
            .collect()
    }

    fn eval_expression(
        &self,
        expression: &IrExpression,
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let args = expression
            .arguments
            .iter()
            .map(|argument| self.eval_expression(argument, context, inputs))
            .collect::<Vec<_>>();

        let value = match expression.path.as_slice() {
            [root] if !args.is_empty() && context.contains_key(root) => {
                self.eval_form_call(root, None, &args, context, inputs)
            }
            [root, property] if !args.is_empty() && context.contains_key(root) => {
                self.eval_form_call(root, Some(property), &args, context, inputs)
            }
            [function] if function == "plural" && args.len() == 1 => {
                plural_key(self.locale, &args[0])
            }
            [function] if !args.is_empty() => self.eval_function(function, &args, context, inputs),
            [root] => inputs
                .get(root)
                .map(SampleValue::as_text)
                .unwrap_or_default(),
            [root, property] if context.contains_key(root) => {
                self.eval_form_property(root, &[property.as_str()], context, inputs)
            }
            [root, tail @ ..] if context.contains_key(root) => {
                let path = tail.iter().map(String::as_str).collect::<Vec<_>>();
                self.eval_form_property(root, &path, context, inputs)
            }
            _ => String::new(),
        };

        self.apply_formatters(value, expression)
    }

    fn eval_form_call(
        &self,
        root: &str,
        property: Option<&String>,
        args: &[String],
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let Some(form) = self.form_for(root, context) else {
            return String::new();
        };
        let Some(variant) = inputs.get(root).map(SampleValue::as_text) else {
            return String::new();
        };
        let Some(variant) = form.variants.iter().find(|item| item.name == variant) else {
            return String::new();
        };

        if let Some(property) = property {
            let value = find_entry_value(&variant.entries, &[property.as_str()]);
            return value
                .map(|value| {
                    self.eval_value(value, args.first().map(String::as_str), context, inputs)
                })
                .unwrap_or_default();
        }

        select_branch(
            args.first().map(String::as_str).unwrap_or("other"),
            variant
                .entries
                .iter()
                .filter_map(|entry| match entry {
                    IrFormEntry::Branch(branch) => Some(branch),
                    IrFormEntry::Attribute { .. } => None,
                })
                .collect::<Vec<_>>(),
            self.locale,
            context,
            inputs,
            self,
        )
    }

    fn eval_form_property(
        &self,
        root: &str,
        path: &[&str],
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let Some(form) = self.form_for(root, context) else {
            return String::new();
        };
        let Some(variant) = inputs.get(root).map(SampleValue::as_text) else {
            return String::new();
        };
        let Some(variant) = form.variants.iter().find(|item| item.name == variant) else {
            return String::new();
        };
        find_entry_value(&variant.entries, path)
            .map(|value| self.eval_value(value, None, context, inputs))
            .unwrap_or_default()
    }

    fn eval_value(
        &self,
        value: &IrValue,
        selector: Option<&str>,
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        match value {
            IrValue::Text(text) => self.render_text(text, context, inputs),
            IrValue::Map(branches) => select_branch(
                selector.unwrap_or("other"),
                branches.iter().collect::<Vec<_>>(),
                self.locale,
                context,
                inputs,
                self,
            ),
            IrValue::Object(_) => String::new(),
        }
    }

    fn eval_function(
        &self,
        name: &str,
        args: &[String],
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let Some(function) = self
            .module
            .functions
            .iter()
            .find(|function| function.name == name)
        else {
            return String::new();
        };
        self.eval_dispatch(function, &function.branches, 0, args, context, inputs)
    }

    fn eval_dispatch(
        &self,
        function: &IrFunction,
        branches: &[IrFunctionBranch],
        depth: usize,
        args: &[String],
        context: &BTreeMap<String, String>,
        inputs: &BTreeMap<String, SampleValue>,
    ) -> String {
        let parameter_index = dispatch_parameter_indices(function)
            .get(depth)
            .copied()
            .unwrap_or(depth);
        let Some(selector) = args.get(parameter_index) else {
            return String::new();
        };
        let key = function
            .parameters
            .get(parameter_index)
            .filter(|parameter| parameter.ty == "Plural")
            .map(|_| plural_key(self.locale, selector))
            .unwrap_or_else(|| selector.clone());
        let Some(branch) = matching_function_branch(branches, &key) else {
            return String::new();
        };
        match &branch.value {
            IrFunctionBranchValue::Text(text) => self.render_text(text, context, inputs),
            IrFunctionBranchValue::Dispatch(branches) => {
                self.eval_dispatch(function, branches, depth + 1, args, context, inputs)
            }
        }
    }

    fn form_for(&self, root: &str, context: &BTreeMap<String, String>) -> Option<&'a IrForm> {
        let ty = context.get(root)?;
        self.module.forms.iter().find(|form| form.name == *ty)
    }

    fn context(&self, message_name: &str) -> BTreeMap<String, String> {
        self.schema
            .messages
            .iter()
            .find(|message| message.name == message_name)
            .map(|message| {
                message
                    .parameters
                    .iter()
                    .map(|parameter| (parameter.name.clone(), parameter.ty.clone()))
                    .collect()
            })
            .unwrap_or_default()
    }

    fn apply_formatters(&self, value: String, expression: &IrExpression) -> String {
        expression
            .formatters
            .iter()
            .fold(value, |current, formatter| match formatter.name.as_str() {
                "currency" => {
                    let code = formatter
                        .arguments
                        .iter()
                        .find(|argument| argument.name == "code")
                        .map(|argument| argument.value.as_str())
                        .unwrap_or("USD");
                    format!("{code} {current}")
                }
                "date" => current,
                _ => current,
            })
    }
}

fn find_entry_value<'a>(entries: &'a [IrFormEntry], path: &[&str]) -> Option<&'a IrValue> {
    let (head, tail) = path.split_first()?;
    for entry in entries {
        if let IrFormEntry::Attribute { name, value } = entry {
            if name == head {
                return if tail.is_empty() {
                    Some(value)
                } else if let IrValue::Object(entries) = value {
                    find_entry_value(entries, tail)
                } else {
                    None
                };
            }
        }
    }
    None
}

fn matching_function_branch<'a>(
    branches: &'a [IrFunctionBranch],
    key: &str,
) -> Option<&'a IrFunctionBranch> {
    branches
        .iter()
        .find(|branch| branch.key == key)
        .or_else(|| branches.iter().find(|branch| branch.key == "_"))
}

fn dispatch_parameter_indices(function: &IrFunction) -> Vec<usize> {
    function
        .parameters
        .iter()
        .enumerate()
        .filter_map(|(index, parameter)| (parameter.ty != "String").then_some(index))
        .collect()
}

fn select_branch(
    selector: &str,
    branches: Vec<&IrBranch>,
    locale: &str,
    context: &BTreeMap<String, String>,
    inputs: &BTreeMap<String, SampleValue>,
    renderer: &Renderer<'_>,
) -> String {
    let key = plural_key(locale, selector);
    let branch = branches
        .iter()
        .copied()
        .find(|branch| branch.keys.iter().any(|candidate| candidate == &key))
        .or_else(|| {
            branches
                .iter()
                .copied()
                .find(|branch| branch.keys.iter().any(|candidate| candidate == "_"))
        })
        .or_else(|| {
            branches
                .iter()
                .copied()
                .find(|branch| branch.keys.iter().any(|candidate| candidate == "other"))
        });
    branch
        .map(|branch| renderer.render_text(&branch.value, context, inputs))
        .unwrap_or_default()
}

fn plural_key(locale: &str, selector: &str) -> String {
    if let Some(rules) = built_in_plural_rules(locale) {
        if let Ok(category) = rules.category_for(selector) {
            return category.to_owned();
        }
    }
    selector.to_owned()
}