Skip to content

Commit

Permalink
[IMP] translation contexts
Browse files Browse the repository at this point in the history
A new directive t-tanslation-context and a new family of directives (of the form
t-translation-context-...) are introduced to allow to translate terms in contexts.

Set t-translation-context="fr" on a node makes every call of the translation
function to be done with "fr" as seconde parameter when translating an
attribute/content within that node or its children (if no closer
t-translation-context directive is found).
A directive t-translation-context-attr="fr" can be used on a node to target its
attribute "attr". For example, if a div has an attribute title="a title",
use t-translation-context-title="pt" will make "a title" to be translated
in the context "pt". Note that this takes precedence over any other directive
t-translation-context found on a parent (or the div itself).
The translation function is in charge of the interpretation of the context: OWL
does not associates any meaning with a translation context.
  • Loading branch information
Polymorphe57 committed Jan 14, 2025
1 parent b365ea5 commit ae6f029
Show file tree
Hide file tree
Showing 8 changed files with 633 additions and 40 deletions.
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 the attribute targeted](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
26 changes: 24 additions & 2 deletions doc/reference/translations.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# 🦉 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 (and an optional string)
and returns a string.

For example:

Expand All @@ -11,7 +12,7 @@ const translations = {
yes: "oui",
no: "non",
};
const translateFn = (str) => translations[str] || str;
const translateFn = (str, ctx) => translations[str] || str;

const app = new App(Root, { templates, tranaslateFn });
// ...
Expand All @@ -27,6 +28,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 (a string or
null) 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 +52,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-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 | null) => string;
translatableAttributes?: string[];
dev?: boolean;
}
Expand Down Expand Up @@ -171,6 +172,7 @@ interface Context {
forceNewBlock: boolean;
isLast?: boolean;
translate: boolean;
translationCtx: string | null;
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 | null) => 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: null,
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 | null): 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 | null
): 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 | null
): 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

0 comments on commit ae6f029

Please sign in to comment.