mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[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:
committed by
Aaron Bohy
parent
4415cc8932
commit
0a544bd7e8
@@ -23,6 +23,12 @@ export class App<T extends typeof Component = any> extends TemplateSet {
|
||||
if (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>> {
|
||||
|
||||
+76
-46
@@ -18,6 +18,7 @@ import {
|
||||
ASTTKey,
|
||||
ASTTRaw,
|
||||
ASTTSet,
|
||||
ASTTranslation,
|
||||
ASTType,
|
||||
parse,
|
||||
} from "./parser";
|
||||
@@ -27,8 +28,14 @@ export type TemplateFunction = (blocks: any, utils: any) => Template;
|
||||
|
||||
type BlockType = "block" | "text" | "multi" | "list" | "html";
|
||||
|
||||
export function compileTemplate(template: string, name?: string): TemplateFunction {
|
||||
const compiler = new QWebCompiler(template, name);
|
||||
export interface CompileOptions {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ class BlockDescription {
|
||||
asXmlString() {
|
||||
// Can't use outerHTML on text/comment nodes
|
||||
// append dom to any element and use innerHTML instead
|
||||
const t = xmlDoc.createElement('t');
|
||||
const t = xmlDoc.createElement("t");
|
||||
t.appendChild(this.dom!);
|
||||
return t.innerHTML;
|
||||
}
|
||||
@@ -114,6 +121,19 @@ interface Context {
|
||||
forceNewBlock: boolean;
|
||||
preventRoot?: 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 {
|
||||
@@ -139,6 +159,9 @@ class CodeTarget {
|
||||
}
|
||||
}
|
||||
|
||||
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
|
||||
export class QWebCompiler {
|
||||
blocks: BlockDescription[] = [];
|
||||
nextId = 1;
|
||||
@@ -154,15 +177,18 @@ export class QWebCompiler {
|
||||
target = new CodeTarget("main");
|
||||
templateName: string;
|
||||
template: string;
|
||||
translateFn: (s: string) => string;
|
||||
translatableAttributes: string[];
|
||||
ast: AST;
|
||||
staticCalls: { id: string; template: string }[] = [];
|
||||
|
||||
constructor(template: string, name?: string) {
|
||||
constructor(template: string, options: CompileOptions = {}) {
|
||||
this.template = template;
|
||||
this.translateFn = options.translateFn || ((s: string) => s);
|
||||
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
|
||||
this.ast = parse(template);
|
||||
// console.warn(this.ast);
|
||||
if (name) {
|
||||
this.templateName = name;
|
||||
if (options.name) {
|
||||
this.templateName = options.name;
|
||||
} else {
|
||||
if (template.length > 250) {
|
||||
this.templateName = template.slice(0, 250) + "...";
|
||||
@@ -177,9 +203,14 @@ export class QWebCompiler {
|
||||
this.isDebug = ast.type === ASTType.TDebug;
|
||||
BlockDescription.nextBlockId = 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();
|
||||
// console.warn(code);
|
||||
return new Function("bdom, helpers", code) as TemplateFunction;
|
||||
}
|
||||
|
||||
@@ -282,13 +313,9 @@ export class QWebCompiler {
|
||||
this.addLine(` let cache = ctx.cache || {};`);
|
||||
this.addLine(` let nextCache = ctx.cache = {};`);
|
||||
}
|
||||
// if (this.shouldDefineKey0) {
|
||||
// this.addLine(` let key0;`);
|
||||
// }
|
||||
for (let line of mainCode) {
|
||||
this.addLine(line);
|
||||
}
|
||||
// console.warn(this.target.code.join('\n'))
|
||||
if (!this.target.hasRoot) {
|
||||
throw new Error("missing root block");
|
||||
}
|
||||
@@ -383,6 +410,9 @@ export class QWebCompiler {
|
||||
case ASTType.TSlot:
|
||||
this.compileTSlot(ast, ctx);
|
||||
break;
|
||||
case ASTType.TTranslation:
|
||||
this.compileTTranslation(ast, ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,15 +445,22 @@ export class QWebCompiler {
|
||||
|
||||
compileText(ast: ASTText, ctx: Context) {
|
||||
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) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${ast.value}\`)`, block, {
|
||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
} else {
|
||||
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);
|
||||
this.blocks.push(block);
|
||||
}
|
||||
|
||||
// attributes
|
||||
const attrs: { [key: string]: string } = {};
|
||||
for (let key in ast.attrs) {
|
||||
@@ -471,7 +507,6 @@ export class QWebCompiler {
|
||||
let expr = interpolate(ast.attrs[key]);
|
||||
const idx = block!.insertData(expr);
|
||||
attrs["block-attribute-" + idx] = key.slice(7);
|
||||
// console.warn('ccc', staticAttrs)
|
||||
} else if (key.startsWith("t-att")) {
|
||||
let expr = compileExpr(ast.attrs[key]);
|
||||
const idx = block!.insertData(expr);
|
||||
@@ -480,6 +515,8 @@ export class QWebCompiler {
|
||||
} else {
|
||||
attrs[`block-attribute-${idx}`] = key.slice(6);
|
||||
}
|
||||
} else if (this.translatableAttributes.includes(key)) {
|
||||
attrs[key] = this.translateFn(ast.attrs[key]);
|
||||
} else {
|
||||
attrs[key] = ast.attrs[key];
|
||||
}
|
||||
@@ -522,12 +559,12 @@ export class QWebCompiler {
|
||||
const children = ast.content;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = ast.content[i];
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
const subCtx: Context = createContext(ctx, {
|
||||
block,
|
||||
index: block!.childNumber,
|
||||
forceNewBlock: false,
|
||||
isLast: ctx.isLast && i === children.length - 1,
|
||||
};
|
||||
});
|
||||
this.compileAST(child, subCtx);
|
||||
}
|
||||
block!.currentDom = initialDom;
|
||||
@@ -582,7 +619,7 @@ export class QWebCompiler {
|
||||
let expr = ast.expr === "0" ? "ctx[zero]" : compileExpr(ast.expr);
|
||||
if (ast.body) {
|
||||
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);
|
||||
expr = `withDefault(${expr}, b${nextId})`;
|
||||
}
|
||||
@@ -603,7 +640,7 @@ export class QWebCompiler {
|
||||
this.addLine(`if (${compileExpr(ast.condition)}) {`);
|
||||
this.target.indentLevel++;
|
||||
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.target.indentLevel--;
|
||||
if (ast.tElif) {
|
||||
@@ -611,11 +648,7 @@ export class QWebCompiler {
|
||||
this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
|
||||
this.target.indentLevel++;
|
||||
this.insertAnchor(block);
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: currentIndex,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
const subCtx: Context = createContext(ctx, { block, index: currentIndex });
|
||||
this.compileAST(clause.content, subCtx);
|
||||
this.target.indentLevel--;
|
||||
}
|
||||
@@ -624,11 +657,7 @@ export class QWebCompiler {
|
||||
this.addLine(`} else {`);
|
||||
this.target.indentLevel++;
|
||||
this.insertAnchor(block);
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: currentIndex,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
const subCtx: Context = createContext(ctx, { block, index: currentIndex });
|
||||
this.compileAST(ast.tElse, subCtx);
|
||||
this.target.indentLevel--;
|
||||
}
|
||||
@@ -708,11 +737,7 @@ export class QWebCompiler {
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
const subCtx: Context = {
|
||||
block: block, //collectionBlock,
|
||||
index: loopVar,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
const subCtx: Context = createContext(ctx, { block, index: loopVar });
|
||||
this.compileAST(ast.body, subCtx);
|
||||
if (!ast.key) {
|
||||
console.warn(
|
||||
@@ -755,13 +780,13 @@ export class QWebCompiler {
|
||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||
const child = ast.content[i];
|
||||
const isTSet = child.type === ASTType.TSet;
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: index,
|
||||
const subCtx: Context = createContext(ctx, {
|
||||
block,
|
||||
index,
|
||||
forceNewBlock: !isTSet,
|
||||
preventRoot: ctx.preventRoot,
|
||||
isLast: ctx.isLast && i === l - 1,
|
||||
};
|
||||
});
|
||||
this.compileAST(child, subCtx);
|
||||
if (!isTSet) {
|
||||
index++;
|
||||
@@ -795,7 +820,7 @@ export class QWebCompiler {
|
||||
if (ast.body) {
|
||||
this.addLine(`ctx = Object.create(ctx);`);
|
||||
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);
|
||||
if (nextId !== BlockDescription.nextBlockId) {
|
||||
this.addLine(`ctx[zero] = b${nextId};`);
|
||||
@@ -820,7 +845,6 @@ export class QWebCompiler {
|
||||
} else {
|
||||
const id = this.generateId(`callTemplate_`);
|
||||
this.staticCalls.push({ id, template: subTemplate });
|
||||
// console.warn('coucoup', this.target.hasRoot)
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(`${id}(ctx, node, ${key})`, block!, { ...ctx, forceNewBlock: !block });
|
||||
}
|
||||
@@ -844,7 +868,7 @@ export class QWebCompiler {
|
||||
this.shouldProtectScope = true;
|
||||
const expr = ast.value ? compileExpr(ast.value || "") : "null";
|
||||
if (ast.body) {
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
const subCtx: Context = createContext(ctx);
|
||||
const nextId = `b${BlockDescription.nextBlockId}`;
|
||||
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||
const value = ast.value ? (nextId ? `withDefault(${expr}, ${nextId})` : expr) : nextId;
|
||||
@@ -914,7 +938,7 @@ export class QWebCompiler {
|
||||
slot.signature = "ctx => (node, key) => {";
|
||||
this.functions.push(slot);
|
||||
this.target = slot;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
const subCtx: Context = createContext(ctx);
|
||||
this.compileAST(ast.slots[slotName], subCtx);
|
||||
if (this.hasRef) {
|
||||
slot.signature = "ctx => node => {";
|
||||
@@ -974,7 +998,7 @@ export class QWebCompiler {
|
||||
slot.signature = "ctx => {";
|
||||
this.functions.push(slot);
|
||||
const initialTarget = this.target;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
const subCtx: Context = createContext(ctx);
|
||||
this.target = slot;
|
||||
this.compileAST(ast.defaultContent, subCtx);
|
||||
this.target = initialTarget;
|
||||
@@ -994,4 +1018,10 @@ export class QWebCompiler {
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
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
@@ -19,6 +19,7 @@ export const enum ASTType {
|
||||
TLog,
|
||||
TSlot,
|
||||
TCallBlock,
|
||||
TTranslation,
|
||||
}
|
||||
|
||||
export interface ASTText {
|
||||
@@ -131,6 +132,11 @@ export interface ASTLog {
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTranslation {
|
||||
type: ASTType.TTranslation;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export type AST =
|
||||
| ASTText
|
||||
| ASTComment
|
||||
@@ -147,7 +153,8 @@ export type AST =
|
||||
| ASTSlot
|
||||
| ASTTCallBlock
|
||||
| ASTLog
|
||||
| ASTDebug;
|
||||
| ASTDebug
|
||||
| ASTTranslation;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Parser
|
||||
@@ -179,6 +186,7 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTRawNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
@@ -457,6 +465,7 @@ function hasNoComponent(ast: AST): boolean {
|
||||
return hasNoComponent(ast.content);
|
||||
case ASTType.TDebug:
|
||||
case ASTType.TLog:
|
||||
case ASTType.TTranslation:
|
||||
return ast.content ? hasNoComponent(ast.content) : true;
|
||||
case ASTType.TForEach:
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// import { compileTemplate, Template } from "./qweb/index";
|
||||
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||
import { compileTemplate, Template } from "./compiler";
|
||||
import { component } from "../component/component_node";
|
||||
import { Template, compileTemplate } from "./compiler";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||
|
||||
@@ -82,6 +81,8 @@ export const UTILS = {
|
||||
export class TemplateSet {
|
||||
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
utils: typeof UTILS;
|
||||
|
||||
constructor() {
|
||||
@@ -107,7 +108,11 @@ export class TemplateSet {
|
||||
if (rawTemplate === undefined) {
|
||||
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
|
||||
// recursive call to the template name
|
||||
|
||||
+22
-14
@@ -1,23 +1,22 @@
|
||||
import {
|
||||
App,
|
||||
Component,
|
||||
onDestroyed,
|
||||
onWillPatch,
|
||||
onWillUnmount,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onWillStart,
|
||||
onWillUpdateProps,
|
||||
useComponent,
|
||||
status,
|
||||
onRender,
|
||||
Component,
|
||||
onWillPatch,
|
||||
onWillStart,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
status,
|
||||
useComponent,
|
||||
} from "../src";
|
||||
import { TemplateSet, globalTemplates, UTILS } from "../src/qweb/template_helpers";
|
||||
import { blockDom } from "../src";
|
||||
import { BDom } from "../src/blockdom";
|
||||
// import { mountBlock } from "../src/blockdom/block";
|
||||
import { compileTemplate, Template } from "../src/qweb/compiler";
|
||||
import { blockDom } from "../src";
|
||||
import { CompileOptions, compileTemplate, Template } from "../src/qweb/compiler";
|
||||
import { globalTemplates, TemplateSet, UTILS } from "../src/qweb/template_helpers";
|
||||
import { xml } from "../src/tags";
|
||||
// import { UTILS } from "../src/template_utils";
|
||||
|
||||
const mount = blockDom.mount;
|
||||
|
||||
@@ -37,8 +36,17 @@ export function makeTestFixture() {
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function snapshotTemplateCode(template: string) {
|
||||
expect(compileTemplate(template).toString()).toMatchSnapshot();
|
||||
export function snapshotTemplateCode(template: string, options?: CompileOptions) {
|
||||
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> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1126,4 +1126,47 @@ describe("qweb parser", () => {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user