From 2279dafbec5ce2bf098f2dd7e0409bedbd448193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 25 Oct 2019 16:03:26 +0200 Subject: [PATCH] [IMP] qweb: add an option to setup a translate function closes #393 --- doc/component.md | 2 +- doc/context.md | 2 +- doc/quick_start.md | 2 +- doc/qweb.md | 78 ++++++++++++++++++---- doc/utils.md | 4 +- src/context.ts | 18 ++--- src/index.ts | 4 +- src/qweb/qweb.ts | 32 +++++++-- tests/context.test.ts | 2 +- tests/qweb/__snapshots__/qweb.test.ts.snap | 67 +++++++++++++++++++ tests/qweb/qweb.test.ts | 55 ++++++++++++++- tools/benchmarks/owl-master/app.js | 2 +- tools/playground/app.js | 2 +- tools/playground/samples.js | 22 +++--- 14 files changed, 242 insertions(+), 50 deletions(-) diff --git a/doc/component.md b/doc/component.md index 4890a224..23fb4cca 100644 --- a/doc/component.md +++ b/doc/component.md @@ -441,7 +441,7 @@ of an Owl application has to be created manually: ```js class App extends owl.Component { ... } -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES }); const env = { qweb: qweb }; const app = new App(env); app.mount(document.body); diff --git a/doc/context.md b/doc/context.md index 09f723c5..624d5b01 100644 --- a/doc/context.md +++ b/doc/context.md @@ -29,7 +29,7 @@ context, and add it to the environment: ```js const deviceContext = new Context({ isMobile: true }); const env = { - qweb: new QWeb(TEMPLATES), + qweb: new QWeb({ templates: TEMPLATES }), deviceContext }; ``` diff --git a/doc/quick_start.md b/doc/quick_start.md index ed76eaa2..13123160 100644 --- a/doc/quick_start.md +++ b/doc/quick_start.md @@ -89,7 +89,7 @@ class ClickCounter extends owl.Component { async function start() { const templates = await owl.utils.loadFile("templates.xml"); const env = { - qweb: new owl.QWeb(templates) + qweb: new owl.QWeb({ templates }) }; const counter = new ClickCounter(env); const target = document.getElementById("main"); diff --git a/doc/qweb.md b/doc/qweb.md index 99ac5d85..11959a40 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -16,6 +16,7 @@ - [Dynamic Attributes](#dynamic-attributes) - [Loops](#loops) - [Rendering Sub Templates](#rendering-sub-templates) + - [Translations](#translations) - [Debugging](#debugging) ## Overview @@ -60,6 +61,7 @@ We present here a list of all standard QWeb directives: | `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) | | `t-call` | [Rendering sub templates](#rendering-sub-templates) | | `t-debug`, `t-log` | [Debugging](#debugging) | +| `t-translation` | [Disabling the translation of a node](#translations) | | `t-name` | [Defining a template (not really a directive)](#qweb-engine) | The component system in Owl requires additional directives, to express various @@ -87,11 +89,14 @@ const qweb = new owl.QWeb(); Its API is quite simple: -- **`constructor(data)`**: constructor. Takes an optional string to add initial - templates (see `addTemplates` for more information on format of the string). +- **`constructor(config)`**: constructor. Takes an optional configuration object + with an optional `templates` string to add initial + templates (see `addTemplates` for more information on format of the string) + and an optional `translateFn` translate function (see the section on + [translations](#translations)). ```js - const qweb = new owl.QWeb(TEMPLATES); + const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t }); ``` - **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template. @@ -201,7 +206,7 @@ root nodes. ### Expression Evaluation QWeb expressions are strings that will be processed at compile time. Each variable in -the javascript expression will be replaced by a lookup in the context (so, the +the javascript expression will be replaced with a lookup in the context (so, the component). For example, `a + b.c(d)` will be converted into: ```js @@ -234,14 +239,14 @@ It is useful to explain the various rules that apply on these expressions: 3. it can use a few special operators to avoid using symbols such as `<`, `>`, `&` or `|`. This is useful to make sure that we still write valid XML. - | Word | will be replaced by | - | ----- | ------------------- | - | `and` | `&&` | - | `or` | `\|\|` | - | `gt` | `>` | - | `gte` | `>=` | - | `lt` | `<` | - | `lte` | `<=` | + | Word | replaced with | + | ----- | ------------- | + | `and` | `&&` | + | `or` | `\|\|` | + | `gt` | `>` | + | `gte` | `>=` | + | `lt` | `<` | + | `lte` | `<=` | So, one can write this: @@ -443,7 +448,7 @@ is equivalent to the previous example. or an object (the current item will be the current key). In addition to the name passed via t-as, `t-foreach` provides a few other -variables for various data points (note: `$as` will be replaced by the name +variables for various data points (note: `$as` will be replaced with the name passed to `t-as`): - `$as_value`: the current iteration value, identical to `$as` for lists and @@ -522,6 +527,53 @@ will result in : ``` +### Translations + +If properly setup, Owl QWeb engine can translate all rendered templates. To do +so, it needs a translate function, which takes a string and returns a string. + +For example: + +```js +const translations = { + hello: "bonjour", + yes: "oui", + no: "non" +}; +const translateFn = str => translations[str] || str; + +const qweb = new QWeb({ translateFn }); +``` + +Once setup, all rendered templates will be translated using `translateFn`: + +- each text node will be replaced with its translation, +- each of the following attribute values will be translated as well: `title`, + `placeholder`, `label` and `alt`, +- translating text nodes can be disabled with the special attribute `t-translation`, + if its value is `off`. + +So, with the above `translateFn`, the following templates: + +```xml +
hello
+
hello
+
Are you sure?
+ +``` + +will be rendered as: + +```xml +
bonjour
+
hello
+
Are you sure?
+ +``` + +Note that the translation is done during the compilation of the template, not +when it is rendered. + ### Debugging The javascript QWeb implementation provides two useful debugging directives: diff --git a/doc/utils.md b/doc/utils.md index dbd0e9ba..c874df64 100644 --- a/doc/utils.md +++ b/doc/utils.md @@ -20,7 +20,7 @@ argument, it executes it as soon as the DOM ready (or directly). ```js Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) { - const qweb = new owl.QWeb(templates); + const qweb = new owl.QWeb({ templates }); const app = new App({ qweb }); app.mount(document.body); }); @@ -62,7 +62,7 @@ initial usecase for this function is to load a template file. For example: ```js async function makeEnv() { const templates = await owl.utils.loadFile("templates.xml"); - const qweb = new owl.QWeb(templates); + const qweb = new owl.QWeb({ templates }); return { qweb }; } ``` diff --git a/src/context.ts b/src/context.ts index 6e381c62..f15ff56a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -81,15 +81,15 @@ export function useContextWithCB(ctx: Context, component: Component, m __owl__.observer.notifyCB = component.render.bind(component); } const currentCB = __owl__.observer.notifyCB; - __owl__.observer.notifyCB = function () { - if (ctx.rev > mapping[id]) { - // in this case, the context has been updated since we were rendering - // last, and we do not need to render here with the observer. A - // rendering is coming anyway, with the correct props. - return; - } - currentCB(); - } + __owl__.observer.notifyCB = function() { + if (ctx.rev > mapping[id]) { + // in this case, the context has been updated since we were rendering + // last, and we do not need to render here with the observer. A + // rendering is coming anyway, with the correct props. + return; + } + currentCB(); + }; mapping[id] = 0; const renderFn = __owl__.renderFn; diff --git a/src/index.ts b/src/index.ts index 93722273..a1e3d515 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { QWeb } from "./qweb/index"; import * as _store from "./store"; import * as _utils from "./utils"; import * as _tags from "./tags"; -import {AsyncRoot} from "./misc/async_root"; +import { AsyncRoot } from "./misc/async_root"; import * as _hooks from "./hooks"; import * as _context from "./context"; import { Link } from "./router/link"; @@ -27,7 +27,7 @@ export const router = { Router, RouteComponent, Link }; export const Store = _store.Store; export const utils = _utils; export const tags = _tags; -export const misc = { AsyncRoot}; +export const misc = { AsyncRoot }; export const hooks = Object.assign({}, _hooks, { useContext: _context.useContext, useDispatch: _store.useDispatch, diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts index 584a3b80..f17536db 100644 --- a/src/qweb/qweb.ts +++ b/src/qweb/qweb.ts @@ -57,12 +57,19 @@ export interface Directive { finalize?(info: CompilationInfo): void; } +interface QWebConfig { + templates?: string; + translateFn?(text: string): string; +} + //------------------------------------------------------------------------------ // Const/global stuff/helpers //------------------------------------------------------------------------------ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"]; +const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; + const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; @@ -134,6 +141,7 @@ function parseXML(xml: string): Document { //------------------------------------------------------------------------------ // QWeb rendering engine //------------------------------------------------------------------------------ + export class QWeb extends EventBus { templates: { [name: string]: Template }; static utils = UTILS; @@ -143,7 +151,8 @@ export class QWeb extends EventBus { name: 1, att: 1, attf: 1, - key: 1 + key: 1, + translation: 1 }; static DIRECTIVES: Directive[] = []; @@ -166,12 +175,16 @@ export class QWeb extends EventBus { recursiveFns = {}; isUpdating: boolean = false; + translateFn?: QWebConfig["translateFn"]; - constructor(data?: string) { + constructor(config: QWebConfig = {}) { super(); this.templates = Object.create(QWeb.TEMPLATES); - if (data) { - this.addTemplates(data); + if (config.templates) { + this.addTemplates(config.templates); + } + if (config.translateFn) { + this.translateFn = config.translateFn; } } @@ -418,6 +431,11 @@ export class QWeb extends EventBus { } text = text.replace(whitespaceRE, " "); } + if (this.translateFn) { + if ((node.parentNode as any).getAttribute("t-translation") !== "off") { + text = this.translateFn(text); + } + } if (ctx.parentNode) { if (node.nodeType === 3) { ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); @@ -617,7 +635,11 @@ export class QWeb extends EventBus { for (let i = 0; i < attributes.length; i++) { let name = attributes[i].name; - const value = attributes[i].textContent!; + let value = attributes[i].textContent!; + + if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) { + value = this.translateFn(value); + } // regular attributes if (!name.startsWith("t-") && !(node).getAttribute("t-attf-" + name)) { diff --git a/tests/context.test.ts b/tests/context.test.ts index 124b9cbd..22e21e59 100644 --- a/tests/context.test.ts +++ b/tests/context.test.ts @@ -207,7 +207,7 @@ describe("Context", () => { expect(fixture.innerHTML).toBe("

1a

"); testContext.state.key = "y"; - testContext.state.y = {n: 2}; + testContext.state.y = { n: 2 }; delete testContext.state.x; await nextTick(); diff --git a/tests/qweb/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap index 9d3ac2b0..3e29adf2 100644 --- a/tests/qweb/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -2415,6 +2415,73 @@ exports[`t-set value priority 1`] = ` }" `; +exports[`translation support can translate node content 1`] = ` +"function anonymous(context,extra +) { + let sibling = null; + var h = this.h; + let c1 = [], p1 = {key:1}; + var vn1 = h('div', p1, c1); + c1.push({text: \`mot\`}); + return vn1; +}" +`; + +exports[`translation support does not translate node content if disabled 1`] = ` +"function anonymous(context,extra +) { + let sibling = null; + var h = this.h; + let c1 = [], p1 = {key:1}; + var vn1 = h('div', p1, c1); + let c2 = [], p2 = {key:2}; + var vn2 = h('span', p2, c2); + c1.push(vn2); + c2.push({text: \`mot\`}); + let c3 = [], p3 = {key:3}; + var vn3 = h('span', p3, c3); + c1.push(vn3); + c3.push({text: \`word\`}); + return vn1; +}" +`; + +exports[`translation support some attributes are translated 1`] = ` +"function anonymous(context,extra +) { + let sibling = null; + var h = this.h; + let c1 = [], p1 = {key:1}; + var vn1 = h('div', p1, c1); + var _2 = 'mot'; + let c3 = [], p3 = {key:3,attrs:{label: _2}}; + var vn3 = h('p', p3, c3); + c1.push(vn3); + c3.push({text: \`mot\`}); + var _4 = 'mot'; + let c5 = [], p5 = {key:5,attrs:{title: _4}}; + var vn5 = h('p', p5, c5); + c1.push(vn5); + c5.push({text: \`mot\`}); + var _6 = 'mot'; + let c7 = [], p7 = {key:7,attrs:{placeholder: _6}}; + var vn7 = h('p', p7, c7); + c1.push(vn7); + c7.push({text: \`mot\`}); + var _8 = 'mot'; + let c9 = [], p9 = {key:9,attrs:{alt: _8}}; + var vn9 = h('p', p9, c9); + c1.push(vn9); + c9.push({text: \`mot\`}); + var _10 = 'word'; + let c11 = [], p11 = {key:11,attrs:{something: _10}}; + var vn11 = h('p', p11, c11); + c1.push(vn11); + c11.push({text: \`mot\`}); + return vn1; +}" +`; + exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = ` "function anonymous(context,extra ) { diff --git a/tests/qweb/qweb.test.ts b/tests/qweb/qweb.test.ts index 6073f289..985e4b18 100644 --- a/tests/qweb/qweb.test.ts +++ b/tests/qweb/qweb.test.ts @@ -1305,12 +1305,12 @@ describe("t-ref", () => { describe("loading templates", () => { test("can initialize qweb with a string", () => { - const data = ` + const templates = `
jupiler
`; - const qweb = new QWeb(data); + const qweb = new QWeb({ templates }); expect(renderToString(qweb, "hey")).toBe("
jupiler
"); }); @@ -1519,3 +1519,54 @@ describe("properly support svg", () => { ); }); }); + +describe("translation support", () => { + test("can translate node content", () => { + const translations = { + word: "mot" + }; + const translateFn = expr => translations[expr] || expr; + const qweb = new QWeb({ translateFn }); + qweb.addTemplate("test", "
word
"); + expect(renderToString(qweb, "test")).toBe("
mot
"); + }); + + test("does not translate node content if disabled", () => { + const translations = { + word: "mot" + }; + const translateFn = expr => translations[expr] || expr; + const qweb = new QWeb({ translateFn }); + qweb.addTemplate( + "test", + ` +
+ word + word +
` + ); + expect(renderToString(qweb, "test")).toBe("
motword
"); + }); + + test("some attributes are translated", () => { + const translations = { + word: "mot" + }; + const translateFn = expr => translations[expr] || expr; + const qweb = new QWeb({ translateFn }); + qweb.addTemplate( + "test", + ` +
+

word

+

word

+

word

+

word

+

word

+
` + ); + expect(renderToString(qweb, "test")).toBe( + '

mot

mot

mot

mot

mot

' + ); + }); +}); diff --git a/tools/benchmarks/owl-master/app.js b/tools/benchmarks/owl-master/app.js index 2e2c76a2..d3f89de2 100644 --- a/tools/benchmarks/owl-master/app.js +++ b/tools/benchmarks/owl-master/app.js @@ -146,7 +146,7 @@ class App extends owl.Component { async function start() { const templates = await owl.utils.loadFile("templates.xml"); const env = { - qweb: new owl.QWeb(templates) + qweb: new owl.QWeb({ templates }) }; const app = new App(env); app.mount(document.body); diff --git a/tools/playground/app.js b/tools/playground/app.js index 07e9b22b..9beed851 100644 --- a/tools/playground/app.js +++ b/tools/playground/app.js @@ -435,7 +435,7 @@ async function start() { owl.utils.loadFile("templates.xml"), owl.utils.whenReady() ]); - const qweb = new owl.QWeb(templates); + const qweb = new owl.QWeb({ templates }); const app = new App({ qweb }); app.mount(document.body); } diff --git a/tools/playground/samples.js b/tools/playground/samples.js index a36ae5bf..3ada1bea 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -17,7 +17,7 @@ App.components = { Greeter }; // Application setup // Note that the xml templates are injected into the global TEMPLATES variable. -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({ qweb }); app.mount(document.body); `; @@ -70,7 +70,7 @@ class App extends Component { } App.components = { Counter }; -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({qweb}); app.mount(document.body); `; @@ -228,7 +228,7 @@ class App extends Component { } App.components = { DemoComponent }; -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({ qweb }); app.mount(document.body); `; @@ -298,7 +298,7 @@ class App extends owl.Component { } // Application setup -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({ qweb }); app.mount(document.body); `; @@ -350,7 +350,7 @@ const themeContext = new Context({ foreground: '#fff', }); const env = { - qweb: new owl.QWeb(TEMPLATES), + qweb: new owl.QWeb({ templates: TEMPLATES}), themeContext: themeContext, }; const app = new App(env); @@ -548,7 +548,7 @@ function makeEnv() { const state = loadState(); const store = new owl.Store({ state, actions }); store.on("update", null, () => saveState(store.state)); - const qweb = new owl.QWeb(TEMPLATES); + const qweb = new owl.QWeb({ templates: TEMPLATES}); return { qweb, store }; } @@ -1041,7 +1041,7 @@ function setupResponsivePlugin(env) { // Application Startup //------------------------------------------------------------------------------ const env = { - qweb: new owl.QWeb(TEMPLATES), + qweb: new owl.QWeb({ templates: TEMPLATES}), }; setupResponsivePlugin(env); @@ -1187,7 +1187,7 @@ class App extends Component { App.components = {Card, Counter}; // Application setup -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({ qweb }); app.mount(document.body);`; @@ -1303,7 +1303,7 @@ class App extends Component { } App.components = {SlowComponent, NotificationList}; -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App({ qweb }); app.mount(document.body); `; @@ -1373,7 +1373,7 @@ class Form extends Component { } // Application setup -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const form = new Form({ qweb }); form.mount(document.body); `; @@ -1540,7 +1540,7 @@ class App extends Component { } App.components = { WindowManager }; -const qweb = new owl.QWeb(TEMPLATES); +const qweb = new owl.QWeb({ templates: TEMPLATES}); const windows = [ { name: "Hello",