2025-09-20 15:47:46 +01:00

203 lines
7.4 KiB
Rust

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<nu_protocol::Example<'_>> {
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<Value, LabeledError> {
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<Vec<String>, LabeledError> {
match value {
Value::List { vals, .. } => {
vals.iter()
.map(|v| value_to_string(v))
.collect::<Result<Vec<_>, _>>()
}
_ => Err(LabeledError::new("Invalid list").with_label("Must be a list of strings", nu_protocol::Span::unknown())),
}
}
fn extract_file_list(value: Value) -> Result<Vec<String>, LabeledError> {
extract_string_list(value)
}
fn load_files_to_bundle(
bundle: &mut FluentBundle<FluentResource>,
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<String> = 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<FluentResource>,
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<FluentResource>,
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<String, LabeledError> {
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()
)),
}
}