roblox-slang 2.0.4

Type-safe internationalization for Roblox experiences
Documentation
use super::types::*;
use crate::utils::flatten;
use crate::utils::validation;
use anyhow::{bail, Result};
use serde_json::Value;
use std::path::Path;

/// Parse a JSON translation file
pub fn parse_json_file(path: &Path, locale: &str) -> Result<Vec<Translation>> {
    // Read file with better error context
    let content = std::fs::read_to_string(path).map_err(|e| {
        anyhow::anyhow!(
            "Failed to read translation file: {}\n\
             Error: {}\n\
             \n\
             Hint: Make sure the file exists and you have read permissions.",
            path.display(),
            e
        )
    })?;

    // Check if file is empty
    if content.trim().is_empty() {
        bail!(
            "Translation file is empty: {}\n\
             \n\
             Hint: Add at least one translation key-value pair.\n\
             Example:\n\
             {{\n\
               \"ui\": {{\n\
                 \"button\": \"Click me\"\n\
               }}\n\
             }}",
            path.display()
        );
    }

    // Parse JSON with detailed error messages
    let json: Value = serde_json::from_str(&content).map_err(|e| {
        let line = e.line();
        let column = e.column();

        // Try to extract the problematic line
        let lines: Vec<&str> = content.lines().collect();
        let context_line = if line > 0 && line <= lines.len() {
            lines[line - 1]
        } else {
            ""
        };

        anyhow::anyhow!(
            "Failed to parse JSON in: {}\n\
             Error at line {}, column {}: {}\n\
             \n\
             Problematic line:\n\
             {}\n\
             {}^\n\
             \n\
             Common JSON errors:\n\
             - Missing or extra commas\n\
             - Missing closing brackets or braces\n\
             - Unquoted keys or values\n\
             - Trailing commas (not allowed in strict JSON)\n\
             \n\
             Hint: Use a JSON validator or linter to check your file.",
            path.display(),
            line,
            column,
            e,
            context_line,
            " ".repeat(column.saturating_sub(1))
        )
    })?;

    let flattened = flatten::flatten_json(&json, String::new());

    // Check if any translations were found - just return empty vector if none
    if flattened.is_empty() {
        log::warn!("No translations found in: {}", path.display());
        return Ok(Vec::new());
    }

    let translations = flattened
        .into_iter()
        .map(|(key, value)| {
            // Validate translation key format
            validation::validate_translation_key(&key).map_err(|e| {
                anyhow::anyhow!(
                    "Invalid translation key in: {}\n\
                     {}\n\
                     \n\
                     Hint: Translation keys should use dot notation (e.g., 'ui.button.buy')",
                    path.display(),
                    e
                )
            })?;

            Ok(Translation {
                key,
                value,
                locale: locale.to_string(),
                context: None,
            })
        })
        .collect::<Result<Vec<_>>>()?;

    Ok(translations)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_parse_json_file_structure() {
        let json = json!({
            "ui": {
                "button": "Buy"
            }
        });

        // Test that flatten works correctly
        let flattened = flatten::flatten_json(&json, String::new());
        assert_eq!(flattened.get("ui.button"), Some(&"Buy".to_string()));
    }

    #[test]
    fn test_flatten_simple() {
        let json = json!({"key": "value"});
        let result = flatten::flatten_json(&json, String::new());
        assert_eq!(result.get("key"), Some(&"value".to_string()));
    }

    #[test]
    fn test_flatten_nested() {
        let json = json!({
            "ui": {
                "button": "Buy"
            }
        });
        let result = flatten::flatten_json(&json, String::new());
        assert_eq!(result.get("ui.button"), Some(&"Buy".to_string()));
    }

    #[test]
    fn test_flatten_deep_nested() {
        let json = json!({
            "ui": {
                "buttons": {
                    "buy": "Buy",
                    "sell": "Sell"
                }
            }
        });
        let result = flatten::flatten_json(&json, String::new());
        assert_eq!(result.get("ui.buttons.buy"), Some(&"Buy".to_string()));
        assert_eq!(result.get("ui.buttons.sell"), Some(&"Sell".to_string()));
    }
}