WIP: start working on translations: @tr()

This commit just do the parsing of the @tr macro.
This commit is contained in:
Olivier Goffart 2023-04-17 18:26:20 +02:00 committed by Olivier Goffart
parent 3a4f3c61d5
commit dcd8450dfa
17 changed files with 161 additions and 52 deletions

View file

@ -11,13 +11,13 @@ export { TableViewPageAdapter }
component App inherits Window {
preferred-width: 700px;
preferred-height: 500px;
title: "Slint Widgets Gallery";
title: @tr("Slint Widgets Gallery");
icon: @image-url("../../logo/slint-logo-small-light-128x128.png");
HorizontalLayout {
side-bar := SideBar {
title: "Slint Widgets Gallery";
model: ["Controls", "ListView", "TableView", "TextEdit", "About"];
title: @tr("Slint Widgets Gallery");
model: [@tr("Controls"), @tr("ListView"), @tr("TableView"), @tr("TextEdit"), @tr("About")];
}
if(side-bar.current-item == 0) : ControlsPage {}

View file

@ -7,38 +7,38 @@ import { GallerySettings } from "../gallery_settings.slint";
import { Page } from "page.slint";
export component ControlsPage inherits Page {
title: "Controls";
description: "This page gives an overview of the default widget set provided by Slint. The widgets are available in different styles native, fluent-(dark/light) and material-(dark/light). The widgets can be imported from \"std-widgets.slint\".";
title: @tr("Controls");
description: @tr("This page gives an overview of the default widget set provided by Slint. The widgets are available in different styles native, fluent-(dark/light) and material-(dark/light). The widgets can be imported from \"std-widgets.slint\".");
GroupBox {
vertical-stretch: 0;
title: "Buttons";
title: @tr("Buttons");
HorizontalBox {
alignment: start;
padding: 0px;
Button {
text: "Regular Button";
text: @tr("Regular Button");
enabled: GallerySettings.widgets-enabled;
}
Button {
text: "Button with Icon";
text: @tr("Button with Icon");
icon: @image-url("../../thumbsup.png");
enabled: GallerySettings.widgets-enabled;
}
Button {
checkable: true;
text: self.checked ? "ON" : "OFF";
text: self.checked ? @tr("ON") : @tr("OFF");
enabled: GallerySettings.widgets-enabled;
}
}
}
GroupBox {
title: "CheckBox - SpinBox - ComboBox - Switch";
title: @tr("CheckBox - SpinBox - ComboBox - Switch");
vertical-stretch: 0;
HorizontalBox {
@ -46,7 +46,7 @@ export component ControlsPage inherits Page {
padding: 0px;
checkbox := CheckBox {
text: checkbox.checked ? "(checked)" : "(unchecked)";
text: checkbox.checked ? @tr("(checked)") : @tr("(unchecked)");
checked: true;
enabled: GallerySettings.widgets-enabled;
}
@ -58,12 +58,12 @@ export component ControlsPage inherits Page {
}
ComboBox {
model: ["Select Something", "From this", "Combobox"];
model: [@tr("Select Something"), @tr("From this"), @tr("Combobox")];
enabled: GallerySettings.widgets-enabled;
}
Switch {
text: "Flight Mode";
text: @tr("Flight Mode");
checked: true;
enabled: GallerySettings.widgets-enabled;
}
@ -71,17 +71,17 @@ export component ControlsPage inherits Page {
}
GroupBox {
title: "LineEdit";
title: @tr("LineEdit");
vertical-stretch: 0;
LineEdit {
placeholder-text: "Enter some text";
placeholder-text: @tr("Enter some text");
enabled: GallerySettings.widgets-enabled;
}
}
GroupBox {
title: "Slider";
title: @tr("Slider");
vertical-stretch: 0;
i-slider := Slider {
@ -94,7 +94,7 @@ export component ControlsPage inherits Page {
}
GroupBox {
title: "ProgressIndicator";
title: @tr("ProgressIndicator");
vertical-stretch: 0;
VerticalLayout {
@ -108,30 +108,30 @@ export component ControlsPage inherits Page {
}
CheckBox {
text: "indeterminate";
text: @tr("indeterminate");
checked <=> i-progress-indicator.indeterminate;
}
}
}
GroupBox {
title: "TabWidget";
title: @tr("TabWidget");
TabWidget {
Tab {
title: "Tab 1";
title: @tr("Tab 1");
VerticalBox {
alignment: start;
GroupBox {
title: "Content of tab 1";
title: @tr("Content of tab 1");
HorizontalBox {
alignment: start;
Button {
text: "Click me";
text: @tr("Click me");
enabled: GallerySettings.widgets-enabled;
}
}
@ -140,19 +140,19 @@ export component ControlsPage inherits Page {
}
Tab {
title: "Tab 2";
title: @tr("Tab 2");
VerticalBox {
alignment: start;
GroupBox {
title: "Content of tab 2";
title: @tr("Content of tab 2");
VerticalBox {
alignment: start;
CheckBox {
text: "Check me";
text: @tr("Check me");
enabled: GallerySettings.widgets-enabled;
}
}
@ -161,11 +161,11 @@ export component ControlsPage inherits Page {
}
Tab {
title: "Tab 3";
title: @tr("Tab 3");
VerticalBox {
Text {
text: "Content of tab 3";
text: @tr("Content of tab 3");
}
}
}

View file

@ -6,8 +6,8 @@ import { GallerySettings } from "../gallery_settings.slint";
import { Page } from "page.slint";
export component ListViewPage inherits Page {
title: "ListView";
description: "ListViews can be used to display a list of elements. The StandardListBox is like the default ListView just with a default text based definition of the visual items. Both can be imported from \"std-widgets.slint\"";
title: @tr("ListView");
description: @tr("ListViews can be used to display a list of elements. The StandardListBox is like the default ListView just with a default text based definition of the visual items. Both can be imported from \"std-widgets.slint\"");
HorizontalBox {
vertical-stretch: 1;
@ -22,7 +22,7 @@ export component ListViewPage inherits Page {
source: @image-url("../../thumbsup.png");
}
Text {
text: "Item " + i;
text: @tr("Item {}", i);
}
}
}
@ -34,13 +34,13 @@ export component ListViewPage inherits Page {
StandardListView {
model: [
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: "Lorem"}, {text: "ipsum"},{text: "dolor"},{text: "sit"},{text: "amet"},{text: "consetetur"},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
{text: @tr("Lorem")}, {text: @tr("ipsum")},{text: @tr("dolor")},{text: @tr("sit")},{text: @tr("amet")},{text: @tr("consetetur")},
];
}
}

View file

@ -22,7 +22,7 @@ export component Page inherits VerticalBox {
Switch {
horizontal-stretch: 0;
text: "Widgets enabled";
text: @tr("Widgets enabled");
checked <=> GallerySettings.widgets-enabled;
enabled: true;
}

View file

@ -19,14 +19,14 @@ export global TableViewPageAdapter {
}
export component TableViewPage inherits Page {
title: "TableView";
description: "StandardTableView can be used to display a list of text elements in columns and rows. It can be imported from \"std-widgets.slint\"";
title: @tr("TableView");
description: @tr("StandardTableView can be used to display a list of text elements in columns and rows. It can be imported from \"std-widgets.slint\"");
HorizontalBox {
vertical-stretch: 1;
GroupBox {
title: "StandardTableView";
title: @tr("StandardTableView");
vertical-stretch: 0;
StandardTableView {
@ -39,10 +39,10 @@ export component TableViewPage inherits Page {
}
columns: [
{ title: "Header 1" },
{ title: "Header 2" },
{ title: "Header 3" },
{ title: "Header 4" },
{ title: @tr("Header 1") },
{ title: @tr("Header 2") },
{ title: @tr("Header 3") },
{ title: @tr("Header 4") },
];
rows: TableViewPageAdapter.row_data;
}

View file

@ -6,23 +6,23 @@ import { GallerySettings } from "../gallery_settings.slint";
import { Page } from "page.slint";
export component TextEditPage inherits Page {
title: "TextEdit";
description: "Similar to LineEdit, but can be used to enter several lines of text. The widget can be imported from \"std-widgets.slint\".";
title: @tr("TextEdit");
description: @tr("Similar to LineEdit, but can be used to enter several lines of text. The widget can be imported from \"std-widgets.slint\".");
HorizontalBox {
GroupBox {
vertical-stretch: 0;
title: "Word-Wrap";
title: @tr("Word-Wrap");
te1 := TextEdit {
// min-width: 200px;
text: "This is our TextEdit widget, which allows for editing text that spans over multiple paragraphs.\nFor example this line starts in a new paragraph.\n\nWhen the amount of lines - due to wrapping and number of paragraphs - exceeds the available vertical height, a vertical scrollbar is shown that allows scrolling.\nYou may want to enter a bit of text here then in order to make them visible.";
text: @tr("This is our TextEdit widget, which allows for editing text that spans over multiple paragraphs.\nFor example this line starts in a new paragraph.\n\nWhen the amount of lines - due to wrapping and number of paragraphs - exceeds the available vertical height, a vertical scrollbar is shown that allows scrolling.\nYou may want to enter a bit of text here then in order to make them visible.");
wrap: word-wrap;
enabled: GallerySettings.widgets-enabled;
}
}
GroupBox {
title: "No-Wrap";
title: @tr("No-Wrap");
vertical-stretch: 0;
te2 := TextEdit {
// min-width: 200px;

View file

@ -61,6 +61,7 @@ pub enum BuiltinFunction {
RegisterCustomFontByPath,
RegisterCustomFontByMemory,
RegisterBitmapFont,
Translate,
}
#[derive(Debug, Clone)]
@ -202,6 +203,10 @@ impl BuiltinFunction {
BuiltinFunction::RegisterBitmapFont => {
Type::Function { return_type: Box::new(Type::Void), args: vec![Type::Int32] }
}
BuiltinFunction::Translate => Type::Function {
return_type: Box::new(Type::String),
args: vec![Type::String, Type::Array(Type::String.into())],
},
}
}
@ -253,6 +258,7 @@ impl BuiltinFunction {
BuiltinFunction::RegisterCustomFontByPath
| BuiltinFunction::RegisterCustomFontByMemory
| BuiltinFunction::RegisterBitmapFont => false,
BuiltinFunction::Translate => false,
}
}
@ -297,6 +303,7 @@ impl BuiltinFunction {
BuiltinFunction::RegisterCustomFontByPath
| BuiltinFunction::RegisterCustomFontByMemory
| BuiltinFunction::RegisterBitmapFont => false,
BuiltinFunction::Translate => true,
}
}
}

View file

@ -2833,6 +2833,9 @@ fn compile_builtin_function_call(
panic!("internal error: invalid args to ImplicitLayoutInfo {:?}", arguments)
}
}
BuiltinFunction::Translate => {
format!("slint::private_api::translate({});", a.join(","))
}
}
}

View file

@ -2441,6 +2441,9 @@ fn compile_builtin_function_call(
let window_adapter_tokens = access_window_adapter_field(ctx);
quote!(slint::private_unstable_api::re_exports::WindowInner::from_pub(#window_adapter_tokens.window()).set_text_input_focused(#(#a)*))
}
BuiltinFunction::Translate => {
todo!("BuiltinFunction::Translate in rust")
}
}
}

View file

@ -106,6 +106,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::DarkColorScheme => isize::MAX,
BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST,
BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
}
}

View file

@ -359,7 +359,7 @@ declare_syntax! {
// FIXME: the test should test that as alternative rather than several of them (but it can also be a literal)
Expression-> [ ?Expression, ?FunctionCallExpression, ?IndexExpression, ?SelfAssignment,
?ConditionalExpression, ?QualifiedName, ?BinaryExpression, ?Array, ?ObjectLiteral,
?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtGradient,
?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtGradient, ?AtTr,
?MemberAccess ],
/// Concatenate the Expressions to make a string (usually expended from a template string)
StringTemplate -> [*Expression],
@ -367,6 +367,8 @@ declare_syntax! {
AtImageUrl -> [],
/// `@linear-gradient(...)` or `@radial-gradient(...)`
AtGradient -> [*Expression],
/// `@tr("foo", ...)` // the string is a StringLiteral
AtTr -> [*Expression],
/// expression()
FunctionCallExpression -> [*Expression],
/// `expression[index]`

View file

@ -212,6 +212,7 @@ fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence)
/// ```test
/// @image-url("/foo/bar.png")
/// @linear-gradient(0deg, blue, red)
/// @tr("foo", bar)
/// ```
fn parse_at_keyword(p: &mut impl Parser) {
debug_assert_eq!(p.peek().kind(), SyntaxKind::At);
@ -230,10 +231,13 @@ fn parse_at_keyword(p: &mut impl Parser) {
"radial-gradient" | "radial_gradient" => {
parse_gradient(p);
}
"tr" => {
parse_tr(p);
}
_ => {
p.consume();
p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
p.error("Expected 'image-url', 'linear-gradient' or 'radial-gradient' after '@'");
p.error("Expected 'image-url', 'tr', 'linear-gradient' or 'radial-gradient' after '@'");
}
}
}
@ -351,3 +355,33 @@ fn parse_gradient(p: &mut impl Parser) {
p.test(SyntaxKind::Comma);
}
}
#[cfg_attr(test, parser_test)]
/// ```test,AtTr
/// @tr("foo")
/// @tr("foo", bar)
/// ```
fn parse_tr(p: &mut impl Parser) {
let mut p = p.start_node(SyntaxKind::AtTr);
p.expect(SyntaxKind::At);
debug_assert_eq!(p.peek().as_str(), "tr");
p.expect(SyntaxKind::Identifier); //"tr"
p.expect(SyntaxKind::LParent);
let peek = p.peek();
if peek.kind() != SyntaxKind::StringLiteral
|| !peek.as_str().starts_with('"')
|| !peek.as_str().ends_with('"')
{
p.error("Expected plain string literal");
return;
}
p.expect(SyntaxKind::StringLiteral);
while p.test(SyntaxKind::Comma) {
if !parse_expression(&mut *p) {
break;
}
}
p.expect(SyntaxKind::RParent);
}

View file

@ -241,6 +241,7 @@ impl Expression {
.map(|n| Self::from_expression_node(n, ctx))
.or_else(|| node.AtImageUrl().map(|n| Self::from_at_image_url_node(n, ctx)))
.or_else(|| node.AtGradient().map(|n| Self::from_at_gradient(n, ctx)))
.or_else(|| node.AtTr().map(|n| Self::from_at_tr(n, ctx)))
.or_else(|| {
node.QualifiedName().map(|n| {
let exp =
@ -515,6 +516,36 @@ impl Expression {
}
}
fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
let Some(string) = node
.child_text(SyntaxKind::StringLiteral)
.and_then(|s| crate::literals::unescape_string(&s))
else {
ctx.diag.push_error("Cannot parse string literal".into(), &node);
return Expression::Invalid;
};
let subs = node.Expression().map(|n| {
Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
Type::String,
&n,
ctx.diag,
)
});
Expression::FunctionCall {
function: Box::new(Expression::BuiltinFunctionReference(
BuiltinFunction::Translate,
Some(node.to_source_location()),
)),
arguments: vec![
Expression::StringLiteral(string),
Expression::Array { element_ty: Type::String, values: subs.collect() },
],
source_location: Some(node.to_source_location()),
}
}
/// Perform the lookup
fn from_qualified_name_node(
node: syntax_nodes::QualifiedName,

View file

@ -0,0 +1,23 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
export component X {
property <string> t1: @tr(boo);
// ^error{Expected plain string literal}
// ^^error{Syntax error: expected ';'}
// ^^^error{Parse error}
property <string> t2: @tr("boo\{t1}oo");
// ^error{Expected plain string literal}
// ^^error{Syntax error: expected ';'}
property <string> t3: @tr("boo" + "foo");
// ^error{Syntax error: expected '\)'}
// ^^error{Syntax error: expected ';'}
property <string> t4: @tr("foo{}", t1);
property <string> t4: @tr("foo{}", t1 t2);
// ^error{Syntax error: expected '\)'}
// ^^error{Syntax error: expected ';'}
}

View file

@ -867,6 +867,9 @@ fn call_builtin_function(
BuiltinFunction::RegisterCustomFontByMemory | BuiltinFunction::RegisterBitmapFont => {
unimplemented!()
}
BuiltinFunction::Translate => {
todo!("translate in the interpeter");
}
}
}

View file

@ -202,6 +202,7 @@ pub(crate) fn completion_at(
{
return Some(
[
("tr", "tr(\"$1\")"),
("image-url", "image-url(\"$1\")"),
("linear-gradient", "linear-gradient($1)"),
("radial-gradient", "radial-gradient(circle, $1)"),

View file

@ -96,6 +96,7 @@ pub fn get_semantic_tokens(
SyntaxKind::ReturnStatement => Some((self::KEYWORD, 0)),
SyntaxKind::AtImageUrl => Some((self::MACRO, 0)),
SyntaxKind::AtGradient => Some((self::MACRO, 0)),
SyntaxKind::AtTr => Some((self::MACRO, 0)),
SyntaxKind::ConditionalExpression => Some((self::KEYWORD, 0)),
SyntaxKind::ObjectMember => Some((self::PROPERTY, 1 << self::DECLARATION)),
SyntaxKind::States => Some((self::KEYWORD, 0)),