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:
Olivier Goffart 2024-10-29 15:07:15 +01:00 committed by GitHub
parent 2f62c60e3c
commit 95f5685789
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1163 additions and 15 deletions

View file

@ -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

View file

@ -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,
};

View file

@ -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" }

View file

@ -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()
}));
}

View file

@ -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),*])
}
)
}

View file

@ -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()),
}
}

View file

@ -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 {

View file

@ -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);
}
}
}
};
}

View file

@ -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 {

View file

@ -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 }
}

View file

@ -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)]

View file

@ -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)));

View file

@ -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)
),
}
}
}
}
}

View 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))",
);
}
}

View file

@ -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(),
}))
}

View file

@ -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)
}
}

View 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());
```
*/

View 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🍓"

View 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🎃"

View file

@ -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"

View file

@ -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));

View file

@ -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" }

View file

@ -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));