[IMP] qweb: add an option to setup a translate function

closes #393
This commit is contained in:
Géry Debongnie
2019-10-25 16:03:26 +02:00
committed by Aaron Bohy
parent f07ec21a07
commit 2279dafbec
14 changed files with 242 additions and 50 deletions
+1 -1
View File
@@ -441,7 +441,7 @@ of an Owl application has to be created manually:
```js ```js
class App extends owl.Component { ... } class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES });
const env = { qweb: qweb }; const env = { qweb: qweb };
const app = new App(env); const app = new App(env);
app.mount(document.body); app.mount(document.body);
+1 -1
View File
@@ -29,7 +29,7 @@ context, and add it to the environment:
```js ```js
const deviceContext = new Context({ isMobile: true }); const deviceContext = new Context({ isMobile: true });
const env = { const env = {
qweb: new QWeb(TEMPLATES), qweb: new QWeb({ templates: TEMPLATES }),
deviceContext deviceContext
}; };
``` ```
+1 -1
View File
@@ -89,7 +89,7 @@ class ClickCounter extends owl.Component {
async function start() { async function start() {
const templates = await owl.utils.loadFile("templates.xml"); const templates = await owl.utils.loadFile("templates.xml");
const env = { const env = {
qweb: new owl.QWeb(templates) qweb: new owl.QWeb({ templates })
}; };
const counter = new ClickCounter(env); const counter = new ClickCounter(env);
const target = document.getElementById("main"); const target = document.getElementById("main");
+65 -13
View File
@@ -16,6 +16,7 @@
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging) - [Debugging](#debugging)
## Overview ## 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-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) | | `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) | | `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) | | `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various 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: Its API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial - **`constructor(config)`**: constructor. Takes an optional configuration object
templates (see `addTemplates` for more information on format of the string). 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 ```js
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
``` ```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template. - **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
@@ -201,7 +206,7 @@ root nodes.
### Expression Evaluation ### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in 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: component). For example, `a + b.c(d)` will be converted into:
```js ```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 `<`, `>`, 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. `&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by | | Word | replaced with |
| ----- | ------------------- | | ----- | ------------- |
| `and` | `&&` | | `and` | `&&` |
| `or` | `\|\|` | | `or` | `\|\|` |
| `gt` | `>` | | `gt` | `>` |
| `gte` | `>=` | | `gte` | `>=` |
| `lt` | `<` | | `lt` | `<` |
| `lte` | `<=` | | `lte` | `<=` |
So, one can write this: 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). 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 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`): passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and - `$as_value`: the current iteration value, identical to `$as` for lists and
@@ -522,6 +527,53 @@ will result in :
</div> </div>
``` ```
### 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
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
### Debugging ### Debugging
The javascript QWeb implementation provides two useful debugging directives: The javascript QWeb implementation provides two useful debugging directives:
+2 -2
View File
@@ -20,7 +20,7 @@ argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) { 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 }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
}); });
@@ -62,7 +62,7 @@ initial usecase for this function is to load a template file. For example:
```js ```js
async function makeEnv() { async function makeEnv() {
const templates = await owl.utils.loadFile("templates.xml"); const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb({ templates });
return { qweb }; return { qweb };
} }
``` ```
+9 -9
View File
@@ -81,15 +81,15 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB; const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () { __owl__.observer.notifyCB = function() {
if (ctx.rev > mapping[id]) { if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering // 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 // last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props. // rendering is coming anyway, with the correct props.
return; return;
} }
currentCB(); currentCB();
} };
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
+2 -2
View File
@@ -10,7 +10,7 @@ import { QWeb } from "./qweb/index";
import * as _store from "./store"; import * as _store from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags"; import * as _tags from "./tags";
import {AsyncRoot} from "./misc/async_root"; import { AsyncRoot } from "./misc/async_root";
import * as _hooks from "./hooks"; import * as _hooks from "./hooks";
import * as _context from "./context"; import * as _context from "./context";
import { Link } from "./router/link"; import { Link } from "./router/link";
@@ -27,7 +27,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store; export const Store = _store.Store;
export const utils = _utils; export const utils = _utils;
export const tags = _tags; export const tags = _tags;
export const misc = { AsyncRoot}; export const misc = { AsyncRoot };
export const hooks = Object.assign({}, _hooks, { export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext, useContext: _context.useContext,
useDispatch: _store.useDispatch, useDispatch: _store.useDispatch,
+27 -5
View File
@@ -57,12 +57,19 @@ export interface Directive {
finalize?(info: CompilationInfo): void; finalize?(info: CompilationInfo): void;
} }
interface QWebConfig {
templates?: string;
translateFn?(text: string): string;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"]; const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
@@ -134,6 +141,7 @@ function parseXML(xml: string): Document {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// QWeb rendering engine // QWeb rendering engine
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class QWeb extends EventBus { export class QWeb extends EventBus {
templates: { [name: string]: Template }; templates: { [name: string]: Template };
static utils = UTILS; static utils = UTILS;
@@ -143,7 +151,8 @@ export class QWeb extends EventBus {
name: 1, name: 1,
att: 1, att: 1,
attf: 1, attf: 1,
key: 1 key: 1,
translation: 1
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
@@ -166,12 +175,16 @@ export class QWeb extends EventBus {
recursiveFns = {}; recursiveFns = {};
isUpdating: boolean = false; isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
constructor(data?: string) { constructor(config: QWebConfig = {}) {
super(); super();
this.templates = Object.create(QWeb.TEMPLATES); this.templates = Object.create(QWeb.TEMPLATES);
if (data) { if (config.templates) {
this.addTemplates(data); this.addTemplates(config.templates);
}
if (config.translateFn) {
this.translateFn = config.translateFn;
} }
} }
@@ -418,6 +431,11 @@ export class QWeb extends EventBus {
} }
text = text.replace(whitespaceRE, " "); text = text.replace(whitespaceRE, " ");
} }
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
text = this.translateFn(text);
}
}
if (ctx.parentNode) { if (ctx.parentNode) {
if (node.nodeType === 3) { if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); 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++) { for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name; 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 // regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) { if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
+1 -1
View File
@@ -207,7 +207,7 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>"); expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
testContext.state.key = "y"; testContext.state.key = "y";
testContext.state.y = {n: 2}; testContext.state.y = { n: 2 };
delete testContext.state.x; delete testContext.state.x;
await nextTick(); await nextTick();
@@ -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`] = ` exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
+53 -2
View File
@@ -1305,12 +1305,12 @@ describe("t-ref", () => {
describe("loading templates", () => { describe("loading templates", () => {
test("can initialize qweb with a string", () => { test("can initialize qweb with a string", () => {
const data = ` const templates = `
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div> <div t-name="hey">jupiler</div>
</templates>`; </templates>`;
const qweb = new QWeb(data); const qweb = new QWeb({ templates });
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>"); expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
}); });
@@ -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", "<div>word</div>");
expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
});
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",
`
<div>
<span>word</span>
<span t-translation="off">word</span>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>mot</span><span>word</span></div>");
});
test("some attributes are translated", () => {
const translations = {
word: "mot"
};
const translateFn = expr => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate(
"test",
`
<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>`
);
expect(renderToString(qweb, "test")).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>'
);
});
});
+1 -1
View File
@@ -146,7 +146,7 @@ class App extends owl.Component {
async function start() { async function start() {
const templates = await owl.utils.loadFile("templates.xml"); const templates = await owl.utils.loadFile("templates.xml");
const env = { const env = {
qweb: new owl.QWeb(templates) qweb: new owl.QWeb({ templates })
}; };
const app = new App(env); const app = new App(env);
app.mount(document.body); app.mount(document.body);
+1 -1
View File
@@ -435,7 +435,7 @@ async function start() {
owl.utils.loadFile("templates.xml"), owl.utils.loadFile("templates.xml"),
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb({ templates });
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
} }
+11 -11
View File
@@ -17,7 +17,7 @@ App.components = { Greeter };
// Application setup // Application setup
// Note that the xml templates are injected into the global TEMPLATES variable. // 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 }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -70,7 +70,7 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({qweb}); const app = new App({qweb});
app.mount(document.body); app.mount(document.body);
`; `;
@@ -228,7 +228,7 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -298,7 +298,7 @@ class App extends owl.Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -350,7 +350,7 @@ const themeContext = new Context({
foreground: '#fff', foreground: '#fff',
}); });
const env = { const env = {
qweb: new owl.QWeb(TEMPLATES), qweb: new owl.QWeb({ templates: TEMPLATES}),
themeContext: themeContext, themeContext: themeContext,
}; };
const app = new App(env); const app = new App(env);
@@ -548,7 +548,7 @@ function makeEnv() {
const state = loadState(); const state = loadState();
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state)); store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
return { qweb, store }; return { qweb, store };
} }
@@ -1041,7 +1041,7 @@ function setupResponsivePlugin(env) {
// Application Startup // Application Startup
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const env = { const env = {
qweb: new owl.QWeb(TEMPLATES), qweb: new owl.QWeb({ templates: TEMPLATES}),
}; };
setupResponsivePlugin(env); setupResponsivePlugin(env);
@@ -1187,7 +1187,7 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body);`; app.mount(document.body);`;
@@ -1303,7 +1303,7 @@ class App extends Component {
} }
App.components = {SlowComponent, NotificationList}; App.components = {SlowComponent, NotificationList};
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1373,7 +1373,7 @@ class Form extends Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const form = new Form({ qweb }); const form = new Form({ qweb });
form.mount(document.body); form.mount(document.body);
`; `;
@@ -1540,7 +1540,7 @@ class App extends Component {
} }
App.components = { WindowManager }; App.components = { WindowManager };
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb({ templates: TEMPLATES});
const windows = [ const windows = [
{ {
name: "Hello", name: "Hello",