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
- 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
222 lines
7.6 KiB
Rust
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(),
|
|
)),
|
|
}
|
|
}
|