Skip to content

Commit b9aeca8

Browse files
committed
feat(i18n): prepend, append HTML content support
1 parent 2576139 commit b9aeca8

File tree

3 files changed

+188
-15
lines changed

3 files changed

+188
-15
lines changed

docs/user-docs/2. app-basics/20. internationalization.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ This is the most common use-case, as well as the default behavior.
344344
<span t="key"></span>
345345
```
346346
Given the above translation, and the view, Aurelia replaces the `textContent` of the `span` with "Hello World".
347+
The same result can also be achieved by explicitly using the `[text]` attribute like `<span t="[text]key"></span>`.
347348

348349
Note that the key expression can also be constructed in view-model and be bound to `t` using `t.bind` syntax.
349350

@@ -398,7 +399,8 @@ This will set the `innerHTML` of the element instead of the `textContent` proper
398399
##### `[append]` or `[prepend]` translations
399400

400401
So far we have seen that contents are replaced.
401-
There are 2 special attributes `[append]`, and `[prepend]` which can be used to append or prepend content to the existing content of the element.
402+
There are two special attributes `[append]`, and `[prepend]` which can be used to append or prepend content to the existing content of the element.
403+
These also support HTML content.
402404

403405
```json translation.json
404406
{

packages/__tests__/i18n/t/translation-integration.spec.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ describe('translation-integration', function () {
3232

3333
html: 'this is a <i>HTML</i> content',
3434
pre: 'tic ',
35+
preHtml: '<b>tic</b><span>foo</span> ',
3536
mid: 'tac',
3637
midHtml: '<i>tac</i>',
3738
post: ' toe',
39+
postHtml: ' <b>toe</b><span>bar</span>',
3840

3941
imgPath: 'foo.jpg'
4042
};
@@ -327,6 +329,30 @@ describe('translation-integration', function () {
327329

328330
assert.equal((host as Element).querySelector('span').innerHTML, 'tic <i>tac</i>');
329331
});
332+
it('works for html content for [prepend] + textContent', async function () {
333+
334+
@customElement({
335+
name: 'app', template: `<span t='[prepend]preHtml;[html]mid'></span>`
336+
})
337+
class App { }
338+
339+
const host = DOM.createElement('app');
340+
await setup(host, new App());
341+
342+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac');
343+
});
344+
it('works for html content for [prepend] + innerHtml', async function () {
345+
346+
@customElement({
347+
name: 'app', template: `<span t='[prepend]preHtml;[html]midHtml'></span>`
348+
})
349+
class App { }
350+
351+
const host = DOM.createElement('app');
352+
await setup(host, new App());
353+
354+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> <i>tac</i>');
355+
});
330356

331357
it('works for [append] only', async function () {
332358

@@ -364,6 +390,30 @@ describe('translation-integration', function () {
364390

365391
assert.equal((host as Element).querySelector('span').innerHTML, '<i>tac</i> toe');
366392
});
393+
it('works for html content for [append] + textContent', async function () {
394+
395+
@customElement({
396+
name: 'app', template: `<span t='[append]postHtml;[html]mid'></span>`
397+
})
398+
class App { }
399+
400+
const host = DOM.createElement('app');
401+
await setup(host, new App());
402+
403+
assert.equal((host as Element).querySelector('span').innerHTML, 'tac <b>toe</b><span>bar</span>');
404+
});
405+
it('works for html content for [append]', async function () {
406+
407+
@customElement({
408+
name: 'app', template: `<span t='[append]postHtml;[html]midHtml'></span>`
409+
})
410+
class App { }
411+
412+
const host = DOM.createElement('app');
413+
await setup(host, new App());
414+
415+
assert.equal((host as Element).querySelector('span').innerHTML, '<i>tac</i> <b>toe</b><span>bar</span>');
416+
});
367417

368418
it('works for [prepend] and [append]', async function () {
369419

@@ -401,6 +451,131 @@ describe('translation-integration', function () {
401451

402452
assert.equal((host as Element).querySelector('span').innerHTML, 'tic <i>tac</i> toe');
403453
});
454+
it('works for html resource for [prepend] and [append] + textContent', async function () {
455+
456+
@customElement({
457+
name: 'app', template: `<span t='[prepend]preHtml;[append]postHtml;mid'></span>`
458+
})
459+
class App { }
460+
461+
const host = DOM.createElement('app');
462+
await setup(host, new App());
463+
464+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
465+
});
466+
it('works for html resource for [prepend] and [append] + innerHtml', async function () {
467+
468+
@customElement({
469+
name: 'app', template: `<span t='[prepend]preHtml;[append]postHtml;[html]midHtml'></span>`
470+
})
471+
class App { }
472+
473+
const host = DOM.createElement('app');
474+
await setup(host, new App());
475+
476+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> <i>tac</i> <b>toe</b><span>bar</span>');
477+
});
478+
479+
it('works correctly with the change of both [prepend], and [append] - textContent', async function () {
480+
481+
@customElement({
482+
name: 'app', template: `<span t.bind='keyExpr'>tac</span>`
483+
})
484+
class App {
485+
public keyExpr: string = '[prepend]preHtml;[append]postHtml';
486+
}
487+
488+
const host = DOM.createElement('app');
489+
const app = new App();
490+
const { ctx } = await setup(host, app);
491+
492+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
493+
app.keyExpr = '[prepend]pre;[append]post';
494+
495+
ctx.lifecycle.processRAFQueue(LifecycleFlags.none);
496+
497+
assert.equal((host as Element).querySelector('span').innerHTML, 'tic tac toe');
498+
});
499+
it('works correctly with the change of both [prepend], and [append] - textContent', async function () {
500+
501+
@customElement({
502+
name: 'app', template: `<span t.bind='keyExpr'>tac</span>`
503+
})
504+
class App {
505+
public keyExpr: string = '[prepend]pre;[append]post';
506+
}
507+
508+
const host = DOM.createElement('app');
509+
const app = new App();
510+
const { ctx } = await setup(host, app);
511+
512+
assert.equal((host as Element).querySelector('span').innerHTML, 'tic tac toe');
513+
app.keyExpr = '[prepend]preHtml;[append]postHtml';
514+
515+
ctx.lifecycle.processRAFQueue(LifecycleFlags.none);
516+
517+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
518+
});
519+
it('works correctly with the removal of [append]', async function () {
520+
521+
@customElement({
522+
name: 'app', template: `<span t.bind='keyExpr'>tac</span>`
523+
})
524+
class App {
525+
public keyExpr: string = '[prepend]preHtml;[append]postHtml';
526+
}
527+
528+
const host = DOM.createElement('app');
529+
const app = new App();
530+
const { ctx } = await setup(host, app);
531+
532+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
533+
app.keyExpr = '[prepend]preHtml';
534+
535+
ctx.lifecycle.processRAFQueue(LifecycleFlags.none);
536+
537+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac');
538+
});
539+
it('works correctly with the removal of [prepend]', async function () {
540+
541+
@customElement({
542+
name: 'app', template: `<span t.bind='keyExpr'>tac</span>`
543+
})
544+
class App {
545+
public keyExpr: string = '[prepend]preHtml;[append]postHtml';
546+
}
547+
548+
const host = DOM.createElement('app');
549+
const app = new App();
550+
const { ctx } = await setup(host, app);
551+
552+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
553+
app.keyExpr = '[append]postHtml';
554+
555+
ctx.lifecycle.processRAFQueue(LifecycleFlags.none);
556+
557+
assert.equal((host as Element).querySelector('span').innerHTML, 'tac <b>toe</b><span>bar</span>');
558+
});
559+
it('works correctly with the removal of both [prepend] and [append]', async function () {
560+
561+
@customElement({
562+
name: 'app', template: `<span t.bind='keyExpr'>tac</span>`
563+
})
564+
class App {
565+
public keyExpr: string = '[prepend]preHtml;[append]postHtml';
566+
}
567+
568+
const host = DOM.createElement('app');
569+
const app = new App();
570+
const { ctx } = await setup(host, app);
571+
572+
assert.equal((host as Element).querySelector('span').innerHTML, '<b>tic</b><span>foo</span> tac <b>toe</b><span>bar</span>');
573+
app.keyExpr = '[html]midHtml';
574+
575+
ctx.lifecycle.processRAFQueue(LifecycleFlags.none);
576+
577+
assert.equal((host as Element).querySelector('span').innerHTML, '<i>tac</i>');
578+
});
404579
});
405580

406581
describe('updates translation', function () {

packages/i18n/src/t/translation-binding.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,30 +221,26 @@ export class TranslationBinding implements IPartialConnectableBinding {
221221
private prepareTemplate(content: ContentValue, marker: string, fallBackContents: ChildNode[]) {
222222
const template = DOM.createTemplate() as HTMLTemplateElement;
223223

224-
this.addTextContentToTemplate(template, content.prepend, marker);
224+
this.addContentToTemplate(template, content.prepend, marker);
225225

226226
// build content: prioritize [html], then textContent, and falls back to original content
227-
if (content.innerHTML) {
228-
const fragment = DOM.createDocumentFragment(content.innerHTML) as DocumentFragment;
229-
for (const child of toArray(fragment.childNodes)) {
230-
Reflect.set(child, marker, true);
231-
template.content.append(child);
232-
}
233-
} else if (!this.addTextContentToTemplate(template, content.textContent, marker)) {
227+
if (!this.addContentToTemplate(template, content.innerHTML || content.textContent, marker)) {
234228
for (const fallbackContent of fallBackContents) {
235229
template.content.append(fallbackContent);
236230
}
237231
}
238232

239-
this.addTextContentToTemplate(template, content.append, marker);
233+
this.addContentToTemplate(template, content.append, marker);
240234
return template;
241235
}
242236

243-
private addTextContentToTemplate(template: HTMLTemplateElement, additionalText: string | undefined, marker: string) {
244-
if (additionalText) {
245-
const addendum = DOM.createTextNode(additionalText) as Node;
246-
Reflect.set(addendum, marker, true);
247-
template.content.append(addendum);
237+
private addContentToTemplate(template: HTMLTemplateElement, content: string | undefined, marker: string) {
238+
if (content) {
239+
const addendum = DOM.createDocumentFragment(content) as Node;
240+
for (const child of toArray(addendum.childNodes)) {
241+
Reflect.set(child, marker, true);
242+
template.content.append(child);
243+
}
248244
return true;
249245
}
250246
return false;

0 commit comments

Comments
 (0)