use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; use nu_protocol::{ Category, LabeledError, Signature, Span, SyntaxShape, Type, Value, record, }; use fluent::{FluentBundle, FluentResource}; use unic_langid::LanguageIdentifier; use crate::FluentPlugin; pub struct CreateBundle; impl SimplePluginCommand for CreateBundle { type Plugin = FluentPlugin; fn name(&self) -> &str { "fluent-create-bundle" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Any, Type::Record(vec![].into())) .required("locale", SyntaxShape::String, "Locale code (e.g., en-US)") .named( "global", SyntaxShape::List(Box::new(SyntaxShape::Filepath)), "Global FTL files to include", Some('g'), ) .named( "page", SyntaxShape::List(Box::new(SyntaxShape::Filepath)), "Page-specific FTL files to include", Some('p'), ) .named( "fallback", SyntaxShape::List(Box::new(SyntaxShape::String)), "Fallback locales in order", Some('f'), ) .switch("override", "Allow page files to override global messages", Some('o')) .category(Category::Strings) } fn description(&self) -> &str { "Create a merged Fluent bundle from global and page-specific locale files" } fn examples(&self) -> Vec> { vec![ nu_protocol::Example { description: "Create bundle with global and page-specific files", example: "fluent-create-bundle en-US --global [global/en-US/common.ftl] --page [pages/blog/en-US/blog.ftl]", result: None, }, nu_protocol::Example { description: "Create bundle with fallback support", example: "fluent-create-bundle es-ES --fallback [en-US] --global [global/es-ES/common.ftl]", result: None, }, ] } fn run( &self, _plugin: &Self::Plugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let locale_code: String = call.req(0)?; // Parse locale let locale: LanguageIdentifier = locale_code.parse() .map_err(|e| LabeledError::new("Invalid locale").with_label(format!("Invalid locale '{}': {}", locale_code, e), call.head))?; // Create fallback locales let mut locales = vec![locale.clone()]; if let Some(fallback_value) = call.get_flag("fallback")? { let fallback_codes = extract_string_list(fallback_value)?; for code in fallback_codes { let fallback_locale: LanguageIdentifier = code.parse() .map_err(|e| LabeledError::new("Invalid fallback locale").with_label(format!("Invalid fallback locale '{}': {}", code, e), call.head))?; locales.push(fallback_locale); } } // Create bundle let mut bundle = FluentBundle::new(locales); let allow_override = call.has_flag("override")?; // Load global files first (lower priority) if let Some(global_value) = call.get_flag("global")? { let global_files = extract_file_list(global_value)?; load_files_to_bundle(&mut bundle, &global_files, "global")?; } // Load page files (higher priority, can override global) if let Some(page_value) = call.get_flag("page")? { let page_files = extract_file_list(page_value)?; if allow_override { // Page files can override global messages load_files_to_bundle(&mut bundle, &page_files, "page")?; } else { // Only add page messages that don't exist in global load_files_to_bundle_no_override(&mut bundle, &page_files)?; } } // Extract bundle information let bundle_info = extract_bundle_info(&bundle, &locale_code, call.head); Ok(bundle_info) } } fn extract_string_list(value: Value) -> Result, LabeledError> { match value { Value::List { vals, .. } => { vals.iter() .map(|v| value_to_string(v)) .collect::, _>>() } _ => Err(LabeledError::new("Invalid list").with_label("Must be a list of strings", nu_protocol::Span::unknown())), } } fn extract_file_list(value: Value) -> Result, LabeledError> { extract_string_list(value) } fn load_files_to_bundle( bundle: &mut FluentBundle, files: &[String], source: &str, ) -> Result<(), LabeledError> { for file_path in files { let content = std::fs::read_to_string(file_path) .map_err(|e| LabeledError::new("Read error").with_label(format!("Failed to read '{}': {}", file_path, e), nu_protocol::Span::unknown()))?; let resource = FluentResource::try_new(content) .map_err(|e| LabeledError::new("Invalid FTL").with_label(format!("Invalid FTL in '{}': {:?}", file_path, e), nu_protocol::Span::unknown()))?; bundle.add_resource(resource) .map_err(|errors| { let error_msgs: Vec = errors.iter() .map(|e| format!("{:?}", e)) .collect(); LabeledError::new("Load error").with_label(format!( "Failed to load {} file '{}': {}", source, file_path, error_msgs.join(", ") ), nu_protocol::Span::unknown()) })?; } Ok(()) } fn load_files_to_bundle_no_override( bundle: &mut FluentBundle, files: &[String], ) -> Result<(), LabeledError> { // This is a simplified implementation - in practice, we'd need to // check for conflicts before adding resources load_files_to_bundle(bundle, files, "page") } fn extract_bundle_info( _bundle: &FluentBundle, locale_code: &str, span: Span, ) -> Value { // Extract message IDs and basic info from the bundle let message_ids = Vec::new(); let message_count = 0; // This is a simplified extraction - the actual FluentBundle API // doesn't directly expose message enumeration, so in practice // we'd need to track this during bundle creation Value::record( record! { "locale" => Value::string(locale_code.to_string(), span), "message_count" => Value::int(message_count, span), "message_ids" => Value::list(message_ids, span), "bundle_type" => Value::string("merged".to_string(), span), }, span, ) } fn value_to_string(value: &Value) -> Result { match value { Value::String { val, .. } => Ok(val.clone()), Value::Int { val, .. } => Ok(val.to_string()), Value::Float { val, .. } => Ok(val.to_string()), Value::Bool { val, .. } => Ok(val.to_string()), _ => Err(LabeledError::new("Type conversion error").with_label( format!("Cannot convert {:?} to string", value.get_type()), nu_protocol::Span::unknown() )), } }