diff --git a/doc/reference/templates.md b/doc/reference/templates.md index 1e88a94b0..551ac531b 100644 --- a/doc/reference/templates.md +++ b/doc/reference/templates.md @@ -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: diff --git a/doc/reference/translations.md b/doc/reference/translations.md index 52fd1e092..ce996f9d3 100644 --- a/doc/reference/translations.md +++ b/doc/reference/translations.md @@ -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 }); // ... @@ -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: @@ -46,6 +62,22 @@ will be rendered as: ``` +and the following template: + +```xml +
hello
+
Are you sure?
+ +``` + +will be rendered as: + +```xml +
bonjour
+
Are you sure?
+ +``` + Note that the translation is done during the compilation of the template, not when it is rendered. diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 4cbab34ec..13cb3cca9 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -24,6 +24,7 @@ import { ASTTOut, ASTTPortal, ASTTranslation, + ASTTranslationContext, ASTTSet, ASTType, Attrs, @@ -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; } @@ -171,6 +172,7 @@ interface Context { forceNewBlock: boolean; isLast?: boolean; translate: boolean; + translationCtx: string; tKeyExpr: string | null; nameSpace?: string; tModelSelectedExpr?: string; @@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial): Context { index: 0, forceNewBlock: true, translate: parentCtx.translate, + translationCtx: parentCtx.translationCtx, tKeyExpr: null, nameSpace: parentCtx.nameSpace, tModelSelectedExpr: parentCtx.tModelSelectedExpr, @@ -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 }[] = []; @@ -303,6 +306,7 @@ export class CodeGenerator { forceNewBlock: false, isLast: true, translate: true, + translationCtx: "", tKeyExpr: null, }); // define blocks and utility functions @@ -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]; } /** @@ -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); } @@ -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, " "); @@ -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; @@ -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})`; @@ -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); } @@ -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 { @@ -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 = ""; @@ -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}`); @@ -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); @@ -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` }); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 5dbf0078f..760b85345 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -27,6 +27,7 @@ export const enum ASTType { TSlot, TCallBlock, TTranslation, + TTranslationContext, TPortal, } @@ -56,6 +57,7 @@ export interface ASTDomNode { tag: string; content: AST[]; attrs: Attrs | null; + attrsTranslationCtx: Attrs | null; ref: string | null; on: EventHandlers | null; model: TModelInfo | null; @@ -127,6 +129,7 @@ interface SlotDefinition { scope: string | null; on: EventHandlers | null; attrs: Attrs | null; + attrsTranslationCtx: Attrs | null; } export interface ASTComponent { @@ -136,6 +139,7 @@ export interface ASTComponent { dynamicProps: string | null; on: EventHandlers | null; props: { [name: string]: string } | null; + propsTranslationCtx: { [name: string]: string } | null; slots: { [name: string]: SlotDefinition } | null; } @@ -143,6 +147,7 @@ export interface ASTSlot { type: ASTType.TSlot; name: string; attrs: Attrs | null; + attrsTranslationCtx: Attrs | null; on: EventHandlers | null; defaultContent: AST | null; } @@ -168,6 +173,12 @@ export interface ASTTranslation { content: AST | null; } +export interface ASTTranslationContext { + type: ASTType.TTranslationContext; + content: AST | null; + translationCtx: string; +} + export interface ASTTPortal { type: ASTType.TPortal; target: string; @@ -192,6 +203,7 @@ export type AST = | ASTLog | ASTDebug | ASTTranslation + | ASTTranslationContext | ASTTPortal; // ----------------------------------------------------------------------------- @@ -245,6 +257,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null { parseTOutNode(node, ctx) || parseTKey(node, ctx) || parseTTranslation(node, ctx) || + parseTTranslationContext(node, ctx) || parseTSlot(node, ctx) || parseComponent(node, ctx) || parseDOMNode(node, ctx) || @@ -368,6 +381,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { const nodeAttrsNames = node.getAttributeNames(); let attrs: ASTDomNode["attrs"] = null; + let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null; let on: EventHandlers | 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}'`); } else if (attr === "xmlns") { ns = value; + } else if (attr.startsWith("t-translation-context-")) { + const attrName = attr.slice(22); + attrsTranslationCtx = attrsTranslationCtx || {}; + attrsTranslationCtx[attrName] = value; } else if (attr !== "t-name") { if (attr.startsWith("t-") && !attr.startsWith("t-att")) { throw new OwlError(`Unknown QWeb directive: '${attr}'`); @@ -450,6 +468,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { tag: tagName, dynamicTag, attrs, + attrsTranslationCtx, on, ref, content: children, @@ -609,7 +628,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null { if (ast && ast.type === ASTType.TComponent) { return { ...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 props: ASTComponent["props"] = null; + let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null; for (let name of node.getAttributeNames()) { 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-")) { on = on || {}; on[name.slice(5)] = value; @@ -794,12 +826,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { const slotAst = parseNode(slotNode, ctx); let on: SlotDefinition["on"] = null; let attrs: Attrs | null = null; + let attrsTranslationCtx: Attrs | null = null; let scope: string | null = null; for (let attributeName of slotNode.getAttributeNames()) { const value = slotNode.getAttribute(attributeName)!; if (attributeName === "t-slot-scope") { scope = value; continue; + } else if (attributeName.startsWith("t-translation-context-")) { + const attrName = attributeName.slice(22); + attrsTranslationCtx = attrsTranslationCtx || {}; + attrsTranslationCtx[attrName] = value; } else if (attributeName.startsWith("t-on-")) { on = on || {}; on[attributeName.slice(5)] = value; @@ -809,7 +846,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { } } slots = slots || {}; - slots[name] = { content: slotAst, on, attrs, scope }; + slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope }; } // default slot @@ -817,10 +854,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { slots = slots || {}; // t-set-slot="default" has priority over content 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")!; node.removeAttribute("t-slot"); let attrs: Attrs | null = null; + let attrsTranslationCtx: Attrs | null = null; let on: ASTComponent["on"] = null; for (let attributeName of node.getAttributeNames()) { const value = node.getAttribute(attributeName)!; if (attributeName.startsWith("t-on-")) { on = on || {}; on[attributeName.slice(5)] = value; + } else if (attributeName.startsWith("t-translation-context-")) { + const attrName = attributeName.slice(22); + attrsTranslationCtx = attrsTranslationCtx || {}; + attrsTranslationCtx[attrName] = value; } else { attrs = attrs || {}; attrs[attributeName] = value; @@ -849,11 +906,16 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null { type: ASTType.TSlot, name, attrs, + attrsTranslationCtx, on, defaultContent: parseChildNodes(node, ctx), }; } +// ----------------------------------------------------------------------------- +// Translation +// ----------------------------------------------------------------------------- + function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { if (node.getAttribute("t-translation") !== "off") { 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 // ----------------------------------------------------------------------------- diff --git a/src/runtime/template_set.ts b/src/runtime/template_set.ts index 4e750e418..5be0b4c07 100644 --- a/src/runtime/template_set.ts +++ b/src/runtime/template_set.ts @@ -12,7 +12,7 @@ const bdom = { text, createBlock, list, multi, html, toggler, comment }; export interface TemplateSetConfig { dev?: boolean; translatableAttributes?: string[]; - translateFn?: (s: string) => string; + translateFn?: (s: string, translationCtx: string) => string; templates?: string | Document | Record; getTemplate?: (s: string) => Element | Function | string | void; customDirectives?: customDirectives; @@ -27,7 +27,7 @@ export class TemplateSet { rawTemplates: typeof globalTemplates = Object.create(globalTemplates); templates: { [name: string]: Template } = {}; getRawTemplate?: (s: string) => Element | Function | string | void; - translateFn?: (s: string) => string; + translateFn?: (s: string, translationCtx: string) => string; translatableAttributes?: string[]; Portal = Portal; customDirectives: customDirectives; diff --git a/tests/compiler/__snapshots__/translation.test.ts.snap b/tests/compiler/__snapshots__/translation.test.ts.snap index 6e493cb33..b7c1ea7c6 100644 --- a/tests/compiler/__snapshots__/translation.test.ts.snap +++ b/tests/compiler/__snapshots__/translation.test.ts.snap @@ -1,5 +1,128 @@ // 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(\`
\`); + + 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(\`\`); + + 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(\`
\`); + + 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(\`
\`); + + 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(\`
word
\`); + let block3 = createBlock(\`
mot
\`); + + 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`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index c264ba548..de3db0bf5 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -43,6 +43,7 @@ describe("qweb parser", () => { dynamicTag: null, content: [], attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -70,6 +71,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -84,6 +86,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -98,6 +101,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -109,6 +113,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -128,6 +133,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -139,6 +145,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -156,6 +163,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -181,6 +189,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -201,6 +210,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -223,6 +233,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -246,6 +257,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -262,6 +274,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: { class: "abc" }, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -280,6 +293,7 @@ describe("qweb parser", () => { height: "90px", width: "100px", }, + attrsTranslationCtx: null, content: [ { attrs: { @@ -290,6 +304,7 @@ describe("qweb parser", () => { stroke: "green", "stroke-width": "1", }, + attrsTranslationCtx: null, content: [], dynamicTag: null, model: null, @@ -312,6 +327,7 @@ describe("qweb parser", () => { parse(``) ).toEqual({ attrs: null, + attrsTranslationCtx: null, content: [ { attrs: { @@ -322,6 +338,7 @@ describe("qweb parser", () => { stroke: "green", "stroke-width": "1", }, + attrsTranslationCtx: null, content: [], dynamicTag: null, model: null, @@ -348,6 +365,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, content: [ @@ -356,6 +374,7 @@ describe("qweb parser", () => { tag: "pre", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, content: [], @@ -391,6 +410,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -413,6 +433,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -455,6 +476,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -469,6 +491,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -489,6 +512,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -530,6 +554,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -607,6 +632,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -626,6 +652,7 @@ describe("qweb parser", () => { tag: "h1", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -639,6 +666,7 @@ describe("qweb parser", () => { tag: "h2", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -685,6 +713,7 @@ describe("qweb parser", () => { { type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, on: null, tag: "div", dynamicTag: null, @@ -705,6 +734,7 @@ describe("qweb parser", () => { { type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, on: null, tag: "div", dynamicTag: null, @@ -742,6 +772,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -811,6 +842,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -853,6 +885,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -887,6 +920,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -920,6 +954,7 @@ describe("qweb parser", () => { "t-att-selected": "category.id==options.active_category_id", "t-att-value": "category.id", }, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -940,6 +975,7 @@ describe("qweb parser", () => { ).toEqual({ type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -987,6 +1023,7 @@ describe("qweb parser", () => { ref: null, model: null, attrs: null, + attrsTranslationCtx: null, ns: null, content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], }, @@ -1010,6 +1047,7 @@ describe("qweb parser", () => { name: "Comp", dynamicProps: null, props: null, + propsTranslationCtx: null, slots: null, on: null, }, @@ -1099,6 +1137,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -1139,6 +1178,7 @@ describe("qweb parser", () => { tag: "button", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: { click: "add" }, ref: null, model: null, @@ -1175,6 +1215,7 @@ describe("qweb parser", () => { tag: "select", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, content: [ @@ -1183,6 +1224,7 @@ describe("qweb parser", () => { tag: "option", dynamicTag: null, attrs: { value: "1" }, + attrsTranslationCtx: null, on: null, ref: null, content: [], @@ -1212,6 +1254,7 @@ describe("qweb parser", () => { tag: "select", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, content: [ @@ -1220,6 +1263,7 @@ describe("qweb parser", () => { tag: "option", dynamicTag: null, attrs: { "t-att-value": "valueVar" }, + attrsTranslationCtx: null, on: null, ref: null, content: [], @@ -1251,6 +1295,7 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: null, isDynamic: false, @@ -1263,6 +1308,7 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: { a: "1", b: "'b'" }, + propsTranslationCtx: null, isDynamic: false, on: null, slots: null, @@ -1275,6 +1321,7 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: "state", props: { a: "1" }, + propsTranslationCtx: null, isDynamic: false, on: null, slots: null, @@ -1287,6 +1334,7 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: { click: "someMethod" }, slots: null, @@ -1329,12 +1377,14 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { content: { type: ASTType.Text, value: "foo" }, attrs: null, + attrsTranslationCtx: null, on: null, scope: null, }, @@ -1350,12 +1400,14 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { content: { type: ASTType.Text, value: "foo" }, attrs: { param: "param" }, + attrsTranslationCtx: null, on: null, scope: null, }, @@ -1370,6 +1422,7 @@ describe("qweb parser", () => { isDynamic: false, dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: { default: { @@ -1381,6 +1434,7 @@ describe("qweb parser", () => { tag: "span", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, content: [], ref: null, model: null, @@ -1392,6 +1446,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, content: [], ref: null, model: null, @@ -1401,6 +1456,7 @@ describe("qweb parser", () => { ], }, attrs: null, + attrsTranslationCtx: null, on: null, scope: null, }, @@ -1415,9 +1471,11 @@ describe("qweb parser", () => { name: "MyComponent", on: null, props: null, + propsTranslationCtx: null, slots: { mySlot: { attrs: null, + attrsTranslationCtx: null, content: null, on: null, scope: null, @@ -1434,9 +1492,16 @@ describe("qweb parser", () => { isDynamic: false, dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, 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, dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: { name: { content: { type: ASTType.Text, value: "foo" }, attrs: { param: "param" }, + attrsTranslationCtx: null, on: null, scope: null, }, @@ -1469,12 +1536,14 @@ describe("qweb parser", () => { isDynamic: false, dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: { name: { content: { type: ASTType.Text, value: "foo" }, on: { click: "doStuff" }, attrs: null, + attrsTranslationCtx: null, scope: null, }, }, @@ -1493,16 +1562,24 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { content: { type: ASTType.Text, value: " " }, attrs: null, + attrsTranslationCtx: null, + on: null, + scope: null, + }, + name: { + content: { type: ASTType.Text, value: "foo" }, + attrs: null, + attrsTranslationCtx: null, on: 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", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { - a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null }, - b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null }, + a: { + 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", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: true, on: null, slots: null, @@ -1545,6 +1636,7 @@ describe("qweb parser", () => { name: "mycomponent", dynamicProps: null, props: { a: "1", b: "'b'" }, + propsTranslationCtx: null, isDynamic: true, on: null, slots: null, @@ -1557,6 +1649,7 @@ describe("qweb parser", () => { name: "mycomponent", dynamicProps: "state", props: { a: "1" }, + propsTranslationCtx: null, isDynamic: true, on: null, slots: null, @@ -1587,12 +1680,14 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null }, attrs: null, + attrsTranslationCtx: null, scope: null, on: null, }, @@ -1613,11 +1708,13 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { attrs: null, + attrsTranslationCtx: null, on: null, scope: null, content: { @@ -1626,11 +1723,13 @@ describe("qweb parser", () => { name: "Child", dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: { brol: { content: { type: ASTType.Text, value: "coucou" }, attrs: null, + attrsTranslationCtx: null, scope: null, on: null, }, @@ -1654,11 +1753,13 @@ describe("qweb parser", () => { name: "MyComponent", dynamicProps: null, props: null, + propsTranslationCtx: null, isDynamic: false, on: null, slots: { default: { attrs: null, + attrsTranslationCtx: null, on: null, scope: null, content: { @@ -1667,11 +1768,13 @@ describe("qweb parser", () => { name: "Child", dynamicProps: null, props: null, + propsTranslationCtx: null, on: null, slots: { brol: { content: { type: ASTType.Text, value: "coucou" }, attrs: null, + attrsTranslationCtx: null, on: null, scope: null, }, @@ -1691,6 +1794,7 @@ describe("qweb parser", () => { type: ASTType.TSlot, name: "default", attrs: null, + attrsTranslationCtx: null, on: null, defaultContent: null, }); @@ -1701,6 +1805,7 @@ describe("qweb parser", () => { type: ASTType.TSlot, name: "header", attrs: null, + attrsTranslationCtx: null, on: null, defaultContent: { type: ASTType.Text, value: "default content" }, }); @@ -1711,6 +1816,7 @@ describe("qweb parser", () => { type: ASTType.TSlot, name: "default", attrs: null, + attrsTranslationCtx: null, on: { "click.prevent": "doSomething" }, defaultContent: null, }); @@ -1728,6 +1834,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -1746,6 +1853,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: null, model: null, @@ -1765,6 +1873,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: "name", model: null, @@ -1779,6 +1888,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: "name", model: null, @@ -1795,6 +1905,7 @@ describe("qweb parser", () => { tag: "div", dynamicTag: null, attrs: null, + attrsTranslationCtx: null, on: null, ref: "name", model: null, @@ -1831,6 +1942,7 @@ describe("qweb parser", () => { body: { content: { attrs: null, + attrsTranslationCtx: null, content: [ { type: ASTType.Text, @@ -1859,6 +1971,92 @@ describe("qweb parser", () => { }); }); + // --------------------------------------------------------------------------- + // t-translation-context + // --------------------------------------------------------------------------- + + test('t-translation-context="fr"', async () => { + expect(parse(`word`)).toEqual({ + type: ASTType.TTranslationContext, + content: { + type: ASTType.Text, + value: "word", + }, + translationCtx: "fr", + }); + + expect(parse(`
word
`)).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( + `
word
` + ) + ).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(``)).toEqual({ + dynamicProps: null, + isDynamic: false, + name: "Comp", + on: null, + props: { + title: "hello", + }, + propsTranslationCtx: { + title: "fr", + }, + slots: null, + type: ASTType.TComponent, + }); + }); + // --------------------------------------------------------------------------- // t-model // --------------------------------------------------------------------------- @@ -1866,6 +2064,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, content: [], on: null, ref: null, @@ -1886,6 +2085,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, content: [], on: null, ref: null, @@ -1906,6 +2106,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.DomNode, attrs: null, + attrsTranslationCtx: null, content: [], on: null, ref: null, @@ -1927,6 +2128,7 @@ describe("qweb parser", () => { expect(parse(`