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] translation contexts #1664

Merged
merged 1 commit into from
Jan 15, 2025
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
24 changes: 13 additions & 11 deletions doc/reference/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,19 @@ extensions.

For reference, here is a list of all standard QWeb directives:

| Name | Description |
| ------------------------------ | --------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
| Name | Description |
| ------------------------------ | ----------------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
| `t-translation-context` | [Context of translations within a node](translations.md) |
| `t-translation-context-*` | [Context of translation for a specific node attribute](translations.md) |

The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
Expand Down
42 changes: 37 additions & 5 deletions doc/reference/translations.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
# 🦉 Translations 🦉

If properly setup, Owl can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
so, it needs a translate function, which takes

- a string (the term to translate)
- a string (the translation context of the term)
and returns a string.

For example:

```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non",
fr: {
hello: "bonjour",
yes: "oui",
no: "non",
},
pt: {
hello: "bom dia",
yes: "sim",
no: "não",
},
};
const translateFn = (str) => translations[str] || str;
const translateFn = (str, ctx) => translations[ctx]?.[str] || str;

const app = new App(Root, { templates, tranaslateFn });
// ...
Expand All @@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
- the translate function receives as second parameter a context that can be used
to contextualized the translation. That context can be set globally on a node
and its children by using `t-translation-context`. If a specific node
attribute `x` needs another context, that context can be specified with a
special directive `t-translation-context-x`.

So, with the above `translateFn`, the following templates:

Expand All @@ -46,6 +62,22 @@ will be rendered as:
<input placeholder="bonjour" other="yes"/>
```

and the following template:

```xml
<div t-translation-context="fr" title="hello">hello</div>
<div>Are you sure?</div>
<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
```

will be rendered as:

```xml
<div title="bonjour">bonjour</div>
<div>Are you sure?</div>
<input placeholder="bom dia" other="yes"/>
```

Note that the translation is done during the compilation of the template, not
when it is rendered.

Expand Down
66 changes: 52 additions & 14 deletions src/compiler/code_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ASTTOut,
ASTTPortal,
ASTTranslation,
ASTTranslationContext,
ASTTSet,
ASTType,
Attrs,
Expand All @@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
const whitespaceRE = /\s+/g;

export interface Config {
translateFn?: (s: string) => string;
translateFn?: (s: string, translationCtx: string) => string;
translatableAttributes?: string[];
dev?: boolean;
}
Expand Down Expand Up @@ -171,6 +172,7 @@ interface Context {
forceNewBlock: boolean;
isLast?: boolean;
translate: boolean;
translationCtx: string;
tKeyExpr: string | null;
nameSpace?: string;
tModelSelectedExpr?: string;
Expand All @@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
index: 0,
forceNewBlock: true,
translate: parentCtx.translate,
translationCtx: parentCtx.translationCtx,
tKeyExpr: null,
nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
Expand Down Expand Up @@ -263,7 +266,7 @@ export class CodeGenerator {
target = new CodeTarget("template");
templateName?: string;
dev: boolean;
translateFn: (s: string) => string;
translateFn: (s: string, translationCtx: string) => string;
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
ast: AST;
staticDefs: { id: string; expr: string }[] = [];
Expand Down Expand Up @@ -303,6 +306,7 @@ export class CodeGenerator {
forceNewBlock: false,
isLast: true,
translate: true,
translationCtx: "",
tKeyExpr: null,
});
// define blocks and utility functions
Expand Down Expand Up @@ -457,9 +461,9 @@ export class CodeGenerator {
.join("");
}

translate(str: string): string {
translate(str: string, translationCtx: string): string {
const match = translationRE.exec(str) as any;
return match[1] + this.translateFn(match[2]) + match[3];
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
}

/**
Expand Down Expand Up @@ -501,6 +505,8 @@ export class CodeGenerator {
return this.compileTSlot(ast, ctx);
case ASTType.TTranslation:
return this.compileTTranslation(ast, ctx);
case ASTType.TTranslationContext:
return this.compileTTranslationContext(ast, ctx);
case ASTType.TPortal:
return this.compileTPortal(ast, ctx);
}
Expand Down Expand Up @@ -542,7 +548,7 @@ export class CodeGenerator {

let value = ast.value;
if (value && ctx.translate !== false) {
value = this.translate(value);
value = this.translate(value, ctx.translationCtx);
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
Expand Down Expand Up @@ -631,7 +637,8 @@ export class CodeGenerator {
}
}
} else if (this.translatableAttributes.includes(key)) {
attrs[key] = this.translateFn(ast.attrs[key]);
const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
} else {
expr = `"${ast.attrs[key]}"`;
attrName = key;
Expand Down Expand Up @@ -1104,7 +1111,7 @@ export class CodeGenerator {
let value: string;
if (ast.defaultValue) {
const defaultValue = toStringExpression(
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
);
if (ast.value) {
value = `withDefault(${expr}, ${defaultValue})`;
Expand Down Expand Up @@ -1139,9 +1146,15 @@ export class CodeGenerator {
* "some-prop" "state" "'some-prop': ctx['state']"
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
*/
formatProp(name: string, value: string): string {
formatProp(
name: string,
value: string,
attrsTranslationCtx: { [name: string]: string } | null,
translationCtx: string
): string {
if (name.endsWith(".translate")) {
value = toStringExpression(this.translateFn(value));
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
} else {
value = this.captureExpression(value);
}
Expand All @@ -1163,8 +1176,14 @@ export class CodeGenerator {
return `${name}: ${value || undefined}`;
}

formatPropObject(obj: { [prop: string]: any }): string[] {
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
formatPropObject(
obj: { [prop: string]: any },
attrsTranslationCtx: { [name: string]: string } | null,
translationCtx: string
): string[] {
return Object.entries(obj).map(([k, v]) =>
this.formatProp(k, v, attrsTranslationCtx, translationCtx)
);
}

getPropString(props: string[], dynProps: string | null): string {
Expand All @@ -1181,7 +1200,9 @@ export class CodeGenerator {
let { block } = ctx;
// props
const hasSlotsProp = "slots" in (ast.props || {});
const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
const props: string[] = ast.props
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
: [];

// slots
let slotDef: string = "";
Expand All @@ -1205,7 +1226,13 @@ export class CodeGenerator {
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
params.push(
...this.formatPropObject(
ast.slots[slotName].attrs!,
ast.slots[slotName].attrsTranslationCtx,
ctx.translationCtx
)
);
}
const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`);
Expand Down Expand Up @@ -1332,7 +1359,9 @@ export class CodeGenerator {
key = this.generateComponentKey(key);
}

const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
const props = ast.attrs
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
: [];
const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
Expand Down Expand Up @@ -1365,6 +1394,15 @@ export class CodeGenerator {
}
return null;
}
compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
if (ast.content) {
return this.compileAST(
ast.content,
Object.assign({}, ctx, { translationCtx: ast.translationCtx })
);
}
return null;
}
compileTPortal(ast: ASTTPortal, ctx: Context): string {
if (!this.staticDefs.find((d) => d.id === "Portal")) {
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
Expand Down
Loading
Loading