use crate::parser::Translation;
use crate::utils::{format, plurals};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
pub fn generate_luau(translations: &[Translation], base_locale: &str) -> Result<String> {
generate_luau_with_config(translations, base_locale, None)
}
pub fn generate_luau_with_config(
translations: &[Translation],
base_locale: &str,
analytics_config: Option<&crate::config::AnalyticsConfig>,
) -> 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");
}
code.push_str("local Translations = {}\n");
code.push_str("Translations.__index = Translations\n\n");
generate_constructor(&mut code, analytics_config);
generate_locale_detection(&mut code);
if let Some(config) = analytics_config {
if config.enabled {
generate_analytics_methods(&mut code, config);
}
}
generate_flat_methods(&mut code, &base_translations, analytics_config);
generate_namespace_structure(&mut code, &base_translations);
code.push_str("\nreturn Translations\n");
Ok(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) {
use crate::utils::locales;
code.push_str("--- Detect player's locale based on their country\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 LocalizationService = game:GetService(\"LocalizationService\")\n");
code.push_str(" \n");
code.push_str(" -- Try to get player's country\n");
code.push_str(" local success, countryCode = pcall(function()\n");
code.push_str(" return LocalizationService:GetCountryRegionForPlayerAsync(player)\n");
code.push_str(" end)\n");
code.push_str(" \n");
code.push_str(" if not success or not countryCode then\n");
code.push_str(" return \"en\" -- Fallback to English\n");
code.push_str(" end\n");
code.push_str(" \n");
code.push_str(" -- Map country code to locale\n");
code.push_str(" local countryLocaleMap = {\n");
let mappings = locales::get_country_locale_map();
for (country, locale) in mappings {
code.push_str(&format!(" [\"{}\"] = \"{}\",\n", country, locale));
}
code.push_str(" }\n");
code.push_str(" \n");
code.push_str(" return countryLocaleMap[countryCode] 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");
}
}
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);
}
}
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");
}
for (base_key, plural_translations) in plural_groups {
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_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);
}
}
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 regular_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.{}.{}(self, params)\n",
namespace, method
));
code.push_str(&format!(" return self:{}(params)\n", flat_method));
} else {
code.push_str(&format!(
"function Translations.{}.{}(self)\n",
namespace, method
));
code.push_str(&format!(" return self:{}()\n", flat_method));
}
code.push_str("end\n\n");
}
for base_key in plural_base_keys {
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.{}.{}(self, 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
}
#[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_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(self, 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(self)"));
assert!(code.contains("function Translations.ui.messages.items(self, 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"));
}