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) {
|
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
@@ -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
@@ -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
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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