[IMP] translation contexts

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 associate any meaning with a translation context.
This commit is contained in:
Mathieu Duckerts-Antoine
2025-01-10 13:46:33 +01:00
committed by Géry Debongnie
parent b365ea5c9c
commit a9be149e1e
8 changed files with 646 additions and 43 deletions
+3 -1
View File
@@ -57,7 +57,7 @@ extensions.
For reference, here is a list of all standard QWeb directives: For reference, here is a list of all standard QWeb directives:
| Name | Description | | Name | Description |
| ------------------------------ | --------------------------------------------------------------- | | ------------------------------ | ----------------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) | | `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) | | `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) | | `t-set`, `t-value` | [Setting variables](#setting-variables) |
@@ -67,6 +67,8 @@ For reference, here is a list of all standard QWeb directives:
| `t-call` | [Rendering sub templates](#sub-templates) | | `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) | | `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) | | `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 The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives: needs. Here is a list of all Owl specific directives:
+34 -2
View File
@@ -1,17 +1,28 @@
# 🦉 Translations 🦉 # 🦉 Translations 🦉
If properly setup, Owl can translate all rendered templates. To do 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: For example:
```js ```js
const translations = { const translations = {
fr: {
hello: "bonjour", hello: "bonjour",
yes: "oui", yes: "oui",
no: "non", 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 }); const app = new App(Root, { templates, tranaslateFn });
// ... // ...
@@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
`placeholder`, `label` and `alt`, `placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`, - translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`. 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: So, with the above `translateFn`, the following templates:
@@ -46,6 +62,22 @@ will be rendered as:
<input placeholder="bonjour" other="yes"/> <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 Note that the translation is done during the compilation of the template, not
when it is rendered. when it is rendered.
+52 -14
View File
@@ -24,6 +24,7 @@ import {
ASTTOut, ASTTOut,
ASTTPortal, ASTTPortal,
ASTTranslation, ASTTranslation,
ASTTranslationContext,
ASTTSet, ASTTSet,
ASTType, ASTType,
Attrs, Attrs,
@@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
export interface Config { export interface Config {
translateFn?: (s: string) => string; translateFn?: (s: string, translationCtx: string) => string;
translatableAttributes?: string[]; translatableAttributes?: string[];
dev?: boolean; dev?: boolean;
} }
@@ -171,6 +172,7 @@ interface Context {
forceNewBlock: boolean; forceNewBlock: boolean;
isLast?: boolean; isLast?: boolean;
translate: boolean; translate: boolean;
translationCtx: string;
tKeyExpr: string | null; tKeyExpr: string | null;
nameSpace?: string; nameSpace?: string;
tModelSelectedExpr?: string; tModelSelectedExpr?: string;
@@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
index: 0, index: 0,
forceNewBlock: true, forceNewBlock: true,
translate: parentCtx.translate, translate: parentCtx.translate,
translationCtx: parentCtx.translationCtx,
tKeyExpr: null, tKeyExpr: null,
nameSpace: parentCtx.nameSpace, nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr, tModelSelectedExpr: parentCtx.tModelSelectedExpr,
@@ -263,7 +266,7 @@ export class CodeGenerator {
target = new CodeTarget("template"); target = new CodeTarget("template");
templateName?: string; templateName?: string;
dev: boolean; dev: boolean;
translateFn: (s: string) => string; translateFn: (s: string, translationCtx: string) => string;
translatableAttributes: string[] = TRANSLATABLE_ATTRS; translatableAttributes: string[] = TRANSLATABLE_ATTRS;
ast: AST; ast: AST;
staticDefs: { id: string; expr: string }[] = []; staticDefs: { id: string; expr: string }[] = [];
@@ -303,6 +306,7 @@ export class CodeGenerator {
forceNewBlock: false, forceNewBlock: false,
isLast: true, isLast: true,
translate: true, translate: true,
translationCtx: "",
tKeyExpr: null, tKeyExpr: null,
}); });
// define blocks and utility functions // define blocks and utility functions
@@ -457,9 +461,9 @@ export class CodeGenerator {
.join(""); .join("");
} }
translate(str: string): string { translate(str: string, translationCtx: string): string {
const match = translationRE.exec(str) as any; 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];
} }
/** /**
@@ -501,6 +505,8 @@ export class CodeGenerator {
return this.compileTSlot(ast, ctx); return this.compileTSlot(ast, ctx);
case ASTType.TTranslation: case ASTType.TTranslation:
return this.compileTTranslation(ast, ctx); return this.compileTTranslation(ast, ctx);
case ASTType.TTranslationContext:
return this.compileTTranslationContext(ast, ctx);
case ASTType.TPortal: case ASTType.TPortal:
return this.compileTPortal(ast, ctx); return this.compileTPortal(ast, ctx);
} }
@@ -542,7 +548,7 @@ export class CodeGenerator {
let value = ast.value; let value = ast.value;
if (value && ctx.translate !== false) { if (value && ctx.translate !== false) {
value = this.translate(value); value = this.translate(value, ctx.translationCtx);
} }
if (!ctx.inPreTag) { if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " "); value = value.replace(whitespaceRE, " ");
@@ -631,7 +637,8 @@ export class CodeGenerator {
} }
} }
} else if (this.translatableAttributes.includes(key)) { } 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 { } else {
expr = `"${ast.attrs[key]}"`; expr = `"${ast.attrs[key]}"`;
attrName = key; attrName = key;
@@ -1104,7 +1111,7 @@ export class CodeGenerator {
let value: string; let value: string;
if (ast.defaultValue) { if (ast.defaultValue) {
const defaultValue = toStringExpression( const defaultValue = toStringExpression(
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
); );
if (ast.value) { if (ast.value) {
value = `withDefault(${expr}, ${defaultValue})`; value = `withDefault(${expr}, ${defaultValue})`;
@@ -1139,9 +1146,15 @@ export class CodeGenerator {
* "some-prop" "state" "'some-prop': ctx['state']" * "some-prop" "state" "'some-prop': ctx['state']"
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])" * "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")) { if (name.endsWith(".translate")) {
value = toStringExpression(this.translateFn(value)); const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
} else { } else {
value = this.captureExpression(value); value = this.captureExpression(value);
} }
@@ -1163,8 +1176,14 @@ export class CodeGenerator {
return `${name}: ${value || undefined}`; return `${name}: ${value || undefined}`;
} }
formatPropObject(obj: { [prop: string]: any }): string[] { formatPropObject(
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v)); 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 { getPropString(props: string[], dynProps: string | null): string {
@@ -1181,7 +1200,9 @@ export class CodeGenerator {
let { block } = ctx; let { block } = ctx;
// props // props
const hasSlotsProp = "slots" in (ast.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 // slots
let slotDef: string = ""; let slotDef: string = "";
@@ -1205,7 +1226,13 @@ export class CodeGenerator {
params.push(`__scope: "${scope}"`); params.push(`__scope: "${scope}"`);
} }
if (ast.slots[slotName].attrs) { 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(", ")}}`; const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`); slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -1332,7 +1359,9 @@ export class CodeGenerator {
key = this.generateComponentKey(key); 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); const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) { if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx); const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
@@ -1365,6 +1394,15 @@ export class CodeGenerator {
} }
return null; 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 { compileTPortal(ast: ASTTPortal, ctx: Context): string {
if (!this.staticDefs.find((d) => d.id === "Portal")) { if (!this.staticDefs.find((d) => d.id === "Portal")) {
this.staticDefs.push({ id: "Portal", expr: `app.Portal` }); this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
+84 -5
View File
@@ -27,6 +27,7 @@ export const enum ASTType {
TSlot, TSlot,
TCallBlock, TCallBlock,
TTranslation, TTranslation,
TTranslationContext,
TPortal, TPortal,
} }
@@ -56,6 +57,7 @@ export interface ASTDomNode {
tag: string; tag: string;
content: AST[]; content: AST[];
attrs: Attrs | null; attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
ref: string | null; ref: string | null;
on: EventHandlers | null; on: EventHandlers | null;
model: TModelInfo | null; model: TModelInfo | null;
@@ -127,6 +129,7 @@ interface SlotDefinition {
scope: string | null; scope: string | null;
on: EventHandlers | null; on: EventHandlers | null;
attrs: Attrs | null; attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
} }
export interface ASTComponent { export interface ASTComponent {
@@ -136,6 +139,7 @@ export interface ASTComponent {
dynamicProps: string | null; dynamicProps: string | null;
on: EventHandlers | null; on: EventHandlers | null;
props: { [name: string]: string } | null; props: { [name: string]: string } | null;
propsTranslationCtx: { [name: string]: string } | null;
slots: { [name: string]: SlotDefinition } | null; slots: { [name: string]: SlotDefinition } | null;
} }
@@ -143,6 +147,7 @@ export interface ASTSlot {
type: ASTType.TSlot; type: ASTType.TSlot;
name: string; name: string;
attrs: Attrs | null; attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
on: EventHandlers | null; on: EventHandlers | null;
defaultContent: AST | null; defaultContent: AST | null;
} }
@@ -168,6 +173,12 @@ export interface ASTTranslation {
content: AST | null; content: AST | null;
} }
export interface ASTTranslationContext {
type: ASTType.TTranslationContext;
content: AST | null;
translationCtx: string;
}
export interface ASTTPortal { export interface ASTTPortal {
type: ASTType.TPortal; type: ASTType.TPortal;
target: string; target: string;
@@ -192,6 +203,7 @@ export type AST =
| ASTLog | ASTLog
| ASTDebug | ASTDebug
| ASTTranslation | ASTTranslation
| ASTTranslationContext
| ASTTPortal; | ASTTPortal;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -245,6 +257,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -368,6 +381,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const nodeAttrsNames = node.getAttributeNames(); const nodeAttrsNames = node.getAttributeNames();
let attrs: ASTDomNode["attrs"] = null; let attrs: ASTDomNode["attrs"] = null;
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
let on: EventHandlers | null = null; let on: EventHandlers | null = null;
let model: TModelInfo | null = null; let model: TModelInfo | null = null;
@@ -428,6 +442,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
throw new OwlError(`Invalid attribute: '${attr}'`); throw new OwlError(`Invalid attribute: '${attr}'`);
} else if (attr === "xmlns") { } else if (attr === "xmlns") {
ns = value; ns = value;
} else if (attr.startsWith("t-translation-context-")) {
const attrName = attr.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else if (attr !== "t-name") { } else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) { if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new OwlError(`Unknown QWeb directive: '${attr}'`); throw new OwlError(`Unknown QWeb directive: '${attr}'`);
@@ -450,6 +468,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
tag: tagName, tag: tagName,
dynamicTag, dynamicTag,
attrs, attrs,
attrsTranslationCtx,
on, on,
ref, ref,
content: children, content: children,
@@ -609,7 +628,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
if (ast && ast.type === ASTType.TComponent) { if (ast && ast.type === ASTType.TComponent) {
return { return {
...ast, ...ast,
slots: { default: { content: tcall, scope: null, on: null, attrs: null } }, slots: {
default: {
content: tcall,
scope: null,
on: null,
attrs: null,
attrsTranslationCtx: null,
},
},
}; };
} }
} }
@@ -744,9 +771,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let on: ASTComponent["on"] = null; let on: ASTComponent["on"] = null;
let props: ASTComponent["props"] = null; let props: ASTComponent["props"] = null;
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
for (let name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!; const value = node.getAttribute(name)!;
if (name.startsWith("t-")) { if (name.startsWith("t-translation-context-")) {
const attrName = name.slice(22);
propsTranslationCtx = propsTranslationCtx || {};
propsTranslationCtx[attrName] = value;
} else if (name.startsWith("t-")) {
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
on = on || {}; on = on || {};
on[name.slice(5)] = value; on[name.slice(5)] = value;
@@ -794,12 +826,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const slotAst = parseNode(slotNode, ctx); const slotAst = parseNode(slotNode, ctx);
let on: SlotDefinition["on"] = null; let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null; let attrs: Attrs | null = null;
let attrsTranslationCtx: Attrs | null = null;
let scope: string | null = null; let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) { for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!; const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") { if (attributeName === "t-slot-scope") {
scope = value; scope = value;
continue; continue;
} else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else if (attributeName.startsWith("t-on-")) { } else if (attributeName.startsWith("t-on-")) {
on = on || {}; on = on || {};
on[attributeName.slice(5)] = value; on[attributeName.slice(5)] = value;
@@ -809,7 +846,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
} }
} }
slots = slots || {}; slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope }; slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
} }
// default slot // default slot
@@ -817,10 +854,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slots = slots || {}; slots = slots || {};
// t-set-slot="default" has priority over content // t-set-slot="default" has priority over content
if (defaultContent && !slots.default) { if (defaultContent && !slots.default) {
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope }; slots.default = {
content: defaultContent,
on,
attrs: null,
attrsTranslationCtx: null,
scope: defaultSlotScope,
};
} }
} }
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on }; return {
type: ASTType.TComponent,
name,
isDynamic,
dynamicProps,
props,
propsTranslationCtx,
slots,
on,
};
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -834,12 +886,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
const name = node.getAttribute("t-slot")!; const name = node.getAttribute("t-slot")!;
node.removeAttribute("t-slot"); node.removeAttribute("t-slot");
let attrs: Attrs | null = null; let attrs: Attrs | null = null;
let attrsTranslationCtx: Attrs | null = null;
let on: ASTComponent["on"] = null; let on: ASTComponent["on"] = null;
for (let attributeName of node.getAttributeNames()) { for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName)!; const value = node.getAttribute(attributeName)!;
if (attributeName.startsWith("t-on-")) { if (attributeName.startsWith("t-on-")) {
on = on || {}; on = on || {};
on[attributeName.slice(5)] = value; on[attributeName.slice(5)] = value;
} else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else { } else {
attrs = attrs || {}; attrs = attrs || {};
attrs[attributeName] = value; attrs[attributeName] = value;
@@ -849,11 +906,16 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
type: ASTType.TSlot, type: ASTType.TSlot,
name, name,
attrs, attrs,
attrsTranslationCtx,
on, on,
defaultContent: parseChildNodes(node, ctx), defaultContent: parseChildNodes(node, ctx),
}; };
} }
// -----------------------------------------------------------------------------
// Translation
// -----------------------------------------------------------------------------
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") { if (node.getAttribute("t-translation") !== "off") {
return null; return null;
@@ -865,6 +927,23 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
}; };
} }
// -----------------------------------------------------------------------------
// Translation Context
// -----------------------------------------------------------------------------
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
const translationCtx = node.getAttribute("t-translation-context");
if (!translationCtx) {
return null;
}
node.removeAttribute("t-translation-context");
return {
type: ASTType.TTranslationContext,
content: parseNode(node, ctx),
translationCtx,
};
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Portal // Portal
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+2 -2
View File
@@ -12,7 +12,7 @@ const bdom = { text, createBlock, list, multi, html, toggler, comment };
export interface TemplateSetConfig { export interface TemplateSetConfig {
dev?: boolean; dev?: boolean;
translatableAttributes?: string[]; translatableAttributes?: string[];
translateFn?: (s: string) => string; translateFn?: (s: string, translationCtx: string) => string;
templates?: string | Document | Record<string, string>; templates?: string | Document | Record<string, string>;
getTemplate?: (s: string) => Element | Function | string | void; getTemplate?: (s: string) => Element | Function | string | void;
customDirectives?: customDirectives; customDirectives?: customDirectives;
@@ -27,7 +27,7 @@ export class TemplateSet {
rawTemplates: typeof globalTemplates = Object.create(globalTemplates); rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
getRawTemplate?: (s: string) => Element | Function | string | void; getRawTemplate?: (s: string) => Element | Function | string | void;
translateFn?: (s: string) => string; translateFn?: (s: string, translationCtx: string) => string;
translatableAttributes?: string[]; translatableAttributes?: string[];
Portal = Portal; Portal = Portal;
customDirectives: customDirectives; customDirectives: customDirectives;
@@ -1,5 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation context body of t-sets are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`traduit\`);
const b2 = text(ctx['label']);
return multi([b2]);
}
}"
`;
exports[`translation context default slot params and content translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function defaultContent1(ctx, node, key = \\"\\") {
return text(\` foo \`);
}
return function template(ctx, node, key = \\"\\") {
const b3 = callSlot(ctx, node, key, 'default', false, {param: \`param\`,title: \`título\`}, defaultContent1.bind(this));
return block1([], [b3]);
}
}"
`;
exports[`translation context props with modifier .translate are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`ChildComponent\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({text: \`jeu\`}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`translation context props with modifier .translate are translated in context 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].text;
return block1([txt1]);
}
}"
`;
exports[`translation context slot attrs and text contents are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const comp1 = app.createComponent(\`ChildComponent\`, true, true, false, []);
function slot1(ctx, node, key = \\"\\") {
return text(\`jeu\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, title: \`título\`}})}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`translation context slot attrs and text contents are translated in context 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'a', false, {});
return block1([], [b2]);
}
}"
`;
exports[`translation context translation of attributes in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div title=\\"titre\\" label=\\"game\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation context translation of text in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block2 = createBlock(\`<div>word</div>\`);
let block3 = createBlock(\`<div>mot</div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = block2();
const b3 = block3();
return multi([b2, b3]);
}
}"
`;
exports[`translation support body of t-sets are translated 1`] = ` exports[`translation support body of t-sets are translated 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+210 -4
View File
@@ -43,6 +43,7 @@ describe("qweb parser", () => {
dynamicTag: null, dynamicTag: null,
content: [], content: [],
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -70,6 +71,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -84,6 +86,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -109,6 +113,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -128,6 +133,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -139,6 +145,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -156,6 +163,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -181,6 +189,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -201,6 +210,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -223,6 +233,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -246,6 +257,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -262,6 +274,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: { class: "abc" }, attrs: { class: "abc" },
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -280,6 +293,7 @@ describe("qweb parser", () => {
height: "90px", height: "90px",
width: "100px", width: "100px",
}, },
attrsTranslationCtx: null,
content: [ content: [
{ {
attrs: { attrs: {
@@ -290,6 +304,7 @@ describe("qweb parser", () => {
stroke: "green", stroke: "green",
"stroke-width": "1", "stroke-width": "1",
}, },
attrsTranslationCtx: null,
content: [], content: [],
dynamicTag: null, dynamicTag: null,
model: null, model: null,
@@ -312,6 +327,7 @@ describe("qweb parser", () => {
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`) parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
).toEqual({ ).toEqual({
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [ content: [
{ {
attrs: { attrs: {
@@ -322,6 +338,7 @@ describe("qweb parser", () => {
stroke: "green", stroke: "green",
"stroke-width": "1", "stroke-width": "1",
}, },
attrsTranslationCtx: null,
content: [], content: [],
dynamicTag: null, dynamicTag: null,
model: null, model: null,
@@ -348,6 +365,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [ content: [
@@ -356,6 +374,7 @@ describe("qweb parser", () => {
tag: "pre", tag: "pre",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [], content: [],
@@ -391,6 +410,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -413,6 +433,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -455,6 +476,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -469,6 +491,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -489,6 +512,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -530,6 +554,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -607,6 +632,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -626,6 +652,7 @@ describe("qweb parser", () => {
tag: "h1", tag: "h1",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -639,6 +666,7 @@ describe("qweb parser", () => {
tag: "h2", tag: "h2",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -685,6 +713,7 @@ describe("qweb parser", () => {
{ {
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
@@ -705,6 +734,7 @@ describe("qweb parser", () => {
{ {
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
@@ -742,6 +772,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -811,6 +842,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -853,6 +885,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -887,6 +920,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -920,6 +954,7 @@ describe("qweb parser", () => {
"t-att-selected": "category.id==options.active_category_id", "t-att-selected": "category.id==options.active_category_id",
"t-att-value": "category.id", "t-att-value": "category.id",
}, },
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -940,6 +975,7 @@ describe("qweb parser", () => {
).toEqual({ ).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -987,6 +1023,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
ns: null, ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
}, },
@@ -1010,6 +1047,7 @@ describe("qweb parser", () => {
name: "Comp", name: "Comp",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
slots: null, slots: null,
on: null, on: null,
}, },
@@ -1099,6 +1137,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -1139,6 +1178,7 @@ describe("qweb parser", () => {
tag: "button", tag: "button",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: { click: "add" }, on: { click: "add" },
ref: null, ref: null,
model: null, model: null,
@@ -1175,6 +1215,7 @@ describe("qweb parser", () => {
tag: "select", tag: "select",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [ content: [
@@ -1183,6 +1224,7 @@ describe("qweb parser", () => {
tag: "option", tag: "option",
dynamicTag: null, dynamicTag: null,
attrs: { value: "1" }, attrs: { value: "1" },
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [], content: [],
@@ -1212,6 +1254,7 @@ describe("qweb parser", () => {
tag: "select", tag: "select",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [ content: [
@@ -1220,6 +1263,7 @@ describe("qweb parser", () => {
tag: "option", tag: "option",
dynamicTag: null, dynamicTag: null,
attrs: { "t-att-value": "valueVar" }, attrs: { "t-att-value": "valueVar" },
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
content: [], content: [],
@@ -1251,6 +1295,7 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: null, slots: null,
isDynamic: false, isDynamic: false,
@@ -1263,6 +1308,7 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: { a: "1", b: "'b'" }, props: { a: "1", b: "'b'" },
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: null, slots: null,
@@ -1275,6 +1321,7 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: "state", dynamicProps: "state",
props: { a: "1" }, props: { a: "1" },
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: null, slots: null,
@@ -1287,6 +1334,7 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: { click: "someMethod" }, on: { click: "someMethod" },
slots: null, slots: null,
@@ -1329,12 +1377,14 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
content: { type: ASTType.Text, value: "foo" }, content: { type: ASTType.Text, value: "foo" },
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
@@ -1350,12 +1400,14 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
content: { type: ASTType.Text, value: "foo" }, content: { type: ASTType.Text, value: "foo" },
attrs: { param: "param" }, attrs: { param: "param" },
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
@@ -1370,6 +1422,7 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
default: { default: {
@@ -1381,6 +1434,7 @@ describe("qweb parser", () => {
tag: "span", tag: "span",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
ref: null, ref: null,
model: null, model: null,
@@ -1392,6 +1446,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
ref: null, ref: null,
model: null, model: null,
@@ -1401,6 +1456,7 @@ describe("qweb parser", () => {
], ],
}, },
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
@@ -1415,9 +1471,11 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
on: null, on: null,
props: null, props: null,
propsTranslationCtx: null,
slots: { slots: {
mySlot: { mySlot: {
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: null, content: null,
on: null, on: null,
scope: null, scope: null,
@@ -1434,9 +1492,16 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null }, name: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
}, },
}); });
}); });
@@ -1448,11 +1513,13 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
name: { name: {
content: { type: ASTType.Text, value: "foo" }, content: { type: ASTType.Text, value: "foo" },
attrs: { param: "param" }, attrs: { param: "param" },
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
@@ -1469,12 +1536,14 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
name: { name: {
content: { type: ASTType.Text, value: "foo" }, content: { type: ASTType.Text, value: "foo" },
on: { click: "doStuff" }, on: { click: "doStuff" },
attrs: null, attrs: null,
attrsTranslationCtx: null,
scope: null, scope: null,
}, },
}, },
@@ -1493,16 +1562,24 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
content: { type: ASTType.Text, value: " " }, content: { type: ASTType.Text, value: " " },
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
name: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
}, },
}); });
}); });
@@ -1518,11 +1595,24 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null }, a: {
b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null }, content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
b: {
content: { type: ASTType.Text, value: "bar" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
}, },
}); });
}); });
@@ -1533,6 +1623,7 @@ describe("qweb parser", () => {
name: "myComponent", name: "myComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: true, isDynamic: true,
on: null, on: null,
slots: null, slots: null,
@@ -1545,6 +1636,7 @@ describe("qweb parser", () => {
name: "mycomponent", name: "mycomponent",
dynamicProps: null, dynamicProps: null,
props: { a: "1", b: "'b'" }, props: { a: "1", b: "'b'" },
propsTranslationCtx: null,
isDynamic: true, isDynamic: true,
on: null, on: null,
slots: null, slots: null,
@@ -1557,6 +1649,7 @@ describe("qweb parser", () => {
name: "mycomponent", name: "mycomponent",
dynamicProps: "state", dynamicProps: "state",
props: { a: "1" }, props: { a: "1" },
propsTranslationCtx: null,
isDynamic: true, isDynamic: true,
on: null, on: null,
slots: null, slots: null,
@@ -1587,12 +1680,14 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null }, content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
attrs: null, attrs: null,
attrsTranslationCtx: null,
scope: null, scope: null,
on: null, on: null,
}, },
@@ -1613,11 +1708,13 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
content: { content: {
@@ -1626,11 +1723,13 @@ describe("qweb parser", () => {
name: "Child", name: "Child",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
brol: { brol: {
content: { type: ASTType.Text, value: "coucou" }, content: { type: ASTType.Text, value: "coucou" },
attrs: null, attrs: null,
attrsTranslationCtx: null,
scope: null, scope: null,
on: null, on: null,
}, },
@@ -1654,11 +1753,13 @@ describe("qweb parser", () => {
name: "MyComponent", name: "MyComponent",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
isDynamic: false, isDynamic: false,
on: null, on: null,
slots: { slots: {
default: { default: {
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
content: { content: {
@@ -1667,11 +1768,13 @@ describe("qweb parser", () => {
name: "Child", name: "Child",
dynamicProps: null, dynamicProps: null,
props: null, props: null,
propsTranslationCtx: null,
on: null, on: null,
slots: { slots: {
brol: { brol: {
content: { type: ASTType.Text, value: "coucou" }, content: { type: ASTType.Text, value: "coucou" },
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
scope: null, scope: null,
}, },
@@ -1691,6 +1794,7 @@ describe("qweb parser", () => {
type: ASTType.TSlot, type: ASTType.TSlot,
name: "default", name: "default",
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
defaultContent: null, defaultContent: null,
}); });
@@ -1701,6 +1805,7 @@ describe("qweb parser", () => {
type: ASTType.TSlot, type: ASTType.TSlot,
name: "header", name: "header",
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
defaultContent: { type: ASTType.Text, value: "default content" }, defaultContent: { type: ASTType.Text, value: "default content" },
}); });
@@ -1711,6 +1816,7 @@ describe("qweb parser", () => {
type: ASTType.TSlot, type: ASTType.TSlot,
name: "default", name: "default",
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: { "click.prevent": "doSomething" }, on: { "click.prevent": "doSomething" },
defaultContent: null, defaultContent: null,
}); });
@@ -1728,6 +1834,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -1746,6 +1853,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: null, ref: null,
model: null, model: null,
@@ -1765,6 +1873,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: "name", ref: "name",
model: null, model: null,
@@ -1779,6 +1888,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: "name", ref: "name",
model: null, model: null,
@@ -1795,6 +1905,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
attrs: null, attrs: null,
attrsTranslationCtx: null,
on: null, on: null,
ref: "name", ref: "name",
model: null, model: null,
@@ -1831,6 +1942,7 @@ describe("qweb parser", () => {
body: { body: {
content: { content: {
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [ content: [
{ {
type: ASTType.Text, type: ASTType.Text,
@@ -1859,6 +1971,92 @@ describe("qweb parser", () => {
}); });
}); });
// ---------------------------------------------------------------------------
// t-translation-context
// ---------------------------------------------------------------------------
test('t-translation-context="fr"', async () => {
expect(parse(`<t t-translation-context="fr">word</t>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
type: ASTType.Text,
value: "word",
},
translationCtx: "fr",
});
expect(parse(`<div t-translation-context="fr">word</div>`)).toEqual({
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
type: 0,
value: "word",
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "div",
type: ASTType.DomNode,
},
translationCtx: "fr",
type: ASTType.TTranslationContext,
});
});
// ---------------------------------------------------------------------------
// t-translation-context-attr
// ---------------------------------------------------------------------------
test('t-translation-context="fr" and t-translation-context-title="pt" for a div attr title', async () => {
expect(
parse(
`<div t-translation-context="fr" title="hello" t-translation-context-title="pt">word</div>`
)
).toEqual({
content: {
attrs: { title: "hello" },
attrsTranslationCtx: { title: "pt" },
content: [
{
type: 0,
value: "word",
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "div",
type: ASTType.DomNode,
},
translationCtx: "fr",
type: ASTType.TTranslationContext,
});
});
test('t-translation-context-title="fr" for component prop title', async () => {
expect(parse(`<Comp title="hello" t-translation-context-title="fr" />`)).toEqual({
dynamicProps: null,
isDynamic: false,
name: "Comp",
on: null,
props: {
title: "hello",
},
propsTranslationCtx: {
title: "fr",
},
slots: null,
type: ASTType.TComponent,
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// t-model // t-model
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1866,6 +2064,7 @@ describe("qweb parser", () => {
expect(parse(`<input t-model="state.stuff" />`)).toEqual({ expect(parse(`<input t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1886,6 +2085,7 @@ describe("qweb parser", () => {
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({ expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1906,6 +2106,7 @@ describe("qweb parser", () => {
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({ expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1927,6 +2128,7 @@ describe("qweb parser", () => {
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({ expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1947,6 +2149,7 @@ describe("qweb parser", () => {
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({ expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: { type: "checkbox" }, attrs: { type: "checkbox" },
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1967,6 +2170,7 @@ describe("qweb parser", () => {
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({ expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: { type: "radio" }, attrs: { type: "radio" },
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -1987,6 +2191,7 @@ describe("qweb parser", () => {
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({ expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: { type: "radio" }, attrs: { type: "radio" },
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
@@ -2012,6 +2217,7 @@ describe("qweb parser", () => {
expect(parse(`<div t-tag="theTag" />`)).toEqual({ expect(parse(`<div t-tag="theTag" />`)).toEqual({
type: ASTType.DomNode, type: ASTType.DomNode,
attrs: null, attrs: null,
attrsTranslationCtx: null,
content: [], content: [],
on: null, on: null,
ref: null, ref: null,
+125 -2
View File
@@ -86,7 +86,7 @@ describe("translation support", () => {
await mount(SomeComponent, fixture, { translateFn }); await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div> mot </div>"); expect(fixture.innerHTML).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word"); expect(translateFn).toHaveBeenCalledWith("word", "");
}); });
test("translation works, even if initial string has inner consecutive white space", async () => { test("translation works, even if initial string has inner consecutive white space", async () => {
@@ -97,7 +97,7 @@ describe("translation support", () => {
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr)); const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
await mount(SomeComponent, fixture, { translateFn }); await mount(SomeComponent, fixture, { translateFn });
expect(translateFn).toHaveBeenCalledWith("some word"); expect(translateFn).toHaveBeenCalledWith("some word", "");
expect(fixture.innerHTML).toBe("<div>un mot</div>"); expect(fixture.innerHTML).toBe("<div>un mot</div>");
}); });
@@ -171,3 +171,126 @@ describe("translation support", () => {
expect(fixture.innerHTML).toBe("translated"); expect(fixture.innerHTML).toBe("translated");
}); });
}); });
describe("translation context", () => {
test("translation of text in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div>word</div>
<div t-translation-context="fr">word</div>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? (expr === "word" ? "mot" : expr) : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>word</div><div>mot</div>");
expect(translateFn).toHaveBeenCalledWith("word", "");
expect(translateFn).toHaveBeenCalledWith("word", "fr");
});
test("translation of attributes in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div t-translation-context="en" t-translation-context-title="fr" title="title" label="game"/>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? (expr === "title" ? "titre" : expr) : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe(`<div title="titre" label="game"></div>`);
expect(translateFn).toHaveBeenCalledWith("title", "fr");
expect(translateFn).toHaveBeenCalledWith("game", "en");
});
test("body of t-sets are translated in context", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label" t-translation-context="fr">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "traduit" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("traduit");
expect(translateFn).toHaveBeenCalledWith("untranslated", "fr");
});
test("props with modifier .translate are translated in context", async () => {
class ChildComponent extends Component {
static props = ["text"];
static template = xml`<span t-esc="props.text"/>`;
}
class SomeComponent extends Component {
static components = { ChildComponent };
static template = xml`
<ChildComponent text.translate="game" t-translation-context-text.translate="fr" />`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "jeu" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<span>jeu</span>");
expect(translateFn).toHaveBeenCalledWith("game", "fr");
});
test("slot attrs and text contents are translated in context", async () => {
class ChildComponent extends Component {
static template = xml`
<div t-translation-context="ja">
<t t-slot="a"/>
</div>`;
}
class SomeComponent extends Component {
static components = { ChildComponent };
static template = xml`
<ChildComponent t-translation-context="fr">
<t t-set-slot="a" title.translate="title" t-translation-context-title.translate="pt">game</t>
</ChildComponent>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "jeu" : translationCtx === "pt" ? "título" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>jeu</div>");
expect(translateFn).toHaveBeenCalledWith("game", "fr");
expect(translateFn).toHaveBeenCalledWith("title", "pt");
});
test("default slot params and content translated in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t
t-slot="default"
t-translation-context="fr"
param.translate="param"
title.translate="title"
t-translation-context-title.translate="pt"
>
foo
</t>
</div>`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "pt" ? "título" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div> foo </div>");
expect(translateFn).toHaveBeenCalledWith("foo", "fr");
expect(translateFn).toHaveBeenCalledWith("param", "fr");
expect(translateFn).toHaveBeenCalledWith("title", "pt");
});
});