Skip to content

Commit e5f419f

Browse files
jebnerMicha Reiser
jebner
authored and
Micha Reiser
committed
Add Angular I18n Translation Extractor (#39)
* add support for angular i18n - new extractor for extracting translations and ids from i18n and i18n-* attributes
1 parent f75bad2 commit e5f419f

File tree

7 files changed

+553
-6
lines changed

7 files changed

+553
-6
lines changed

README.MD

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,73 @@ The loaders accepts the name of a loader that should be applied in advance. E.g.
7474
}
7575
```
7676

77+
### Custom HTML Translation extractors
78+
79+
The htmlLoader supports registering custom HTML text extractors. The API of an extractor is:
80+
81+
```
82+
export interface HtmlTranslationExtractor {
83+
(element: AngularElement, context: HtmlTranslationExtractionContext): void;
84+
}
85+
86+
export interface AngularElement {
87+
tagName: string;
88+
attributes: Attribute[];
89+
texts: Text[];
90+
startPosition: number;
91+
}
92+
93+
export interface HtmlTranslationExtractionContext {
94+
emitError(message: string, position: number): void;
95+
emitSuppressableError(message: string, position: number): void;
96+
registerTranslation(translation: {
97+
translationId: string;
98+
defaultText?: string;
99+
position: number;
100+
}): void;
101+
102+
asHtml(): void;
103+
}
104+
```
105+
106+
The extractor receives an angular element with all its attributes and its direct text siblings as well as a context that can be used to either register a new translation or emit a warning/error.
107+
The [translate-directive-translation-extractor.ts](src/html/translate-directive-translation-extractor.ts) contains an implementation of an extractor.
108+
Custom extractors can be specified with the html loader:
109+
110+
```js
111+
{
112+
loader: WebPackAngularTranslate.htmlLoader({
113+
translationExtractors: [(element, context) => { ... }]
114+
})
115+
}
116+
```
117+
118+
#### AngularI18nTranslationsExtractor
119+
120+
WebpackAngularTranslates provides the `angularI18nTranslationsExtractor` to support extractions of translations in applications using [angular](https://angular.io/).
121+
It extracts translations from the `i18n` and `i18n-[attr]` directives, used by [Angular for Internationalization](https://angular.io/guide/i18n).
122+
123+
```js
124+
{
125+
use: [{
126+
loader: WebPackAngularTranslate.htmlLoader(),
127+
options: {
128+
translationExtractors: [WebPackAngularTranslate.angularI18nTranslationsExtractor]
129+
}
130+
}]
131+
}
132+
```
133+
134+
Examples:
135+
136+
`<h1 i18n="@@A title">A title</h1>` results in a translation with `{id: "A title", defaultTranslation: "A title"}`
137+
138+
`<p i18n="@@loan-intro-description-text">This is a very long text for the loan intro!</p>` results in a translation with `{id: "loan-intro-description-text", defaultTranslation: "This is a very long text for the loan intro!"}`
139+
140+
`<img src=... title="My image title" i18n-title="@@MyImage" />` results in a translation with `{id: "MyImage", defaultTranslation: "My image title"}`
141+
142+
Note: The extraction will only work for labels with an explicitly provided `@@id` and default translation.
143+
77144
## Supported Expressions
78145

79146
### Directive
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
AngularElement,
3+
HtmlTranslationExtractionContext
4+
} from "./html-translation-extractor";
5+
import { Attribute } from "./element-context";
6+
7+
const I18N_ATTRIBUTE_REGEX = /^i18n-.*$/;
8+
const I18N_ATTRIBUTE_NAME = "i18n";
9+
const ID_INDICATOR = "@@";
10+
11+
/**
12+
* Angular uses i18n and i18n-[attr] attributes for internationalization.
13+
* The angularI18nTranslationsExtractor looks for these attributes on elements
14+
* and extracts the found ids and default translations from the elements.
15+
*
16+
* @example
17+
* <div i18n="@@translationId">some translation</div>
18+
* results in a translation with id: 'translationId' and default translation: 'some translation'
19+
*
20+
* @example
21+
* <div i18n-title="@@titleId" title="some title"></div>
22+
* results in a translation with id: 'titleId' and default translation: 'some title'
23+
*
24+
* @param element the element to check for translations
25+
* @param context the current context
26+
*/
27+
export default function angularI18nTranslationsExtractor(
28+
element: AngularElement,
29+
context: HtmlTranslationExtractionContext
30+
): void {
31+
const i18nElementTranslation = element.attributes.find(
32+
attribute => attribute.name === I18N_ATTRIBUTE_NAME
33+
);
34+
35+
if (i18nElementTranslation) {
36+
handleTranslationsOfElements(element, context, i18nElementTranslation);
37+
}
38+
39+
const i18nAttributeTranslations = element.attributes.filter(attribute =>
40+
I18N_ATTRIBUTE_REGEX.test(attribute.name)
41+
);
42+
43+
handleTranslationsOfAttributes(element, context, i18nAttributeTranslations);
44+
}
45+
46+
function handleTranslationsOfElements(
47+
element: AngularElement,
48+
context: HtmlTranslationExtractionContext,
49+
attribute: Attribute
50+
): void {
51+
const translationIdExtraction = extractTranslationId(attribute, context);
52+
53+
if (translationIdExtraction.valid === false) {
54+
context.emitError(translationIdExtraction.error, attribute.startPosition);
55+
} else if (element.texts.length === 0) {
56+
context.emitError(
57+
`The element ${context.asHtml()} with attribute ${
58+
attribute.name
59+
} is empty and is therefore missing the default translation.`,
60+
attribute.startPosition
61+
);
62+
} else if (element.texts.length === 1) {
63+
context.registerTranslation({
64+
translationId: translationIdExtraction.translationId,
65+
defaultText: element.texts[0].text,
66+
position: element.startPosition
67+
});
68+
} else if (element.texts.length > 1) {
69+
context.emitError(
70+
`The element ${context.asHtml()} has multiple child elements and, therefore, the default translation cannot be extracted.`,
71+
attribute.startPosition
72+
);
73+
}
74+
}
75+
76+
function handleTranslationsOfAttributes(
77+
element: AngularElement,
78+
context: HtmlTranslationExtractionContext,
79+
i18nAttributes: Attribute[]
80+
): void {
81+
for (const i18nAttribute of i18nAttributes) {
82+
handleAttribute(element, context, i18nAttribute);
83+
}
84+
}
85+
86+
function handleAttribute(
87+
element: AngularElement,
88+
context: HtmlTranslationExtractionContext,
89+
i18nAttribute: Attribute
90+
): void {
91+
const translationIdExtraction = extractTranslationId(i18nAttribute, context);
92+
if (translationIdExtraction.valid === false) {
93+
context.emitError(
94+
translationIdExtraction.error,
95+
i18nAttribute.startPosition
96+
);
97+
return;
98+
}
99+
100+
const attributeName = i18nAttribute.name.substr(
101+
`${I18N_ATTRIBUTE_NAME}-`.length
102+
);
103+
const attribute = element.attributes.find(
104+
attribute => attribute.name === attributeName
105+
);
106+
107+
if (!attribute) {
108+
context.emitError(
109+
`The element ${context.asHtml()} with ${
110+
i18nAttribute.name
111+
} is missing a corresponding ${attributeName} attribute.`,
112+
element.startPosition
113+
);
114+
return;
115+
}
116+
117+
const defaultText = attribute.value;
118+
119+
if (!defaultText) {
120+
context.emitError(
121+
`The element ${context.asHtml()} with ${
122+
i18nAttribute.name
123+
} is missing a value for the corresponding ${attributeName} attribute.`,
124+
element.startPosition
125+
);
126+
return;
127+
}
128+
129+
context.registerTranslation({
130+
translationId: translationIdExtraction.translationId,
131+
defaultText: defaultText,
132+
position: i18nAttribute.startPosition
133+
});
134+
}
135+
136+
function extractTranslationId(
137+
attribute: Attribute,
138+
context: HtmlTranslationExtractionContext
139+
): { valid: true; translationId: string } | { valid: false; error: string } {
140+
const index = attribute.value.indexOf(ID_INDICATOR);
141+
if (index < 0) {
142+
return {
143+
valid: false,
144+
error: `The attribute ${
145+
attribute.name
146+
} on element ${context.asHtml()} attribute is missing the custom id indicator '${ID_INDICATOR}'.`
147+
};
148+
} else if (index + ID_INDICATOR.length === attribute.value.length) {
149+
return {
150+
valid: false,
151+
error: `The attribute ${
152+
attribute.name
153+
} on element ${context.asHtml()} defines an empty ID.`
154+
};
155+
} else {
156+
return {
157+
valid: true,
158+
translationId: attribute.value.substr(index + ID_INDICATOR.length)
159+
};
160+
}
161+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export { default as Plugin } from "./angular-translate-plugin";
2+
export {
3+
default as angularI18nTranslationsExtractor
4+
} from "./html/angular-i18n-translations-extractor";
25

36
export function htmlLoader(before: string, options: any): string {
47
const loader = require.resolve("./html/html-loader");

test/cases/translate-and-i18n.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title></title>
6+
</head>
7+
<body>
8+
<span translate translate-default="Translate translation">translateId</span>
9+
<span i18n="@@i18nId">I18n translation</span>
10+
</body>
11+
</html>
12+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if no default translation is provided 1`] = `
4+
Array [
5+
Array [
6+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element <div i18n='@@Simple Case id'>...</div> with attribute i18n is empty and is therefore missing the default translation.],
7+
],
8+
]
9+
`;
10+
11+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if the attribute value contains an empty id 1`] = `
12+
Array [
13+
Array [
14+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The attribute i18n on element <div i18n='@@'>Simple case</div> defines an empty ID.],
15+
],
16+
]
17+
`;
18+
19+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if the attribute value does not contain the id indicator '@@'. 1`] = `
20+
Array [
21+
Array [
22+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The attribute i18n on element <div i18n='Simple Case id'>Simple case</div> attribute is missing the custom id indicator '@@'.],
23+
],
24+
]
25+
`;
26+
27+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if the content of the element is an expression 1`] = `
28+
Array [
29+
Array [
30+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element '<div i18n='@@Simple Case id'>{{someValue}}</div>' uses an angular expression as translation id ('Simple Case id') or as default text ('{{someValue}}'). This is not supported. Either use a string literal as translation id and default text or suppress this error by adding the 'suppress-dynamic-translation-error' attribute to this element or any of its parents.],
31+
],
32+
]
33+
`;
34+
35+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if the element contains multiple child nodes 1`] = `
36+
Array [
37+
Array [
38+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element <div i18n='@@Simple Case id'>Created by at 13:34</div> has multiple child elements and, therefore, the default translation cannot be extracted.],
39+
],
40+
]
41+
`;
42+
43+
exports[`StatefulHtmlParserSpecs <any i18n> emits an error if the value of the translate id is an expression 1`] = `
44+
Array [
45+
Array [
46+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element '<div i18n='@@{{id}}'>Not an expression</div>' uses an angular expression as translation id ('{{id}}') or as default text ('Not an expression'). This is not supported. Either use a string literal as translation id and default text or suppress this error by adding the 'suppress-dynamic-translation-error' attribute to this element or any of its parents.],
47+
],
48+
]
49+
`;
50+
51+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if no corresponding attribute [attr] exist for the i18n-[attr] attribute 1`] = `
52+
Array [
53+
Array [
54+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element <div i18n-title='@@test attribute id'>...</div> with i18n-title is missing a corresponding title attribute.],
55+
],
56+
]
57+
`;
58+
59+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if the attribute value contains an empty id 1`] = `
60+
Array [
61+
Array [
62+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The attribute i18n-title on element <div i18n-title='@@' title='test attribute'>...</div> defines an empty ID.],
63+
],
64+
]
65+
`;
66+
67+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if the attribute value does not contain the id indicator '@@'. 1`] = `
68+
Array [
69+
Array [
70+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The attribute i18n-title on element <div i18n-title='test attribute id' title='test attribute'>...</div> attribute is missing the custom id indicator '@@'.],
71+
],
72+
]
73+
`;
74+
75+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if the corresponding attribute [attr] is empty 1`] = `
76+
Array [
77+
Array [
78+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element <div i18n-title='@@test attribute id' title=''>...</div> with i18n-title is missing a value for the corresponding title attribute.],
79+
],
80+
]
81+
`;
82+
83+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if the default translation is an expression 1`] = `
84+
Array [
85+
Array [
86+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element '<div i18n-title='@@myTitle' title='{{myTitle}}'>...</div>' uses an angular expression as translation id ('myTitle') or as default text ('{{myTitle}}'). This is not supported. Either use a string literal as translation id and default text or suppress this error by adding the 'suppress-dynamic-translation-error' attribute to this element or any of its parents.],
87+
],
88+
]
89+
`;
90+
91+
exports[`StatefulHtmlParserSpecs <any i18n-*> emits an error if the i18n-[attr] uses an expression as id 1`] = `
92+
Array [
93+
Array [
94+
[Error: Failed to extract the angular-translate translations from 'test.html':1:0: The element '<div i18n-title='@@{{id}}' title='My title'>...</div>' uses an angular expression as translation id ('{{id}}') or as default text ('My title'). This is not supported. Either use a string literal as translation id and default text or suppress this error by adding the 'suppress-dynamic-translation-error' attribute to this element or any of its parents.],
95+
],
96+
]
97+
`;

0 commit comments

Comments
 (0)