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("