use crate::utils::locales;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub base_locale: String,
pub supported_locales: Vec<String>,
#[serde(default = "default_input_directory")]
pub input_directory: String,
#[serde(default = "default_output_directory")]
pub output_directory: String,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub overrides: Option<OverrideConfig>,
#[serde(default)]
pub analytics: Option<AnalyticsConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OverrideConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_override_file")]
pub file: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AnalyticsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_true")]
pub track_missing: bool,
#[serde(default)]
pub track_usage: bool,
#[serde(default)]
pub callback: Option<String>,
}
impl Config {
pub fn validate(&self) -> Result<()> {
if self.base_locale.is_empty() {
bail!(
"Configuration error: base_locale cannot be empty\n\
\n\
Expected format: base_locale: en\n\
\n\
Hint: The base_locale is your primary language (fallback).\n\
Common values: en, es, pt, de, fr, ja, ko, zh-cn, zh-tw"
);
}
if self.supported_locales.is_empty() {
bail!(
"Configuration error: supported_locales cannot be empty\n\
\n\
Expected format:\n\
supported_locales:\n\
- en\n\
- id\n\
- es\n\
\n\
Hint: List all languages your game will support."
);
}
if !self.supported_locales.contains(&self.base_locale) {
bail!(
"Configuration error: base_locale '{}' must be included in supported_locales\n\
\n\
Current supported_locales: [{}]\n\
\n\
Fix: Add '{}' to your supported_locales list:\n\
supported_locales:\n\
- {}\n\
{}",
self.base_locale,
self.supported_locales.join(", "),
self.base_locale,
self.base_locale,
self.supported_locales
.iter()
.map(|l| format!(" - {}", l))
.collect::<Vec<_>>()
.join("\n")
);
}
let mut unsupported = Vec::new();
for locale in &self.supported_locales {
if !locales::is_roblox_locale(locale) {
unsupported.push(locale.clone());
}
}
if !unsupported.is_empty() {
let supported = locales::get_supported_locale_codes();
bail!(
"Configuration error: Unsupported locale(s): {}\n\
\n\
Roblox supports these 17 locales:\n\
{}\n\
\n\
Common mistakes:\n\
- Using uppercase (use 'en' not 'EN')\n\
- Using wrong format (use 'zh-cn' not 'zh_CN')\n\
- Using unsupported locales\n\
\n\
Hint: Check https://create.roblox.com/docs/production/localization for details.",
unsupported.join(", "),
supported
.iter()
.map(|l| format!(" • {}", l))
.collect::<Vec<_>>()
.join("\n")
);
}
if self.input_directory.is_empty() {
bail!(
"Configuration error: input_directory cannot be empty\n\
\n\
Expected format: input_directory: translations\n\
\n\
Hint: This is where your JSON/YAML translation files are located."
);
}
if self.output_directory.is_empty() {
bail!(
"Configuration error: output_directory cannot be empty\n\
\n\
Expected format: output_directory: output\n\
\n\
Hint: This is where generated Luau code will be placed."
);
}
if self.input_directory == self.output_directory {
bail!(
"Configuration error: input_directory and output_directory cannot be the same\n\
\n\
Current value: '{}'\n\
\n\
Hint: Use different directories to avoid overwriting source files.\n\
Example:\n\
input_directory: translations\n\
output_directory: output",
self.input_directory
);
}
Ok(())
}
}
fn default_input_directory() -> String {
"translations".to_string()
}
fn default_output_directory() -> String {
"output".to_string()
}
fn default_override_file() -> String {
"overrides.yaml".to_string()
}
fn default_true() -> bool {
true
}