use crate::parser::Translation;
use crate::utils::{format, plurals};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
#[allow(dead_code)]
pub fn generate_luau(translations: &[Translation], base_locale: &str) -> Result<String> {
generate_luau_with_config(translations, base_locale, None)
}
#[allow(dead_code)]
pub fn generate_luau_with_config(
translations: &[Translation],
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) -> Result<String> {
generate_luau_with_full_config(translations, base_locale, analytics_config, None)
}
pub fn generate_luau_with_full_config(
translations: &[Translation],
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
localization_config: Option<&crate::config::LocalizationConfig>,
) -> Result<String> {
let mut code = String::new();
code.push_str("--[[\n");
code.push_str(" Roblox Slang - Type-Safe Internationalization\n");
code.push_str(" \n");
code.push_str(" This file is auto-generated by roblox-slang CLI tool.\n");
code.push_str(" DO NOT MODIFY BY HAND - Your changes will be overwritten!\n");
code.push_str(" \n");
code.push_str(" Generated from translation files in your project.\n");
code.push_str(" To update translations, edit your JSON/YAML files and run:\n");
code.push_str(" roblox-slang build\n");
code.push_str(" \n");
code.push_str(" Learn more: https://github.com/mathtechstudio/roblox-slang\n");
code.push_str("--]]\n\n");
let base_translations: Vec<_> = translations
.iter()
.filter(|t| t.locale == base_locale)
.collect();
if base_translations.is_empty() {
return Ok(code + "-- No translations found\nreturn {}\n");
}
let mode = localization_config
.map(|c| c.mode.as_str())
.unwrap_or("embedded");
if mode == "embedded" || mode == "hybrid" {
let supported_locales: Vec<String> = translations
.iter()
.map(|t| t.locale.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
generate_embedded_data(&mut code, translations, &supported_locales);
}
code.push_str("local Translations = {}\n");
code.push_str("Translations.__index = Translations\n\n");
match mode {
"embedded" => generate_constructor_embedded(&mut code, analytics_config),
"cloud" => generate_constructor_cloud(&mut code, analytics_config),
"hybrid" => generate_constructor_hybrid(&mut code, base_locale, analytics_config),
_ => unreachable!(
"Invalid localization mode: {}. Expected 'embedded', 'cloud', or 'hybrid'",
mode
),
}
if mode == "cloud" || mode == "hybrid" {
generate_locale_detection(&mut code);
}
if let Some(config) = analytics_config {
if config.enabled {
generate_analytics_methods(&mut code, config);
}
}
generate_flat_methods_with_mode(
&mut code,
&base_translations,
base_locale,
analytics_config,
mode,
);
generate_namespace_structure(&mut code, &base_translations);
code.push_str("\nreturn Translations\n");
Ok(code)
}
#[allow(dead_code)]
fn generate_constructor(
code: &mut String,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
code.push_str("--- Create a new Translations instance\n");
code.push_str("--- @param locale string The locale to use (e.g., \"en\", \"id\")\n");
code.push_str("--- @return Translations\n");
code.push_str("function Translations.new(locale)\n");
code.push_str(" local self = setmetatable({}, Translations)\n");
code.push_str(" self._locale = locale or \"en\"\n");
code.push_str(" self._localeChangedCallbacks = {}\n");
if let Some(config) = analytics_config {
if config.enabled {
code.push_str(" \n");
code.push_str(" -- Analytics initialization\n");
code.push_str(" self._analytics_enabled = true\n");
code.push_str(&format!(
" self._track_missing = {}\n",
config.track_missing
));
code.push_str(&format!(" self._track_usage = {}\n", config.track_usage));
code.push_str(" self._usage_stats = {}\n");
if let Some(callback_path) = &config.callback {
code.push_str(&format!(
" self._analytics_callback = require({})\n",
callback_path
));
} else {
code.push_str(" self._analytics_callback = nil\n");
}
}
}
code.push_str(" \n");
code.push_str(" -- Get LocalizationService translator\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(self._locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if not success then\n");
code.push_str(" warn(\"Failed to get translator for locale: \" .. self._locale .. \", falling back to base locale\")\n");
code.push_str(" -- Fallback to base locale (works on both client and server)\n");
code.push_str(" translator = LocalizationService:GetTranslatorForLocaleAsync(\"en\")\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" self._translator = translator\n");
code.push_str(" \n");
code.push_str(" return self\n");
code.push_str("end\n\n");
code.push_str("--- Switch to a different locale\n");
code.push_str("--- @param locale string The new locale to use\n");
code.push_str("function Translations:setLocale(locale)\n");
code.push_str(" if self._locale == locale then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" local oldLocale = self._locale\n");
code.push_str(" self._locale = locale\n");
code.push_str(" \n");
code.push_str(" -- Get new translator\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" self._translator = translator\n");
code.push_str(" else\n");
code.push_str(" warn(\"Failed to switch to locale: \" .. locale)\n");
code.push_str(" self._locale = oldLocale\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fire locale changed callbacks\n");
code.push_str(" for _, callback in ipairs(self._localeChangedCallbacks) do\n");
code.push_str(" task.spawn(callback, locale, oldLocale)\n");
code.push_str(" end\n");
code.push_str("end\n\n");
code.push_str("--- Get current locale\n");
code.push_str("--- @return string\n");
code.push_str("function Translations:getLocale()\n");
code.push_str(" return self._locale\n");
code.push_str("end\n\n");
code.push_str("--- Register a callback for locale changes\n");
code.push_str("--- @param callback function(newLocale: string, oldLocale: string)\n");
code.push_str("function Translations:onLocaleChanged(callback)\n");
code.push_str(" table.insert(self._localeChangedCallbacks, callback)\n");
code.push_str("end\n\n");
code.push_str("--- Get localized asset ID\n");
code.push_str("--- @param assetKey string The asset key\n");
code.push_str("--- @return string The asset ID for current locale\n");
code.push_str("function Translations:getAsset(assetKey)\n");
code.push_str(" local key = \"assets.\" .. assetKey .. \".\" .. self._locale\n");
code.push_str(" local success, result = pcall(function()\n");
code.push_str(" return self._translator:FormatByKey(key)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" return result\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fallback to base locale\n");
code.push_str(" local fallbackKey = \"assets.\" .. assetKey .. \".en\"\n");
code.push_str(" return self._translator:FormatByKey(fallbackKey)\n");
code.push_str("end\n\n");
}
fn generate_locale_detection(code: &mut String) {
code.push_str("--- Detect player's locale based on their account preference\n");
code.push_str("--- @param player Player The player to detect locale for\n");
code.push_str("--- @return string The detected locale code\n");
code.push_str("function Translations.detectLocale(player)\n");
code.push_str(" local localeId = player.LocaleId\n");
code.push_str(" if not localeId or localeId == \"\" then\n");
code.push_str(" return \"en\" -- Fallback to English\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(
" -- Normalize and extract language code, preserving hyphenated locales like zh-cn\n",
);
code.push_str(" local normalized = string.lower(localeId):gsub(\"_\", \"-\")\n");
code.push_str(" local baseCode = string.match(normalized, \"^(%w+)\")\n");
code.push_str(" if baseCode == \"zh\" then\n");
code.push_str(" return normalized\n");
code.push_str(" end\n");
code.push_str(" return baseCode or \"en\"\n");
code.push_str("end\n\n");
code.push_str("--- Create a new Translations instance for a player (auto-detect locale)\n");
code.push_str("--- @param player Player The player to create translations for\n");
code.push_str("--- @return Translations\n");
code.push_str("function Translations.newForPlayer(player)\n");
code.push_str(" local locale = Translations.detectLocale(player)\n");
code.push_str(" return Translations.new(locale)\n");
code.push_str("end\n\n");
}
fn generate_analytics_methods(code: &mut String, config: &crate::config::AnalyticsConfig) {
code.push_str("--- Track missing translation\n");
code.push_str("--- @param key string The translation key\n");
code.push_str("function Translations:_trackMissing(key)\n");
code.push_str(" if not self._analytics_enabled or not self._track_missing then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Try custom callback first\n");
code.push_str(" if self._analytics_callback then\n");
code.push_str(" pcall(function()\n");
code.push_str(" self._analytics_callback(\"missing_translation\", {\n");
code.push_str(" key = key,\n");
code.push_str(" locale = self._locale,\n");
code.push_str(" timestamp = os.time()\n");
code.push_str(" })\n");
code.push_str(" end)\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Default: warn in output\n");
code.push_str(
" warn(string.format(\"[Slang] Missing translation: %s (%s)\", key, self._locale))\n",
);
code.push_str("end\n\n");
if config.track_usage {
code.push_str("--- Track translation usage\n");
code.push_str("--- @param key string The translation key\n");
code.push_str("function Translations:_trackUsage(key)\n");
code.push_str(" if not self._analytics_enabled or not self._track_usage then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Increment usage counter\n");
code.push_str(" self._usage_stats[key] = (self._usage_stats[key] or 0) + 1\n");
code.push_str("end\n\n");
code.push_str("--- Get usage statistics\n");
code.push_str("--- @return table Usage statistics\n");
code.push_str("function Translations:getUsageStats()\n");
code.push_str(" return self._usage_stats\n");
code.push_str("end\n\n");
}
}
#[allow(dead_code)]
fn generate_flat_methods(
code: &mut String,
translations: &[&Translation],
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
code.push_str("-- Internal methods (flat keys)\n\n");
let mut plural_groups: HashMap<String, Vec<&Translation>> = HashMap::new();
let mut regular_translations = Vec::new();
for translation in translations {
if plurals::is_plural_key(&translation.key) {
let base_key = plurals::extract_base_key(&translation.key);
plural_groups.entry(base_key).or_default().push(translation);
} else {
regular_translations.push(*translation);
}
}
regular_translations.sort_by(|a, b| a.key.cmp(&b.key));
let analytics_enabled = analytics_config.map(|c| c.enabled).unwrap_or(false);
let track_usage = analytics_config.map(|c| c.track_usage).unwrap_or(false);
let track_missing = analytics_config.map(|c| c.track_missing).unwrap_or(false);
for translation in regular_translations {
let method_name = translation.key.replace(".", "_");
let params_with_format = format::extract_parameters_with_format(&translation.value);
if !params_with_format.is_empty() {
code.push_str(&format!("function Translations:{}(params)\n", method_name));
code.push_str(" params = params or {}\n");
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
for (param_name, specifier) in ¶ms_with_format {
if *specifier != format::FormatSpecifier::None {
let format_code = format::generate_format_code(param_name, specifier);
if !format_code.is_empty() {
code.push_str(" ");
code.push_str(&format_code);
code.push('\n');
}
}
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local value = self._translator:FormatByKey(\"{}\", params)\n",
translation.key
));
code.push_str(" if value == \"\" or value == \"{}\" then\n");
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(&format!(
" return \"{}\" -- Return key as fallback\n",
translation.key
));
code.push_str(" end\n");
code.push_str(" return value\n");
} else {
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}\", params)\n",
translation.key
));
}
} else {
code.push_str(&format!("function Translations:{}()\n", method_name));
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local value = self._translator:FormatByKey(\"{}\")\n",
translation.key
));
code.push_str(" if value == \"\" or value == \"{}\" then\n");
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(&format!(
" return \"{}\" -- Return key as fallback\n",
translation.key
));
code.push_str(" end\n");
code.push_str(" return value\n");
} else {
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}\")\n",
translation.key
));
}
}
code.push_str("end\n\n");
}
let mut plural_keys_sorted: Vec<_> = plural_groups.keys().collect();
plural_keys_sorted.sort();
for base_key in plural_keys_sorted {
let plural_translations = &plural_groups[base_key];
generate_plural_method(code, base_key, plural_translations);
}
}
fn generate_plural_method(code: &mut String, base_key: &str, _translations: &[&Translation]) {
let method_name = base_key.replace(".", "_");
code.push_str(&format!(
"function Translations:{}(count, params)\n",
method_name
));
code.push_str(" params = params or {}\n");
code.push_str(" params.count = count\n");
code.push_str(" \n");
code.push_str(" -- Determine plural category\n");
code.push_str(" local category = \"other\"\n");
code.push_str(" \n");
code.push_str(" if self._locale == \"en\" then\n");
code.push_str(" if count == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ru\" or self._locale == \"uk\" then\n");
code.push_str(" local mod10 = math.abs(count) % 10\n");
code.push_str(" local mod100 = math.abs(count) % 100\n");
code.push_str(" if mod10 == 1 and mod100 ~= 11 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(
" elseif mod10 >= 2 and mod10 <= 4 and (mod100 < 12 or mod100 > 14) then\n",
);
code.push_str(" category = \"few\"\n");
code.push_str(" else\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ar\" then\n");
code.push_str(" local absCount = math.abs(count)\n");
code.push_str(" local mod100 = absCount % 100\n");
code.push_str(" if absCount == 0 then\n");
code.push_str(" category = \"zero\"\n");
code.push_str(" elseif absCount == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" elseif absCount == 2 then\n");
code.push_str(" category = \"two\"\n");
code.push_str(" elseif mod100 >= 3 and mod100 <= 10 then\n");
code.push_str(" category = \"few\"\n");
code.push_str(" elseif mod100 >= 11 and mod100 <= 99 then\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Try to get translation for category\n");
code.push_str(&format!(
" local key = \"{}(\" .. category .. \")\"\n",
base_key
));
code.push_str(" local success, result = pcall(function()\n");
code.push_str(" return self._translator:FormatByKey(key, params)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" return result\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fallback to 'other' category\n");
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}(other)\", params)\n",
base_key
));
code.push_str("end\n\n");
}
fn generate_plural_method_embedded(
code: &mut String,
base_key: &str,
_translations: &[&Translation],
base_locale: &str,
) {
let method_name = base_key.replace(".", "_");
code.push_str(&format!(
"function Translations:{}(count, params)\n",
method_name
));
code.push_str(" params = params or {}\n");
code.push_str(" params.count = count\n");
code.push_str(" \n");
code.push_str(" -- Determine plural category\n");
code.push_str(" local category = \"other\"\n");
code.push_str(" \n");
code.push_str(" if self._locale == \"en\" then\n");
code.push_str(" if count == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ru\" or self._locale == \"uk\" then\n");
code.push_str(" local mod10 = math.abs(count) % 10\n");
code.push_str(" local mod100 = math.abs(count) % 100\n");
code.push_str(" if mod10 == 1 and mod100 ~= 11 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(
" elseif mod10 >= 2 and mod10 <= 4 and (mod100 < 12 or mod100 > 14) then\n",
);
code.push_str(" category = \"few\"\n");
code.push_str(" else\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ar\" then\n");
code.push_str(" local absCount = math.abs(count)\n");
code.push_str(" local mod100 = absCount % 100\n");
code.push_str(" if absCount == 0 then\n");
code.push_str(" category = \"zero\"\n");
code.push_str(" elseif absCount == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" elseif absCount == 2 then\n");
code.push_str(" category = \"two\"\n");
code.push_str(" elseif mod100 >= 3 and mod100 <= 10 then\n");
code.push_str(" category = \"few\"\n");
code.push_str(" elseif mod100 >= 11 and mod100 <= 99 then\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Get translation from embedded data\n");
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
code.push_str(&format!(
" local key = \"{}(\" .. category .. \")\"\n",
base_key
));
code.push_str(" local template = locale_data[key]\n");
code.push_str(" \n");
code.push_str(" -- Fallback to 'other' category if specific category not found\n");
code.push_str(" if not template then\n");
code.push_str(&format!(
" template = locale_data[\"{}(other)\"] or key\n",
base_key
));
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Simple parameter interpolation\n");
code.push_str(" local result = template\n");
code.push_str(" for paramKey, value in pairs(params) do\n");
code.push_str(" result = result:gsub(\"{\" .. paramKey .. \"}\", tostring(value))\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" return result\n");
code.push_str("end\n\n");
}
fn generate_plural_method_cloud(code: &mut String, base_key: &str, _translations: &[&Translation]) {
let method_name = base_key.replace(".", "_");
code.push_str(&format!(
"function Translations:{}(count, params)\n",
method_name
));
code.push_str(" params = params or {}\n");
code.push_str(" params.count = count\n");
code.push_str(" \n");
code.push_str(" -- Determine plural category\n");
code.push_str(" local category = \"other\"\n");
code.push_str(" \n");
code.push_str(" if self._locale == \"en\" then\n");
code.push_str(" if count == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ru\" or self._locale == \"uk\" then\n");
code.push_str(" local mod10 = math.abs(count) % 10\n");
code.push_str(" local mod100 = math.abs(count) % 100\n");
code.push_str(" if mod10 == 1 and mod100 ~= 11 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(
" elseif mod10 >= 2 and mod10 <= 4 and (mod100 < 12 or mod100 > 14) then\n",
);
code.push_str(" category = \"few\"\n");
code.push_str(" else\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ar\" then\n");
code.push_str(" local absCount = math.abs(count)\n");
code.push_str(" local mod100 = absCount % 100\n");
code.push_str(" if absCount == 0 then\n");
code.push_str(" category = \"zero\"\n");
code.push_str(" elseif absCount == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" elseif absCount == 2 then\n");
code.push_str(" category = \"two\"\n");
code.push_str(" elseif mod100 >= 3 and mod100 <= 10 then\n");
code.push_str(" category = \"few\"\n");
code.push_str(" elseif mod100 >= 11 and mod100 <= 99 then\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Try to get translation for category\n");
code.push_str(&format!(
" local key = \"{}(\" .. category .. \")\"\n",
base_key
));
code.push_str(" local success, result = pcall(function()\n");
code.push_str(" return self._translator:FormatByKey(key, params)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" return result\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fallback to 'other' category\n");
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}(other)\", params)\n",
base_key
));
code.push_str("end\n\n");
}
fn generate_plural_method_hybrid(
code: &mut String,
base_key: &str,
_translations: &[&Translation],
_base_locale: &str,
) {
let method_name = base_key.replace(".", "_");
code.push_str(&format!(
"function Translations:{}(count, params)\n",
method_name
));
code.push_str(" params = params or {}\n");
code.push_str(" params.count = count\n");
code.push_str(" \n");
code.push_str(" -- Determine plural category\n");
code.push_str(" local category = \"other\"\n");
code.push_str(" \n");
code.push_str(" if self._locale == \"en\" then\n");
code.push_str(" if count == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ru\" or self._locale == \"uk\" then\n");
code.push_str(" local mod10 = math.abs(count) % 10\n");
code.push_str(" local mod100 = math.abs(count) % 100\n");
code.push_str(" if mod10 == 1 and mod100 ~= 11 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(
" elseif mod10 >= 2 and mod10 <= 4 and (mod100 < 12 or mod100 > 14) then\n",
);
code.push_str(" category = \"few\"\n");
code.push_str(" else\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" elseif self._locale == \"ar\" then\n");
code.push_str(" local absCount = math.abs(count)\n");
code.push_str(" local mod100 = absCount % 100\n");
code.push_str(" if absCount == 0 then\n");
code.push_str(" category = \"zero\"\n");
code.push_str(" elseif absCount == 1 then\n");
code.push_str(" category = \"one\"\n");
code.push_str(" elseif absCount == 2 then\n");
code.push_str(" category = \"two\"\n");
code.push_str(" elseif mod100 >= 3 and mod100 <= 10 then\n");
code.push_str(" category = \"few\"\n");
code.push_str(" elseif mod100 >= 11 and mod100 <= 99 then\n");
code.push_str(" category = \"many\"\n");
code.push_str(" end\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Build plural key and resolve via shared helper\n");
code.push_str(&format!(
" local pluralKey = \"{}(\" .. category .. \")\"\n",
base_key
));
code.push_str(" local result = self:_resolve(pluralKey, params)\n");
code.push_str(" \n");
code.push_str(" -- If category key was missing, fall back to the 'other' form\n");
code.push_str(" if result == pluralKey then\n");
code.push_str(&format!(
" return self:_resolve(\"{}(other)\", params)\n",
base_key
));
code.push_str(" end\n");
code.push_str(" return result\n");
code.push_str("end\n\n");
}
fn generate_flat_methods_with_mode(
code: &mut String,
translations: &[&Translation],
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
mode: &str,
) {
code.push_str("-- Internal methods (flat keys)\n\n");
let mut plural_groups: HashMap<String, Vec<&Translation>> = HashMap::new();
let mut regular_translations = Vec::new();
for translation in translations {
if plurals::is_plural_key(&translation.key) {
let base_key = plurals::extract_base_key(&translation.key);
plural_groups.entry(base_key).or_default().push(translation);
} else {
regular_translations.push(*translation);
}
}
regular_translations.sort_by(|a, b| a.key.cmp(&b.key));
for translation in regular_translations {
match mode {
"embedded" => {
generate_method_embedded(code, translation, base_locale, analytics_config)
}
"cloud" => generate_method_cloud(code, translation, analytics_config),
"hybrid" => generate_method_hybrid(code, translation, base_locale, analytics_config),
_ => unreachable!(
"Invalid localization mode: {}. Expected 'embedded', 'cloud', or 'hybrid'",
mode
),
}
}
let mut plural_keys_sorted: Vec<_> = plural_groups.keys().collect();
plural_keys_sorted.sort();
for base_key in plural_keys_sorted {
let plural_translations = &plural_groups[base_key];
match mode {
"embedded" => {
generate_plural_method_embedded(code, base_key, plural_translations, base_locale)
}
"cloud" => generate_plural_method_cloud(code, base_key, plural_translations),
"hybrid" => {
generate_plural_method_hybrid(code, base_key, plural_translations, base_locale)
}
_ => unreachable!(
"Invalid localization mode: {}. Expected 'embedded', 'cloud', or 'hybrid'",
mode
),
}
}
}
fn generate_namespace_structure(code: &mut String, translations: &[&Translation]) {
code.push_str("-- Namespace structure (syntax sugar)\n\n");
let mut plural_base_keys: HashSet<String> = HashSet::new();
let mut regular_translations = Vec::new();
for translation in translations {
if plurals::is_plural_key(&translation.key) {
let base_key = plurals::extract_base_key(&translation.key);
plural_base_keys.insert(base_key);
} else {
regular_translations.push(*translation);
}
}
regular_translations.sort_by(|a, b| a.key.cmp(&b.key));
let mut all_namespaces: HashSet<String> = HashSet::new();
for translation in ®ular_translations {
let parts: Vec<&str> = translation.key.split('.').collect();
for i in 0..parts.len() - 1 {
let namespace = parts[0..=i].join(".");
all_namespaces.insert(namespace);
}
}
for base_key in &plural_base_keys {
let parts: Vec<&str> = base_key.split('.').collect();
for i in 0..parts.len() - 1 {
let namespace = parts[0..=i].join(".");
all_namespaces.insert(namespace);
}
}
let mut sorted_namespaces: Vec<_> = all_namespaces.iter().collect();
sorted_namespaces.sort();
for namespace in sorted_namespaces {
let parts: Vec<&str> = namespace.split('.').collect();
let accessor = parts.join(".");
code.push_str(&format!("Translations.{} = {{}}\n", accessor));
}
code.push('\n');
for translation in ®ular_translations {
let parts: Vec<&str> = translation.key.split('.').collect();
let namespace = parts[0..parts.len() - 1].join(".");
let method = parts[parts.len() - 1];
let flat_method = translation.key.replace(".", "_");
let params_with_format = format::extract_parameters_with_format(&translation.value);
if !params_with_format.is_empty() {
code.push_str(&format!(
"function Translations.{}:{}(params)\n",
namespace, method
));
code.push_str(&format!(" return self:{}(params)\n", flat_method));
} else {
code.push_str(&format!(
"function Translations.{}:{}()\n",
namespace, method
));
code.push_str(&format!(" return self:{}()\n", flat_method));
}
code.push_str("end\n\n");
}
let mut plural_keys_sorted: Vec<_> = plural_base_keys.iter().collect();
plural_keys_sorted.sort();
for base_key in &plural_keys_sorted {
let parts: Vec<&str> = base_key.split('.').collect();
let namespace = parts[0..parts.len() - 1].join(".");
let method = parts[parts.len() - 1];
let flat_method = base_key.replace(".", "_");
code.push_str(&format!(
"function Translations.{}:{}(count, params)\n",
namespace, method
));
code.push_str(&format!(" return self:{}(count, params)\n", flat_method));
code.push_str("end\n\n");
}
}
pub fn extract_parameters(text: &str) -> Vec<String> {
let mut params = Vec::new();
let mut in_param = false;
let mut current_param = String::new();
for ch in text.chars() {
match ch {
'{' => {
in_param = true;
current_param.clear();
}
'}' => {
if in_param && !current_param.is_empty() {
let param_name = current_param
.split(':')
.next()
.unwrap_or(¤t_param)
.trim()
.to_string();
if !param_name.is_empty() {
params.push(param_name);
}
}
in_param = false;
}
_ => {
if in_param {
current_param.push(ch);
}
}
}
}
params
}
pub fn escape_lua_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn generate_embedded_data(
code: &mut String,
translations: &[Translation],
supported_locales: &[String],
) {
code.push_str("-- Embedded translation data (generated at build time)\n");
code.push_str("local EMBEDDED_TRANSLATIONS = {\n");
for locale in supported_locales {
code.push_str(&format!(" [\"{}\"] = {{\n", locale));
let mut locale_translations: Vec<_> = translations
.iter()
.filter(|t| t.locale == *locale)
.collect();
locale_translations.sort_by(|a, b| a.key.cmp(&b.key));
for translation in locale_translations {
let escaped_value = escape_lua_string(&translation.value);
code.push_str(&format!(
" [\"{}\"] = \"{}\",\n",
translation.key, escaped_value
));
}
code.push_str(" },\n");
}
code.push_str("}\n\n");
}
fn generate_constructor_embedded(
code: &mut String,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
code.push_str("--- Create a new Translations instance\n");
code.push_str("--- @param locale string The locale to use (e.g., \"en\", \"id\")\n");
code.push_str("--- @return Translations\n");
code.push_str("function Translations.new(locale)\n");
code.push_str(" local self = setmetatable({}, Translations)\n");
code.push_str(" self._locale = locale or \"en\"\n");
code.push_str(" self._localeChangedCallbacks = {}\n");
if let Some(config) = analytics_config {
if config.enabled {
code.push_str(" \n");
code.push_str(" -- Analytics initialization\n");
code.push_str(" self._analytics_enabled = true\n");
code.push_str(&format!(
" self._track_missing = {}\n",
config.track_missing
));
code.push_str(&format!(" self._track_usage = {}\n", config.track_usage));
code.push_str(" self._usage_stats = {}\n");
if let Some(callback_path) = &config.callback {
code.push_str(&format!(
" self._analytics_callback = require({})\n",
callback_path
));
} else {
code.push_str(" self._analytics_callback = nil\n");
}
}
}
code.push_str(" \n");
code.push_str(" return self\n");
code.push_str("end\n\n");
code.push_str("--- Switch to a different locale\n");
code.push_str("--- @param locale string The new locale to use\n");
code.push_str("function Translations:setLocale(locale)\n");
code.push_str(" if self._locale == locale then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" local oldLocale = self._locale\n");
code.push_str(" self._locale = locale\n");
code.push_str(" \n");
code.push_str(" -- Fire locale changed callbacks\n");
code.push_str(" for _, callback in ipairs(self._localeChangedCallbacks) do\n");
code.push_str(" task.spawn(callback, locale, oldLocale)\n");
code.push_str(" end\n");
code.push_str("end\n\n");
code.push_str("--- Get current locale\n");
code.push_str("--- @return string\n");
code.push_str("function Translations:getLocale()\n");
code.push_str(" return self._locale\n");
code.push_str("end\n\n");
code.push_str("--- Register a callback for locale changes\n");
code.push_str("--- @param callback function(newLocale: string, oldLocale: string)\n");
code.push_str("function Translations:onLocaleChanged(callback)\n");
code.push_str(" table.insert(self._localeChangedCallbacks, callback)\n");
code.push_str("end\n\n");
}
fn generate_constructor_cloud(
code: &mut String,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
code.push_str("--- Create a new Translations instance\n");
code.push_str("--- @param locale string The locale to use (e.g., \"en\", \"id\")\n");
code.push_str("--- @return Translations\n");
code.push_str("function Translations.new(locale)\n");
code.push_str(" local self = setmetatable({}, Translations)\n");
code.push_str(" self._locale = locale or \"en\"\n");
code.push_str(" self._localeChangedCallbacks = {}\n");
if let Some(config) = analytics_config {
if config.enabled {
code.push_str(" \n");
code.push_str(" -- Analytics initialization\n");
code.push_str(" self._analytics_enabled = true\n");
code.push_str(&format!(
" self._track_missing = {}\n",
config.track_missing
));
code.push_str(&format!(" self._track_usage = {}\n", config.track_usage));
code.push_str(" self._usage_stats = {}\n");
if let Some(callback_path) = &config.callback {
code.push_str(&format!(
" self._analytics_callback = require({})\n",
callback_path
));
} else {
code.push_str(" self._analytics_callback = nil\n");
}
}
}
code.push_str(" \n");
code.push_str(" -- Get LocalizationService translator (required)\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(self._locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if not success then\n");
code.push_str(" error(\n");
code.push_str(
" \"Failed to get translator for locale: \" .. self._locale .. \"\\n\" ..\n",
);
code.push_str(
" \"\\nHint: Make sure you've uploaded translations to Roblox Cloud.\" ..\n",
);
code.push_str(" \"\\nRun: roblox-slang upload --table-id YOUR_TABLE_ID\"\n");
code.push_str(" )\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" self._translator = translator\n");
code.push_str(" \n");
code.push_str(" return self\n");
code.push_str("end\n\n");
code.push_str("--- Switch to a different locale\n");
code.push_str("--- @param locale string The new locale to use\n");
code.push_str("function Translations:setLocale(locale)\n");
code.push_str(" if self._locale == locale then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" local oldLocale = self._locale\n");
code.push_str(" self._locale = locale\n");
code.push_str(" \n");
code.push_str(" -- Get new translator\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" self._translator = translator\n");
code.push_str(" else\n");
code.push_str(" warn(\"Failed to switch to locale: \" .. locale)\n");
code.push_str(" self._locale = oldLocale\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fire locale changed callbacks\n");
code.push_str(" for _, callback in ipairs(self._localeChangedCallbacks) do\n");
code.push_str(" task.spawn(callback, locale, oldLocale)\n");
code.push_str(" end\n");
code.push_str("end\n\n");
code.push_str("--- Get current locale\n");
code.push_str("--- @return string\n");
code.push_str("function Translations:getLocale()\n");
code.push_str(" return self._locale\n");
code.push_str("end\n\n");
code.push_str("--- Register a callback for locale changes\n");
code.push_str("--- @param callback function(newLocale: string, oldLocale: string)\n");
code.push_str("function Translations:onLocaleChanged(callback)\n");
code.push_str(" table.insert(self._localeChangedCallbacks, callback)\n");
code.push_str("end\n\n");
code.push_str("--- Get localized asset ID\n");
code.push_str("--- @param assetKey string The asset key\n");
code.push_str("--- @return string The asset ID for current locale\n");
code.push_str("function Translations:getAsset(assetKey)\n");
code.push_str(" local key = \"assets.\" .. assetKey .. \".\" .. self._locale\n");
code.push_str(" local success, result = pcall(function()\n");
code.push_str(" return self._translator:FormatByKey(key)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" return result\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fallback to base locale\n");
code.push_str(" local fallbackKey = \"assets.\" .. assetKey .. \".en\"\n");
code.push_str(" return self._translator:FormatByKey(fallbackKey)\n");
code.push_str("end\n\n");
}
fn generate_constructor_hybrid(
code: &mut String,
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
code.push_str("--- Create a new Translations instance\n");
code.push_str("--- @param locale string The locale to use (e.g., \"en\", \"id\")\n");
code.push_str("--- @return Translations\n");
code.push_str("function Translations.new(locale)\n");
code.push_str(" local self = setmetatable({}, Translations)\n");
code.push_str(" self._locale = locale or \"en\"\n");
code.push_str(" self._localeChangedCallbacks = {}\n");
if let Some(config) = analytics_config {
if config.enabled {
code.push_str(" \n");
code.push_str(" -- Analytics initialization\n");
code.push_str(" self._analytics_enabled = true\n");
code.push_str(&format!(
" self._track_missing = {}\n",
config.track_missing
));
code.push_str(&format!(" self._track_usage = {}\n", config.track_usage));
code.push_str(" self._usage_stats = {}\n");
if let Some(callback_path) = &config.callback {
code.push_str(&format!(
" self._analytics_callback = require({})\n",
callback_path
));
} else {
code.push_str(" self._analytics_callback = nil\n");
}
}
}
code.push_str(" \n");
code.push_str(" -- Try to get LocalizationService translator (optional)\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(self._locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" self._translator = translator\n");
code.push_str(" else\n");
code.push_str(
" warn(\"LocalizationService unavailable, using embedded translations\")\n",
);
code.push_str(" self._translator = nil\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" return self\n");
code.push_str("end\n\n");
code.push_str("--- Switch to a different locale\n");
code.push_str("--- @param locale string The new locale to use\n");
code.push_str("function Translations:setLocale(locale)\n");
code.push_str(" if self._locale == locale then\n");
code.push_str(" return\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" local oldLocale = self._locale\n");
code.push_str(" self._locale = locale\n");
code.push_str(" \n");
code.push_str(" -- Try to get new translator\n");
code.push_str(" local LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" local success, translator = pcall(function()\n");
code.push_str(" return LocalizationService:GetTranslatorForLocaleAsync(locale)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if success then\n");
code.push_str(" self._translator = translator\n");
code.push_str(" else\n");
code.push_str(" self._translator = nil\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Fire locale changed callbacks\n");
code.push_str(" for _, callback in ipairs(self._localeChangedCallbacks) do\n");
code.push_str(" task.spawn(callback, locale, oldLocale)\n");
code.push_str(" end\n");
code.push_str("end\n\n");
code.push_str("--- Get current locale\n");
code.push_str("--- @return string\n");
code.push_str("function Translations:getLocale()\n");
code.push_str(" return self._locale\n");
code.push_str("end\n\n");
code.push_str("--- Register a callback for locale changes\n");
code.push_str("--- @param callback function(newLocale: string, oldLocale: string)\n");
code.push_str("function Translations:onLocaleChanged(callback)\n");
code.push_str(" table.insert(self._localeChangedCallbacks, callback)\n");
code.push_str("end\n\n");
code.push_str(
"--- Internal: resolve a translation key via cloud translator with embedded fallback\n",
);
code.push_str("--- @param key string The translation key\n");
code.push_str("--- @param params table? Optional interpolation parameters\n");
code.push_str("--- @return string\n");
code.push_str("function Translations:_resolve(key, params)\n");
code.push_str(" if self._translator then\n");
code.push_str(" local success, result = pcall(function()\n");
code.push_str(" if params then\n");
code.push_str(" return self._translator:FormatByKey(key, params)\n");
code.push_str(" else\n");
code.push_str(" return self._translator:FormatByKey(key)\n");
code.push_str(" end\n");
code.push_str(" end)\n");
code.push_str(" if success and result ~= \"\" then\n");
code.push_str(" return result\n");
code.push_str(" end\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
code.push_str(" if params then\n");
code.push_str(" local template = locale_data[key] or key\n");
code.push_str(" local result = template\n");
code.push_str(" for paramKey, value in pairs(params) do\n");
code.push_str(
" result = result:gsub(\"{\" .. paramKey .. \"}\", tostring(value))\n",
);
code.push_str(" end\n");
code.push_str(" return result\n");
code.push_str(" else\n");
code.push_str(" return locale_data[key] or key\n");
code.push_str(" end\n");
code.push_str("end\n\n");
}
fn generate_method_embedded(
code: &mut String,
translation: &Translation,
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
let method_name = translation.key.replace(".", "_");
let params_with_format = format::extract_parameters_with_format(&translation.value);
let analytics_enabled = analytics_config.map(|c| c.enabled).unwrap_or(false);
let track_usage = analytics_config.map(|c| c.track_usage).unwrap_or(false);
let track_missing = analytics_config.map(|c| c.track_missing).unwrap_or(false);
if !params_with_format.is_empty() {
code.push_str(&format!("function Translations:{}(params)\n", method_name));
code.push_str(" params = params or {}\n");
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
for (param_name, specifier) in ¶ms_with_format {
if *specifier != format::FormatSpecifier::None {
let format_code = format::generate_format_code(param_name, specifier);
if !format_code.is_empty() {
code.push_str(" ");
code.push_str(&format_code);
code.push('\n');
}
}
}
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
code.push_str(&format!(
" local template = locale_data[\"{}\"] or \"{}\"\n",
translation.key, translation.key
));
if analytics_enabled && track_missing {
code.push_str(&format!(
" if not locale_data[\"{}\"] then\n",
translation.key
));
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(" end\n");
}
code.push_str(" \n");
code.push_str(" -- Simple parameter interpolation\n");
code.push_str(" local result = template\n");
code.push_str(" for paramKey, value in pairs(params) do\n");
code.push_str(
" result = result:gsub(\"{\" .. paramKey .. \"}\", tostring(value))\n",
);
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" return result\n");
} else {
code.push_str(&format!("function Translations:{}()\n", method_name));
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
if analytics_enabled && track_missing {
code.push_str(&format!(
" if not locale_data[\"{}\"] then\n",
translation.key
));
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(" end\n");
}
code.push_str(&format!(
" return locale_data[\"{}\"] or \"{}\"\n",
translation.key, translation.key
));
}
code.push_str("end\n\n");
}
fn generate_method_cloud(
code: &mut String,
translation: &Translation,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
let method_name = translation.key.replace(".", "_");
let params_with_format = format::extract_parameters_with_format(&translation.value);
let analytics_enabled = analytics_config.map(|c| c.enabled).unwrap_or(false);
let track_usage = analytics_config.map(|c| c.track_usage).unwrap_or(false);
let track_missing = analytics_config.map(|c| c.track_missing).unwrap_or(false);
if !params_with_format.is_empty() {
code.push_str(&format!("function Translations:{}(params)\n", method_name));
code.push_str(" params = params or {}\n");
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
for (param_name, specifier) in ¶ms_with_format {
if *specifier != format::FormatSpecifier::None {
let format_code = format::generate_format_code(param_name, specifier);
if !format_code.is_empty() {
code.push_str(" ");
code.push_str(&format_code);
code.push('\n');
}
}
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local value = self._translator:FormatByKey(\"{}\", params)\n",
translation.key
));
code.push_str(" if value == \"\" or value == \"{}\" then\n");
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(&format!(
" return \"{}\" -- Return key as fallback\n",
translation.key
));
code.push_str(" end\n");
code.push_str(" return value\n");
} else {
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}\", params)\n",
translation.key
));
}
} else {
code.push_str(&format!("function Translations:{}()\n", method_name));
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local value = self._translator:FormatByKey(\"{}\")\n",
translation.key
));
code.push_str(" if value == \"\" or value == \"{}\" then\n");
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(&format!(
" return \"{}\" -- Return key as fallback\n",
translation.key
));
code.push_str(" end\n");
code.push_str(" return value\n");
} else {
code.push_str(&format!(
" return self._translator:FormatByKey(\"{}\")\n",
translation.key
));
}
}
code.push_str("end\n\n");
}
fn generate_method_hybrid(
code: &mut String,
translation: &Translation,
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) {
let method_name = translation.key.replace(".", "_");
let params_with_format = format::extract_parameters_with_format(&translation.value);
let analytics_enabled = analytics_config.map(|c| c.enabled).unwrap_or(false);
let track_usage = analytics_config.map(|c| c.track_usage).unwrap_or(false);
let track_missing = analytics_config.map(|c| c.track_missing).unwrap_or(false);
if !params_with_format.is_empty() {
code.push_str(&format!("function Translations:{}(params)\n", method_name));
code.push_str(" params = params or {}\n");
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
for (param_name, specifier) in ¶ms_with_format {
if *specifier != format::FormatSpecifier::None {
let format_code = format::generate_format_code(param_name, specifier);
if !format_code.is_empty() {
code.push_str(" ");
code.push_str(&format_code);
code.push('\n');
}
}
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
code.push_str(&format!(
" if not locale_data[\"{}\"] then\n",
translation.key
));
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(" end\n");
}
code.push_str(&format!(
" return self:_resolve(\"{}\", params)\n",
translation.key
));
} else {
code.push_str(&format!("function Translations:{}()\n", method_name));
if analytics_enabled && track_usage {
code.push_str(&format!(" self:_trackUsage(\"{}\")\n", translation.key));
}
if analytics_enabled && track_missing {
code.push_str(&format!(
" local locale_data = EMBEDDED_TRANSLATIONS[self._locale] or EMBEDDED_TRANSLATIONS[\"{}\"]\n",
base_locale
));
code.push_str(&format!(
" if not locale_data[\"{}\"] then\n",
translation.key
));
code.push_str(&format!(
" self:_trackMissing(\"{}\")\n",
translation.key
));
code.push_str(" end\n");
}
code.push_str(&format!(
" return self:_resolve(\"{}\")\n",
translation.key
));
}
code.push_str("end\n\n");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_parameters() {
let params = extract_parameters("Hello {name}, you have {count:int} items");
assert_eq!(params, vec!["name", "count"]);
}
#[test]
fn test_extract_parameters_no_params() {
let params = extract_parameters("Hello world");
assert!(params.is_empty());
}
#[test]
fn test_escape_lua_string_basic() {
assert_eq!(escape_lua_string("Hello"), "Hello");
assert_eq!(escape_lua_string("Simple text"), "Simple text");
}
#[test]
fn test_escape_lua_string_quotes() {
assert_eq!(escape_lua_string("Hello \"World\""), "Hello \\\"World\\\"");
assert_eq!(escape_lua_string("Say \"Hi\""), "Say \\\"Hi\\\"");
}
#[test]
fn test_escape_lua_string_newlines() {
assert_eq!(escape_lua_string("Line1\nLine2"), "Line1\\nLine2");
assert_eq!(
escape_lua_string("First\nSecond\nThird"),
"First\\nSecond\\nThird"
);
}
#[test]
fn test_escape_lua_string_tabs() {
assert_eq!(escape_lua_string("Tab\there"), "Tab\\there");
assert_eq!(escape_lua_string("A\tB\tC"), "A\\tB\\tC");
}
#[test]
fn test_escape_lua_string_backslashes() {
assert_eq!(escape_lua_string("Back\\slash"), "Back\\\\slash");
assert_eq!(escape_lua_string("Path\\to\\file"), "Path\\\\to\\\\file");
}
#[test]
fn test_escape_lua_string_carriage_return() {
assert_eq!(escape_lua_string("Line1\rLine2"), "Line1\\rLine2");
}
#[test]
fn test_escape_lua_string_complex() {
let input = "He said: \"Hello\"\nNext line\tTabbed\\Backslash";
let expected = "He said: \\\"Hello\\\"\\nNext line\\tTabbed\\\\Backslash";
assert_eq!(escape_lua_string(input), expected);
}
#[test]
fn test_escape_lua_string_multiple_special_chars() {
let input = "\"Quote\"\n\\Backslash\\\t\tDouble Tab\r\nWindows Line";
let expected = "\\\"Quote\\\"\\n\\\\Backslash\\\\\\t\\tDouble Tab\\r\\nWindows Line";
assert_eq!(escape_lua_string(input), expected);
}
#[test]
fn test_generate_embedded_data_single_locale() {
let translations = vec![
Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.message".to_string(),
value: "Hello".to_string(),
locale: "en".to_string(),
context: None,
},
];
let mut code = String::new();
generate_embedded_data(&mut code, &translations, &["en".to_string()]);
assert!(code.contains("local EMBEDDED_TRANSLATIONS = {"));
assert!(code.contains("[\"en\"] = {"));
assert!(code.contains("[\"ui.button\"] = \"Buy\""));
assert!(code.contains("[\"ui.message\"] = \"Hello\""));
}
#[test]
fn test_generate_embedded_data_multiple_locales() {
let translations = vec![
Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.button".to_string(),
value: "Beli".to_string(),
locale: "id".to_string(),
context: None,
},
Translation {
key: "ui.button".to_string(),
value: "Comprar".to_string(),
locale: "es".to_string(),
context: None,
},
];
let mut code = String::new();
generate_embedded_data(
&mut code,
&translations,
&["en".to_string(), "id".to_string(), "es".to_string()],
);
assert!(code.contains("[\"en\"] = {"));
assert!(code.contains("[\"id\"] = {"));
assert!(code.contains("[\"es\"] = {"));
assert!(code.contains("[\"ui.button\"] = \"Buy\""));
assert!(code.contains("[\"ui.button\"] = \"Beli\""));
assert!(code.contains("[\"ui.button\"] = \"Comprar\""));
}
#[test]
fn test_generate_embedded_data_with_special_chars() {
let translations = vec![Translation {
key: "ui.message".to_string(),
value: "Hello \"World\"\nNew line".to_string(),
locale: "en".to_string(),
context: None,
}];
let mut code = String::new();
generate_embedded_data(&mut code, &translations, &["en".to_string()]);
assert!(code.contains("Hello \\\"World\\\"\\nNew line"));
}
#[test]
fn test_generate_embedded_data_sorted_keys() {
let translations = vec![
Translation {
key: "ui.zebra".to_string(),
value: "Z".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.apple".to_string(),
value: "A".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.banana".to_string(),
value: "B".to_string(),
locale: "en".to_string(),
context: None,
},
];
let mut code = String::new();
generate_embedded_data(&mut code, &translations, &["en".to_string()]);
let apple_pos = code.find("ui.apple").unwrap();
let banana_pos = code.find("ui.banana").unwrap();
let zebra_pos = code.find("ui.zebra").unwrap();
assert!(apple_pos < banana_pos);
assert!(banana_pos < zebra_pos);
}
#[test]
fn test_generate_embedded_data_empty_locale() {
let translations = vec![];
let mut code = String::new();
generate_embedded_data(&mut code, &translations, &["en".to_string()]);
assert!(code.contains("local EMBEDDED_TRANSLATIONS = {"));
assert!(code.contains("[\"en\"] = {"));
}
#[test]
fn test_generate_constructor_embedded_no_localization_service() {
let mut code = String::new();
generate_constructor_embedded(&mut code, None);
assert!(!code.contains("LocalizationService"));
assert!(!code.contains("GetTranslatorForLocaleAsync"));
assert!(!code.contains("_translator"));
assert!(code.contains("function Translations.new(locale)"));
assert!(code.contains("self._locale = locale or \"en\""));
assert!(code.contains("self._localeChangedCallbacks = {}"));
assert!(code.contains("function Translations:setLocale(locale)"));
assert!(code.contains("function Translations:getLocale()"));
assert!(code.contains("function Translations:onLocaleChanged(callback)"));
}
#[test]
fn test_generate_constructor_cloud_has_localization_service() {
let mut code = String::new();
generate_constructor_cloud(&mut code, None);
assert!(code.contains("LocalizationService"));
assert!(code.contains("GetTranslatorForLocaleAsync"));
assert!(code.contains("self._translator = translator"));
assert!(code.contains("if not success then"));
assert!(code.contains("error("));
assert!(code.contains("Make sure you've uploaded translations"));
assert!(code.contains("function Translations:getAsset(assetKey)"));
}
#[test]
fn test_generate_constructor_hybrid_optional_cloud() {
let mut code = String::new();
generate_constructor_hybrid(&mut code, "en", None);
assert!(code.contains("LocalizationService"));
assert!(code.contains("GetTranslatorForLocaleAsync"));
assert!(code.contains("pcall(function()"));
assert!(code.contains("if success then"));
assert!(code.contains("self._translator = translator"));
assert!(code.contains("else"));
assert!(code.contains("warn(\"LocalizationService unavailable"));
assert!(code.contains("self._translator = nil"));
assert!(!code.contains("error("));
}
#[test]
fn test_generate_constructor_embedded_with_analytics() {
use crate::config::AnalyticsConfig;
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: true,
callback: None,
};
let mut code = String::new();
generate_constructor_embedded(&mut code, Some(&analytics_config));
assert!(code.contains("self._analytics_enabled = true"));
assert!(code.contains("self._track_missing = true"));
assert!(code.contains("self._track_usage = true"));
assert!(code.contains("self._usage_stats = {}"));
assert!(!code.contains("LocalizationService"));
}
#[test]
fn test_generate_constructor_cloud_with_analytics() {
use crate::config::AnalyticsConfig;
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: false,
track_usage: true,
callback: Some("game.Analytics".to_string()),
};
let mut code = String::new();
generate_constructor_cloud(&mut code, Some(&analytics_config));
assert!(code.contains("self._analytics_enabled = true"));
assert!(code.contains("self._analytics_callback = require(game.Analytics)"));
assert!(code.contains("LocalizationService"));
}
#[test]
fn test_generate_constructor_hybrid_with_analytics() {
use crate::config::AnalyticsConfig;
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: false,
callback: None,
};
let mut code = String::new();
generate_constructor_hybrid(&mut code, "en", Some(&analytics_config));
assert!(code.contains("self._analytics_enabled = true"));
assert!(code.contains("self._track_missing = true"));
assert!(code.contains("pcall(function()"));
assert!(code.contains("self._translator = nil"));
}
#[cfg(test)]
mod property_tests {
use super::*;
use quickcheck::{quickcheck, TestResult};
#[test]
fn prop_escape_no_unescaped_special_chars() {
fn property(s: String) -> TestResult {
let escaped = escape_lua_string(&s);
let has_unescaped_newline = escaped.contains('\n');
let has_unescaped_tab = escaped.contains('\t');
let has_unescaped_carriage = escaped.contains('\r');
let has_unescaped_quote = escaped.contains('"') && !escaped.contains("\\\"");
if has_unescaped_newline
|| has_unescaped_tab
|| has_unescaped_carriage
|| has_unescaped_quote
{
return TestResult::failed();
}
TestResult::passed()
}
quickcheck(property as fn(String) -> TestResult);
}
#[test]
fn prop_escape_idempotent_on_safe_strings() {
fn property(s: String) -> TestResult {
if s.contains('\\')
|| s.contains('"')
|| s.contains('\n')
|| s.contains('\r')
|| s.contains('\t')
{
return TestResult::discard();
}
let escaped = escape_lua_string(&s);
TestResult::from_bool(escaped == s)
}
quickcheck(property as fn(String) -> TestResult);
}
#[test]
fn prop_escape_length_increases_or_stays_same() {
fn property(s: String) -> bool {
let escaped = escape_lua_string(&s);
escaped.len() >= s.len()
}
quickcheck(property as fn(String) -> bool);
}
#[test]
fn prop_escape_preserves_safe_characters() {
fn property(s: String) -> TestResult {
if !s.chars().all(|c| c.is_alphanumeric() || c == ' ') {
return TestResult::discard();
}
let escaped = escape_lua_string(&s);
TestResult::from_bool(escaped == s)
}
quickcheck(property as fn(String) -> TestResult);
}
#[test]
fn prop_embedded_data_completeness() {
fn property(keys: Vec<String>, values: Vec<String>, locale: String) -> TestResult {
if keys.is_empty() || values.is_empty() || locale.is_empty() {
return TestResult::discard();
}
let valid_keys: Vec<String> = keys
.into_iter()
.filter(|k| !k.is_empty() && k.chars().all(|c| c.is_alphanumeric() || c == '.'))
.collect();
if valid_keys.is_empty() || !locale.chars().all(|c| c.is_alphanumeric() || c == '-')
{
return TestResult::discard();
}
let translations: Vec<Translation> = valid_keys
.iter()
.zip(values.iter().cycle())
.map(|(key, value)| Translation {
key: key.clone(),
value: value.clone(),
locale: locale.clone(),
context: None,
})
.collect();
let mut code = String::new();
generate_embedded_data(&mut code, &translations, std::slice::from_ref(&locale));
for translation in &translations {
let key_pattern = format!("[\"{}\"]", translation.key);
if !code.contains(&key_pattern) {
return TestResult::failed();
}
}
TestResult::passed()
}
quickcheck(property as fn(Vec<String>, Vec<String>, String) -> TestResult);
}
#[test]
fn prop_embedded_data_all_locales_present() {
fn property(locales: Vec<String>) -> TestResult {
if locales.is_empty() {
return TestResult::discard();
}
let valid_locales: Vec<String> = locales
.into_iter()
.filter(|l| !l.is_empty() && l.chars().all(|c| c.is_alphanumeric() || c == '-'))
.collect();
if valid_locales.is_empty() {
return TestResult::discard();
}
let translations = vec![];
let mut code = String::new();
generate_embedded_data(&mut code, &translations, &valid_locales);
for locale in &valid_locales {
let locale_pattern = format!("[\"{}\"] = {{", locale);
if !code.contains(&locale_pattern) {
return TestResult::failed();
}
}
TestResult::passed()
}
quickcheck(property as fn(Vec<String>) -> TestResult);
}
}
#[test]
fn test_generate_constructor_server_safe() {
let mut code = String::new();
generate_constructor(&mut code, None);
assert!(!code.contains("game.Players.LocalPlayer"));
assert!(code.contains("LocalizationService:GetTranslatorForLocaleAsync(\"en\")"));
assert!(code.contains("falling back to base locale"));
}
#[test]
fn test_generate_namespace_with_plurals() {
let translations = [
Translation {
key: "ui.messages.items(one)".to_string(),
value: "{count} item".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.messages.items(other)".to_string(),
value: "{count} items".to_string(),
locale: "en".to_string(),
context: None,
},
];
let refs: Vec<_> = translations.iter().collect();
let mut code = String::new();
generate_namespace_structure(&mut code, &refs);
let count = code
.matches("function Translations.ui.messages:items")
.count();
assert_eq!(
count, 1,
"Should generate only one namespace method for plural"
);
assert!(code.contains("function Translations.ui.messages:items(count, params)"));
assert!(!code.contains("items(one)"));
assert!(!code.contains("items(other)"));
}
#[test]
fn test_generate_flat_methods_with_plurals() {
let translations = [
Translation {
key: "ui.messages.items(one)".to_string(),
value: "{count} item".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.messages.items(other)".to_string(),
value: "{count} items".to_string(),
locale: "en".to_string(),
context: None,
},
];
let refs: Vec<_> = translations.iter().collect();
let mut code = String::new();
generate_flat_methods(&mut code, &refs, None);
let count = code
.matches("function Translations:ui_messages_items")
.count();
assert_eq!(count, 1, "Should generate only one flat method for plural");
assert!(code.contains("function Translations:ui_messages_items(count, params)"));
assert!(code.contains("Determine plural category"));
assert!(code.contains("local category = \"other\""));
}
#[test]
fn test_generate_luau_complete() {
let translations = vec![
Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.messages.items(one)".to_string(),
value: "{count} item".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.messages.items(other)".to_string(),
value: "{count} items".to_string(),
locale: "en".to_string(),
context: None,
},
];
let code = generate_luau(&translations, "en").unwrap();
assert!(code.contains("auto-generated by roblox-slang"));
assert!(code.contains("DO NOT MODIFY BY HAND"));
assert!(code.contains("function Translations.new(locale)"));
assert!(!code.contains("game.Players.LocalPlayer"));
assert!(code.contains("function Translations:ui_button()"));
assert!(code.contains("function Translations:ui_messages_items(count, params)"));
assert!(code.contains("Translations.ui = {}"));
assert!(code.contains("function Translations.ui:button()"));
assert!(code.contains("function Translations.ui.messages:items(count, params)"));
assert!(code.contains("return Translations"));
}
#[test]
fn test_generate_with_format_specifiers() {
let translations = [
Translation {
key: "ui.price".to_string(),
value: "Price: ${price:fixed(2)}".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.score".to_string(),
value: "Score: {score:int}".to_string(),
locale: "en".to_string(),
context: None,
},
];
let refs: Vec<_> = translations.iter().collect();
let mut code = String::new();
generate_flat_methods(&mut code, &refs, None);
assert!(code.contains("string.format(\"%.2f\""));
assert!(code.contains("math.floor"));
}
}
#[test]
fn test_analytics_generation() {
use crate::config::AnalyticsConfig;
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: true,
callback: None,
};
let code = generate_luau_with_config(&translations, "en", Some(&analytics_config)).unwrap();
assert!(code.contains("self._analytics_enabled = true"));
assert!(code.contains("self._track_missing = true"));
assert!(code.contains("self._track_usage = true"));
assert!(code.contains("self._usage_stats = {}"));
assert!(code.contains("function Translations:_trackMissing(key)"));
assert!(code.contains("function Translations:_trackUsage(key)"));
assert!(code.contains("function Translations:getUsageStats()"));
assert!(code.contains("self:_trackUsage(\"test.key\")"));
assert!(code.contains("self:_trackMissing(\"test.key\")"));
}
#[test]
fn test_analytics_disabled() {
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let code = generate_luau(&translations, "en").unwrap();
assert!(!code.contains("self._analytics_enabled"));
assert!(!code.contains("function Translations:_trackMissing"));
assert!(!code.contains("self:_trackUsage"));
}
#[test]
fn test_analytics_with_custom_callback() {
use crate::config::AnalyticsConfig;
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: false,
callback: Some("game.ReplicatedStorage.AnalyticsHandler".to_string()),
};
let code = generate_luau_with_config(&translations, "en", Some(&analytics_config)).unwrap();
assert!(code
.contains("self._analytics_callback = require(game.ReplicatedStorage.AnalyticsHandler)"));
assert!(!code.contains("function Translations:_trackUsage"));
assert!(!code.contains("function Translations:getUsageStats"));
}
#[test]
fn test_generate_method_embedded_simple() {
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_embedded(&mut code, &translation, "en", None);
assert!(code.contains("function Translations:ui_button()"));
assert!(code.contains("EMBEDDED_TRANSLATIONS[self._locale]"));
assert!(code.contains("EMBEDDED_TRANSLATIONS[\"en\"]"));
assert!(code.contains("[\"ui.button\"]"));
assert!(!code.contains("FormatByKey"));
assert!(!code.contains("_translator"));
}
#[test]
fn test_generate_method_embedded_with_params() {
let translation = Translation {
key: "ui.greeting".to_string(),
value: "Hello, {name}!".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_embedded(&mut code, &translation, "en", None);
assert!(code.contains("function Translations:ui_greeting(params)"));
assert!(code.contains("params = params or {}"));
assert!(code.contains("EMBEDDED_TRANSLATIONS[self._locale]"));
assert!(code.contains("local template = locale_data[\"ui.greeting\"]"));
assert!(code.contains("Simple parameter interpolation"));
assert!(code.contains("for paramKey, value in pairs(params) do"));
assert!(code.contains("result:gsub(\"{\" .. paramKey .. \"}\", tostring(value))"));
}
#[test]
fn test_generate_method_cloud_simple() {
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_cloud(&mut code, &translation, None);
assert!(code.contains("function Translations:ui_button()"));
assert!(code.contains("self._translator:FormatByKey(\"ui.button\")"));
assert!(!code.contains("EMBEDDED_TRANSLATIONS"));
}
#[test]
fn test_generate_method_cloud_with_params() {
let translation = Translation {
key: "ui.greeting".to_string(),
value: "Hello, {name}!".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_cloud(&mut code, &translation, None);
assert!(code.contains("function Translations:ui_greeting(params)"));
assert!(code.contains("params = params or {}"));
assert!(code.contains("self._translator:FormatByKey(\"ui.greeting\", params)"));
}
#[test]
fn test_generate_method_hybrid_simple() {
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_hybrid(&mut code, &translation, "en", None);
assert!(code.contains("function Translations:ui_button()"));
assert!(code.contains("self:_resolve(\"ui.button\")"));
assert!(!code.contains("if self._translator then"));
assert!(!code.contains("pcall(function()"));
}
#[test]
fn test_generate_method_hybrid_with_params() {
let translation = Translation {
key: "ui.greeting".to_string(),
value: "Hello, {name}!".to_string(),
locale: "en".to_string(),
context: None,
};
let mut code = String::new();
generate_method_hybrid(&mut code, &translation, "en", None);
assert!(code.contains("function Translations:ui_greeting(params)"));
assert!(code.contains("self:_resolve(\"ui.greeting\", params)"));
assert!(!code.contains("self._translator:FormatByKey(\"ui.greeting\", params)"));
}
#[test]
fn test_generate_method_embedded_with_analytics() {
use crate::config::AnalyticsConfig;
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: true,
callback: None,
};
let mut code = String::new();
generate_method_embedded(&mut code, &translation, "en", Some(&analytics_config));
assert!(code.contains("self:_trackUsage(\"ui.button\")"));
assert!(code.contains("self:_trackMissing(\"ui.button\")"));
}
#[test]
fn test_generate_method_cloud_with_analytics() {
use crate::config::AnalyticsConfig;
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: true,
callback: None,
};
let mut code = String::new();
generate_method_cloud(&mut code, &translation, Some(&analytics_config));
assert!(code.contains("self:_trackUsage(\"ui.button\")"));
assert!(code.contains("local value = self._translator:FormatByKey"));
assert!(code.contains("if value == \"\" or value == \"{}\" then"));
assert!(code.contains("self:_trackMissing(\"ui.button\")"));
}
#[test]
fn test_generate_method_hybrid_with_analytics() {
use crate::config::AnalyticsConfig;
let translation = Translation {
key: "ui.button".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
};
let analytics_config = AnalyticsConfig {
enabled: true,
track_missing: true,
track_usage: true,
callback: None,
};
let mut code = String::new();
generate_method_hybrid(&mut code, &translation, "en", Some(&analytics_config));
assert!(code.contains("self:_trackUsage(\"ui.button\")"));
assert!(code.contains("self:_trackMissing(\"ui.button\")"));
}
#[test]
fn test_generate_luau_with_full_config_embedded() {
use crate::config::LocalizationConfig;
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let localization_config = LocalizationConfig {
mode: "embedded".to_string(),
};
let code =
generate_luau_with_full_config(&translations, "en", None, Some(&localization_config))
.unwrap();
assert!(code.contains("EMBEDDED_TRANSLATIONS"));
assert!(code.contains("[\"test.key\"] = \"Test Value\""));
assert!(!code.contains("LocalizationService"));
assert!(!code.contains("FormatByKey"));
}
#[test]
fn test_generate_luau_with_full_config_cloud() {
use crate::config::LocalizationConfig;
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let localization_config = LocalizationConfig {
mode: "cloud".to_string(),
};
let code =
generate_luau_with_full_config(&translations, "en", None, Some(&localization_config))
.unwrap();
assert!(!code.contains("EMBEDDED_TRANSLATIONS"));
assert!(code.contains("LocalizationService"));
assert!(code.contains("FormatByKey"));
}
#[test]
fn test_generate_luau_with_full_config_hybrid() {
use crate::config::LocalizationConfig;
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let localization_config = LocalizationConfig {
mode: "hybrid".to_string(),
};
let code =
generate_luau_with_full_config(&translations, "en", None, Some(&localization_config))
.unwrap();
assert!(code.contains("EMBEDDED_TRANSLATIONS"));
assert!(code.contains("LocalizationService"));
assert!(code.contains("FormatByKey"));
assert!(code.contains("pcall"));
}
#[test]
fn test_generate_luau_with_full_config_default_mode() {
let translations = vec![Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
}];
let code = generate_luau_with_full_config(&translations, "en", None, None).unwrap();
assert!(code.contains("EMBEDDED_TRANSLATIONS"));
assert!(!code.contains("LocalizationService"));
}
#[test]
fn test_generate_luau_with_full_config_multiple_locales() {
use crate::config::LocalizationConfig;
let translations = vec![
Translation {
key: "test.key".to_string(),
value: "Test Value".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "test.key".to_string(),
value: "Nilai Test".to_string(),
locale: "id".to_string(),
context: None,
},
];
let localization_config = LocalizationConfig {
mode: "embedded".to_string(),
};
let code =
generate_luau_with_full_config(&translations, "en", None, Some(&localization_config))
.unwrap();
assert!(code.contains("[\"en\"]"));
assert!(code.contains("[\"id\"]"));
assert!(code.contains("\"Test Value\""));
assert!(code.contains("\"Nilai Test\""));
}