use crate::parser::Translation;
use crate::utils::plurals;
use anyhow::Result;
use std::collections::HashSet;
pub fn generate_type_definitions(
translations: &[Translation],
base_locale: &str,
) -> Result<String> {
let mut code = String::new();
code.push_str("--[[\n");
code.push_str(" Roblox Slang - Type Definitions\n");
code.push_str(" \n");
code.push_str(" This file provides type definitions for Luau LSP autocomplete.\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 type definitions, edit your JSON/YAML files and run:\n");
code.push_str(" roblox-slang build\n");
code.push_str(" \n");
code.push_str(" Usage:\n");
code.push_str(" Place this file alongside your Translations.lua module.\n");
code.push_str(" Your IDE/LSP will automatically provide autocomplete and type checking.\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 + "export type Translations = {}\n");
}
code.push_str("export type Translations = {\n");
code.push_str(" new: (locale: string?) -> TranslationsInstance,\n");
code.push_str("}\n\n");
code.push_str("export type TranslationsInstance = {\n");
code.push_str(" _locale: string,\n");
code.push_str(" _translator: any,\n");
code.push_str(" _localeChangedCallbacks: {any},\n\n");
code.push_str(" setLocale: (self: TranslationsInstance, locale: string) -> (),\n");
code.push_str(" getLocale: (self: TranslationsInstance) -> string,\n");
code.push_str(" onLocaleChanged: (self: TranslationsInstance, callback: (newLocale: string, oldLocale: string) -> ()) -> (),\n");
code.push_str(" getAsset: (self: TranslationsInstance, assetKey: string) -> string,\n\n");
let mut plural_base_keys: HashSet<String> = HashSet::new();
let mut regular_translations = Vec::new();
for translation in &base_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));
for translation in ®ular_translations {
let method_name = translation.key.replace(".", "_");
let params = super::luau::extract_parameters(&translation.value);
if params.is_empty() {
code.push_str(&format!(
" {}: (self: TranslationsInstance) -> string,\n",
method_name
));
} else {
code.push_str(&format!(
" {}: (self: TranslationsInstance, params: {{}}) -> string,\n",
method_name
));
}
}
let mut plural_keys_sorted: Vec<_> = plural_base_keys.iter().collect();
plural_keys_sorted.sort();
for base_key in &plural_keys_sorted {
let method_name = base_key.replace(".", "_");
code.push_str(&format!(
" {}: (self: TranslationsInstance, count: number, params: {{}}?) -> string,\n",
method_name
));
}
code.push('\n');
let namespaces = build_namespace_tree(®ular_translations, &plural_base_keys);
generate_namespace_types(
&mut code,
&namespaces,
®ular_translations,
&plural_base_keys,
);
code.push_str("}\n");
Ok(code)
}
fn build_namespace_tree(
translations: &[&Translation],
plural_base_keys: &HashSet<String>,
) -> HashSet<String> {
let mut namespaces = HashSet::new();
for translation in translations {
let parts: Vec<&str> = translation.key.split('.').collect();
for i in 0..parts.len() - 1 {
let namespace = parts[0..=i].join(".");
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(".");
namespaces.insert(namespace);
}
}
namespaces
}
fn generate_namespace_types(
code: &mut String,
namespaces: &HashSet<String>,
translations: &[&Translation],
plural_base_keys: &HashSet<String>,
) {
let mut sorted_namespaces: Vec<_> = namespaces.iter().collect();
sorted_namespaces.sort();
for namespace in &sorted_namespaces {
let parts: Vec<&str> = namespace.split('.').collect();
let last_part = parts[parts.len() - 1];
if parts.len() == 1 {
code.push_str(&format!(" {}: {{\n", last_part));
for translation in translations {
if translation.key.starts_with(&format!("{}.", namespace)) {
let key_parts: Vec<&str> = translation.key.split('.').collect();
if key_parts.len() == 2 {
let method = key_parts[1];
let params = super::luau::extract_parameters(&translation.value);
if params.is_empty() {
code.push_str(&format!(
" {}: (self: TranslationsInstance) -> string,\n",
method
));
} else {
code.push_str(&format!(
" {}: (self: TranslationsInstance, params: {{}}) -> string,\n",
method
));
}
}
}
}
for nested in sorted_namespaces.iter() {
if nested.starts_with(&format!("{}.", namespace)) {
let nested_parts: Vec<&str> = nested.split('.').collect();
if nested_parts.len() == 2 {
let nested_name = nested_parts[1];
code.push_str(&format!(" {}: {{\n", nested_name));
for translation in translations {
if translation.key.starts_with(&format!("{}.", nested)) {
let key_parts: Vec<&str> = translation.key.split('.').collect();
if key_parts.len() == 3 {
let method = key_parts[2];
let params =
super::luau::extract_parameters(&translation.value);
if params.is_empty() {
code.push_str(&format!(
" {}: (self: TranslationsInstance) -> string,\n",
method
));
} else {
code.push_str(&format!(
" {}: (self: TranslationsInstance, params: {{}}) -> string,\n",
method
));
}
}
}
}
for base_key in plural_base_keys {
if base_key.starts_with(&format!("{}.", nested)) {
let key_parts: Vec<&str> = base_key.split('.').collect();
if key_parts.len() == 3 {
let method = key_parts[2];
code.push_str(&format!(
" {}: (self: TranslationsInstance, count: number, params: {{}}?) -> string,\n",
method
));
}
}
}
code.push_str(" },\n");
}
}
}
code.push_str(" },\n");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_namespace_tree() {
let translations = [
Translation {
key: "ui.buttons.buy".to_string(),
value: "Buy".to_string(),
locale: "en".to_string(),
context: None,
},
Translation {
key: "ui.labels.welcome".to_string(),
value: "Welcome".to_string(),
locale: "en".to_string(),
context: None,
},
];
let refs: Vec<_> = translations.iter().collect();
let plural_base_keys = HashSet::new();
let namespaces = build_namespace_tree(&refs, &plural_base_keys);
assert!(namespaces.contains("ui"));
assert!(namespaces.contains("ui.buttons"));
assert!(namespaces.contains("ui.labels"));
}
#[test]
fn test_generate_type_definitions_with_plurals() {
let translations = vec![
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_type_definitions(&translations, "en").unwrap();
assert!(code.contains(
"ui_messages_items: (self: TranslationsInstance, count: number, params: {}?) -> string"
));
assert!(code
.contains("items: (self: TranslationsInstance, count: number, params: {}?) -> string"));
assert!(!code.contains("items(one)"));
assert!(!code.contains("items(other)"));
}
}