[ADD] Translation feature

This commit brings back the possibility to translate text nodes and
the attributes "label", "title", "placeholder", and "alt" in an app
configured with a suitable translation function.

It is also possible to deactivate translations under a node via
the directive t-translation="off".

For flexibility it is possible to define the list of translatable
attributes in the app.
This commit is contained in:
Mathieu Duckerts-Antoine
2021-10-13 16:49:34 +02:00
committed by Aaron Bohy
parent 4415cc8932
commit 0a544bd7e8
8 changed files with 363 additions and 64 deletions
+6
View File
@@ -23,6 +23,12 @@ export class App<T extends typeof Component = any> extends TemplateSet {
if (params.env) { if (params.env) {
this.env = params.env; this.env = params.env;
} }
if (params.translateFn) {
this.translateFn = params.translateFn;
}
if (params.translatableAttributes) {
this.translatableAttributes = params.translatableAttributes;
}
} }
mount(target: HTMLElement): Promise<InstanceType<T>> { mount(target: HTMLElement): Promise<InstanceType<T>> {
+76 -46
View File
@@ -18,6 +18,7 @@ import {
ASTTKey, ASTTKey,
ASTTRaw, ASTTRaw,
ASTTSet, ASTTSet,
ASTTranslation,
ASTType, ASTType,
parse, parse,
} from "./parser"; } from "./parser";
@@ -27,8 +28,14 @@ export type TemplateFunction = (blocks: any, utils: any) => Template;
type BlockType = "block" | "text" | "multi" | "list" | "html"; type BlockType = "block" | "text" | "multi" | "list" | "html";
export function compileTemplate(template: string, name?: string): TemplateFunction { export interface CompileOptions {
const compiler = new QWebCompiler(template, name); name?: string;
translateFn?: (s: string) => string;
translatableAttributes?: string[];
}
export function compileTemplate(template: string, options?: CompileOptions): TemplateFunction {
const compiler = new QWebCompiler(template, options);
return compiler.compile(); return compiler.compile();
} }
@@ -97,7 +104,7 @@ class BlockDescription {
asXmlString() { asXmlString() {
// Can't use outerHTML on text/comment nodes // Can't use outerHTML on text/comment nodes
// append dom to any element and use innerHTML instead // append dom to any element and use innerHTML instead
const t = xmlDoc.createElement('t'); const t = xmlDoc.createElement("t");
t.appendChild(this.dom!); t.appendChild(this.dom!);
return t.innerHTML; return t.innerHTML;
} }
@@ -114,6 +121,19 @@ interface Context {
forceNewBlock: boolean; forceNewBlock: boolean;
preventRoot?: boolean; preventRoot?: boolean;
isLast?: boolean; isLast?: boolean;
translate: boolean;
}
function createContext(parentCtx: Context, params?: Partial<Context>) {
return Object.assign(
{
block: null,
index: 0,
forceNewBlock: true,
translate: parentCtx.translate,
},
params
);
} }
class CodeTarget { class CodeTarget {
@@ -139,6 +159,9 @@ class CodeTarget {
} }
} }
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
export class QWebCompiler { export class QWebCompiler {
blocks: BlockDescription[] = []; blocks: BlockDescription[] = [];
nextId = 1; nextId = 1;
@@ -154,15 +177,18 @@ export class QWebCompiler {
target = new CodeTarget("main"); target = new CodeTarget("main");
templateName: string; templateName: string;
template: string; template: string;
translateFn: (s: string) => string;
translatableAttributes: string[];
ast: AST; ast: AST;
staticCalls: { id: string; template: string }[] = []; staticCalls: { id: string; template: string }[] = [];
constructor(template: string, name?: string) { constructor(template: string, options: CompileOptions = {}) {
this.template = template; this.template = template;
this.translateFn = options.translateFn || ((s: string) => s);
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
this.ast = parse(template); this.ast = parse(template);
// console.warn(this.ast); if (options.name) {
if (name) { this.templateName = options.name;
this.templateName = name;
} else { } else {
if (template.length > 250) { if (template.length > 250) {
this.templateName = template.slice(0, 250) + "..."; this.templateName = template.slice(0, 250) + "...";
@@ -177,9 +203,14 @@ export class QWebCompiler {
this.isDebug = ast.type === ASTType.TDebug; this.isDebug = ast.type === ASTType.TDebug;
BlockDescription.nextBlockId = 1; BlockDescription.nextBlockId = 1;
BlockDescription.nextDataId = 1; BlockDescription.nextDataId = 1;
this.compileAST(ast, { block: null, index: 0, forceNewBlock: false, isLast: true }); this.compileAST(ast, {
block: null,
index: 0,
forceNewBlock: false,
isLast: true,
translate: true,
});
const code = this.generateCode(); const code = this.generateCode();
// console.warn(code);
return new Function("bdom, helpers", code) as TemplateFunction; return new Function("bdom, helpers", code) as TemplateFunction;
} }
@@ -282,13 +313,9 @@ export class QWebCompiler {
this.addLine(` let cache = ctx.cache || {};`); this.addLine(` let cache = ctx.cache || {};`);
this.addLine(` let nextCache = ctx.cache = {};`); this.addLine(` let nextCache = ctx.cache = {};`);
} }
// if (this.shouldDefineKey0) {
// this.addLine(` let key0;`);
// }
for (let line of mainCode) { for (let line of mainCode) {
this.addLine(line); this.addLine(line);
} }
// console.warn(this.target.code.join('\n'))
if (!this.target.hasRoot) { if (!this.target.hasRoot) {
throw new Error("missing root block"); throw new Error("missing root block");
} }
@@ -383,6 +410,9 @@ export class QWebCompiler {
case ASTType.TSlot: case ASTType.TSlot:
this.compileTSlot(ast, ctx); this.compileTSlot(ast, ctx);
break; break;
case ASTType.TTranslation:
this.compileTTranslation(ast, ctx);
break;
} }
} }
@@ -415,15 +445,22 @@ export class QWebCompiler {
compileText(ast: ASTText, ctx: Context) { compileText(ast: ASTText, ctx: Context) {
let { block, forceNewBlock } = ctx; let { block, forceNewBlock } = ctx;
let value = ast.value;
if (value && ctx.translate !== false) {
const match = translationRE.exec(value) as any;
value = match[1] + this.translateFn(match[2]) + match[3];
}
if (!block || forceNewBlock) { if (!block || forceNewBlock) {
block = this.createBlock(block, "text", ctx); block = this.createBlock(block, "text", ctx);
this.insertBlock(`text(\`${ast.value}\`)`, block, { this.insertBlock(`text(\`${value}\`)`, block, {
...ctx, ...ctx,
forceNewBlock: forceNewBlock && !block, forceNewBlock: forceNewBlock && !block,
}); });
} else { } else {
const createFn = ast.type === ASTType.Text ? xmlDoc.createTextNode : xmlDoc.createComment; const createFn = ast.type === ASTType.Text ? xmlDoc.createTextNode : xmlDoc.createComment;
block.insert(createFn.call(xmlDoc, ast.value)); block.insert(createFn.call(xmlDoc, value));
} }
} }
@@ -463,7 +500,6 @@ export class QWebCompiler {
block = this.createBlock(block, "block", ctx); block = this.createBlock(block, "block", ctx);
this.blocks.push(block); this.blocks.push(block);
} }
// attributes // attributes
const attrs: { [key: string]: string } = {}; const attrs: { [key: string]: string } = {};
for (let key in ast.attrs) { for (let key in ast.attrs) {
@@ -471,7 +507,6 @@ export class QWebCompiler {
let expr = interpolate(ast.attrs[key]); let expr = interpolate(ast.attrs[key]);
const idx = block!.insertData(expr); const idx = block!.insertData(expr);
attrs["block-attribute-" + idx] = key.slice(7); attrs["block-attribute-" + idx] = key.slice(7);
// console.warn('ccc', staticAttrs)
} else if (key.startsWith("t-att")) { } else if (key.startsWith("t-att")) {
let expr = compileExpr(ast.attrs[key]); let expr = compileExpr(ast.attrs[key]);
const idx = block!.insertData(expr); const idx = block!.insertData(expr);
@@ -480,6 +515,8 @@ export class QWebCompiler {
} else { } else {
attrs[`block-attribute-${idx}`] = key.slice(6); attrs[`block-attribute-${idx}`] = key.slice(6);
} }
} else if (this.translatableAttributes.includes(key)) {
attrs[key] = this.translateFn(ast.attrs[key]);
} else { } else {
attrs[key] = ast.attrs[key]; attrs[key] = ast.attrs[key];
} }
@@ -522,12 +559,12 @@ export class QWebCompiler {
const children = ast.content; const children = ast.content;
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = ast.content[i]; const child = ast.content[i];
const subCtx: Context = { const subCtx: Context = createContext(ctx, {
block: block, block,
index: block!.childNumber, index: block!.childNumber,
forceNewBlock: false, forceNewBlock: false,
isLast: ctx.isLast && i === children.length - 1, isLast: ctx.isLast && i === children.length - 1,
}; });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
} }
block!.currentDom = initialDom; block!.currentDom = initialDom;
@@ -582,7 +619,7 @@ export class QWebCompiler {
let expr = ast.expr === "0" ? "ctx[zero]" : compileExpr(ast.expr); let expr = ast.expr === "0" ? "ctx[zero]" : compileExpr(ast.expr);
if (ast.body) { if (ast.body) {
const nextId = BlockDescription.nextBlockId; const nextId = BlockDescription.nextBlockId;
const subCtx: Context = { block: null, index: 0, forceNewBlock: true }; const subCtx: Context = createContext(ctx);
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
expr = `withDefault(${expr}, b${nextId})`; expr = `withDefault(${expr}, b${nextId})`;
} }
@@ -603,7 +640,7 @@ export class QWebCompiler {
this.addLine(`if (${compileExpr(ast.condition)}) {`); this.addLine(`if (${compileExpr(ast.condition)}) {`);
this.target.indentLevel++; this.target.indentLevel++;
this.insertAnchor(block!); this.insertAnchor(block!);
const subCtx: Context = { block: block, index: currentIndex, forceNewBlock: true }; const subCtx: Context = createContext(ctx, { block, index: currentIndex });
this.compileAST(ast.content, subCtx); this.compileAST(ast.content, subCtx);
this.target.indentLevel--; this.target.indentLevel--;
if (ast.tElif) { if (ast.tElif) {
@@ -611,11 +648,7 @@ export class QWebCompiler {
this.addLine(`} else if (${compileExpr(clause.condition)}) {`); this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
this.target.indentLevel++; this.target.indentLevel++;
this.insertAnchor(block); this.insertAnchor(block);
const subCtx: Context = { const subCtx: Context = createContext(ctx, { block, index: currentIndex });
block: block,
index: currentIndex,
forceNewBlock: true,
};
this.compileAST(clause.content, subCtx); this.compileAST(clause.content, subCtx);
this.target.indentLevel--; this.target.indentLevel--;
} }
@@ -624,11 +657,7 @@ export class QWebCompiler {
this.addLine(`} else {`); this.addLine(`} else {`);
this.target.indentLevel++; this.target.indentLevel++;
this.insertAnchor(block); this.insertAnchor(block);
const subCtx: Context = { const subCtx: Context = createContext(ctx, { block, index: currentIndex });
block: block,
index: currentIndex,
forceNewBlock: true,
};
this.compileAST(ast.tElse, subCtx); this.compileAST(ast.tElse, subCtx);
this.target.indentLevel--; this.target.indentLevel--;
} }
@@ -708,11 +737,7 @@ export class QWebCompiler {
this.addLine("}"); this.addLine("}");
} }
const subCtx: Context = { const subCtx: Context = createContext(ctx, { block, index: loopVar });
block: block, //collectionBlock,
index: loopVar,
forceNewBlock: true,
};
this.compileAST(ast.body, subCtx); this.compileAST(ast.body, subCtx);
if (!ast.key) { if (!ast.key) {
console.warn( console.warn(
@@ -755,13 +780,13 @@ export class QWebCompiler {
for (let i = 0, l = ast.content.length; i < l; i++) { for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i]; const child = ast.content[i];
const isTSet = child.type === ASTType.TSet; const isTSet = child.type === ASTType.TSet;
const subCtx: Context = { const subCtx: Context = createContext(ctx, {
block: block, block,
index: index, index,
forceNewBlock: !isTSet, forceNewBlock: !isTSet,
preventRoot: ctx.preventRoot, preventRoot: ctx.preventRoot,
isLast: ctx.isLast && i === l - 1, isLast: ctx.isLast && i === l - 1,
}; });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
if (!isTSet) { if (!isTSet) {
index++; index++;
@@ -795,7 +820,7 @@ export class QWebCompiler {
if (ast.body) { if (ast.body) {
this.addLine(`ctx = Object.create(ctx);`); this.addLine(`ctx = Object.create(ctx);`);
const nextId = BlockDescription.nextBlockId; const nextId = BlockDescription.nextBlockId;
const subCtx: Context = { block: null, index: 0, forceNewBlock: true, preventRoot: true }; const subCtx: Context = createContext(ctx, { preventRoot: true });
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
if (nextId !== BlockDescription.nextBlockId) { if (nextId !== BlockDescription.nextBlockId) {
this.addLine(`ctx[zero] = b${nextId};`); this.addLine(`ctx[zero] = b${nextId};`);
@@ -820,7 +845,6 @@ export class QWebCompiler {
} else { } else {
const id = this.generateId(`callTemplate_`); const id = this.generateId(`callTemplate_`);
this.staticCalls.push({ id, template: subTemplate }); this.staticCalls.push({ id, template: subTemplate });
// console.warn('coucoup', this.target.hasRoot)
block = this.createBlock(block, "multi", ctx); block = this.createBlock(block, "multi", ctx);
this.insertBlock(`${id}(ctx, node, ${key})`, block!, { ...ctx, forceNewBlock: !block }); this.insertBlock(`${id}(ctx, node, ${key})`, block!, { ...ctx, forceNewBlock: !block });
} }
@@ -844,7 +868,7 @@ export class QWebCompiler {
this.shouldProtectScope = true; this.shouldProtectScope = true;
const expr = ast.value ? compileExpr(ast.value || "") : "null"; const expr = ast.value ? compileExpr(ast.value || "") : "null";
if (ast.body) { if (ast.body) {
const subCtx: Context = { block: null, index: 0, forceNewBlock: true }; const subCtx: Context = createContext(ctx);
const nextId = `b${BlockDescription.nextBlockId}`; const nextId = `b${BlockDescription.nextBlockId}`;
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
const value = ast.value ? (nextId ? `withDefault(${expr}, ${nextId})` : expr) : nextId; const value = ast.value ? (nextId ? `withDefault(${expr}, ${nextId})` : expr) : nextId;
@@ -914,7 +938,7 @@ export class QWebCompiler {
slot.signature = "ctx => (node, key) => {"; slot.signature = "ctx => (node, key) => {";
this.functions.push(slot); this.functions.push(slot);
this.target = slot; this.target = slot;
const subCtx: Context = { block: null, index: 0, forceNewBlock: true }; const subCtx: Context = createContext(ctx);
this.compileAST(ast.slots[slotName], subCtx); this.compileAST(ast.slots[slotName], subCtx);
if (this.hasRef) { if (this.hasRef) {
slot.signature = "ctx => node => {"; slot.signature = "ctx => node => {";
@@ -974,7 +998,7 @@ export class QWebCompiler {
slot.signature = "ctx => {"; slot.signature = "ctx => {";
this.functions.push(slot); this.functions.push(slot);
const initialTarget = this.target; const initialTarget = this.target;
const subCtx: Context = { block: null, index: 0, forceNewBlock: true }; const subCtx: Context = createContext(ctx);
this.target = slot; this.target = slot;
this.compileAST(ast.defaultContent, subCtx); this.compileAST(ast.defaultContent, subCtx);
this.target = initialTarget; this.target = initialTarget;
@@ -994,4 +1018,10 @@ export class QWebCompiler {
block = this.createBlock(block, "multi", ctx); block = this.createBlock(block, "multi", ctx);
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false }); this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
} }
compileTTranslation(ast: ASTTranslation, ctx: Context) {
if (ast.content) {
this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
}
}
} }
+21 -1
View File
@@ -19,6 +19,7 @@ export const enum ASTType {
TLog, TLog,
TSlot, TSlot,
TCallBlock, TCallBlock,
TTranslation,
} }
export interface ASTText { export interface ASTText {
@@ -131,6 +132,11 @@ export interface ASTLog {
content: AST | null; content: AST | null;
} }
export interface ASTTranslation {
type: ASTType.TTranslation;
content: AST | null;
}
export type AST = export type AST =
| ASTText | ASTText
| ASTComment | ASTComment
@@ -147,7 +153,8 @@ export type AST =
| ASTSlot | ASTSlot
| ASTTCallBlock | ASTTCallBlock
| ASTLog | ASTLog
| ASTDebug; | ASTDebug
| ASTTranslation;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Parser // Parser
@@ -179,6 +186,7 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
parseTCallBlock(node, ctx) || parseTCallBlock(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseTRawNode(node, ctx) || parseTRawNode(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
@@ -457,6 +465,7 @@ function hasNoComponent(ast: AST): boolean {
return hasNoComponent(ast.content); return hasNoComponent(ast.content);
case ASTType.TDebug: case ASTType.TDebug:
case ASTType.TLog: case ASTType.TLog:
case ASTType.TTranslation:
return ast.content ? hasNoComponent(ast.content) : true; return ast.content ? hasNoComponent(ast.content) : true;
case ASTType.TForEach: case ASTType.TForEach:
return ast.hasNoComponent; return ast.hasNoComponent;
@@ -711,6 +720,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
}; };
} }
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") {
return null;
}
node.removeAttribute("t-translation");
return {
type: ASTType.TTranslation,
content: parseNode(node, ctx),
};
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// helpers // helpers
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+8 -3
View File
@@ -1,7 +1,6 @@
// import { compileTemplate, Template } from "./qweb/index";
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom"; import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
import { compileTemplate, Template } from "./compiler";
import { component } from "../component/component_node"; import { component } from "../component/component_node";
import { Template, compileTemplate } from "./compiler";
const bdom = { text, createBlock, list, multi, html, toggler, component }; const bdom = { text, createBlock, list, multi, html, toggler, component };
@@ -82,6 +81,8 @@ export const UTILS = {
export class TemplateSet { export class TemplateSet {
rawTemplates: { [name: string]: string } = Object.create(globalTemplates); rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
utils: typeof UTILS; utils: typeof UTILS;
constructor() { constructor() {
@@ -107,7 +108,11 @@ export class TemplateSet {
if (rawTemplate === undefined) { if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`); throw new Error(`Missing template: "${name}"`);
} }
const templateFn = compileTemplate(rawTemplate, name); const templateFn = compileTemplate(rawTemplate, {
name,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
// first add a function to lazily get the template, in case there is a // first add a function to lazily get the template, in case there is a
// recursive call to the template name // recursive call to the template name
+22 -14
View File
@@ -1,23 +1,22 @@
import { import {
App,
Component,
onDestroyed, onDestroyed,
onWillPatch,
onWillUnmount,
onMounted, onMounted,
onPatched, onPatched,
onWillStart,
onWillUpdateProps,
useComponent,
status,
onRender, onRender,
Component, onWillPatch,
onWillStart,
onWillUnmount,
onWillUpdateProps,
status,
useComponent,
} from "../src"; } from "../src";
import { TemplateSet, globalTemplates, UTILS } from "../src/qweb/template_helpers";
import { blockDom } from "../src";
import { BDom } from "../src/blockdom"; import { BDom } from "../src/blockdom";
// import { mountBlock } from "../src/blockdom/block"; import { blockDom } from "../src";
import { compileTemplate, Template } from "../src/qweb/compiler"; import { CompileOptions, compileTemplate, Template } from "../src/qweb/compiler";
import { globalTemplates, TemplateSet, UTILS } from "../src/qweb/template_helpers";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
// import { UTILS } from "../src/template_utils";
const mount = blockDom.mount; const mount = blockDom.mount;
@@ -37,8 +36,17 @@ export function makeTestFixture() {
return fixture; return fixture;
} }
export function snapshotTemplateCode(template: string) { export function snapshotTemplateCode(template: string, options?: CompileOptions) {
expect(compileTemplate(template).toString()).toMatchSnapshot(); expect(compileTemplate(template, options).toString()).toMatchSnapshot();
}
export function snapshotApp(app: App) {
const Root = app.Root;
const template = app.rawTemplates[Root.template];
snapshotTemplateCode(template, {
translateFn: app.translateFn,
translatableAttributes: app.translatableAttributes,
});
} }
export async function nextTick(): Promise<void> { export async function nextTick(): Promise<void> {
@@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation support can translate node content 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div>mot</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support does not translate node content if disabled 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div><span>mot</span><span>word</span></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support some attributes are translated 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div><p label=\\"mot\\">mot</p><p title=\\"mot\\">mot</p><p placeholder=\\"mot\\">mot</p><p alt=\\"mot\\">mot</p><p something=\\"word\\">mot</p></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div> mot </div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support can set translatable attributes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"word\\">text</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
+43
View File
@@ -1126,4 +1126,47 @@ describe("qweb parser", () => {
name: "myBlock", name: "myBlock",
}); });
}); });
// ---------------------------------------------------------------------------
// t-translation
// ---------------------------------------------------------------------------
test('t-translation="off"', async () => {
expect(parse(`<t t-translation="off">word</t>`)).toEqual({
type: ASTType.TTranslation,
content: {
type: ASTType.Text,
value: "word",
},
});
expect(parse(`<div t-foreach="list" t-translation="off" t-as="item">word</div>`)).toEqual({
body: {
content: {
attrs: {},
content: [
{
type: 0,
value: "word",
},
],
on: {},
ref: null,
tag: "div",
type: 2,
},
type: 16,
},
collection: "list",
elem: "item",
hasNoComponent: true,
hasNoFirst: true,
hasNoIndex: true,
hasNoLast: true,
hasNoValue: true,
isOnlyChild: false,
key: null,
memo: "",
type: 9,
});
});
}); });
+116
View File
@@ -0,0 +1,116 @@
import { App, Component } from "../../src";
import { makeTestFixture, snapshotApp } from "../helpers";
import { xml } from "../../src/tags";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
describe("translation support", () => {
test("can translate node content", async () => {
class SomeComponent extends Component {
static template = xml`<div>word</div>`;
}
const app = new App(SomeComponent);
app.configure({
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
const comp = await app.mount(fixture);
const el = comp.el as HTMLElement;
expect(el.outerHTML).toBe("<div>mot</div>");
snapshotApp(app);
});
test("does not translate node content if disabled", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<span>word</span>
<span t-translation="off">word</span>
</div>
`;
}
const app = new App(SomeComponent);
app.configure({
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
const comp = await app.mount(fixture);
const el = comp.el as HTMLElement;
expect(el.outerHTML).toBe("<div><span>mot</span><span>word</span></div>");
snapshotApp(app);
});
test("some attributes are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<p label="word">word</p>
<p title="word">word</p>
<p placeholder="word">word</p>
<p alt="word">word</p>
<p something="word">word</p>
</div>
`;
}
const app = new App(SomeComponent);
app.configure({
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
const comp = await app.mount(fixture);
const el = comp.el as HTMLElement;
expect(el.outerHTML).toBe(
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
);
snapshotApp(app);
});
test("can set translatable attributes", async () => {
class SomeComponent extends Component {
static template = xml`
<div tomato="word" potato="word" title="word">text</div>
`;
}
const app = new App(SomeComponent);
app.configure({
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
translatableAttributes: ["potato"],
});
const comp = await app.mount(fixture);
const el = comp.el as HTMLElement;
expect(el.outerHTML).toBe('<div tomato="word" potato="mot" title="word">text</div>');
snapshotApp(app);
});
test("translation is done on the trimmed text, with extra spaces readded after", async () => {
class SomeComponent extends Component {
static template = xml`
<div> word </div>
`;
}
const translateFn = jest.fn((expr: string) => (expr === "word" ? "mot" : expr));
const app = new App(SomeComponent);
app.configure({ translateFn });
const comp = await app.mount(fixture);
const el = comp.el as HTMLElement;
expect(el.outerHTML).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
snapshotApp(app);
});
});