[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
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);
+1 -1
View File
@@ -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
};
```
+1 -1
View File
@@ -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");
+65 -13
View File
@@ -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 :
</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
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
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 };
}
```
+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);
}
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;
+2 -2
View File
@@ -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,
+27 -5
View File
@@ -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-") && !(<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>");
testContext.state.key = "y";
testContext.state.y = {n: 2};
testContext.state.y = { n: 2 };
delete testContext.state.x;
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`] = `
"function anonymous(context,extra
) {
+53 -2
View File
@@ -1305,12 +1305,12 @@ describe("t-ref", () => {
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const data = `
const templates = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const qweb = new QWeb(data);
const qweb = new QWeb({ templates });
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() {
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);
+1 -1
View File
@@ -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);
}
+11 -11
View File
@@ -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",