Jesús Pérez d9ef2f0d5b
Some checks failed
Build and Test / Validate Setup (push) Has been cancelled
Build and Test / Build (darwin-amd64) (push) Has been cancelled
Build and Test / Build (darwin-arm64) (push) Has been cancelled
Build and Test / Build (linux-amd64) (push) Has been cancelled
Build and Test / Build (windows-amd64) (push) Has been cancelled
Build and Test / Build (linux-arm64) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Package Results (push) Has been cancelled
Build and Test / Quality Gate (push) Has been cancelled
Nightly Build / Check for Changes (push) Has been cancelled
Nightly Build / Validate Setup (push) Has been cancelled
Nightly Build / Nightly Build (darwin-amd64) (push) Has been cancelled
Nightly Build / Nightly Build (darwin-arm64) (push) Has been cancelled
Nightly Build / Nightly Build (linux-amd64) (push) Has been cancelled
Nightly Build / Nightly Build (windows-amd64) (push) Has been cancelled
Nightly Build / Nightly Build (linux-arm64) (push) Has been cancelled
Nightly Build / Create Nightly Pre-release (push) Has been cancelled
Nightly Build / Notify Build Status (push) Has been cancelled
Nightly Build / Nightly Maintenance (push) Has been cancelled
chore: update all plugins to Nushell 0.111.0
- Bump all 18 plugins from 0.110.0 to 0.111.0
  - Update rust-toolchain.toml channel to 1.93.1 (nu 0.111.0 requires ≥1.91.1)

  Fixes:
  - interprocess pin =2.2.x → ^2.3.1 in nu_plugin_mcp, nu_plugin_nats, nu_plugin_typedialog
    (required by nu-plugin-core 0.111.0)
  - nu_plugin_typedialog: BackendType::Web initializer — add open_browser: false field
  - nu_plugin_auth: implement missing user_info_to_value helper referenced in tests

  Scripts:
  - update_all_plugins.nu: fix [package].version update on minor bumps; add [dev-dependencies]
    pass; add nu-plugin-test-support to managed crates
  - download_nushell.nu: rustup override unset before rm -rf on nushell dir replace;
    fix unclosed ) in string interpolation
2026-03-11 03:22:42 +00:00

222 lines
7.6 KiB
Rust

use crate::FluentPlugin;
use fluent::{FluentBundle, FluentResource};
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand};
use nu_protocol::{record, Category, LabeledError, Signature, Span, SyntaxShape, Type, Value};
use unic_langid::LanguageIdentifier;
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(),
)),
}
}