Skip to content

Commit 37fa36c

Browse files
committed
feat: implement check-text i18n rule
1 parent c746bbc commit 37fa36c

File tree

2 files changed

+122
-10
lines changed

2 files changed

+122
-10
lines changed

src/i18nRule.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const getSemicolonReplacements = (
1414
return [new Lint.Replacement(absolutePosition, 1, '; ')];
1515
};
1616

17-
type Option = 'check-id' | 'check-i18n';
17+
type Option = 'check-id' | 'check-text';
1818

1919
interface ConfigurableVisitor {
2020
getOption(): Option;
@@ -47,24 +47,41 @@ class I18NAttrVisitor extends BasicTemplateAstVisitor
4747

4848
class I18NTextVisitor extends BasicTemplateAstVisitor
4949
implements ConfigurableVisitor {
50-
visitAttr(attr: ast.AttrAst, context: BasicTemplateAstVisitor) {
51-
if (attr.name === 'i18n' && attr.value) {
52-
const parts = attr.value.split('@@');
53-
if (parts.length <= 1 || parts[1].length === 0) {
54-
const span = attr.sourceSpan;
50+
private hasI18n = false;
51+
private nestedElements = [];
52+
private visited = new Set<ast.TextAst>();
53+
54+
visitText(text: ast.TextAst, context: BasicTemplateAstVisitor) {
55+
if (!this.visited.has(text)) {
56+
this.visited.add(text);
57+
const textNonEmpty = text.value.trim().length > 0;
58+
if (
59+
(!this.hasI18n && textNonEmpty && this.nestedElements.length) ||
60+
(textNonEmpty && !this.nestedElements.length)
61+
) {
62+
const span = text.sourceSpan;
5563
context.addFailure(
5664
context.createFailure(
5765
span.start.offset,
5866
span.end.offset - span.start.offset,
59-
'Missing custom message identifier. For more information visit https://angular.io/guide/i18n'
67+
'Each element containing text node should has an i18n attribute'
6068
)
6169
);
6270
}
6371
}
64-
super.visitAttr(attr, context);
72+
super.visitText(text, context);
73+
}
74+
75+
visitElement(element: ast.ElementAst, context: BasicTemplateAstVisitor) {
76+
this.hasI18n = element.attrs.some(e => e.name === 'i18n');
77+
this.nestedElements.push(element.name);
78+
super.visitElement(element, context);
79+
this.nestedElements.pop();
80+
this.hasI18n = false;
6581
}
82+
6683
getOption(): Option {
67-
return 'check-id';
84+
return 'check-text';
6885
}
6986
}
7087

@@ -97,6 +114,26 @@ class I18NTemplateVisitor extends BasicTemplateAstVisitor {
97114
.forEach(f => this.addFailure(f));
98115
super.visitAttr(attr, context);
99116
}
117+
118+
visitElement(element: ast.ElementAst, context: any): any {
119+
const options = this.getOptions();
120+
this.visitors
121+
.filter(v => options.indexOf(v.getOption()) >= 0)
122+
.map(v => v.visitElement(element, this))
123+
.filter(f => !!f)
124+
.forEach(f => this.addFailure(f));
125+
super.visitElement(element, context);
126+
}
127+
128+
visitText(text: ast.TextAst, context: any): any {
129+
const options = this.getOptions();
130+
this.visitors
131+
.filter(v => options.indexOf(v.getOption()) >= 0)
132+
.map(v => v.visitText(text, this))
133+
.filter(f => !!f)
134+
.forEach(f => this.addFailure(f));
135+
super.visitText(text, context);
136+
}
100137
}
101138

102139
export class Rule extends Lint.Rules.AbstractRule {

test/i18nRule.spec.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { expect } from 'chai';
88
import { FsFileResolver } from '../src/angular/fileResolver/fsFileResolver';
99
import { MetadataReader } from '../src/angular/metadataReader';
1010
import * as ts from 'typescript';
11+
import { assertFailure } from './testHelper';
1112
import chai = require('chai');
1213

1314
const getAst = (code: string, file = 'file.ts') => {
1415
return ts.createSourceFile(file, code, ts.ScriptTarget.ES5, true);
1516
};
1617

17-
describe.only('i18n', () => {
18+
describe('i18n', () => {
1819
describe('check-id', () => {
1920
it('should work with proper id', () => {
2021
let source = `
@@ -78,4 +79,78 @@ describe.only('i18n', () => {
7879
});
7980
});
8081
});
82+
83+
describe('check-id', () => {
84+
it('should work with i18n attribute', () => {
85+
let source = `
86+
@Component({
87+
template: \`
88+
<div i18n>Text</div>
89+
\`
90+
})
91+
class Bar {}
92+
`;
93+
assertSuccess('i18n', source, ['check-text']);
94+
});
95+
96+
it('should work without i18n attribute & interpolation', () => {
97+
let source = `
98+
@Component({
99+
template: \`
100+
<div>{{text}}</div>
101+
\`
102+
})
103+
class Bar {}
104+
`;
105+
assertSuccess('i18n', source, ['check-text']);
106+
});
107+
108+
it('should fail with missing id string', () => {
109+
let source = `
110+
@Component({
111+
template: \`
112+
<div>Text</div>
113+
~~~~
114+
\`
115+
})
116+
class Bar {}
117+
`;
118+
assertAnnotated({
119+
ruleName: 'i18n',
120+
options: ['check-text'],
121+
source,
122+
message:
123+
'Each element containing text node should has an i18n attribute'
124+
});
125+
});
126+
127+
it('should fail with text outside element with i18n attribute', () => {
128+
let source = `
129+
@Component({
130+
template: \`
131+
<div i18n>Text</div>
132+
foo
133+
\`
134+
})
135+
class Bar {}
136+
`;
137+
assertFailure(
138+
'i18n',
139+
source,
140+
{
141+
message:
142+
'Each element containing text node should has an i18n attribute',
143+
startPosition: {
144+
line: 3,
145+
character: 30
146+
},
147+
endPosition: {
148+
line: 5,
149+
character: 8
150+
}
151+
},
152+
['check-text']
153+
);
154+
});
155+
});
81156
});

0 commit comments

Comments
 (0)