mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
Bundle translations (#6661)
This currently doesn't have public API to enable it yet. TODO: - Error handling in the compiler - Public API in the compiler configuration - Documentation
This commit is contained in:
parent
2f62c60e3c
commit
95f5685789
23 changed files with 1163 additions and 15 deletions
|
@ -1265,6 +1265,39 @@ inline SharedString translate(const SharedString &original, const SharedString &
|
|||
return result;
|
||||
}
|
||||
|
||||
inline SharedString translate_from_bundle(std::span<const char8_t *const> strs,
|
||||
cbindgen_private::Slice<SharedString> arguments)
|
||||
{
|
||||
SharedString result;
|
||||
cbindgen_private::slint_translate_from_bundle(
|
||||
cbindgen_private::Slice<const char *>(
|
||||
const_cast<char const **>(reinterpret_cast<char const *const *>(strs.data())),
|
||||
strs.size()),
|
||||
arguments, &result);
|
||||
return result;
|
||||
}
|
||||
inline SharedString
|
||||
translate_from_bundle_with_plural(std::span<const char8_t *const> strs,
|
||||
std::span<const uint32_t> indices,
|
||||
std::span<uintptr_t (*const)(int32_t)> plural_rules,
|
||||
cbindgen_private::Slice<SharedString> arguments, int n)
|
||||
{
|
||||
SharedString result;
|
||||
cbindgen_private::Slice<const char *> strs_slice(
|
||||
const_cast<char const **>(reinterpret_cast<char const *const *>(strs.data())),
|
||||
strs.size());
|
||||
cbindgen_private::Slice<uint32_t> indices_slice(
|
||||
const_cast<uint32_t *>(reinterpret_cast<const uint32_t *>(indices.data())),
|
||||
indices.size());
|
||||
cbindgen_private::Slice<uintptr_t (*)(int32_t)> plural_rules_slice(
|
||||
const_cast<uintptr_t (**)(int32_t)>(
|
||||
reinterpret_cast<uintptr_t (*const *)(int32_t)>(plural_rules.data())),
|
||||
plural_rules.size());
|
||||
cbindgen_private::slint_translate_from_bundle_with_plural(
|
||||
strs_slice, indices_slice, plural_rules_slice, arguments, n, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace private_api
|
||||
|
||||
#ifdef SLINT_FEATURE_GETTEXT
|
||||
|
|
|
@ -213,6 +213,9 @@ pub mod re_exports {
|
|||
};
|
||||
pub use i_slint_core::slice::Slice;
|
||||
pub use i_slint_core::timers::{Timer, TimerMode};
|
||||
pub use i_slint_core::translations::{
|
||||
set_language_internal, translate_from_bundle, translate_from_bundle_with_plural,
|
||||
};
|
||||
pub use i_slint_core::window::{
|
||||
InputMethodRequest, WindowAdapter, WindowAdapterRc, WindowInner,
|
||||
};
|
||||
|
|
|
@ -29,9 +29,11 @@ display-diagnostics = ["codemap", "codemap-diagnostic"]
|
|||
|
||||
# Enabled the support to render images and font in the binary
|
||||
software-renderer = ["image", "dep:resvg", "fontdue", "i-slint-common/shared-fontdb", "dep:rayon"]
|
||||
|
||||
embed-glyphs-as-sdf = ["dep:fdsm", "dep:ttf-parser-fdsm", "dep:nalgebra", "dep:image-fdsm", "dep:rayon"]
|
||||
|
||||
# Translation bundler
|
||||
bundle-translations = ["dep:polib"]
|
||||
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
|
@ -65,6 +67,8 @@ ttf-parser-fdsm = { package = "ttf-parser", version = "0.24.1", optional = true
|
|||
image-fdsm = { package = "image", version = "0.25", optional = true, default-features = false }
|
||||
nalgebra = { version = "0.33.0", optional = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
# translations
|
||||
polib = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
i-slint-parser-test-macro = { path = "./parser-test-macro" }
|
||||
|
|
|
@ -710,6 +710,11 @@ pub fn generate(
|
|||
|
||||
let llr = llr::lower_to_item_tree::lower_to_item_tree(&doc, compiler_config);
|
||||
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
if let Some(translations) = &llr.translations {
|
||||
generate_translation(translations, &llr, &mut file.resources);
|
||||
}
|
||||
|
||||
// Forward-declare the root so that sub-components can access singletons, the window, etc.
|
||||
file.declarations.extend(
|
||||
llr.public_components
|
||||
|
@ -3369,6 +3374,16 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
|
|||
)
|
||||
}
|
||||
Expression::EmptyComponentFactory => panic!("component-factory not yet supported in C++"),
|
||||
Expression::TranslationReference { format_args, string_index, plural } => {
|
||||
let args = compile_expression(format_args, ctx);
|
||||
match plural {
|
||||
Some(plural) => {
|
||||
let plural = compile_expression(plural, ctx);
|
||||
format!("slint::private_api::translate_from_bundle_with_plural(slint_translation_bundle_plural_{string_index}_str, slint_translation_bundle_plural_{string_index}_idx, slint_translated_plural_rules, {args}, {plural})")
|
||||
}
|
||||
None => format!("slint::private_api::translate_from_bundle(slint_translation_bundle_{string_index}, {args})"),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3828,3 +3843,106 @@ fn generate_type_aliases(file: &mut File, doc: &Document) {
|
|||
|
||||
file.declarations.extend(type_aliases);
|
||||
}
|
||||
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
fn generate_translation(
|
||||
translations: &llr::translations::Translations,
|
||||
compilation_unit: &llr::CompilationUnit,
|
||||
declarations: &mut Vec<Declaration>,
|
||||
) {
|
||||
for (idx, m) in translations.strings.iter().enumerate() {
|
||||
declarations.push(Declaration::Var(Var {
|
||||
ty: "const char8_t* const".into(),
|
||||
name: format_smolstr!("slint_translation_bundle_{idx}"),
|
||||
array_size: Some(m.len()),
|
||||
init: Some(format!(
|
||||
"{{ {} }}",
|
||||
m.iter()
|
||||
.map(|s| match s {
|
||||
Some(s) => format_smolstr!("u8\"{}\"", escape_string(s.as_str())),
|
||||
None => "nullptr".into(),
|
||||
})
|
||||
.join(", ")
|
||||
)),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
for (idx, ms) in translations.plurals.iter().enumerate() {
|
||||
let all_strs = ms.iter().flatten().flatten();
|
||||
let all_strs_len = all_strs.clone().count();
|
||||
declarations.push(Declaration::Var(Var {
|
||||
ty: "const char8_t* const".into(),
|
||||
name: format_smolstr!("slint_translation_bundle_plural_{}_str", idx),
|
||||
array_size: Some(all_strs_len),
|
||||
init: Some(format!(
|
||||
"{{ {} }}",
|
||||
all_strs.map(|s| format_smolstr!("u8\"{}\"", escape_string(s.as_str()))).join(", ")
|
||||
)),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let mut count = 0;
|
||||
declarations.push(Declaration::Var(Var {
|
||||
ty: "const uint32_t".into(),
|
||||
name: format_smolstr!("slint_translation_bundle_plural_{}_idx", idx),
|
||||
array_size: Some(ms.len()),
|
||||
init: Some(format!(
|
||||
"{{ {} }}",
|
||||
ms.iter()
|
||||
.map(|x| {
|
||||
count += x.as_ref().map_or(0, |x| x.len());
|
||||
count
|
||||
})
|
||||
.join(", ")
|
||||
)),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
let lang_len = translations.languages.len();
|
||||
declarations.push(Declaration::Function(Function {
|
||||
name: "slint_set_language".into(),
|
||||
signature: "(std::string_view lang)".into(),
|
||||
is_inline: true,
|
||||
statements: Some(vec![
|
||||
format!("std::array<slint::cbindgen_private::Slice<uint8_t>, {lang_len}> languages {{ {} }};", translations.languages.iter().map(|l| format!("slint::private_api::string_to_slice({l:?})")).join(", ")),
|
||||
format!("return slint::cbindgen_private::slint_translate_set_language(slint::private_api::string_to_slice(lang), {{ languages.data(), {lang_len} }});"
|
||||
)]),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let ctx = EvaluationContext {
|
||||
compilation_unit,
|
||||
current_sub_component: None,
|
||||
current_global: None,
|
||||
generator_state: CppGeneratorContext {
|
||||
global_access: "\n#error \"language rule can't access state\";".into(),
|
||||
conditional_includes: &Default::default(),
|
||||
},
|
||||
parent: None,
|
||||
argument_types: &[Type::Int32],
|
||||
};
|
||||
declarations.push(Declaration::Var(Var {
|
||||
ty: format_smolstr!(
|
||||
"const std::array<uintptr_t (*const)(int32_t), {}>",
|
||||
translations.plural_rules.len()
|
||||
),
|
||||
name: "slint_translated_plural_rules".into(),
|
||||
init: Some(format!(
|
||||
"{{ {} }}",
|
||||
translations
|
||||
.plural_rules
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Some(s) => {
|
||||
format!(
|
||||
"[]([[maybe_unused]] int32_t arg_0) -> uintptr_t {{ return {}; }}",
|
||||
compile_expression(s, &ctx)
|
||||
)
|
||||
}
|
||||
None => "nullptr".into(),
|
||||
})
|
||||
.join(", ")
|
||||
)),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -210,6 +210,15 @@ pub fn generate(doc: &Document, compiler_config: &CompilerConfiguration) -> Toke
|
|||
.map(|c| format_ident!("slint_generated{}", ident(&c.id)))
|
||||
.unwrap_or_else(|| format_ident!("slint_generated"));
|
||||
|
||||
#[cfg(not(feature = "bundle-translations"))]
|
||||
let (translations, exported_tr) = (quote!(), quote!());
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
let (translations, exported_tr) = llr
|
||||
.translations
|
||||
.as_ref()
|
||||
.map(|t| (generate_translations(t, &llr), quote!(slint_set_language,)))
|
||||
.unzip();
|
||||
|
||||
quote! {
|
||||
#[allow(non_snake_case, non_camel_case_types)]
|
||||
#[allow(unused_braces, unused_parens)]
|
||||
|
@ -224,10 +233,11 @@ pub fn generate(doc: &Document, compiler_config: &CompilerConfiguration) -> Toke
|
|||
#(#public_components)*
|
||||
#shared_globals
|
||||
#(#resource_symbols)*
|
||||
#translations
|
||||
const _THE_SAME_VERSION_MUST_BE_USED_FOR_THE_COMPILER_AND_THE_RUNTIME : slint::#version_check = slint::#version_check;
|
||||
}
|
||||
#[allow(unused_imports)]
|
||||
pub use #generated_mod::{#(#compo_ids,)* #(#structs_and_enums_ids,)* #(#globals_ids,)* #(#named_exports,)*};
|
||||
pub use #generated_mod::{#(#compo_ids,)* #(#structs_and_enums_ids,)* #(#globals_ids,)* #(#named_exports,)* #exported_tr};
|
||||
#[allow(unused_imports)]
|
||||
pub use slint::{ComponentHandle as _, Global as _, ModelExt as _};
|
||||
}
|
||||
|
@ -2574,6 +2584,22 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
|
|||
}
|
||||
}
|
||||
Expression::EmptyComponentFactory => quote!(slint::ComponentFactory::default()),
|
||||
Expression::TranslationReference { format_args, string_index, plural } => {
|
||||
let args = compile_expression(format_args, ctx);
|
||||
match plural {
|
||||
Some(plural) => {
|
||||
let plural = compile_expression(plural, ctx);
|
||||
quote!(sp::translate_from_bundle_with_plural(
|
||||
&self::_SLINT_TRANSLATED_STRINGS_PLURALS[#string_index],
|
||||
&self::_SLINT_TRANSLATED_PLURAL_RULES,
|
||||
sp::Slice::<sp::SharedString>::from(#args).as_slice(),
|
||||
#plural as _
|
||||
))
|
||||
}
|
||||
None => quote!(sp::translate_from_bundle(&self::_SLINT_TRANSLATED_STRINGS[#string_index], sp::Slice::<sp::SharedString>::from(#args).as_slice())),
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3138,3 +3164,59 @@ fn generate_named_exports(doc: &Document) -> Vec<TokenStream> {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
fn generate_translations(
|
||||
translations: &llr::translations::Translations,
|
||||
compilation_unit: &llr::CompilationUnit,
|
||||
) -> TokenStream {
|
||||
let strings = translations.strings.iter().map(|strings| {
|
||||
let array = strings.iter().map(|s| match s.as_ref().map(SmolStr::as_str) {
|
||||
Some(s) => quote!(Some(#s)),
|
||||
None => quote!(None),
|
||||
});
|
||||
quote!(&[#(#array),*])
|
||||
});
|
||||
let plurals = translations.plurals.iter().map(|plurals| {
|
||||
let array = plurals.iter().map(|p| match p {
|
||||
Some(p) => {
|
||||
let p = p.iter().map(SmolStr::as_str);
|
||||
quote!(Some(&[#(#p),*]))
|
||||
}
|
||||
None => quote!(None),
|
||||
});
|
||||
quote!(&[#(#array),*])
|
||||
});
|
||||
|
||||
let ctx = EvaluationContext {
|
||||
compilation_unit,
|
||||
current_sub_component: None,
|
||||
current_global: None,
|
||||
generator_state: RustGeneratorContext {
|
||||
global_access: quote!(compile_error!("language rule can't access state")),
|
||||
},
|
||||
parent: None,
|
||||
argument_types: &[Type::Int32],
|
||||
};
|
||||
let rules = translations.plural_rules.iter().map(|rule| {
|
||||
let rule = match rule {
|
||||
Some(rule) => {
|
||||
let rule = compile_expression(rule, &ctx);
|
||||
quote!(Some(|arg: i32| { let args = (arg,); (#rule) as usize } ))
|
||||
}
|
||||
None => quote!(None),
|
||||
};
|
||||
quote!(#rule)
|
||||
});
|
||||
let lang = translations.languages.iter().map(SmolStr::as_str).map(|lang| quote!(#lang));
|
||||
|
||||
quote!(
|
||||
const _SLINT_TRANSLATED_STRINGS: &[&[sp::Option<&str>]] = &[#(#strings),*];
|
||||
const _SLINT_TRANSLATED_STRINGS_PLURALS: &[&[sp::Option<&[&str]>]] = &[#(#plurals),*];
|
||||
#[allow(unused)]
|
||||
const _SLINT_TRANSLATED_PLURAL_RULES: &[sp::Option<fn(i32) -> usize>] = &[#(#rules),*];
|
||||
pub fn slint_set_language(lang: &str) -> bool {
|
||||
sp::set_language_internal(lang, &[#(#lang),*])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -132,6 +132,9 @@ pub struct CompilerConfiguration {
|
|||
|
||||
/// The domain used as one of the parameter to the translate function
|
||||
pub translation_domain: Option<String>,
|
||||
/// When Some, this is the path where the translations are looked at to bundle the translations
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
pub translation_path_bundle: Option<std::path::PathBuf>,
|
||||
|
||||
/// C++ namespace
|
||||
pub cpp_namespace: Option<String>,
|
||||
|
@ -220,6 +223,10 @@ impl CompilerConfiguration {
|
|||
components_to_generate: ComponentSelection::ExportedWindows,
|
||||
#[cfg(feature = "software-renderer")]
|
||||
font_cache: Default::default(),
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
translation_path_bundle: std::env::var("SLINT_BUNDLE_TRANSLATIONS")
|
||||
.ok()
|
||||
.map(|x| x.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ pub use item_tree::*;
|
|||
pub mod lower_expression;
|
||||
pub mod lower_to_item_tree;
|
||||
pub mod pretty_print;
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
pub mod translations;
|
||||
|
||||
/// The optimization passes over the LLR
|
||||
pub mod optim_passes {
|
||||
|
|
|
@ -190,6 +190,15 @@ pub enum Expression {
|
|||
},
|
||||
|
||||
EmptyComponentFactory,
|
||||
|
||||
/// A reference to bundled translated string
|
||||
TranslationReference {
|
||||
/// An expression of type array of strings
|
||||
format_args: Box<Expression>,
|
||||
string_index: usize,
|
||||
/// The `n` value to use for the plural form if it is a plural form
|
||||
plural: Option<Box<Expression>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
|
@ -306,6 +315,7 @@ impl Expression {
|
|||
}
|
||||
Self::MinMax { ty, .. } => ty.clone(),
|
||||
Self::EmptyComponentFactory => Type::ComponentFactory,
|
||||
Self::TranslationReference { .. } => Type::String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -388,6 +398,12 @@ macro_rules! visit_impl {
|
|||
$visitor(rhs);
|
||||
}
|
||||
Expression::EmptyComponentFactory => {}
|
||||
Expression::TranslationReference { format_args, plural, string_index: _ } => {
|
||||
$visitor(format_args);
|
||||
if let Some(plural) = plural {
|
||||
$visitor(plural);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -368,6 +368,8 @@ pub struct CompilationUnit {
|
|||
pub sub_components: Vec<Rc<SubComponent>>,
|
||||
pub globals: Vec<GlobalComponent>,
|
||||
pub has_debug_info: bool,
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
pub translations: Option<super::translations::Translations>,
|
||||
}
|
||||
|
||||
impl CompilationUnit {
|
||||
|
|
|
@ -135,6 +135,12 @@ pub fn lower_expression(
|
|||
if let llr_Expression::Array { as_model, .. } = &mut arguments[3] {
|
||||
*as_model = false;
|
||||
}
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
if let Some(mut translation_builder) =
|
||||
ctx.state.translation_builder.as_ref().map(|x| x.borrow_mut())
|
||||
{
|
||||
return translation_builder.lower_translate_call(arguments);
|
||||
}
|
||||
}
|
||||
llr_Expression::BuiltinFunctionCall { function: f.clone(), arguments }
|
||||
}
|
||||
|
|
|
@ -20,6 +20,17 @@ pub fn lower_to_item_tree(
|
|||
) -> CompilationUnit {
|
||||
let mut state = LoweringState::default();
|
||||
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
if let Some(path) = &compiler_config.translation_path_bundle {
|
||||
state.translation_builder = Some(std::cell::RefCell::new(
|
||||
super::translations::TranslationsBuilder::load_translations(
|
||||
path,
|
||||
compiler_config.translation_domain.as_deref().unwrap_or(""),
|
||||
)
|
||||
.unwrap_or_else(|e| todo!("TODO: handle error loading translations: {e}")),
|
||||
));
|
||||
}
|
||||
|
||||
let mut globals = Vec::new();
|
||||
for g in &document.used_types.borrow().globals {
|
||||
let count = globals.len();
|
||||
|
@ -64,6 +75,8 @@ pub fn lower_to_item_tree(
|
|||
})
|
||||
.collect(),
|
||||
has_debug_info: compiler_config.debug_info,
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
translations: state.translation_builder.take().map(|x| x.into_inner().result()),
|
||||
};
|
||||
super::optim_passes::run_passes(&root);
|
||||
root
|
||||
|
@ -73,6 +86,8 @@ pub fn lower_to_item_tree(
|
|||
pub struct LoweringState {
|
||||
global_properties: HashMap<NamedReference, PropertyReference>,
|
||||
sub_components: HashMap<ByAddress<Rc<Component>>, LoweredSubComponent>,
|
||||
#[cfg(feature = "bundle-translations")]
|
||||
pub translation_builder: Option<std::cell::RefCell<super::translations::TranslationsBuilder>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -62,6 +62,7 @@ fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize {
|
|||
Expression::ComputeDialogLayoutCells { .. } => return isize::MAX,
|
||||
Expression::MinMax { .. } => 10,
|
||||
Expression::EmptyComponentFactory => 10,
|
||||
Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST,
|
||||
};
|
||||
|
||||
exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx)));
|
||||
|
|
|
@ -349,6 +349,23 @@ impl<'a, T> Display for DisplayExpression<'a, T> {
|
|||
MinMaxOp::Max => write!(f, "max({}, {})", e(lhs), e(rhs)),
|
||||
},
|
||||
Expression::EmptyComponentFactory => write!(f, "<empty-component-factory>",),
|
||||
Expression::TranslationReference { format_args, string_index, plural } => {
|
||||
match plural {
|
||||
Some(plural) => write!(
|
||||
f,
|
||||
"@tr({:?} % {}, {})",
|
||||
string_index,
|
||||
DisplayExpression(plural, ctx),
|
||||
DisplayExpression(format_args, ctx)
|
||||
),
|
||||
None => write!(
|
||||
f,
|
||||
"@tr({:?}, {})",
|
||||
string_index,
|
||||
DisplayExpression(format_args, ctx)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
432
internal/compiler/llr/translations.rs
Normal file
432
internal/compiler/llr/translations.rs
Normal file
|
@ -0,0 +1,432 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::Expression;
|
||||
use core::ops::Not;
|
||||
use smol_str::{SmolStr, ToSmolStr};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Translations {
|
||||
/// An array with all the array of string
|
||||
/// The first vector index is stored in the LLR.
|
||||
/// The inner vector index is the language id. (The first is the original)
|
||||
/// Only contains the string that are not having plural forms
|
||||
pub strings: Vec<Vec<Option<SmolStr>>>,
|
||||
/// An array with all the strings that are used in a plural form.
|
||||
/// The first vector index is stored in the LLR.
|
||||
/// The inner vector index is the language. (The first is the original string)
|
||||
/// The last vector contains each form
|
||||
pub plurals: Vec<Vec<Option<Vec<SmolStr>>>>,
|
||||
|
||||
/// Expression is a function that maps its first and only argument (an integer)
|
||||
/// to the plural form index (an integer)
|
||||
/// It can only do basic mathematical operations.
|
||||
/// The expression cannot reference properties or variable.
|
||||
/// Only builtin math functions, and its first argument
|
||||
pub plural_rules: Vec<Option<Expression>>,
|
||||
|
||||
/// The "names" of the languages
|
||||
pub languages: Vec<SmolStr>,
|
||||
}
|
||||
|
||||
pub struct TranslationsBuilder {
|
||||
result: Translations,
|
||||
/// Maps (msgid, msgid_plural, msgctx) to the index in the result
|
||||
/// (the index is in strings or plurals depending if there is a plural)
|
||||
map: HashMap<(SmolStr, SmolStr, SmolStr), usize>,
|
||||
|
||||
/// The catalog containing the translations
|
||||
catalogs: Vec<polib::catalog::Catalog>,
|
||||
}
|
||||
|
||||
impl TranslationsBuilder {
|
||||
pub fn load_translations(path: &Path, domain: &str) -> std::io::Result<Self> {
|
||||
let mut languages = vec!["".into()];
|
||||
let mut catalogs = Vec::new();
|
||||
let mut plural_rules =
|
||||
vec![Some(plural_rule_parser::parse_rule_expression("n!=1").unwrap())];
|
||||
for l in std::fs::read_dir(path)? {
|
||||
let l = l?;
|
||||
let path = l.path().join("LC_MESSAGES").join(format!("{}.po", domain));
|
||||
if path.exists() {
|
||||
let catalog = polib::po_file::parse(&path).map_err(|e| {
|
||||
std::io::Error::other(format!("Error parsing {}: {e}", path.display()))
|
||||
})?;
|
||||
languages.push(l.file_name().to_string_lossy().into());
|
||||
plural_rules.push(Some(
|
||||
plural_rule_parser::parse_rule_expression(&catalog.metadata.plural_rules.expr)
|
||||
.map_err(|_| {
|
||||
std::io::Error::other(format!(
|
||||
"Error parsing plural rules in {}",
|
||||
path.display()
|
||||
))
|
||||
})?,
|
||||
));
|
||||
catalogs.push(catalog);
|
||||
}
|
||||
}
|
||||
if catalogs.is_empty() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"No translations found. We look for files in '{}/<lang>/LC_MESSAGES/{domain}.po",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok(Self {
|
||||
result: Translations {
|
||||
strings: Vec::new(),
|
||||
plurals: Vec::new(),
|
||||
plural_rules,
|
||||
languages,
|
||||
},
|
||||
map: HashMap::new(),
|
||||
catalogs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lower_translate_call(&mut self, args: Vec<Expression>) -> Expression {
|
||||
let [original, contextid, _domain, format_args, n, plural] = args
|
||||
.try_into()
|
||||
.expect("The resolving pass should have ensured that the arguments are correct");
|
||||
let original = get_string(original).expect("original must be a string");
|
||||
let contextid = get_string(contextid).expect("contextid must be a string");
|
||||
let plural = get_string(plural).expect("plural must be a string");
|
||||
|
||||
let is_plural =
|
||||
!plural.is_empty() || !matches!(n, Expression::NumberLiteral(f) if f == 1.0);
|
||||
|
||||
match self.map.entry((original.clone(), plural.clone(), contextid.clone())) {
|
||||
Entry::Occupied(entry) => Expression::TranslationReference {
|
||||
format_args: format_args.into(),
|
||||
string_index: *entry.get(),
|
||||
plural: is_plural.then(|| n.into()),
|
||||
},
|
||||
Entry::Vacant(entry) => {
|
||||
let messages = self.catalogs.iter().map(|catalog| {
|
||||
catalog.find_message(
|
||||
contextid.is_empty().not().then_some(contextid.as_str()),
|
||||
&original,
|
||||
is_plural.then_some(plural.as_str()),
|
||||
)
|
||||
});
|
||||
let idx = if is_plural {
|
||||
let messages = std::iter::once(Some(vec![original.clone(), plural.clone()]))
|
||||
.chain(messages.map(|x| {
|
||||
x.and_then(|x| {
|
||||
Some(
|
||||
x.msgstr_plural()
|
||||
.ok()?
|
||||
.iter()
|
||||
.map(|x| x.to_smolstr())
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.collect();
|
||||
self.result.plurals.push(messages);
|
||||
self.result.plurals.len() - 1
|
||||
} else {
|
||||
let messages = std::iter::once(Some(original.clone()))
|
||||
.chain(
|
||||
messages
|
||||
.map(|x| x.and_then(|x| x.msgstr().ok()).map(|x| x.to_smolstr())),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
self.result.strings.push(messages);
|
||||
self.result.strings.len() - 1
|
||||
};
|
||||
Expression::TranslationReference {
|
||||
format_args: format_args.into(),
|
||||
string_index: *entry.insert(idx),
|
||||
plural: is_plural.then(|| n.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn result(self) -> Translations {
|
||||
self.result
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string(plural: Expression) -> Option<SmolStr> {
|
||||
match plural {
|
||||
Expression::StringLiteral(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
mod plural_rule_parser {
|
||||
use super::Expression;
|
||||
pub struct ParseError<'a>(&'static str, &'a [u8]);
|
||||
impl std::fmt::Debug for ParseError<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "ParseError({}, rest={:?})", self.0, std::str::from_utf8(self.1).unwrap())
|
||||
}
|
||||
}
|
||||
pub fn parse_rule_expression(string: &str) -> Result<Expression, ParseError> {
|
||||
let ascii = string.as_bytes();
|
||||
let s = parse_expression(ascii)?;
|
||||
if !s.rest.is_empty() {
|
||||
return Err(ParseError("extra character in string", s.rest));
|
||||
}
|
||||
match s.ty {
|
||||
Ty::Number => Ok(s.expr),
|
||||
Ty::Boolean => Ok(Expression::Condition {
|
||||
condition: s.expr.into(),
|
||||
true_expr: Expression::NumberLiteral(1.).into(),
|
||||
false_expr: Expression::NumberLiteral(0.).into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum Ty {
|
||||
Number,
|
||||
Boolean,
|
||||
}
|
||||
|
||||
struct ParsingState<'a> {
|
||||
expr: Expression,
|
||||
rest: &'a [u8],
|
||||
ty: Ty,
|
||||
}
|
||||
|
||||
impl ParsingState<'_> {
|
||||
fn skip_whitespace(self) -> Self {
|
||||
let rest = skip_whitespace(self.rest);
|
||||
Self { rest, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// `<condition> ('?' <expr> : <expr> )?`
|
||||
fn parse_expression(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let state = parse_condition(string)?.skip_whitespace();
|
||||
if state.ty != Ty::Boolean {
|
||||
return Ok(state);
|
||||
}
|
||||
if let Some(rest) = state.rest.strip_prefix(b"?") {
|
||||
let s1 = parse_expression(rest)?.skip_whitespace();
|
||||
let rest = s1.rest.strip_prefix(b":").ok_or(ParseError("expected ':'", s1.rest))?;
|
||||
let s2 = parse_expression(rest)?;
|
||||
if s1.ty != s2.ty {
|
||||
return Err(ParseError("incompatible types in ternary operator", s2.rest));
|
||||
}
|
||||
Ok(ParsingState {
|
||||
expr: Expression::Condition {
|
||||
condition: state.expr.into(),
|
||||
true_expr: s1.expr.into(),
|
||||
false_expr: s2.expr.into(),
|
||||
},
|
||||
rest: skip_whitespace(s2.rest),
|
||||
ty: s2.ty,
|
||||
})
|
||||
} else {
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// `<and_expr> ("||" <condition>)?`
|
||||
fn parse_condition(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let state = parse_and_expr(string)?.skip_whitespace();
|
||||
if state.rest.is_empty() {
|
||||
return Ok(state);
|
||||
}
|
||||
if let Some(rest) = state.rest.strip_prefix(b"||") {
|
||||
let state2 = parse_condition(rest)?;
|
||||
if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
|
||||
return Err(ParseError("incompatible types in || operator", state2.rest));
|
||||
}
|
||||
Ok(ParsingState {
|
||||
expr: Expression::BinaryExpression {
|
||||
lhs: state.expr.into(),
|
||||
rhs: state2.expr.into(),
|
||||
op: '|',
|
||||
},
|
||||
ty: Ty::Boolean,
|
||||
rest: skip_whitespace(state2.rest),
|
||||
})
|
||||
} else {
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// `<cmp_expr> ("&&" <and_expr>)?`
|
||||
fn parse_and_expr(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let state = parse_cmp_expr(string)?.skip_whitespace();
|
||||
if state.rest.is_empty() {
|
||||
return Ok(state);
|
||||
}
|
||||
if let Some(rest) = state.rest.strip_prefix(b"&&") {
|
||||
let state2 = parse_and_expr(rest)?;
|
||||
if state.ty != Ty::Boolean || state2.ty != Ty::Boolean {
|
||||
return Err(ParseError("incompatible types in || operator", state2.rest));
|
||||
}
|
||||
Ok(ParsingState {
|
||||
expr: Expression::BinaryExpression {
|
||||
lhs: state.expr.into(),
|
||||
rhs: state2.expr.into(),
|
||||
op: '&',
|
||||
},
|
||||
ty: Ty::Boolean,
|
||||
rest: skip_whitespace(state2.rest),
|
||||
})
|
||||
} else {
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// `<value> ('=='|'!='|'<'|'>'|'<='|'>=' <cmp_expr>)?`
|
||||
fn parse_cmp_expr(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let mut state = parse_value(string)?;
|
||||
state.rest = skip_whitespace(state.rest);
|
||||
if state.rest.is_empty() {
|
||||
return Ok(state);
|
||||
}
|
||||
for (token, op) in [
|
||||
(b"==" as &[u8], '='),
|
||||
(b"!=", '!'),
|
||||
(b"<=", '≤'),
|
||||
(b">=", '≥'),
|
||||
(b"<", '<'),
|
||||
(b">", '>'),
|
||||
] {
|
||||
if let Some(rest) = state.rest.strip_prefix(token) {
|
||||
let state2 = parse_cmp_expr(rest)?;
|
||||
if state.ty != Ty::Number || state2.ty != Ty::Number {
|
||||
return Err(ParseError("incompatible types in comparison", state2.rest));
|
||||
}
|
||||
return Ok(ParsingState {
|
||||
expr: Expression::BinaryExpression {
|
||||
lhs: state.expr.into(),
|
||||
rhs: state2.expr.into(),
|
||||
op,
|
||||
},
|
||||
ty: Ty::Boolean,
|
||||
rest: skip_whitespace(state2.rest),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// `<term> ('%' <term>)?`
|
||||
fn parse_value(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let mut state = parse_term(string)?;
|
||||
state.rest = skip_whitespace(state.rest);
|
||||
if state.rest.is_empty() {
|
||||
return Ok(state);
|
||||
}
|
||||
if let Some(rest) = state.rest.strip_prefix(b"%") {
|
||||
let state2 = parse_term(rest)?;
|
||||
if state.ty != Ty::Number || state2.ty != Ty::Number {
|
||||
return Err(ParseError("incompatible types in % operator", state2.rest));
|
||||
}
|
||||
Ok(ParsingState {
|
||||
expr: Expression::BuiltinFunctionCall {
|
||||
function: crate::expression_tree::BuiltinFunction::Mod,
|
||||
arguments: vec![state.expr.into(), state2.expr.into()],
|
||||
},
|
||||
ty: Ty::Number,
|
||||
rest: skip_whitespace(state2.rest),
|
||||
})
|
||||
} else {
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_term(string: &[u8]) -> Result<ParsingState, ParseError> {
|
||||
let string = skip_whitespace(string);
|
||||
let state = match string.first().ok_or(ParseError("unexpected end of string", string))? {
|
||||
b'n' => ParsingState {
|
||||
expr: Expression::FunctionParameterReference { index: 0 },
|
||||
rest: &string[1..],
|
||||
ty: Ty::Number,
|
||||
},
|
||||
b'(' => {
|
||||
let mut s = parse_expression(&string[1..])?;
|
||||
s.rest = s.rest.strip_prefix(b")").ok_or(ParseError("expected ')'", s.rest))?;
|
||||
s
|
||||
}
|
||||
x if x.is_ascii_digit() => {
|
||||
let (n, rest) = parse_number(string)?;
|
||||
ParsingState { expr: Expression::NumberLiteral(n as _), rest, ty: Ty::Number }
|
||||
}
|
||||
_ => return Err(ParseError("unexpected token", string)),
|
||||
};
|
||||
Ok(state)
|
||||
}
|
||||
fn parse_number(string: &[u8]) -> Result<(i32, &[u8]), ParseError> {
|
||||
let end = string.iter().position(|&c| !c.is_ascii_digit()).unwrap_or(string.len());
|
||||
let n = std::str::from_utf8(&string[..end])
|
||||
.expect("string is valid utf-8")
|
||||
.parse()
|
||||
.map_err(|_| ParseError("can't parse number", string))?;
|
||||
Ok((n, &string[end..]))
|
||||
}
|
||||
fn skip_whitespace(mut string: &[u8]) -> &[u8] {
|
||||
// slice::trim_ascii_start when MSRV >= 1.80
|
||||
while !string.is_empty() && string[0].is_ascii_whitespace() {
|
||||
string = &string[1..];
|
||||
}
|
||||
string
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rule_expression() {
|
||||
#[track_caller]
|
||||
fn p(string: &str) -> String {
|
||||
let ctx = crate::llr::EvaluationContext {
|
||||
compilation_unit: &crate::llr::CompilationUnit {
|
||||
public_components: Vec::new(),
|
||||
sub_components: Vec::new(),
|
||||
globals: Vec::new(),
|
||||
has_debug_info: false,
|
||||
translations: None,
|
||||
},
|
||||
current_sub_component: None,
|
||||
current_global: None,
|
||||
generator_state: (),
|
||||
parent: None,
|
||||
argument_types: &[crate::langtype::Type::Int32],
|
||||
};
|
||||
crate::llr::pretty_print::DisplayExpression(
|
||||
&parse_rule_expression(string).expect("parse error"),
|
||||
&ctx,
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// en
|
||||
assert_eq!(p("n != 1"), "((arg_0 ! 1.0) ? 1.0 : 0.0)");
|
||||
// fr
|
||||
assert_eq!(p("n > 1"), "((arg_0 > 1.0) ? 1.0 : 0.0)");
|
||||
// ar
|
||||
assert_eq!(
|
||||
p("(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5)"),
|
||||
"((arg_0 = 0.0) ? 0.0 : ((arg_0 = 1.0) ? 1.0 : ((arg_0 = 2.0) ? 2.0 : (((Mod(arg_0, 100.0) ≥ 3.0) & (Mod(arg_0, 100.0) ≤ 10.0)) ? 3.0 : ((Mod(arg_0, 100.0) ≥ 11.0) ? 4.0 : 5.0)))))"
|
||||
);
|
||||
// ga
|
||||
assert_eq!(p("n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4"), "((arg_0 = 1.0) ? 0.0 : ((arg_0 = 2.0) ? 1.0 : (((arg_0 > 2.0) & (arg_0 < 7.0)) ? 2.0 : (((arg_0 > 6.0) & (arg_0 < 11.0)) ? 3.0 : 4.0))))");
|
||||
// ja
|
||||
assert_eq!(p("0"), "0.0");
|
||||
// pl
|
||||
assert_eq!(
|
||||
p("(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"),
|
||||
"((arg_0 = 1.0) ? 0.0 : (((Mod(arg_0, 10.0) ≥ 2.0) & ((Mod(arg_0, 10.0) ≤ 4.0) & ((Mod(arg_0, 100.0) < 10.0) | (Mod(arg_0, 100.0) ≥ 20.0)))) ? 1.0 : 2.0))",
|
||||
);
|
||||
|
||||
// ru
|
||||
assert_eq!(
|
||||
p("(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"),
|
||||
"(((Mod(arg_0, 10.0) = 1.0) & (Mod(arg_0, 100.0) ! 11.0)) ? 0.0 : (((Mod(arg_0, 10.0) ≥ 2.0) & ((Mod(arg_0, 10.0) ≤ 4.0) & ((Mod(arg_0, 100.0) < 10.0) | (Mod(arg_0, 100.0) ≥ 20.0)))) ? 1.0 : 2.0))",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,9 +18,10 @@ thread_local! {
|
|||
pub(crate) struct SlintContextInner {
|
||||
platform: Box<dyn Platform>,
|
||||
pub(crate) window_count: core::cell::RefCell<isize>,
|
||||
/// This property is read by all translations, and marked dirty when the language change
|
||||
/// so that every translated string gets re-translated
|
||||
pub(crate) translations_dirty: core::pin::Pin<Box<Property<()>>>,
|
||||
/// This property is read by all translations, and marked dirty when the language changes,
|
||||
/// so that every translated string gets re-translated. The property's value is the current selected
|
||||
/// language when bundling translations.
|
||||
pub(crate) translations_dirty: core::pin::Pin<Box<Property<usize>>>,
|
||||
pub(crate) window_shown_hook:
|
||||
core::cell::RefCell<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>>,
|
||||
}
|
||||
|
@ -37,7 +38,7 @@ impl SlintContext {
|
|||
Self(Rc::new(SlintContextInner {
|
||||
platform,
|
||||
window_count: 0.into(),
|
||||
translations_dirty: Box::pin(Property::new_named((), "SlintContext::translations")),
|
||||
translations_dirty: Box::pin(Property::new_named(0, "SlintContext::translations")),
|
||||
window_shown_hook: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -192,10 +192,7 @@ pub fn translate(
|
|||
|
||||
#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
|
||||
fn translate_gettext(string: &str, ctx: &str, domain: &str, n: i32, plural: &str) -> String {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return };
|
||||
ctx.0.translations_dirty.as_ref().get();
|
||||
});
|
||||
global_translation_property();
|
||||
fn mangle_context(ctx: &str, s: &str) -> String {
|
||||
format!("{}\u{4}{}", ctx, s)
|
||||
}
|
||||
|
@ -224,6 +221,14 @@ fn translate_gettext(string: &str, ctx: &str, domain: &str, n: i32, plural: &str
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the language index and make sure to register a dependency
|
||||
fn global_translation_property() -> usize {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return 0 };
|
||||
ctx.0.translations_dirty.as_ref().get()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mark_all_translations_dirty() {
|
||||
#[cfg(all(feature = "gettext-rs", target_family = "unix"))]
|
||||
{
|
||||
|
@ -260,6 +265,65 @@ pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> st
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn translate_from_bundle(
|
||||
strs: &[Option<&str>],
|
||||
arguments: &(impl FormatArgs + ?Sized),
|
||||
) -> SharedString {
|
||||
let idx = global_translation_property();
|
||||
let mut output = SharedString::default();
|
||||
let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
|
||||
else {
|
||||
return output;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, arguments)).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
pub fn translate_from_bundle_with_plural(
|
||||
strs: &[Option<&[&str]>],
|
||||
plural_rules: &[Option<fn(i32) -> usize>],
|
||||
arguments: &(impl FormatArgs + ?Sized),
|
||||
n: i32,
|
||||
) -> SharedString {
|
||||
let idx = global_translation_property();
|
||||
let mut output = SharedString::default();
|
||||
let en = |n| (n != 1) as usize;
|
||||
let (translations, rule) = match strs.get(idx) {
|
||||
Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
|
||||
_ => match strs.first() {
|
||||
Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
|
||||
_ => return output,
|
||||
},
|
||||
};
|
||||
let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
|
||||
else {
|
||||
return output;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
pub fn set_language_internal(language: &str, languages: &[&str]) -> bool {
|
||||
let idx = languages.iter().position(|x| *x == language);
|
||||
if let Some(idx) = idx {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return false };
|
||||
ctx.0.translations_dirty.as_ref().set(idx);
|
||||
true
|
||||
})
|
||||
} else if language == "en" {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return false };
|
||||
ctx.0.translations_dirty.as_ref().set(0);
|
||||
true
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
@ -285,4 +349,77 @@ mod ffi {
|
|||
pub extern "C" fn slint_translations_mark_dirty() {
|
||||
mark_all_translations_dirty();
|
||||
}
|
||||
|
||||
/// Safety: The slice must contain valid null-terminated utf-8 strings
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn slint_translate_from_bundle(
|
||||
strs: Slice<*const core::ffi::c_char>,
|
||||
arguments: Slice<SharedString>,
|
||||
output: &mut SharedString,
|
||||
) {
|
||||
*output = SharedString::default();
|
||||
let idx = global_translation_property();
|
||||
let Some(translated) = strs
|
||||
.get(idx)
|
||||
.filter(|x| !x.is_null())
|
||||
.or_else(|| strs.first())
|
||||
.map(|x| core::ffi::CStr::from_ptr(*x).to_str().unwrap())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
|
||||
}
|
||||
/// strs is all the strings variant of all languages.
|
||||
/// indices is the array of indices such that for each language, the corresponding indice is one past the last index of the string for that language.
|
||||
/// So to get the string array for that language, one would do `strs[indices[lang-1]..indices[lang]]`
|
||||
/// (where indices[-1] is 0)
|
||||
///
|
||||
/// Safety; the strs must be pointer to valid null-terminated utf-8 strings
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
|
||||
strs: Slice<*const core::ffi::c_char>,
|
||||
indices: Slice<u32>,
|
||||
plural_rules: Slice<Option<fn(i32) -> usize>>,
|
||||
arguments: Slice<SharedString>,
|
||||
n: i32,
|
||||
output: &mut SharedString,
|
||||
) {
|
||||
*output = SharedString::default();
|
||||
let idx = global_translation_property();
|
||||
let en = |n| (n != 1) as usize;
|
||||
let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
|
||||
let (translations, rule) = match indices.get(idx) {
|
||||
Some(end) if *end != begin => (
|
||||
&strs.as_slice()[begin as usize..*end as usize],
|
||||
plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
|
||||
),
|
||||
_ => (
|
||||
&strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
|
||||
plural_rules.first().and_then(|x| *x).unwrap_or(en),
|
||||
),
|
||||
};
|
||||
let Some(translated) = translations
|
||||
.get(rule(n))
|
||||
.or_else(|| translations.first())
|
||||
.map(|x| core::ffi::CStr::from_ptr(*x).to_str().unwrap())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn slint_translate_set_language(
|
||||
lang: Slice<u8>,
|
||||
languages: Slice<Slice<u8>>,
|
||||
) -> bool {
|
||||
let languages = languages
|
||||
.iter()
|
||||
.map(|x| core::str::from_utf8(x).unwrap())
|
||||
.collect::<alloc::vec::Vec<_>>();
|
||||
set_language_internal(core::str::from_utf8(&lang).unwrap(), &languages)
|
||||
}
|
||||
}
|
||||
|
|
104
tests/cases/translations/bundle.slint
Normal file
104
tests/cases/translations/bundle.slint
Normal file
|
@ -0,0 +1,104 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
|
||||
//bundle-translations
|
||||
|
||||
export component TestCase inherits Window {
|
||||
property <int> int_value: 42;
|
||||
|
||||
out property <string> t1: @tr("Hello World{{}}.");
|
||||
out property <string> t2: @tr("Hello {}.", "World");
|
||||
out property <string> t3: @tr("{} Hello {}", int_value, "World");
|
||||
out property <string> t4: @tr("{1} Hello {0}🌍", @tr("World"), int_value + 1);
|
||||
|
||||
property <string> c1: @tr("Context" => "xx{0}xx", @tr("CC" => "aa"));
|
||||
|
||||
function make_plural1(xx: int, yy: string) -> string { return @tr("there is one file in my {}" | "there are {n} files in my {}" % xx, yy); }
|
||||
function make_plural2(xx: int) -> string { return @tr("Ctx=>" => "xx{n}xx" | "yy{n}yy" % xx); }
|
||||
|
||||
out property <string> result1: make_plural1(1, @tr("Plop")) + "\n" + make_plural1(2, @tr("Flop🎃")) + "\n" + make_plural1(10, t1);
|
||||
out property <string> result2: make_plural2(1) + "\n" + make_plural2(-999) + "\n" + make_plural2(0) + "\n" + make_plural2(42);
|
||||
|
||||
out property <bool> test: t1 == "Hello World{}." && t2 == "Hello World." && t3 == "42 Hello World" && t4 == "43 Hello World🌍"
|
||||
&& c1 == "xxaaxx"
|
||||
&& result1 == "there is one file in my Plop\nthere are 2 files in my Flop🎃\nthere are 10 files in my Hello World{}."
|
||||
&& result2 == "xx1xx\nyy-999yy\nyy0yy\nyy42yy";
|
||||
}
|
||||
/*
|
||||
```cpp
|
||||
auto handle = TestCase::create();
|
||||
const TestCase &instance = *handle;
|
||||
auto result1 = "there is one file in my Plop\nthere are 2 files in my Flop🎃\nthere are 10 files in my Hello World{}.";
|
||||
auto result2 = "xx1xx\nyy-999yy\nyy0yy\nyy42yy";
|
||||
assert_eq(instance.get_result1(), result1);
|
||||
assert_eq(instance.get_result2(), result2);
|
||||
assert(instance.get_test());
|
||||
|
||||
assert(!slint_set_language("abc"));
|
||||
assert_eq(instance.get_result1(), result1);
|
||||
assert_eq(instance.get_result2(), result2);
|
||||
assert(instance.get_test());
|
||||
|
||||
assert(slint_set_language("up"));
|
||||
std::string result1_upper = result1;
|
||||
std::transform(result1_upper.begin(), result1_upper.end(), result1_upper.begin(), ::toupper);
|
||||
std::string result2_upper = result2;
|
||||
std::transform(result2_upper.begin(), result2_upper.end(), result2_upper.begin(), ::toupper);
|
||||
assert_eq(std::string_view(instance.get_result1()), result1_upper);
|
||||
assert_eq(std::string_view(instance.get_result2()), result2_upper);
|
||||
assert(!instance.get_test());
|
||||
|
||||
assert(!slint_set_language("def"));
|
||||
assert_eq(std::string_view(instance.get_result1()), result1_upper);
|
||||
assert_eq(std::string_view(instance.get_result2()), result2_upper);
|
||||
assert(!instance.get_test());
|
||||
|
||||
assert(slint_set_language(""));
|
||||
assert_eq(instance.get_result1(), result1);
|
||||
assert_eq(instance.get_result2(), result2);
|
||||
assert(instance.get_test());
|
||||
|
||||
assert(slint_set_language("fr"));
|
||||
assert_eq(instance.get_result1(), "Il y a 1 fichier dans mon Plouf\nIl y a 2 fichiers dans mon Floup🍓\nIl y a 10 fichiers dans mon Bonjour Monde{}.");
|
||||
assert_eq(instance.get_result2(), "rr1rr\nrr-999rr\nrr0rr\nss42ss");
|
||||
assert(!instance.get_test());
|
||||
```
|
||||
|
||||
|
||||
```rust
|
||||
let instance = TestCase::new().unwrap();
|
||||
let result1 = "there is one file in my Plop\nthere are 2 files in my Flop🎃\nthere are 10 files in my Hello World{}.";
|
||||
let result2 = "xx1xx\nyy-999yy\nyy0yy\nyy42yy";
|
||||
assert_eq!(instance.get_result1(), result1);
|
||||
assert_eq!(instance.get_result2(), result2);
|
||||
assert!(instance.get_test());
|
||||
|
||||
assert!(!slint_set_language("abc"));
|
||||
assert_eq!(instance.get_result1(), result1);
|
||||
assert_eq!(instance.get_result2(), result2);
|
||||
assert!(instance.get_test());
|
||||
|
||||
assert!(slint_set_language("up"));
|
||||
assert_eq!(instance.get_result1(), result1.to_uppercase());
|
||||
assert_eq!(instance.get_result2(), result2.to_uppercase());
|
||||
assert!(!instance.get_test());
|
||||
|
||||
assert!(!slint_set_language("def"));
|
||||
assert_eq!(instance.get_result1(), result1.to_uppercase());
|
||||
assert_eq!(instance.get_result2(), result2.to_uppercase());
|
||||
assert!(!instance.get_test());
|
||||
|
||||
assert!(slint_set_language(""));
|
||||
assert_eq!(instance.get_result1(), result1);
|
||||
assert_eq!(instance.get_result2(), result2);
|
||||
assert!(instance.get_test());
|
||||
|
||||
assert!(slint_set_language("fr"));
|
||||
assert_eq!(instance.get_result1(), "Il y a 1 fichier dans mon Plouf\nIl y a 2 fichiers dans mon Floup🍓\nIl y a 10 fichiers dans mon Bonjour Monde{}.");
|
||||
assert_eq!(instance.get_result2(), "rr1rr\nrr-999rr\nrr0rr\nss42ss");
|
||||
assert!(!instance.get_test());
|
||||
|
||||
```
|
||||
|
||||
*/
|
75
tests/cases/translations/fr/LC_MESSAGES/bundle.po
Normal file
75
tests/cases/translations/fr/LC_MESSAGES/bundle.po
Normal file
|
@ -0,0 +1,75 @@
|
|||
|
||||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2024-10-29 09:15+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n >= 2);\n"
|
||||
|
||||
#: bundle.slint:10
|
||||
msgctxt "TestCase"
|
||||
msgid "Hello World{{}}."
|
||||
msgstr "Bonjour Monde{{}}."
|
||||
|
||||
#: bundle.slint:11
|
||||
msgctxt "TestCase"
|
||||
msgid "Hello {}."
|
||||
msgstr "Bonjour {}."
|
||||
|
||||
#: bundle.slint:12
|
||||
msgctxt "TestCase"
|
||||
msgid "{} Hello {}"
|
||||
msgstr "{} Bonjour {}"
|
||||
|
||||
#: bundle.slint:13
|
||||
msgctxt "TestCase"
|
||||
msgid "{1} Hello {0}🌍"
|
||||
msgstr "{0} Bonjour {1}🌍"
|
||||
|
||||
#: bundle.slint:13
|
||||
msgctxt "TestCase"
|
||||
msgid "World"
|
||||
msgstr "Monde"
|
||||
|
||||
#: bundle.slint:15
|
||||
msgctxt "Context"
|
||||
msgid "xx{0}xx"
|
||||
msgstr "rr{0}rr"
|
||||
|
||||
#: bundle.slint:15
|
||||
msgctxt "CC"
|
||||
msgid "aa"
|
||||
msgstr "bb"
|
||||
|
||||
#: bundle.slint:17
|
||||
msgctxt "TestCase"
|
||||
msgid "there is one file in my {}"
|
||||
msgid_plural "there are {n} files in my {}"
|
||||
msgstr[0] "Il y a {n} fichier dans mon {}"
|
||||
msgstr[1] "Il y a {n} fichiers dans mon {}"
|
||||
|
||||
#: bundle.slint:18
|
||||
msgctxt "Ctx=>"
|
||||
msgid "xx{n}xx"
|
||||
msgid_plural "yy{n}yy"
|
||||
msgstr[0] "rr{n}rr"
|
||||
msgstr[1] "ss{n}ss"
|
||||
|
||||
#: bundle.slint:20
|
||||
msgctxt "TestCase"
|
||||
msgid "Plop"
|
||||
msgstr "Plouf"
|
||||
|
||||
#: bundle.slint:20
|
||||
msgctxt "TestCase"
|
||||
msgid "Flop🎃"
|
||||
msgstr "Floup🍓"
|
||||
|
75
tests/cases/translations/up/LC_MESSAGES/bundle.po
Normal file
75
tests/cases/translations/up/LC_MESSAGES/bundle.po
Normal file
|
@ -0,0 +1,75 @@
|
|||
|
||||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2024-10-29 09:15+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: bundle.slint:10
|
||||
msgctxt "TestCase"
|
||||
msgid "Hello World{{}}."
|
||||
msgstr "HELLO WORLD{{}}."
|
||||
|
||||
#: bundle.slint:11
|
||||
msgctxt "TestCase"
|
||||
msgid "Hello {}."
|
||||
msgstr "HELLO {}."
|
||||
|
||||
#: bundle.slint:12
|
||||
msgctxt "TestCase"
|
||||
msgid "{} Hello {}"
|
||||
msgstr "{} HELLO {}"
|
||||
|
||||
#: bundle.slint:13
|
||||
msgctxt "TestCase"
|
||||
msgid "{1} Hello {0}🌍"
|
||||
msgstr "{1} HELLO {0}🌍"
|
||||
|
||||
#: bundle.slint:13
|
||||
msgctxt "TestCase"
|
||||
msgid "World"
|
||||
msgstr "WORLD"
|
||||
|
||||
#: bundle.slint:15
|
||||
msgctxt "Context"
|
||||
msgid "xx{0}xx"
|
||||
msgstr "XX{0}XX"
|
||||
|
||||
#: bundle.slint:15
|
||||
msgctxt "CC"
|
||||
msgid "aa"
|
||||
msgstr "AA"
|
||||
|
||||
#: bundle.slint:17
|
||||
msgctxt "TestCase"
|
||||
msgid "there is one file in my {}"
|
||||
msgid_plural "there are {n} files in my {}"
|
||||
msgstr[0] "THERE IS ONE FILE IN MY {}"
|
||||
msgstr[1] "THERE ARE {n} FILES IN MY {}"
|
||||
|
||||
#: bundle.slint:18
|
||||
msgctxt "Ctx=>"
|
||||
msgid "xx{n}xx"
|
||||
msgid_plural "yy{n}yy"
|
||||
msgstr[0] "XX{n}XX"
|
||||
msgstr[1] "YY{n}YY"
|
||||
|
||||
#: bundle.slint:20
|
||||
msgctxt "TestCase"
|
||||
msgid "Plop"
|
||||
msgstr "PLOP"
|
||||
|
||||
#: bundle.slint:20
|
||||
msgctxt "TestCase"
|
||||
msgid "Flop🎃"
|
||||
msgstr "FLOP🎃"
|
||||
|
|
@ -21,7 +21,7 @@ name = "test-driver-cpp"
|
|||
slint-cpp = { workspace = true, features = ["testing", "std", "experimental"] }
|
||||
|
||||
[dev-dependencies]
|
||||
i-slint-compiler = { workspace = true, features = ["default", "cpp", "display-diagnostics"] }
|
||||
i-slint-compiler = { workspace = true, features = ["default", "cpp", "display-diagnostics", "bundle-translations"] }
|
||||
|
||||
cc = "1.0.54"
|
||||
scopeguard = "1.1.0"
|
||||
|
|
|
@ -30,6 +30,12 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
|
|||
compiler_config.library_paths = library_paths;
|
||||
compiler_config.style = testcase.requested_style.map(str::to_string);
|
||||
compiler_config.debug_info = true;
|
||||
if source.contains("//bundle-translations") {
|
||||
compiler_config.translation_path_bundle =
|
||||
Some(testcase.absolute_path.parent().unwrap().to_path_buf());
|
||||
compiler_config.translation_domain =
|
||||
Some(testcase.absolute_path.file_stem().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
let (root_component, diag, loader) =
|
||||
spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ slint-interpreter = { workspace = true, features = ["std", "compat-1-2", "intern
|
|||
spin_on = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
i-slint-compiler = { workspace = true, features = ["default", "rust", "display-diagnostics"], optional = true }
|
||||
i-slint-compiler = { workspace = true, features = ["default", "rust", "display-diagnostics", "bundle-translations"], optional = true}
|
||||
|
||||
spin_on = { workspace = true, optional = true }
|
||||
test_driver_lib = { path = "../driverlib" }
|
||||
|
|
|
@ -17,7 +17,13 @@ fn main() -> std::io::Result<()> {
|
|||
}
|
||||
writeln!(generated_file, "#[path=\"{0}.rs\"] mod r#{0};", module_name)?;
|
||||
let source = std::fs::read_to_string(&testcase.absolute_path)?;
|
||||
let ignored = testcase.is_ignored("rust");
|
||||
let ignored = if testcase.is_ignored("rust") {
|
||||
"#[ignore = \"testcase ignored for rust\"]"
|
||||
} else if cfg!(not(feature = "build-time")) && source.contains("//bundle-translations") {
|
||||
"#[ignore = \"translation bundle not working with the macro\"]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut output = BufWriter::new(std::fs::File::create(
|
||||
Path::new(&std::env::var_os("OUT_DIR").unwrap()).join(format!("{}.rs", module_name)),
|
||||
|
@ -43,7 +49,7 @@ fn main() -> std::io::Result<()> {
|
|||
{}
|
||||
Ok(())
|
||||
}}",
|
||||
if ignored { "#[ignore]" } else { "" },
|
||||
ignored,
|
||||
i,
|
||||
x.source.replace('\n', "\n ")
|
||||
)?;
|
||||
|
@ -69,7 +75,7 @@ fn generate_macro(
|
|||
) -> Result<bool, std::io::Error> {
|
||||
if source.contains("\\{") {
|
||||
// Unfortunately, \{ is not valid in a rust string so it cannot be used in a slint! macro
|
||||
output.write_all(b"#[test] #[ignore] fn ignored_because_string_template() {{}}")?;
|
||||
output.write_all(b"#[test] #[ignore = \"string template don't work in macros\"] fn ignored_because_string_template() {{}}")?;
|
||||
return Ok(false);
|
||||
}
|
||||
// to silence all the warnings in .slint files that would be turned into errors
|
||||
|
@ -141,6 +147,12 @@ fn generate_source(
|
|||
compiler_config.library_paths = library_paths;
|
||||
compiler_config.style = Some(testcase.requested_style.unwrap_or("fluent").to_string());
|
||||
compiler_config.debug_info = true;
|
||||
if source.contains("//bundle-translations") {
|
||||
compiler_config.translation_path_bundle =
|
||||
Some(testcase.absolute_path.parent().unwrap().to_path_buf());
|
||||
compiler_config.translation_domain =
|
||||
Some(testcase.absolute_path.file_stem().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
let (root_component, diag, loader) =
|
||||
spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue