Skip to content

Commit

Permalink
feat(compiler): add source files to xmb/xliff translations
Browse files Browse the repository at this point in the history
  • Loading branch information
ocombe committed Feb 24, 2017
1 parent 32c2fd5 commit 1ed2ce1
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 72 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/dist/
node_modules
bower_components
e2e_test.*

# Include when developing application packages.
pubspec.lock
Expand Down
18 changes: 15 additions & 3 deletions modules/@angular/compiler-cli/integrationtest/test/i18n_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="8136548302122759730" desc="desc" meaning="meaning">translate me</msg>
<msg id="3492007542396725315">Welcome</msg>
<msg id="3772663375917578720">other-3rdP-component</msg>
<msg id="8136548302122759730" desc="desc" meaning="meaning"><source>/src/basic.ts:0:0</source>translate me</msg>
<msg id="3492007542396725315"><source>/src/basic.ts:4:4</source>Welcome</msg>
<msg id="3772663375917578720"><source>/node_modules/third_party/other_comp.d.ts:0:0</source>other-3rdP-component</msg>
</messagebundle>
`;

Expand All @@ -47,16 +47,28 @@ const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<trans-unit id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" datatype="html">
<source>translate me</source>
<target/>
<context-group purpose="location">
<context context-type="sourcefile">/src/basic.ts</context>
<context context-type="linenumber">0</context>
</context-group>
<note priority="1" from="description">desc</note>
<note priority="1" from="meaning">meaning</note>
</trans-unit>
<trans-unit id="65cc4ab3b4c438e07c89be2b677d08369fb62da2" datatype="html">
<source>Welcome</source>
<target/>
<context-group purpose="location">
<context context-type="sourcefile">/src/basic.ts</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit id="63a85808f03b8181e36a952e0fa38202c2304862" datatype="html">
<source>other-3rdP-component</source>
<target/>
<context-group purpose="location">
<context context-type="sourcefile">/node_modules/third_party/other_comp.d.ts</context>
<context context-type="linenumber">0</context>
</context-group>
</trans-unit>
</body>
</file>
Expand Down
3 changes: 1 addition & 2 deletions modules/@angular/compiler-cli/src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ export class Extractor {
default:
serializer = new compiler.Xliff();
}

return bundle.write(serializer);
return bundle.write(serializer, this.options.basePath);
}

getExtension(formatName: string): string {
Expand Down
21 changes: 19 additions & 2 deletions modules/@angular/compiler/src/i18n/i18n_ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import {ParseSourceSpan} from '../parse_util';

export class Message {
sources: MessageSource[] = [];

/**
* @param nodes message AST
* @param placeholders maps placeholder names to static content
Expand All @@ -20,7 +22,22 @@ export class Message {
constructor(
public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public description: string, public id: string) {}
public description: string, public id: string) {
if (nodes.length) {
this.sources.push({
filePath: nodes[0].sourceSpan.start.file.url,
startLine: nodes[0].sourceSpan.start.line,
endLine: nodes[nodes.length - 1].sourceSpan.end.line
});
}
}
}

export interface MessageSource {
filePath: string;
// startLine / endline are index-0 based (first line has a value of 0, not 1)
startLine: number;
endLine: number;
}

export interface Node {
Expand Down Expand Up @@ -131,4 +148,4 @@ export class RecurseVisitor implements Visitor {
visitPlaceholder(ph: Placeholder, context?: any): any{};

visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any{};
}
}
15 changes: 13 additions & 2 deletions modules/@angular/compiler/src/i18n/message_bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class MessageBundle {
// The public (serialized) format might be different, see the `write` method.
getMessages(): i18n.Message[] { return this._messages; }

write(serializer: Serializer): string {
write(serializer: Serializer, sourcesFilter: string = ''): string {
const messages: {[id: string]: i18n.Message} = {};
const mapperVisitor = new MapPlaceholderNames();

Expand All @@ -56,6 +56,8 @@ export class MessageBundle {
const id = serializer.digest(message);
if (!messages.hasOwnProperty(id)) {
messages[id] = message;
} else {
messages[id].sources.push(...message.sources);
}
});

Expand All @@ -64,11 +66,20 @@ export class MessageBundle {
const mapper = serializer.createNameMapper(messages[id]);
const src = messages[id];
const nodes = mapper ? mapperVisitor.convert(src.nodes, mapper) : src.nodes;
return new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
let transformedMessage = new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
transformedMessage.sources = this.filterMessageSourcePaths(src.sources, sourcesFilter);
return transformedMessage;
});

return serializer.write(msgList, this._locale);
}

// filter the message source paths relative to another path
filterMessageSourcePaths(sources: i18n.MessageSource[], filter: string) {
sources.forEach(
(source: i18n.MessageSource) => { source.filePath = source.filePath.replace(filter, ''); });
return sources;
}
}

// Transform an i18n AST by renaming the placeholder nodes with the given mapper
Expand Down
17 changes: 16 additions & 1 deletion modules/@angular/compiler/src/i18n/serializers/xliff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const _FILE_TAG = 'file';
const _SOURCE_TAG = 'source';
const _TARGET_TAG = 'target';
const _UNIT_TAG = 'trans-unit';
const _CONTEXT_GROUP_TAG = 'context-group';
const _CONTEXT_TAG = 'context';

// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
Expand All @@ -34,10 +36,23 @@ export class Xliff extends Serializer {
const transUnits: xml.Node[] = [];

messages.forEach(message => {
let contextTags: xml.Node[] = [];
message.sources.forEach((source: i18n.MessageSource) => {
contextTags.push(
new xml.CR(10),
new xml.Tag(
_CONTEXT_TAG, {'context-type': 'sourcefile'}, [new xml.Text(source.filePath)]),
new xml.CR(10), new xml.Tag(
_CONTEXT_TAG, {'context-type': 'linenumber'},
[new xml.Text(`${source.startLine}`)]),
new xml.CR(8));
});

const transUnit = new xml.Tag(_UNIT_TAG, {id: message.id, datatype: 'html'});
transUnit.children.push(
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
new xml.CR(8), new xml.Tag(_TARGET_TAG));
new xml.CR(8), new xml.Tag(_TARGET_TAG), new xml.CR(8),
new xml.Tag(_CONTEXT_GROUP_TAG, {purpose: 'location'}, contextTags));

if (message.description) {
transUnit.children.push(
Expand Down
11 changes: 10 additions & 1 deletion modules/@angular/compiler/src/i18n/serializers/xmb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const _MESSAGES_TAG = 'messagebundle';
const _MESSAGE_TAG = 'msg';
const _PLACEHOLDER_TAG = 'ph';
const _EXEMPLE_TAG = 'ex';
const _SOURCE_TAG = 'source';

const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
<!ATTLIST messagebundle class CDATA #IMPLIED>
Expand Down Expand Up @@ -54,8 +55,16 @@ export class Xmb extends Serializer {
attrs['meaning'] = message.meaning;
}

let sourceTags: xml.Tag[] = [];
message.sources.forEach((source: i18n.MessageSource) => {
sourceTags.push(new xml.Tag(
_SOURCE_TAG, {},
[new xml.Text(`${source.filePath}:${source.startLine}:${source.endLine}`)]));
});

rootNode.children.push(
new xml.CR(2), new xml.Tag(_MESSAGE_TAG, attrs, visitor.serialize(message.nodes)));
new xml.CR(2),
new xml.Tag(_MESSAGE_TAG, attrs, [...sourceTags, ...visitor.serialize(message.nodes)]));
});

rootNode.children.push(new xml.CR());
Expand Down
3 changes: 2 additions & 1 deletion modules/@angular/compiler/src/i18n/serializers/xtb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {digest, toPublicName} from './xmb';
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph';
const _SOURCE_TAG = 'source';

export class Xtb extends Serializer {
write(messages: i18n.Message[], locale: string|null): string { throw new Error('Unsupported'); }
Expand Down Expand Up @@ -195,7 +196,7 @@ class XmlToI18n implements ml.Visitor {
}

this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
} else if (el.name !== _SOURCE_TAG) {
this._addError(el, `Unexpected tag`);
}
}
Expand Down
1 change: 1 addition & 0 deletions modules/@angular/compiler/test/i18n/digest_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function main(): void {
placeholderToMessage: {},
meaning: '',
description: '',
sources: []
})).toEqual('i');
});
});
Expand Down
82 changes: 41 additions & 41 deletions modules/@angular/compiler/test/i18n/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function main() {
it('should extract from templates', () => {
const catalog = new MessageBundle(new HtmlParser, [], {});
const serializer = new Xmb();
catalog.updateFromTemplate(HTML, '', DEFAULT_INTERPOLATION_CONFIG);
catalog.updateFromTemplate(HTML, 'file.ts', DEFAULT_INTERPOLATION_CONFIG);

expect(catalog.write(serializer)).toContain(XMB);
});
Expand Down Expand Up @@ -141,59 +141,59 @@ class FrLocalization extends NgLocalization {

const XTB = `
<translationbundle>
<translation id="615790887472569365">attributs i18n sur les balises</translation>
<translation id="3707494640264351337">imbriqué</translation>
<translation id="5539162898278769904">imbriqué</translation>
<translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="5525133077318024839">sur des balises non traductibles</translation>
<translation id="8670732454866344690">sur des balises traductibles</translation>
<translation id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="1746565782635215"><ph name="ICU"/></translation>
<translation id="5868084092545682515">{VAR_SELECT, select, m {homme} f {femme}}</translation>
<translation id="4851788426695310455"><ph name="INTERPOLATION"/></translation>
<translation id="9013357158046221374">sexe = <ph name="INTERPOLATION"/></translation>
<translation id="8324617391167353662"><ph name="CUSTOM_NAME"/></translation>
<translation id="7685649297917455806">dans une section traductible</translation>
<translation id="2387287228265107305">
<translation id="615790887472569365"><source>file.ts:2:2</source>attributs i18n sur les balises</translation>
<translation id="3707494640264351337"><source>file.ts:4:4</source>imbriqué</translation>
<translation id="5539162898278769904"><source>file.ts:6:6</source>imbriqué</translation>
<translation id="3780349238193953556"><source>file.ts:8:8</source><source>file.ts:9:9</source><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="5525133077318024839"><source>file.ts:12:12</source>sur des balises non traductibles</translation>
<translation id="8670732454866344690"><source>file.ts:13:13</source>sur des balises traductibles</translation>
<translation id="4593805537723189714"><source>file.ts:18:18</source><source>file.ts:35:35</source>{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="1746565782635215"><source>file.ts:20:22</source><source>file.ts:23:25</source><ph name="ICU"/></translation>
<translation id="5868084092545682515"><source>file.ts:21:21</source><source>file.ts:24:24</source>{VAR_SELECT, select, m {homme} f {femme}}</translation>
<translation id="4851788426695310455"><source>file.ts:27:27</source><ph name="INTERPOLATION"/></translation>
<translation id="9013357158046221374"><source>file.ts:28:28</source>sexe = <ph name="INTERPOLATION"/></translation>
<translation id="8324617391167353662"><source>file.ts:29:29</source><ph name="CUSTOM_NAME"/></translation>
<translation id="7685649297917455806"><source>file.ts:34:34</source><source>file.ts:52:52</source>dans une section traductible</translation>
<translation id="2387287228265107305"><source>file.ts:32:36</source>
<ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/>
<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/>
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</translation>
<translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
<translation id="i18n16">avec un ID explicite</translation>
<translation id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph
<translation id="1491627405349178954"><source>file.ts:38:38</source>ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
<translation id="i18n16"><source>file.ts:40:40</source>avec un ID explicite</translation>
<translation id="i18n17"><source>file.ts:41:41</source>{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph
name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>beaucoup<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</translation>
<translation id="4085484936881858615">{VAR_PLURAL, plural, =0 {Pas de réponse} =1 {une réponse} other {<ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> réponse} }</translation>
<translation id="4035252431381981115">FOO<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>BAR<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></translation>
<translation id="5339604010413301604"><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></translation>
<translation id="4085484936881858615"><source>file.ts:44:50</source>{VAR_PLURAL, plural, =0 {Pas de réponse} =1 {une réponse} other {<ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> réponse} }</translation>
<translation id="4035252431381981115"><source>file.ts:52:52</source>FOO<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>BAR<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></translation>
<translation id="5339604010413301604"><source>file.ts:54:54</source><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></translation>
</translationbundle>`;

const XMB = ` <msg id="615790887472569365">i18n attribute on tags</msg>
<msg id="3707494640264351337">nested</msg>
<msg id="5539162898278769904" meaning="different meaning">nested</msg>
<msg id="3780349238193953556"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="5525133077318024839">on not translatable node</msg>
<msg id="8670732454866344690">on translatable node</msg>
<msg id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="1746565782635215">
const XMB = ` <msg id="615790887472569365"><source>file.ts:2:2</source>i18n attribute on tags</msg>
<msg id="3707494640264351337"><source>file.ts:4:4</source>nested</msg>
<msg id="5539162898278769904" meaning="different meaning"><source>file.ts:6:6</source>nested</msg>
<msg id="3780349238193953556"><source>file.ts:8:8</source><source>file.ts:9:9</source><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="5525133077318024839"><source>file.ts:12:12</source>on not translatable node</msg>
<msg id="8670732454866344690"><source>file.ts:13:13</source>on translatable node</msg>
<msg id="4593805537723189714"><source>file.ts:18:18</source><source>file.ts:35:35</source>{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="1746565782635215"><source>file.ts:20:22</source><source>file.ts:23:25</source>
<ph name="ICU"><ex>ICU</ex></ph>
</msg>
<msg id="5868084092545682515">{VAR_SELECT, select, m {male} f {female} }</msg>
<msg id="4851788426695310455"><ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="9013357158046221374">sex = <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="8324617391167353662"><ph name="CUSTOM_NAME"><ex>CUSTOM_NAME</ex></ph></msg>
<msg id="7685649297917455806">in a translatable section</msg>
<msg id="2387287228265107305">
<msg id="5868084092545682515"><source>file.ts:21:21</source><source>file.ts:24:24</source>{VAR_SELECT, select, m {male} f {female} }</msg>
<msg id="4851788426695310455"><source>file.ts:27:27</source><ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="9013357158046221374"><source>file.ts:28:28</source>sex = <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="8324617391167353662"><source>file.ts:29:29</source><ph name="CUSTOM_NAME"><ex>CUSTOM_NAME</ex></ph></msg>
<msg id="7685649297917455806"><source>file.ts:34:34</source><source>file.ts:52:52</source>in a translatable section</msg>
<msg id="2387287228265107305"><source>file.ts:32:36</source>
<ph name="START_HEADING_LEVEL1"><ex>&lt;h1&gt;</ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex>&lt;/h1&gt;</ex></ph>
<ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
<ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"><ex>ICU</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
</msg>
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
<msg id="i18n16">with an explicit ID</msg>
<msg id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="4085484936881858615" desc="desc">{VAR_PLURAL, plural, =0 {Found no results} =1 {Found one result} other {Found <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> results} }</msg>
<msg id="4035252431381981115">foo<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>bar<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></msg>
<msg id="5339604010413301604"><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></msg>`;
<msg id="1491627405349178954"><source>file.ts:38:38</source>it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
<msg id="i18n16"><source>file.ts:40:40</source>with an explicit ID</msg>
<msg id="i18n17"><source>file.ts:41:41</source>{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="4085484936881858615" desc="desc"><source>file.ts:44:50</source>{VAR_PLURAL, plural, =0 {Found no results} =1 {Found one result} other {Found <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> results} }</msg>
<msg id="4035252431381981115"><source>file.ts:52:52</source>foo<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>bar<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></msg>
<msg id="5339604010413301604"><source>file.ts:54:54</source><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></msg>`;

const HTML = `
<div>
Expand Down
Loading

0 comments on commit 1ed2ce1

Please sign in to comment.