text_components 0.1.6

A Rust implementation of Minecraft's Text Components
Documentation
use heck::ToShoutySnakeCase;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use serde_json::Value;
use std::fs;

/// Count the number of parameters in a translation string
fn count_parameters(text: &str) -> usize {
    let sequential = text.matches("%s").count();
    let mut positional = 0;
    for i in 1..=8 {
        if text.contains(&format!("%{i}$s")) {
            positional = positional.max(i);
        }
    }
    sequential.max(positional)
}

pub fn build_translations(path: &str) -> TokenStream {
    println!("cargo:rerun-if-changed={path}");

    let lang_file =
        fs::read_to_string(&path).expect(&format!("Failed to read {path} language file"));

    let translations: serde_json::Map<String, Value> =
        serde_json::from_str(&lang_file).expect(&format!("Failed to parse {path}"));

    let mut stream = TokenStream::new();

    // Add imports
    stream.extend(quote! {
        #![allow(dead_code)]
        use text_components::translation::Translation;
    });

    // Generate constants for each translation
    let mut translations_vec: Vec<_> = translations.iter().collect();
    translations_vec.sort_by_key(|(k, _)| *k);

    // Track used constant names to handle collisions
    let mut used_names = rustc_hash::FxHashMap::default();

    for (key, value) in translations_vec {
        let Some(text) = value.as_str() else {
            eprintln!("Warning: Translation key '{key}' has non-string value, skipping");
            continue;
        };

        let param_count = count_parameters(text);

        // Skip translations with more than 8 parameters
        if param_count > 8 {
            eprintln!(
                "Warning: Translation '{key}' has {param_count} parameters (max 8 supported), skipping"
            );
            continue;
        }

        let mut const_name_str = key.to_shouty_snake_case();

        // Handle collisions by appending a number
        if let Some(count) = used_names.get_mut(&const_name_str) {
            *count += 1;
            const_name_str = format!("{const_name_str}_{count}");
        } else {
            used_names.insert(const_name_str.clone(), 0);
        }

        let const_name = Ident::new(&const_name_str, Span::call_site());

        stream.extend(quote! {
            pub static #const_name: Translation<#param_count> = Translation(#key);
        });
    }

    stream
}