Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] compiler: add support for the .translate suffix #1613

Merged
merged 1 commit into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/reference/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,28 @@ class SomeComponent extends Component {
The `.bind` suffix also implies `.alike`, so these props will not cause additional
renderings.

## Translatable props

When you need to pass a user-facing string to a subcomponent, you likely want it
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
be practical for Owl to find out which parts of the expression are strings and translate
them, and it also makes it difficult for tooling to extract these strings to generate
terms to translate. While you can work around this issue by doing the translation in
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
and passing the variable as a prop, this is a sufficiently common use case that Owl
provides a suffix for this purpose: `.translate`.

```xml
<t t-name="ParentComponent">
<Child someProp.translate="some message"/>
</t>
```

Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
it is treated as a string, as if it was an attribute on an HTML element, and translated
before being passed to the component. If you need to interpolate some data into the
string, you will still have to do this in JavaScript.

## Dynamic Props

The `t-props` directive can be used to specify totally dynamic props:
Expand Down
7 changes: 4 additions & 3 deletions doc/reference/slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,17 @@ use this `Notebook` component:

```xml
<Notebook>
<t t-set-slot="page1" title="'Page 1'">
<t t-set-slot="page1" title.translate="Page 1">
<div>this is in the page 1</div>
</t>
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
<div>this is in the page 2</div>
</t>
</Notebook>
```

Slot params works like normal props, so one can use the `.bind` suffix to
Slot params works like normal props, so one can use suffixes like `.translate`
when a prop is a user facing string and should be translated, or `.bind` to
bind a function if needed.

## Slot scopes
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/code_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,11 @@ export class CodeGenerator {
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
*/
formatProp(name: string, value: string): string {
value = this.captureExpression(value);
if (name.endsWith(".translate")) {
value = toStringExpression(this.translateFn(value));
} else {
value = this.captureExpression(value);
}
if (name.includes(".")) {
let [_name, suffix] = name.split(".");
name = _name;
Expand All @@ -1145,6 +1149,7 @@ export class CodeGenerator {
value = `(${value}).bind(this)`;
break;
case "alike":
case "translate":
break;
default:
throw new OwlError("Invalid prop suffix");
Expand Down
46 changes: 46 additions & 0 deletions tests/components/__snapshots__/props.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ exports[`.alike suffix in a simple case 2`] = `
}"
`;

exports[`.translate props are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);

return function template(ctx, node, key = \\"\\") {
return comp1({message: \`translated message\`}, key + \`__1\`, node, this, null);
}
}"
`;

exports[`.translate props are translated 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].message);
}
}"
`;

exports[`basics accept ES6-like syntax for props (with getters) 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down Expand Up @@ -412,6 +435,29 @@ exports[`can bind function prop with bind suffix 2`] = `
}"
`;

exports[`can use .translate suffix 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);

return function template(ctx, node, key = \\"\\") {
return comp1({message: \`some message\`}, key + \`__1\`, node, this, null);
}
}"
`;

exports[`can use .translate suffix 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].message);
}
}"
`;

exports[`do not crash when binding anonymous function prop with bind suffix 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down
50 changes: 50 additions & 0 deletions tests/components/__snapshots__/slots.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`slots .translate slot props are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, true, false, []);

return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'default': {message: \`translated message\`}})}, key + \`__1\`, node, this, null);
}
}"
`;

exports[`slots .translate slot props are translated 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].slots.default.message);
}
}"
`;

exports[`slots can define a default content 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down Expand Up @@ -201,6 +226,31 @@ exports[`slots can render only empty slot 1`] = `
}"
`;

exports[`slots can use .translate suffix on slot props 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, true, false, []);

return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'default': {message: \`some message\`}})}, key + \`__1\`, node, this, null);
}
}"
`;

exports[`slots can use .translate suffix on slot props 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].slots.default.message);
}
}"
`;

exports[`slots can use component in default-content of t-slot 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down
28 changes: 28 additions & 0 deletions tests/components/props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,34 @@ test("bound functions are considered 'alike'", async () => {
expect(fixture.innerHTML).toBe("3child");
});

test("can use .translate suffix", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.message"/>`;
}

class Parent extends Component {
static template = xml`<Child message.translate="some message"/>`;
static components = { Child };
}

await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("some message");
});

test(".translate props are translated", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.message"/>`;
}

class Parent extends Component {
static template = xml`<Child message.translate="some message"/>`;
static components = { Child };
}

await mount(Parent, fixture, { translateFn: () => "translated message" });
expect(fixture.innerHTML).toBe("translated message");
});

test("throw if prop uses an unknown suffix", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.val"/>`;
Expand Down
28 changes: 28 additions & 0 deletions tests/components/slots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,34 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("<span>default empty</span>");
});

test("can use .translate suffix on slot props", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.slots.default.message"/>`;
}

class Parent extends Component {
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
static components = { Child };
}

await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("some message");
});

test(".translate slot props are translated", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.slots.default.message"/>`;
}

class Parent extends Component {
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
static components = { Child };
}

await mount(Parent, fixture, { translateFn: () => "translated message" });
expect(fixture.innerHTML).toBe("translated message");
});

test("default slot with slot scope: shorthand syntax", async () => {
let child: any;
class Child extends Component {
Expand Down
Loading