├── .github
└── workflows
│ ├── nodejs.yml
│ └── release.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── docs
└── rules
│ └── no-literal-string.md
├── examples
├── app-with-eslint7
│ ├── .eslintrc.js
│ ├── index.js
│ ├── package.json
│ └── test.mjs
└── app-with-eslint9
│ ├── eslint.config.mjs
│ ├── index.js
│ └── package.json
├── lib
├── constants.js
├── helper
│ ├── generateFullMatchRegExp.js
│ ├── getNearestAncestor.js
│ ├── index.js
│ ├── matchPatterns.js
│ └── shouldSkip.js
├── index.d.ts
├── index.js
├── options
│ ├── defaults.js
│ ├── htmlEntities.js
│ └── schema.json
└── rules
│ └── no-literal-string.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tests
└── lib
│ ├── fixtures
│ ├── invalid-jsx-only.jsx
│ ├── invalid.jsx
│ ├── valid-jsx-text-only.jsx
│ ├── valid-typescript.ts
│ └── valid.jsx
│ ├── helpers
│ ├── runTest.js
│ └── testFile.js
│ └── rules
│ └── no-literal-string
│ ├── all.js
│ ├── callees.js
│ ├── default.js
│ ├── jsx-attributes.js
│ ├── jsx-components.js
│ ├── jsx-only.js
│ ├── jsx-text-only.js
│ ├── object-properties.js
│ ├── should-validate-template.js
│ ├── tsconfig.json
│ ├── typescript.js
│ ├── vue.js
│ └── words.js
└── v5.md
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 | pull_request:
9 | branches:
10 | - main
11 | - next
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x, 20.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v1
23 | - uses: pnpm/action-setup@v2
24 | with:
25 | version: 9.13.2
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v4
28 | with:
29 | cache: pnpm
30 | node-version: ${{ matrix.node-version }}
31 | - name: build and test
32 | run: |
33 | pnpm i --frozen-lockfile
34 | pnpm test
35 | env:
36 | CI: true
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | release:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: pnpm/action-setup@v2
14 | with:
15 | version: 9.13.2
16 | - name: Use Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | registry-url: 'https://registry.npmjs.org/'
20 | node-version: 20.*
21 | cache: pnpm
22 | - name: Configure committer
23 | run: |
24 | git config --global user.name ${GITHUB_ACTOR}
25 | git config --global user.email ${GITHUB_ACTOR}@users.noreply.github.com
26 | - name: npm install, build, and release
27 | run: |
28 | pnpm i --frozen-lockfile
29 | pnpx standard-version
30 | npm publish
31 | git push --follow-tags
32 | env:
33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | CI: true
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | !.github
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5"
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### 6.1.1 (2024-11-24)
6 |
7 | ## [6.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.11...v6.1.0) (2024-09-14)
8 |
9 |
10 | ### Features
11 |
12 | * use pnpm ([394250d](https://github.com/edvardchen/eslint-plugin-i18next/commit/394250df22c835611c80d5035b304f302799330b))
13 |
14 | ### 6.0.11 (2024-09-12)
15 |
16 | ### 6.0.10 (2024-07-20)
17 |
18 | ### 6.0.9 (2024-07-13)
19 |
20 | ### 6.0.8 (2024-07-13)
21 |
22 | ### 6.0.7 (2024-07-07)
23 |
24 | ### 6.0.6 (2024-05-14)
25 |
26 | ### 6.0.5 (2023-10-26)
27 |
28 | ### 6.0.4 (2023-08-01)
29 |
30 | ### 6.0.3 (2023-06-13)
31 |
32 | ### 6.0.2 (2023-06-06)
33 |
34 | ### 6.0.1 (2023-05-05)
35 |
36 | ## 6.0.0 (2023-04-26)
37 |
38 | ## [6.0.0-8](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-7...v6.0.0-8) (2023-03-23)
39 |
40 | ## [6.0.0-7](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-6...v6.0.0-7) (2023-03-20)
41 |
42 |
43 | ### Features
44 |
45 | * treat template literals without expressions as normal strings ([f740c1f](https://github.com/edvardchen/eslint-plugin-i18next/commit/f740c1f10bd0cde9644e0948172a37b5189b6245))
46 |
47 |
48 | ### Bug Fixes
49 |
50 | * fixes [#94](https://github.com/edvardchen/eslint-plugin-i18next/issues/94) ([995aa78](https://github.com/edvardchen/eslint-plugin-i18next/commit/995aa78806a1059822bf3af828656bfa622c6a50))
51 |
52 | ## [6.0.0-6](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-5...v6.0.0-6) (2022-11-21)
53 |
54 | ## [6.0.0-5](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-4...v6.0.0-5) (2022-07-14)
55 |
56 |
57 | ### Bug Fixes
58 |
59 | * corrected typo ([9fba5c2](https://github.com/edvardchen/eslint-plugin-i18next/commit/9fba5c287f9f55f2e71c3e41f163ae8793fe5cc1))
60 | * remove the problematic typescript validation ([a021fe0](https://github.com/edvardchen/eslint-plugin-i18next/commit/a021fe06c0db26d732739bb56604bcbf28aad431))
61 |
62 | ## [6.0.0-4](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-3...v6.0.0-4) (2022-05-31)
63 |
64 | ## [6.0.0-3](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-2...v6.0.0-3) (2022-05-18)
65 |
66 |
67 | ### Bug Fixes
68 |
69 | * support to ignore member expression in assignments ([178b703](https://github.com/edvardchen/eslint-plugin-i18next/commit/178b703100e6c13ceafb4fd1032696cae47e55c8))
70 |
71 | ## [6.0.0-2](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-1...v6.0.0-2) (2022-05-15)
72 |
73 |
74 | ### Bug Fixes
75 |
76 | * validate correctly call chains ([dc33346](https://github.com/edvardchen/eslint-plugin-i18next/commit/dc33346)), closes [#54](https://github.com/edvardchen/eslint-plugin-i18next/issues/54)
77 |
78 | ## [6.0.0-1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.0.0-0...v6.0.0-1) (2022-05-15)
79 |
80 |
81 | ### Features
82 |
83 | * support union type ([a885683](https://github.com/edvardchen/eslint-plugin-i18next/commit/a885683))
84 |
85 | ## [6.0.0-0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.2.1...v6.0.0-0) (2022-05-15)
86 |
87 |
88 | ### Features
89 |
90 | * make mode defaults to jsx-text-only ([7cc75ab](https://github.com/edvardchen/eslint-plugin-i18next/commit/7cc75ab))
91 | * support callees option ([1934216](https://github.com/edvardchen/eslint-plugin-i18next/commit/1934216))
92 | * support option jsx-attributes ([08ae48b](https://github.com/edvardchen/eslint-plugin-i18next/commit/08ae48b))
93 | * support option jsx-components ([99af7e1](https://github.com/edvardchen/eslint-plugin-i18next/commit/99af7e1))
94 | * support option mode ([3193108](https://github.com/edvardchen/eslint-plugin-i18next/commit/3193108))
95 | * support option object-properties ([484fcfb](https://github.com/edvardchen/eslint-plugin-i18next/commit/484fcfb))
96 | * support option words ([874a694](https://github.com/edvardchen/eslint-plugin-i18next/commit/874a694))
97 |
98 | ### 5.2.1 (2022-05-05)
99 |
100 | ## 5.2.0 (2022-05-05)
101 |
102 |
103 | ### Features
104 |
105 | * adding message as config option ([4ce1b18](https://github.com/edvardchen/eslint-plugin-i18next/commit/4ce1b1894af15c0f0e91f4094e527790322b8bc0))
106 |
107 | ### 5.1.3 (2022-05-05)
108 |
109 | ### 5.1.2 (2021-09-12)
110 |
111 | ### [5.1.1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.1.0...v5.1.1) (2021-04-08)
112 |
113 |
114 | ### Bug Fixes
115 |
116 | * ignore strings as object property name ([e70bb8f](https://github.com/edvardchen/eslint-plugin-i18next/commit/e70bb8f)), closes [#42](https://github.com/edvardchen/eslint-plugin-i18next/issues/42)
117 |
118 | ## [5.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.0.0...v5.1.0) (2021-03-28)
119 |
120 |
121 | ### Features
122 |
123 | * ignore module declaration ([97c0c34](https://github.com/edvardchen/eslint-plugin-i18next/commit/97c0c34)), closes [#41](https://github.com/edvardchen/eslint-plugin-i18next/issues/41)
124 |
125 | ## [5.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.0.0-1...v5.0.0) (2020-09-17)
126 |
127 |
128 | ### Features
129 |
130 | * ignore TSEnumMember ([63ea9e4](https://github.com/edvardchen/eslint-plugin-i18next/commit/63ea9e4))
131 |
132 | ## [5.0.0-1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.0.0-0...v5.0.0-1) (2020-09-02)
133 |
134 |
135 | ### Bug Fixes
136 |
137 | * recognize react JSXFragment ([719f496](https://github.com/edvardchen/eslint-plugin-i18next/commit/719f496))
138 |
139 | ## [5.0.0-0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.5.0...v5.0.0-0) (2020-08-28)
140 |
141 |
142 | ### ⚠ BREAKING CHANGES
143 |
144 | * maybe break in some cases although all test cases passed
145 |
146 | * reuse all rules to template literal ([b5cdda6](https://github.com/edvardchen/eslint-plugin-i18next/commit/b5cdda6)), closes [#20](https://github.com/edvardchen/eslint-plugin-i18next/issues/20)
147 |
148 | ## [4.5.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.4.0...v4.5.0) (2020-08-11)
149 |
150 |
151 | ### Features
152 |
153 | * add option validateTemplate ([ba0ec60](https://github.com/edvardchen/eslint-plugin-i18next/commit/ba0ec60))
154 |
155 | ## [4.4.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.3.0...v4.4.0) (2020-08-03)
156 |
157 |
158 | ### Bug Fixes
159 |
160 | * misuse typescript ([72bd375](https://github.com/edvardchen/eslint-plugin-i18next/commit/72bd375)), closes [#27](https://github.com/edvardchen/eslint-plugin-i18next/issues/27)
161 |
162 |
163 | ### Features
164 |
165 | * ignore UPPER_CASE arrays ([0d7a69a](https://github.com/edvardchen/eslint-plugin-i18next/commit/0d7a69a)), closes [#12](https://github.com/edvardchen/eslint-plugin-i18next/issues/12)
166 |
167 | ## [4.3.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.2.0...v4.3.0) (2020-07-21)
168 |
169 |
170 | ### Features
171 |
172 | * add onlyAttribute option ([7ebf98b](https://github.com/edvardchen/eslint-plugin-i18next/commit/7ebf98b)), closes [#25](https://github.com/edvardchen/eslint-plugin-i18next/issues/25)
173 |
174 | ## [4.2.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.1.0...v4.2.0) (2020-07-01)
175 |
176 |
177 | ### Bug Fixes
178 |
179 | * remove console ([f361e63](https://github.com/edvardchen/eslint-plugin-i18next/commit/f361e63))
180 |
181 |
182 | ### Features
183 |
184 | * allow ignoring lists of components ([eebb338](https://github.com/edvardchen/eslint-plugin-i18next/commit/eebb338))
185 |
186 | ## [4.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.0.0...v4.1.0) (2020-06-30)
187 |
188 |
189 | ### Features
190 |
191 | * support multibyte characters ([99ed9f2](https://github.com/edvardchen/eslint-plugin-i18next/commit/99ed9f2))
192 |
193 | ## [4.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.8.0...v4.0.0) (2020-06-28)
194 |
195 |
196 | ### ⚠ BREAKING CHANGES
197 |
198 | * all patterns in ignoreCallee would be treated as regular expression
199 |
200 | ### Features
201 |
202 | * allow regex in ignore and ignoreCallee ([0cfe340](https://github.com/edvardchen/eslint-plugin-i18next/commit/0cfe340)), closes [#19](https://github.com/edvardchen/eslint-plugin-i18next/issues/19)
203 |
204 | ## [3.8.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.7.0...v3.8.0) (2020-06-03)
205 |
206 |
207 | ### Features
208 |
209 | * add markupOnly option ([7bb225c](https://github.com/edvardchen/eslint-plugin-i18next/commit/7bb225c))
210 |
211 | ## [3.7.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.6.0...v3.7.0) (2020-05-06)
212 |
213 |
214 | ### Features
215 |
216 | * ignore element ([b98e0f8](https://github.com/edvardchen/eslint-plugin-i18next/commit/b98e0f8))
217 | * ignore element ([56b8b08](https://github.com/edvardchen/eslint-plugin-i18next/commit/56b8b08))
218 |
219 | ## [3.6.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.5.0...v3.6.0) (2020-04-17)
220 |
221 |
222 | ### Features
223 |
224 | * support to access enum value through string like Enum['key'] ([db68147](https://github.com/edvardchen/eslint-plugin-i18next/commit/db68147))
225 |
226 | ## [3.5.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.4.0...v3.5.0) (2020-04-16)
227 |
228 |
229 | ### Features
230 |
231 | * ignore JSX attrs style and key ([34a5d6d](https://github.com/edvardchen/eslint-plugin-i18next/commit/34a5d6d))
232 |
233 | ## [3.4.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.3.0...v3.4.0) (2020-04-14)
234 |
235 |
236 | ### Features
237 |
238 | * recognize ImportExpresion ([e54daee](https://github.com/edvardchen/eslint-plugin-i18next/commit/e54daee))
239 |
240 | ## [3.3.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.2.0...v3.3.0) (2020-02-11)
241 |
242 |
243 | ### Features
244 |
245 | * ignore property ([3355026](https://github.com/edvardchen/eslint-plugin-i18next/commit/3355026))
246 |
247 | ## [3.2.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.1.1...v3.2.0) (2019-10-21)
248 |
249 |
250 | ### Features
251 |
252 | * allow displayName property in classes ([5362281](https://github.com/edvardchen/eslint-plugin-i18next/commit/5362281))
253 |
254 | ### [3.1.1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.1.0...v3.1.1) (2019-10-10)
255 |
256 |
257 | ### Bug Fixes
258 |
259 | * add missing plugin in recommended config ([dde83ed](https://github.com/edvardchen/eslint-plugin-i18next/commit/dde83ed))
260 |
261 | ## [3.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v3.0.0...v3.1.0) (2019-10-10)
262 |
263 |
264 | ### Features
265 |
266 | * ignore not-word string ([1752cbe](https://github.com/edvardchen/eslint-plugin-i18next/commit/1752cbe))
267 |
268 | ## [3.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.5.0...v3.0.0) (2019-10-09)
269 |
270 |
271 | ### ⚠ BREAKING CHANGES
272 |
273 | * SInce the whitelist was cut short, it would complain when the removed attributes
274 | were added to custom component like
275 |
276 | ### Features
277 |
278 | * ignore most DOM attrs ([71483c2](https://github.com/edvardchen/eslint-plugin-i18next/commit/71483c2))
279 |
280 | ## [2.5.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.4.0...v2.5.0) (2019-10-08)
281 |
282 |
283 | ### Features
284 |
285 | * add more ignored attributes and callee ([0f9e2ec](https://github.com/edvardchen/eslint-plugin-i18next/commit/0f9e2ec))
286 |
287 | ## [2.4.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.3.1...v2.4.0) (2019-10-08)
288 |
289 |
290 | ### Features
291 |
292 | * add ignoreAttribute option ([c854313](https://github.com/edvardchen/eslint-plugin-i18next/commit/c854313))
293 |
294 | ### [2.3.1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.3.0...v2.3.1) (2019-09-16)
295 |
296 |
297 | ### Bug Fixes
298 |
299 | * whitelist addEventListener and few SVG attributes ([46241a6](https://github.com/edvardchen/eslint-plugin-i18next/commit/46241a6))
300 |
301 | ## [2.3.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.2.0...v2.3.0) (2019-07-26)
302 |
303 |
304 | ### Features
305 |
306 | * skip literal in SwitchCase statement ([d270343](https://github.com/edvardchen/eslint-plugin-i18next/commit/d270343)), closes [#2](https://github.com/edvardchen/eslint-plugin-i18next/issues/2)
307 |
308 |
309 |
310 | ## [2.2.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.1.1...v2.2.0) (2019-07-24)
311 |
312 |
313 | ### Bug Fixes
314 |
315 | * enhance ExportNamedDeclaration ([29e9f29](https://github.com/edvardchen/eslint-plugin-i18next/commit/29e9f29))
316 |
317 |
318 | ### Features
319 |
320 | * allow string comparison ([a78d150](https://github.com/edvardchen/eslint-plugin-i18next/commit/a78d150))
321 |
322 |
323 |
324 | ### [2.1.1](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.1.0...v2.1.1) (2019-07-24)
325 |
326 |
327 |
328 | ## [2.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.0.0...v2.1.0) (2019-07-11)
329 |
330 |
331 | ### Features
332 |
333 | * more TS supports ([382ccab](https://github.com/edvardchen/eslint-plugin-i18next/commit/382ccab))
334 | * skip literal with LiteralType ([40c54b1](https://github.com/edvardchen/eslint-plugin-i18next/commit/40c54b1))
335 |
336 |
337 |
338 | ## [2.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.2.0...v2.0.0) (2019-07-10)
339 |
340 |
341 | ### Bug Fixes
342 |
343 | * wrongly handle Literal node in VExpressionContainer ([dd279c6](https://github.com/edvardchen/eslint-plugin-i18next/commit/dd279c6))
344 |
345 |
346 | ### Features
347 |
348 | * dont check literal in export declaration ([1527eae](https://github.com/edvardchen/eslint-plugin-i18next/commit/1527eae))
349 | * dont check TSLiteralType ([fd93861](https://github.com/edvardchen/eslint-plugin-i18next/commit/fd93861))
350 |
351 |
352 | ### refactor
353 |
354 | * use rule selectors to reduce code complexity ([28d73ff](https://github.com/edvardchen/eslint-plugin-i18next/commit/28d73ff))
355 |
356 |
357 | ### BREAKING CHANGES
358 |
359 | * Disable fix because key in the call i18next.t(key) ussally was not same as the plain text
360 |
361 |
362 |
363 | ## [1.2.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.1.3...v1.2.0) (2019-06-20)
364 |
365 |
366 | ### Features
367 |
368 | * skip checking import(...) ([7306038](https://github.com/edvardchen/eslint-plugin-i18next/commit/7306038))
369 |
370 |
371 |
372 | ## [1.1.3](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.1.2...v1.1.3) (2019-04-08)
373 |
374 |
375 | ### Bug Fixes
376 |
377 | * disallow uppercase strings in JSX ([715cba4](https://github.com/edvardchen/eslint-plugin-i18next/commit/715cba4))
378 |
379 |
380 |
381 | ## [1.1.2](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.1.1...v1.1.2) (2019-04-08)
382 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Edvard Chen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-i18next
2 |
3 | ESLint plugin for i18n
4 |
5 | > For old versions below v6, plz refer [this document](./v5.md)
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install eslint-plugin-i18next --save-dev
11 | ```
12 |
13 | ## Usage
14 |
15 | For ESLint 9 flat configuration,
16 |
17 | ```js
18 | // eslint.config.mjs
19 | import i18next from 'eslint-plugin-i18next';
20 |
21 | export default [
22 | // your other configs
23 | i18next.configs['flat/recommended'],
24 | ];
25 | ```
26 |
27 | For ESLint 8 and below,
28 |
29 | ```json
30 | // .eslintrc
31 | {
32 | "extends": ["plugin:i18next/recommended"]
33 | }
34 | ```
35 |
36 | ## Rule `no-literal-string`
37 |
38 | This rule aims to avoid developers to display literal string directly to users without translating them.
39 |
40 | > Note: Disable auto-fix because key in the call `i18next.t(key)` usually was not the same as the literal
41 |
42 | Example of incorrect code:
43 |
44 | ```js
45 | /*eslint i18next/no-literal-string: "error"*/
46 |
hello world
47 | ```
48 |
49 | Example of correct code:
50 |
51 | ```js
52 | /*eslint i18next/no-literal-string: "error"*/
53 | {i18next.t('HELLO_KEY')}
54 | ```
55 |
56 | More options can be found [here](./docs/rules/no-literal-string.md)
57 |
58 | ### Breaking change
59 |
60 | By default, it will only validate the plain text in JSX markup instead of all literal strings in previous versions.
61 | [You can change it easily](./docs/rules/no-literal-string.md)
62 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | const conventional = require('@commitlint/config-conventional');
2 |
3 | module.exports = {
4 | extends: ['@commitlint/config-conventional'],
5 | rules: {
6 | 'type-enum': [2, 'always', [...conventional.rules['type-enum'][2], 'dev']]
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/docs/rules/no-literal-string.md:
--------------------------------------------------------------------------------
1 | # disallow literal string (no-literal-string)
2 |
3 | This rule aims to avoid developers to display literal string directly to users without translating them.
4 |
5 | ## Rule Details
6 |
7 | Example of incorrect code:
8 |
9 | ```jsx
10 | /*eslint i18next/no-literal-string: "error"*/
11 | hello world
12 | ```
13 |
14 | Example of correct code:
15 |
16 | ```jsx
17 | /*eslint i18next/no-literal-string: "error"*/
18 | {i18next.t('HELLO_KEY')}
19 | ```
20 |
21 | ## Options
22 |
23 | The option's typing definition looks like:
24 |
25 | ```typescript
26 | type MySchema = {
27 | [key in
28 | | 'words'
29 | | 'jsx-components'
30 | | 'jsx-attributes'
31 | | 'callees'
32 | | 'object-properties'
33 | | 'class-properties']?: {
34 | include?: string[];
35 | exclude?: string[];
36 | };
37 | } & {
38 | framework: 'react' | 'vue';
39 | mode?: 'jsx-text-only' | 'jsx-only' | 'all' | 'vue-template-ony';
40 | message?: string;
41 | 'should-validate-template'?: boolean;
42 | };
43 | ```
44 |
45 | ### `exclude` and `include`
46 |
47 | Instead of expanding options immoderately, a standard and scalable way to set options is provided
48 |
49 | You can use `exclude` and `include` of each options to control which should be validated and which should be ignored.
50 |
51 | The values of these two fields are treated as regular expressions.
52 |
53 | 1. If both are used, both conditions need to be satisfied
54 | 2. If both are emitted, it will be validated
55 |
56 | ### Option `words`
57 |
58 | `words` decides whether literal strings are allowed (in any situation), solely based on **the content of the string**
59 |
60 | e.g. if `.*foo.*` is excluded, the following literals are allowed no matter where they are used
61 |
62 | ```js
63 | method('afoo');
64 | const message = 'foob';
65 |
66 | ;
67 | ```
68 |
69 | ### Selector options
70 |
71 | - `jsx-components` decides whether literal strings as children within a component are allowed, based on the component name
72 |
73 | e.g. by default, `Trans` is excluded, so `Hello World` in the following is allowed.
74 |
75 | ```jsx
76 | Hello World
77 | ```
78 |
79 | - `jsx-attributes` decides whether literal strings are allowed as JSX attribute values, based on the name of the attribute
80 |
81 | e.g. if `data-testid` is excluded, `important-button` in the following is allowed
82 |
83 | ```jsx
84 |
87 | ```
88 |
89 | - `callees` decides whether literal strings are allowed as function arguments, based on the identifier of the function being called
90 |
91 | e.g. if `window.open` is excluded, `http://example.com` in the following is allowed
92 |
93 | ```js
94 | window.open('http://example.com');
95 | ```
96 |
97 | `callees` also covers object constructors, such as `new Error('string')` or `new URL('string')`
98 |
99 | - `object-properties` decides whether literal strings are allowed as object property values, based on the property key
100 |
101 | e.g. if `fieldName` is excluded but `label` is not, `currency_code` is allowed but `Currency` is not:
102 |
103 | ```js
104 | const fieldConfig = {
105 | fieldName: 'currency_code',
106 | label: 'Currency',
107 | };
108 | ```
109 |
110 | - `class-properties` decides whether literal strings are allowed as class property values, based on the property key
111 |
112 | e.g. by default, `displayName` is excluded, so `MyComponent` is allowed
113 |
114 | ```js
115 | class My extends Component {
116 | displayName = 'MyComponent';
117 | }
118 | ```
119 |
120 | ### Other options
121 |
122 | - `framework` specifies the type of framework currently in use.
123 | - `react` It defaults to 'react' which means you want to validate react component
124 | - `vue` If you want to validate vue component, can set the value to be this
125 | - `mode` provides a straightforward way to decides the range you want to validate literal strings.
126 | It defaults to `jsx-text-only` which only forbids to write plain text in JSX markup,available when framework option is 'react'
127 | - `jsx-only` validates the JSX attributes as well,available when framework option is 'react'
128 | - `all` validates all literal strings,available when the value of the framework option is 'react' and 'vue'
129 | - `vue-template-only`, only validate vue component template part,available when framework option value is 'vue'.
130 | - `message` defines the custom error message
131 | - `should-validate-template` decides if we should validate the string templates
132 |
133 | You can see [the default options here](../../lib/options/defaults.js)
134 |
135 | ## When Not To Use It
136 |
137 | Your project maybe not need to support multi-language or you don't care to spread literal string anywhere.
138 |
--------------------------------------------------------------------------------
/examples/app-with-eslint7/.eslintrc.js:
--------------------------------------------------------------------------------
1 | require('@rushstack/eslint-patch/modern-module-resolution');
2 |
3 | module.exports = {
4 | root: true,
5 | env: {
6 | browser: true,
7 | es6: true,
8 | },
9 | extends: ['plugin:i18next/recommended'],
10 | globals: {
11 | Atomics: 'readonly',
12 | SharedArrayBuffer: 'readonly',
13 | },
14 | parserOptions: {
15 | ecmaVersion: 2018,
16 | sourceType: 'module',
17 | },
18 | rules: {
19 | 'i18next/no-literal-string': ['error', { mode: 'all' }],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/examples/app-with-eslint7/index.js:
--------------------------------------------------------------------------------
1 | const a = 'hello';
2 | console.log(a);
3 |
--------------------------------------------------------------------------------
/examples/app-with-eslint7/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-with-eslint7",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "node test.mjs"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "eslint-plugin-i18next": "workspace:*",
14 | "zx": "^8.1.6"
15 | },
16 | "devDependencies": {
17 | "@rushstack/eslint-patch": "^1.10.4",
18 | "eslint": "^7.0.0",
19 | "globals": "^15.8.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/app-with-eslint7/test.mjs:
--------------------------------------------------------------------------------
1 | import { $ } from 'zx';
2 | import assert from 'assert';
3 |
4 | (async () => {
5 | await $({})`eslint index.js -f json`.catch(e => {
6 | const [error] = JSON.parse(e.stdout);
7 | assert(error.errorCount === 1, 'expect to have one lint error');
8 | });
9 | })();
10 |
--------------------------------------------------------------------------------
/examples/app-with-eslint9/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import pluginJs from '@eslint/js';
3 | import i18next from 'eslint-plugin-i18next';
4 |
5 | export default [
6 | {
7 | languageOptions: { globals: globals.browser },
8 | linterOptions: { reportUnusedDisableDirectives: 'error' },
9 | },
10 | pluginJs.configs.recommended,
11 | i18next.configs['flat/recommended'],
12 | {
13 | rules: { 'i18next/no-literal-string': ['error', { mode: 'all' }] },
14 | },
15 | ];
16 |
--------------------------------------------------------------------------------
/examples/app-with-eslint9/index.js:
--------------------------------------------------------------------------------
1 | // if it doesn't break the rule no-literal-string, it will report unused disable directive
2 |
3 | // eslint-disable-next-line i18next/no-literal-string
4 | const a = 'hello';
5 | console.log(a);
6 |
--------------------------------------------------------------------------------
/examples/app-with-eslint9/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-with-eslint9",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "eslint index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "eslint-plugin-i18next": "workspace:*"
14 | },
15 | "devDependencies": {
16 | "@eslint/js": "^9.6.0",
17 | "eslint": "^9.6.0",
18 | "globals": "^15.8.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | exports.DOM_TAGS = [
2 | 'a',
3 | 'abbr',
4 | 'acronym',
5 | 'address',
6 | 'applet',
7 | 'area',
8 | 'article',
9 | 'aside',
10 | 'audio',
11 | 'b',
12 | 'base',
13 | 'basefont',
14 | 'bdi',
15 | 'bdo',
16 | 'big',
17 | 'blockquote',
18 | 'body',
19 | 'br',
20 | 'button',
21 | 'canvas',
22 | 'caption',
23 | 'center',
24 | 'cite',
25 | 'code',
26 | 'col',
27 | 'colgroup',
28 | 'data',
29 | 'datalist',
30 | 'dd',
31 | 'del',
32 | 'details',
33 | 'dfn',
34 | 'dialog',
35 | 'dir',
36 | 'div',
37 | 'dl',
38 | 'dt',
39 | 'em',
40 | 'embed',
41 | 'fieldset',
42 | 'figcaption',
43 | 'figure',
44 | 'font',
45 | 'footer',
46 | 'form',
47 | 'frame',
48 | 'frameset',
49 | 'h1 to h6',
50 | 'head',
51 | 'header',
52 | 'hr',
53 | 'html',
54 | 'i',
55 | 'iframe',
56 | 'img',
57 | 'input',
58 | 'ins',
59 | 'kbd',
60 | 'label',
61 | 'legend',
62 | 'li',
63 | 'link',
64 | 'main',
65 | 'map',
66 | 'mark',
67 | 'meta',
68 | 'meter',
69 | 'nav',
70 | 'noframes',
71 | 'noscript',
72 | 'object',
73 | 'ol',
74 | 'optgroup',
75 | 'option',
76 | 'output',
77 | 'p',
78 | 'param',
79 | 'picture',
80 | 'pre',
81 | 'progress',
82 | 'q',
83 | 'rp',
84 | 'rt',
85 | 'ruby',
86 | 's',
87 | 'samp',
88 | 'script',
89 | 'section',
90 | 'select',
91 | 'small',
92 | 'source',
93 | 'span',
94 | 'strike',
95 | 'strong',
96 | 'style',
97 | 'sub',
98 | 'summary',
99 | 'sup',
100 | 'svg',
101 | 'table',
102 | 'tbody',
103 | 'td',
104 | 'template',
105 | 'textarea',
106 | 'tfoot',
107 | 'th',
108 | 'thead',
109 | 'time',
110 | 'title',
111 | 'tr',
112 | 'track',
113 | 'tt',
114 | 'u',
115 | 'ul',
116 | 'var',
117 | 'video',
118 | 'wbr'
119 | ];
120 |
121 | exports.SVG_TAGS = [
122 | 'a',
123 | 'animate',
124 | 'animateMotion',
125 | 'animateTransform',
126 | 'circle',
127 | 'clipPath',
128 | 'color-profile',
129 | 'defs',
130 | 'desc',
131 | 'discard',
132 | 'ellipse',
133 | 'feBlend',
134 | 'feColorMatrix',
135 | 'feComponentTransfer',
136 | 'feComposite',
137 | 'feConvolveMatrix',
138 | 'feDiffuseLighting',
139 | 'feDisplacementMap',
140 | 'feDistantLight',
141 | 'feDropShadow',
142 | 'feFlood',
143 | 'feFuncA',
144 | 'feFuncB',
145 | 'feFuncG',
146 | 'feFuncR',
147 | 'feGaussianBlur',
148 | 'feImage',
149 | 'feMerge',
150 | 'feMergeNode',
151 | 'feMorphology',
152 | 'feOffset',
153 | 'fePointLight',
154 | 'feSpecularLighting',
155 | 'feSpotLight',
156 | 'feTile',
157 | 'feTurbulence',
158 | 'filter',
159 | 'foreignObject',
160 | 'g',
161 | 'hatch',
162 | 'hatchpath',
163 | 'image',
164 | 'line',
165 | 'linearGradient',
166 | 'marker',
167 | 'mask',
168 | 'mesh',
169 | 'meshgradient',
170 | 'meshpatch',
171 | 'meshrow',
172 | 'metadata',
173 | 'mpath',
174 | 'path',
175 | 'pattern',
176 | 'polygon',
177 | 'polyline',
178 | 'radialGradient',
179 | 'rect',
180 | 'script',
181 | 'set',
182 | 'solidcolor',
183 | 'stop',
184 | 'style',
185 | 'svg',
186 | 'switch',
187 | 'symbol',
188 | 'text',
189 | 'textPath',
190 | 'title',
191 | 'tspan',
192 | 'unknown',
193 | 'use',
194 | 'view'
195 | ];
196 |
--------------------------------------------------------------------------------
/lib/helper/generateFullMatchRegExp.js:
--------------------------------------------------------------------------------
1 | module.exports = function generateFullMatchRegExp(source) {
2 | if (source instanceof RegExp) {
3 | return source;
4 | }
5 | if (typeof source !== 'string') {
6 | throw new Error('generateFullMatchRegExp: expect string but get', source);
7 | }
8 | // allow dot ahead
9 | return new RegExp(`(^|\\.)${source}${source.endsWith('$') ? '' : '$'}`);
10 | };
11 |
--------------------------------------------------------------------------------
/lib/helper/getNearestAncestor.js:
--------------------------------------------------------------------------------
1 | module.exports = function getNearestAncestor(node, type) {
2 | let temp = node.parent;
3 | while (temp) {
4 | if (temp.type === type) {
5 | return temp;
6 | }
7 | temp = temp.parent;
8 | }
9 | return temp;
10 | };
11 |
--------------------------------------------------------------------------------
/lib/helper/index.js:
--------------------------------------------------------------------------------
1 | const { DOM_TAGS, SVG_TAGS } = require('../constants');
2 |
3 | function isUpperCase(str) {
4 | return /^[A-Z_-]+$/.test(str);
5 | }
6 |
7 | function isNativeDOMTag(str) {
8 | return DOM_TAGS.includes(str);
9 | }
10 |
11 | function isSvgTag(str) {
12 | return SVG_TAGS.includes(str);
13 | }
14 |
15 | const blacklistAttrs = ['placeholder', 'alt', 'aria-label', 'value', 'title'];
16 | function isAllowedDOMAttr(tag, attr) {
17 | if (isSvgTag(tag)) return true;
18 | if (isNativeDOMTag(tag)) {
19 | return !blacklistAttrs.includes(attr);
20 | }
21 | return false;
22 | }
23 |
24 | exports.isUpperCase = isUpperCase;
25 | exports.isAllowedDOMAttr = isAllowedDOMAttr;
26 | exports.generateFullMatchRegExp = require('./generateFullMatchRegExp');
27 | exports.matchPatterns = require('./matchPatterns');
28 | exports.shouldSkip = require('./shouldSkip');
29 | exports.getNearestAncestor = require('./getNearestAncestor');
30 |
--------------------------------------------------------------------------------
/lib/helper/matchPatterns.js:
--------------------------------------------------------------------------------
1 | const { generateFullMatchRegExp } = require('.');
2 |
3 | const cache = new WeakMap();
4 |
5 | module.exports = function matchPatterns(patterns, text) {
6 | let handler = cache.get(patterns);
7 | if (handler) {
8 | return handler(text);
9 | }
10 | handler = str => {
11 | return patterns.map(generateFullMatchRegExp).some(item => item.test(str));
12 | };
13 | cache.set(patterns, handler);
14 | return handler(text);
15 | };
16 |
--------------------------------------------------------------------------------
/lib/helper/shouldSkip.js:
--------------------------------------------------------------------------------
1 | const matchPatterns = require('./matchPatterns');
2 |
3 | module.exports = function shouldSkip({ exclude = [], include = [] }, text) {
4 | if (!include.length && !exclude.length) return false;
5 |
6 | if (include.length && matchPatterns(include, text)) return false;
7 |
8 | if (exclude.length && !matchPatterns(exclude, text)) return false;
9 |
10 | return true;
11 | };
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { ESLint } from 'eslint';
2 |
3 | declare const plugin: ESLint.Plugin & {
4 | configs: {
5 | 'flat/recommended': ESLint.ConfigData
6 | }
7 | };
8 |
9 | export = plugin;
10 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview ESLint plugin for i18n
3 | * @author edvardchen
4 | */
5 | 'use strict';
6 |
7 | //------------------------------------------------------------------------------
8 | // Requirements
9 | //------------------------------------------------------------------------------
10 |
11 | var requireIndex = require('requireindex');
12 |
13 | //------------------------------------------------------------------------------
14 | // Plugin Definition
15 | //------------------------------------------------------------------------------
16 |
17 | // import all rules in lib/rules
18 | const rules = requireIndex(__dirname + '/rules');
19 | /**
20 | * @type {import('eslint').ESLint.Plugin}
21 | */
22 | const plugin = {
23 | rules,
24 |
25 | configs: {
26 | // for ESLint v9
27 | 'flat/recommended': {
28 | plugins: { i18next: { rules } },
29 | rules: {
30 | 'i18next/no-literal-string': [2],
31 | },
32 | },
33 |
34 | // for ESLint below v9
35 | recommended: {
36 | plugins: ['i18next'],
37 | rules: {
38 | 'i18next/no-literal-string': [2],
39 | },
40 | },
41 | },
42 | };
43 |
44 | module.exports = plugin;
45 |
--------------------------------------------------------------------------------
/lib/options/defaults.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | framework: 'react',
3 | mode: 'jsx-text-only',
4 | 'jsx-components': {
5 | include: [],
6 | exclude: ['Trans'],
7 | },
8 | 'jsx-attributes': {
9 | include: [],
10 | exclude: [
11 | 'className',
12 | 'styleName',
13 | 'style',
14 | 'type',
15 | 'key',
16 | 'id',
17 | 'width',
18 | 'height',
19 | ],
20 | },
21 | words: {
22 | exclude: [
23 | '[0-9!-/:-@[-`{-~]+',
24 | '[A-Z_-]+',
25 | require('./htmlEntities'),
26 | /^\p{Emoji}+$/u,
27 | ],
28 | },
29 | callees: {
30 | exclude: [
31 | 'i18n(ext)?',
32 | 't',
33 | 'require',
34 | 'addEventListener',
35 | 'removeEventListener',
36 | 'postMessage',
37 | 'getElementById',
38 | 'dispatch',
39 | 'commit',
40 | 'includes',
41 | 'indexOf',
42 | 'endsWith',
43 | 'startsWith',
44 | ],
45 | },
46 | 'object-properties': {
47 | include: [],
48 | exclude: ['[A-Z_-]+'],
49 | },
50 | 'class-properties': {
51 | include: [],
52 | exclude: ['displayName'],
53 | },
54 | message: 'disallow literal string',
55 | 'should-validate-template': false,
56 | };
57 |
--------------------------------------------------------------------------------
/lib/options/htmlEntities.js:
--------------------------------------------------------------------------------
1 | // copy from https://github.com/babel/babel/blob/8d17ae6/packages/babel-parser/src/plugins/jsx/xhtml.ts
2 | const entities = {
3 | quot: '\u0022',
4 | amp: '&',
5 | apos: '\u0027',
6 | lt: '<',
7 | gt: '>',
8 | nbsp: '\u00A0',
9 | iexcl: '\u00A1',
10 | cent: '\u00A2',
11 | pound: '\u00A3',
12 | curren: '\u00A4',
13 | yen: '\u00A5',
14 | brvbar: '\u00A6',
15 | sect: '\u00A7',
16 | uml: '\u00A8',
17 | copy: '\u00A9',
18 | ordf: '\u00AA',
19 | laquo: '\u00AB',
20 | not: '\u00AC',
21 | shy: '\u00AD',
22 | reg: '\u00AE',
23 | macr: '\u00AF',
24 | deg: '\u00B0',
25 | plusmn: '\u00B1',
26 | sup2: '\u00B2',
27 | sup3: '\u00B3',
28 | acute: '\u00B4',
29 | micro: '\u00B5',
30 | para: '\u00B6',
31 | middot: '\u00B7',
32 | cedil: '\u00B8',
33 | sup1: '\u00B9',
34 | ordm: '\u00BA',
35 | raquo: '\u00BB',
36 | frac14: '\u00BC',
37 | frac12: '\u00BD',
38 | frac34: '\u00BE',
39 | iquest: '\u00BF',
40 | Agrave: '\u00C0',
41 | Aacute: '\u00C1',
42 | Acirc: '\u00C2',
43 | Atilde: '\u00C3',
44 | Auml: '\u00C4',
45 | Aring: '\u00C5',
46 | AElig: '\u00C6',
47 | Ccedil: '\u00C7',
48 | Egrave: '\u00C8',
49 | Eacute: '\u00C9',
50 | Ecirc: '\u00CA',
51 | Euml: '\u00CB',
52 | Igrave: '\u00CC',
53 | Iacute: '\u00CD',
54 | Icirc: '\u00CE',
55 | Iuml: '\u00CF',
56 | ETH: '\u00D0',
57 | Ntilde: '\u00D1',
58 | Ograve: '\u00D2',
59 | Oacute: '\u00D3',
60 | Ocirc: '\u00D4',
61 | Otilde: '\u00D5',
62 | Ouml: '\u00D6',
63 | times: '\u00D7',
64 | Oslash: '\u00D8',
65 | Ugrave: '\u00D9',
66 | Uacute: '\u00DA',
67 | Ucirc: '\u00DB',
68 | Uuml: '\u00DC',
69 | Yacute: '\u00DD',
70 | THORN: '\u00DE',
71 | szlig: '\u00DF',
72 | agrave: '\u00E0',
73 | aacute: '\u00E1',
74 | acirc: '\u00E2',
75 | atilde: '\u00E3',
76 | auml: '\u00E4',
77 | aring: '\u00E5',
78 | aelig: '\u00E6',
79 | ccedil: '\u00E7',
80 | egrave: '\u00E8',
81 | eacute: '\u00E9',
82 | ecirc: '\u00EA',
83 | euml: '\u00EB',
84 | igrave: '\u00EC',
85 | iacute: '\u00ED',
86 | icirc: '\u00EE',
87 | iuml: '\u00EF',
88 | eth: '\u00F0',
89 | ntilde: '\u00F1',
90 | ograve: '\u00F2',
91 | oacute: '\u00F3',
92 | ocirc: '\u00F4',
93 | otilde: '\u00F5',
94 | ouml: '\u00F6',
95 | divide: '\u00F7',
96 | oslash: '\u00F8',
97 | ugrave: '\u00F9',
98 | uacute: '\u00FA',
99 | ucirc: '\u00FB',
100 | uuml: '\u00FC',
101 | yacute: '\u00FD',
102 | thorn: '\u00FE',
103 | yuml: '\u00FF',
104 | OElig: '\u0152',
105 | oelig: '\u0153',
106 | Scaron: '\u0160',
107 | scaron: '\u0161',
108 | Yuml: '\u0178',
109 | fnof: '\u0192',
110 | circ: '\u02C6',
111 | tilde: '\u02DC',
112 | Alpha: '\u0391',
113 | Beta: '\u0392',
114 | Gamma: '\u0393',
115 | Delta: '\u0394',
116 | Epsilon: '\u0395',
117 | Zeta: '\u0396',
118 | Eta: '\u0397',
119 | Theta: '\u0398',
120 | Iota: '\u0399',
121 | Kappa: '\u039A',
122 | Lambda: '\u039B',
123 | Mu: '\u039C',
124 | Nu: '\u039D',
125 | Xi: '\u039E',
126 | Omicron: '\u039F',
127 | Pi: '\u03A0',
128 | Rho: '\u03A1',
129 | Sigma: '\u03A3',
130 | Tau: '\u03A4',
131 | Upsilon: '\u03A5',
132 | Phi: '\u03A6',
133 | Chi: '\u03A7',
134 | Psi: '\u03A8',
135 | Omega: '\u03A9',
136 | alpha: '\u03B1',
137 | beta: '\u03B2',
138 | gamma: '\u03B3',
139 | delta: '\u03B4',
140 | epsilon: '\u03B5',
141 | zeta: '\u03B6',
142 | eta: '\u03B7',
143 | theta: '\u03B8',
144 | iota: '\u03B9',
145 | kappa: '\u03BA',
146 | lambda: '\u03BB',
147 | mu: '\u03BC',
148 | nu: '\u03BD',
149 | xi: '\u03BE',
150 | omicron: '\u03BF',
151 | pi: '\u03C0',
152 | rho: '\u03C1',
153 | sigmaf: '\u03C2',
154 | sigma: '\u03C3',
155 | tau: '\u03C4',
156 | upsilon: '\u03C5',
157 | phi: '\u03C6',
158 | chi: '\u03C7',
159 | psi: '\u03C8',
160 | omega: '\u03C9',
161 | thetasym: '\u03D1',
162 | upsih: '\u03D2',
163 | piv: '\u03D6',
164 | ensp: '\u2002',
165 | emsp: '\u2003',
166 | thinsp: '\u2009',
167 | zwnj: '\u200C',
168 | zwj: '\u200D',
169 | lrm: '\u200E',
170 | rlm: '\u200F',
171 | ndash: '\u2013',
172 | mdash: '\u2014',
173 | lsquo: '\u2018',
174 | rsquo: '\u2019',
175 | sbquo: '\u201A',
176 | ldquo: '\u201C',
177 | rdquo: '\u201D',
178 | bdquo: '\u201E',
179 | dagger: '\u2020',
180 | Dagger: '\u2021',
181 | bull: '\u2022',
182 | hellip: '\u2026',
183 | permil: '\u2030',
184 | prime: '\u2032',
185 | Prime: '\u2033',
186 | lsaquo: '\u2039',
187 | rsaquo: '\u203A',
188 | oline: '\u203E',
189 | frasl: '\u2044',
190 | euro: '\u20AC',
191 | image: '\u2111',
192 | weierp: '\u2118',
193 | real: '\u211C',
194 | trade: '\u2122',
195 | alefsym: '\u2135',
196 | larr: '\u2190',
197 | uarr: '\u2191',
198 | rarr: '\u2192',
199 | darr: '\u2193',
200 | harr: '\u2194',
201 | crarr: '\u21B5',
202 | lArr: '\u21D0',
203 | uArr: '\u21D1',
204 | rArr: '\u21D2',
205 | dArr: '\u21D3',
206 | hArr: '\u21D4',
207 | forall: '\u2200',
208 | part: '\u2202',
209 | exist: '\u2203',
210 | empty: '\u2205',
211 | nabla: '\u2207',
212 | isin: '\u2208',
213 | notin: '\u2209',
214 | ni: '\u220B',
215 | prod: '\u220F',
216 | sum: '\u2211',
217 | minus: '\u2212',
218 | lowast: '\u2217',
219 | radic: '\u221A',
220 | prop: '\u221D',
221 | infin: '\u221E',
222 | ang: '\u2220',
223 | and: '\u2227',
224 | or: '\u2228',
225 | cap: '\u2229',
226 | cup: '\u222A',
227 | int: '\u222B',
228 | there4: '\u2234',
229 | sim: '\u223C',
230 | cong: '\u2245',
231 | asymp: '\u2248',
232 | ne: '\u2260',
233 | equiv: '\u2261',
234 | le: '\u2264',
235 | ge: '\u2265',
236 | sub: '\u2282',
237 | sup: '\u2283',
238 | nsub: '\u2284',
239 | sube: '\u2286',
240 | supe: '\u2287',
241 | oplus: '\u2295',
242 | otimes: '\u2297',
243 | perp: '\u22A5',
244 | sdot: '\u22C5',
245 | lceil: '\u2308',
246 | rceil: '\u2309',
247 | lfloor: '\u230A',
248 | rfloor: '\u230B',
249 | lang: '\u2329',
250 | rang: '\u232A',
251 | loz: '\u25CA',
252 | spades: '\u2660',
253 | clubs: '\u2663',
254 | hearts: '\u2665',
255 | diams: '\u2666',
256 | };
257 | module.exports = new RegExp(`^(${Object.values(entities).join('|')})+$`);
258 |
--------------------------------------------------------------------------------
/lib/options/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "properties": {
5 | "framework":{
6 | "type":"string",
7 | "enum": ["react","vue"]
8 | },
9 | "mode": {
10 | "type": "string",
11 | "enum": ["jsx-text-only", "jsx-only", "all","vue-template-only"]
12 | },
13 | "jsx-components": {
14 | "type": "object",
15 | "properties": {
16 | "include": { "type": "array" },
17 | "exclude": { "type": "array" }
18 | }
19 | },
20 | "jsx-attributes": {
21 | "type": "object",
22 | "properties": {
23 | "include": { "type": "array" },
24 | "exclude": { "type": "array" }
25 | }
26 | },
27 | "words": {
28 | "type": "object",
29 | "properties": {
30 | "exclude": { "type": "array" }
31 | }
32 | },
33 | "callees": {
34 | "type": "object",
35 | "properties": {
36 | "include": { "type": "array" },
37 | "exclude": { "type": "array" }
38 | }
39 | },
40 | "object-properties": {
41 | "type": "object",
42 | "properties": {
43 | "include": { "type": "array" },
44 | "exclude": { "type": "array" }
45 | }
46 | },
47 | "class-properties": {
48 | "type": "object",
49 | "properties": {
50 | "include": { "type": "array" },
51 | "exclude": { "type": "array" }
52 | }
53 | },
54 | "message": { "type": "string" },
55 | "should-validate-template": { "type": "boolean" }
56 | }
57 | }
--------------------------------------------------------------------------------
/lib/rules/no-literal-string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview disallow literal string
3 | * @author edvardchen
4 | */
5 | 'use strict';
6 |
7 | const _ = require('lodash');
8 | const {
9 | isUpperCase,
10 | getNearestAncestor,
11 | isAllowedDOMAttr,
12 | shouldSkip,
13 | } = require('../helper');
14 |
15 | function isValidFunctionCall(context, options, { callee }) {
16 | if (callee.type === 'Import') return true;
17 |
18 | const sourceText = context.getSourceCode().getText(callee);
19 |
20 | return shouldSkip(options.callees, sourceText);
21 | }
22 |
23 | function isValidLiteral(options, { value }) {
24 | if (typeof value !== 'string') {
25 | return true;
26 | }
27 | const trimed = value.trim();
28 | if (!trimed) return true;
29 |
30 | if (shouldSkip(options.words, trimed)) return true;
31 | }
32 |
33 | /**
34 | * @param {VDirective | VAttribute} node
35 | * @returns {string | null}
36 | */
37 | function getAttributeName(node) {
38 | if (!node.directive) {
39 | return node.key.rawName;
40 | }
41 |
42 | if (
43 | (node.key.name.name === 'bind' || node.key.name.name === 'model') &&
44 | node.key.argument &&
45 | node.key.argument.type === 'VIdentifier'
46 | ) {
47 | return node.key.argument.rawName;
48 | }
49 |
50 | return null;
51 | }
52 |
53 | //------------------------------------------------------------------------------
54 | // Rule Definition
55 | //------------------------------------------------------------------------------
56 |
57 | module.exports = {
58 | meta: {
59 | docs: {
60 | description: 'disallow literal string',
61 | category: 'Best Practices',
62 | recommended: true,
63 | },
64 | schema: [require('../options/schema.json')],
65 | },
66 |
67 | create(context) {
68 | // variables should be defined here
69 | const parserServices =
70 | context.parserServices || context.sourceCode.parserServices;
71 | const options = _.defaults(
72 | {},
73 | context.options[0],
74 | require('../options/defaults')
75 | );
76 |
77 | const {
78 | mode,
79 | 'should-validate-template': validateTemplate,
80 | message,
81 | framework,
82 | } = options;
83 | const onlyValidateJSX = ['jsx-only', 'jsx-text-only'].includes(mode);
84 |
85 | const onlyValidateVueTemplate =
86 | framework === 'vue' && mode === 'vue-template-only';
87 |
88 | //----------------------------------------------------------------------
89 | // Helpers
90 | //----------------------------------------------------------------------
91 |
92 | const indicatorStack = [];
93 |
94 | function endIndicator() {
95 | indicatorStack.pop();
96 | }
97 |
98 | /**
99 | * detect if current "scope" is valid
100 | */
101 | function isValidScope() {
102 | return indicatorStack.some(item => item);
103 | }
104 |
105 | //----------------------------------------------------------------------
106 | // Public
107 | //----------------------------------------------------------------------
108 |
109 | function report(node) {
110 | context.report({
111 | node,
112 | message: `${message}: ${context.getSourceCode().getText(node.parent)}`,
113 | });
114 | }
115 |
116 | const { esTreeNodeToTSNodeMap, program } = parserServices;
117 | let typeChecker;
118 | if (program && esTreeNodeToTSNodeMap)
119 | typeChecker = program.getTypeChecker();
120 |
121 | function validateBeforeReport(node) {
122 | if (isValidScope()) return;
123 | if (isValidLiteral(options, node)) return;
124 |
125 | // ─── Typescript ──────────────────────────────────────
126 |
127 | if (typeChecker) {
128 | const tsNode = esTreeNodeToTSNodeMap.get(node);
129 | const typeObj = typeChecker.getContextualType(tsNode);
130 | if (typeObj) {
131 | // var a: 'abc' = 'abc'
132 | if (typeObj.isStringLiteral()) {
133 | return;
134 | }
135 |
136 | // var a: 'abc' | 'name' = 'abc'
137 | if (typeObj.isUnion()) {
138 | const found = typeObj.types.some(item => {
139 | if (item.isStringLiteral() && item.value === node.value) {
140 | return true;
141 | }
142 | });
143 | if (found) return;
144 | }
145 | }
146 | }
147 | // ─────────────────────────────────────────────────────
148 |
149 | report(node);
150 | }
151 |
152 | function filterOutJSX(node) {
153 | if (onlyValidateJSX) {
154 | const isInsideJSX = (
155 | context.getAncestors || context.sourceCode.getAncestors
156 | )(node).some(item => ['JSXElement', 'JSXFragment'].includes(item.type));
157 |
158 | if (!isInsideJSX) return true;
159 |
160 | if (
161 | mode === 'jsx-text-only' &&
162 | !['JSXElement', 'JSXFragment'].includes(node.parent.type)
163 | ) {
164 | // Under mode jsx-text-only, if the direct parent isn't JSXElement or JSXFragment then skip
165 | return true;
166 | }
167 | }
168 | return false;
169 | }
170 |
171 | const scriptVisitor = {
172 | //
173 | // ─── EXPORT AND IMPORT ───────────────────────────────────────────
174 | //
175 |
176 | ImportExpression(node) {
177 | // allow (import('abc'))
178 | indicatorStack.push(true);
179 | },
180 | 'ImportExpression:exit': endIndicator,
181 |
182 | ImportDeclaration(node) {
183 | // allow (import abc form 'abc')
184 | indicatorStack.push(true);
185 | },
186 | 'ImportDeclaration:exit': endIndicator,
187 |
188 | ExportAllDeclaration(node) {
189 | // allow export * from 'mod'
190 | indicatorStack.push(true);
191 | },
192 | 'ExportAllDeclaration:exit': endIndicator,
193 |
194 | 'ExportNamedDeclaration[source]'(node) {
195 | // allow export { named } from 'mod'
196 | indicatorStack.push(true);
197 | },
198 | 'ExportNamedDeclaration[source]:exit': endIndicator,
199 | // ─────────────────────────────────────────────────────────────────
200 |
201 | //
202 | // ─── JSX ─────────────────────────────────────────────────────────
203 | //
204 |
205 | JSXElement(node) {
206 | const fullComponentName = context
207 | .getSourceCode()
208 | .getText(node.openingElement.name);
209 | indicatorStack.push(
210 | shouldSkip(options['jsx-components'], fullComponentName)
211 | );
212 | },
213 | 'JSXElement:exit': endIndicator,
214 |
215 | JSXAttribute(node) {
216 | const attrName = node.name.name;
217 |
218 | // allow
219 | if (shouldSkip(options['jsx-attributes'], attrName)) {
220 | indicatorStack.push(true);
221 | return;
222 | }
223 |
224 | const jsxElement = getNearestAncestor(node, 'JSXOpeningElement');
225 | const tagName = jsxElement.name.name;
226 | if (isAllowedDOMAttr(tagName, attrName)) {
227 | indicatorStack.push(true);
228 | return;
229 | }
230 | indicatorStack.push(false);
231 | },
232 | 'JSXAttribute:exit': endIndicator,
233 |
234 | // @typescript-eslint/parser would parse string literal as JSXText node
235 | JSXText(node) {
236 | validateBeforeReport(node);
237 | },
238 | // ─────────────────────────────────────────────────────────────────
239 |
240 | //
241 | // ─── Vue ──────────────────────────────────────────────────
242 | //
243 | VElement(node) {
244 | indicatorStack.push(
245 | shouldSkip(options['jsx-components'], node.rawName)
246 | );
247 | },
248 | 'VElement:exit': endIndicator,
249 | VAttribute(node) {
250 | const attrName = getAttributeName(node);
251 | indicatorStack.push(shouldSkip(options['jsx-attributes'], attrName));
252 | },
253 | 'VAttribute:exit': endIndicator,
254 | // ─────────────────────────────────────────────────────────────────
255 |
256 | //
257 | // ─── TYPESCRIPT ──────────────────────────────────────────────────
258 | //
259 |
260 | TSModuleDeclaration() {
261 | indicatorStack.push(true);
262 | },
263 | 'TSModuleDeclaration:exit': endIndicator,
264 |
265 | TSLiteralType(node) {
266 | // allow var a: Type['member'];
267 | indicatorStack.push(true);
268 | },
269 | 'TSLiteralType:exit': endIndicator,
270 | TSEnumMember(node) {
271 | // allow enum E { "a b" = 1 }
272 | indicatorStack.push(true);
273 | },
274 | 'TSEnumMember:exit': endIndicator,
275 | // ─────────────────────────────────────────────────────────────────
276 |
277 | ClassProperty(node) {
278 | indicatorStack.push(
279 | !!(node.key && shouldSkip(options['class-properties'], node.key.name))
280 | );
281 | },
282 | 'ClassProperty:exit': endIndicator,
283 |
284 | VariableDeclarator(node) {
285 | // allow statements like const A_B = "test"
286 | indicatorStack.push(isUpperCase(node.id.name));
287 | },
288 | 'VariableDeclarator:exit': endIndicator,
289 |
290 | Property(node) {
291 | // pick up key.name if key is Identifier or key.value if key is Literal
292 | // dont care whether if this is computed or not
293 | const result = shouldSkip(
294 | options['object-properties'],
295 | node.key.name || node.key.value
296 | );
297 | indicatorStack.push(result);
298 | },
299 | 'Property:exit': endIndicator,
300 |
301 | BinaryExpression(node) {
302 | const { operator } = node;
303 | // allow name === 'Android'
304 | indicatorStack.push(operator !== '+');
305 | },
306 | 'BinaryExpression:exit': endIndicator,
307 |
308 | AssignmentPattern(node) {
309 | // allow function bar(input = 'foo') {}
310 | indicatorStack.push(true);
311 | },
312 | 'AssignmentPattern:exit': endIndicator,
313 |
314 | NewExpression(node) {
315 | indicatorStack.push(isValidFunctionCall(context, options, node));
316 | },
317 | 'NewExpression:exit': endIndicator,
318 |
319 | CallExpression(node) {
320 | indicatorStack.push(isValidFunctionCall(context, options, node));
321 | },
322 | 'CallExpression:exit': endIndicator,
323 |
324 | TaggedTemplateExpression(node) {
325 | indicatorStack.push(
326 | isValidFunctionCall(context, options, { callee: node.tag })
327 | );
328 | },
329 | 'TaggedTemplateExpression:exit': endIndicator,
330 | 'AssignmentExpression[left.type="MemberExpression"]'(node) {
331 | // allow Enum['value']
332 | indicatorStack.push(
333 | shouldSkip(options['object-properties'], node.left.property.name)
334 | );
335 | },
336 | 'AssignmentExpression[left.type="MemberExpression"]:exit'(node) {
337 | endIndicator();
338 | },
339 | TemplateLiteral(node) {
340 | if (!validateTemplate) {
341 | return;
342 | }
343 |
344 | if (framework === 'react' && filterOutJSX(node)) {
345 | return;
346 | }
347 |
348 | if (isValidScope()) return;
349 | const { quasis = [] } = node;
350 | quasis.some(({ value: { raw } }) => {
351 | if (isValidLiteral(options, { value: raw })) return;
352 | report(node);
353 | return true; // break
354 | });
355 | },
356 | Literal(node) {
357 | // allow Enum['value'] and literal that follows the 'case' keyword in a switch statement.
358 | if (['MemberExpression', 'SwitchCase'].includes(node?.parent?.type)) {
359 | return;
360 | }
361 |
362 | if (framework === 'react' && filterOutJSX(node)) {
363 | return;
364 | }
365 |
366 | if (onlyValidateVueTemplate) {
367 | const parents = context.getAncestors();
368 | if (
369 | parents.length &&
370 | parents.every(
371 | item =>
372 | ![
373 | 'VElement',
374 | 'VAttribute',
375 | 'VText',
376 | 'VExpressionContainer',
377 | ].includes(item.type)
378 | )
379 | ) {
380 | return true;
381 | }
382 | }
383 |
384 | // ignore `var a = { "foo": 123 }`
385 | if (node.parent.key === node) {
386 | return;
387 | }
388 |
389 | validateBeforeReport(node);
390 | },
391 | };
392 |
393 | return (
394 | (parserServices.defineTemplateBodyVisitor &&
395 | parserServices.defineTemplateBodyVisitor(
396 | {
397 | VText(node) {
398 | scriptVisitor['JSXText'](node);
399 | },
400 | VLiteral(node) {
401 | scriptVisitor['JSXText'](node);
402 | },
403 | VElement(node) {
404 | scriptVisitor['VElement'](node);
405 | },
406 | 'VElement:exit'(node) {
407 | scriptVisitor['VElement:exit'](node);
408 | },
409 | VAttribute(node) {
410 | scriptVisitor['VAttribute'](node);
411 | },
412 | 'VAttribute:exit'(node) {
413 | scriptVisitor['VAttribute:exit'](node);
414 | },
415 | 'VExpressionContainer CallExpression'(node) {
416 | scriptVisitor['CallExpression'](node);
417 | },
418 | 'VExpressionContainer CallExpression:exit'(node) {
419 | scriptVisitor['CallExpression:exit'](node);
420 | },
421 | 'VExpressionContainer Literal'(node) {
422 | scriptVisitor['Literal'](node);
423 | },
424 | },
425 | scriptVisitor
426 | )) ||
427 | scriptVisitor
428 | );
429 | },
430 | };
431 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-i18next",
3 | "version": "6.1.1",
4 | "description": "ESLint plugin for i18n",
5 | "keywords": [
6 | "eslint",
7 | "eslintplugin",
8 | "eslint-plugin",
9 | "i18n",
10 | "i10next",
11 | "internationalization",
12 | "localization"
13 | ],
14 | "author": "edvardchen",
15 | "main": "lib/index.js",
16 | "files": [
17 | "lib"
18 | ],
19 | "types": "lib/index.d.ts",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/edvardchen/eslint-plugin-i18next.git"
23 | },
24 | "scripts": {
25 | "preversion": "npm run test",
26 | "postpublish": "git push --follow-tags",
27 | "test:watch": "npm t -- --watch",
28 | "test": "mocha --timeout 50000 tests/lib/rules/no-literal-string/ && pnpm --filter='./examples/*' run lint"
29 | },
30 | "dependencies": {
31 | "lodash": "^4.17.21",
32 | "requireindex": "~1.1.0"
33 | },
34 | "devDependencies": {
35 | "@commitlint/cli": "^17.6.1",
36 | "@commitlint/config-conventional": "^7.6.0",
37 | "@typescript-eslint/parser": "^1.10.2",
38 | "babel-eslint": "^10.0.1",
39 | "eslint": "^5.16.0",
40 | "husky": "^1.3.1",
41 | "lint-staged": "^9.4.2",
42 | "mocha": "^10.1.0",
43 | "prettier": "^1.18.2",
44 | "typescript": "^3.5.2",
45 | "vue-eslint-parser": "^6.0.3"
46 | },
47 | "engines": {
48 | "node": ">=0.10.0"
49 | },
50 | "lint-staged": {
51 | "*.js": [
52 | "prettier --write",
53 | "git add"
54 | ]
55 | },
56 | "husky": {
57 | "hooks": {
58 | "pre-commit": "lint-staged",
59 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
60 | }
61 | },
62 | "license": "ISC"
63 | }
64 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - examples/*
3 |
--------------------------------------------------------------------------------
/tests/lib/fixtures/invalid-jsx-only.jsx:
--------------------------------------------------------------------------------
1 | var bar = 'ffo' // valid
2 |
3 | const e =
4 | <>
5 | foo
6 | <>foo999>
7 | {"hello world"}
8 |
9 |
10 | >
--------------------------------------------------------------------------------
/tests/lib/fixtures/invalid.jsx:
--------------------------------------------------------------------------------
1 | i18nextXt('taa');
2 |
3 | a + 'b';
4 |
5 | var emojis = "🌼🌺🌸hello world";
6 |
7 | switch (a) {
8 | case '':
9 | var a = 'b';
10 | break;
11 | default:
12 | break;
13 | }
14 |
15 | export const b = 'hello_string';
16 | const c = 'foo';
17 | const d = call('Ffo');
18 | var e = { foo: 'bar' };
19 |
20 | class Form extends Component {
21 | property = 'Something';
22 | }
23 |
24 | <>
25 | foo
26 | フー
27 |
28 |
29 |
30 | 🌸
31 | >
32 |
--------------------------------------------------------------------------------
/tests/lib/fixtures/valid-jsx-text-only.jsx:
--------------------------------------------------------------------------------
1 | a + 'b';
2 |
3 |
4 | {t("No user details were found")}!
5 |
;
6 |
7 | <>
8 | {import('abc')}
9 | {[].map(item => 'cat')}
10 | {'hello' + 'world'}
11 |
12 | >;
13 |
--------------------------------------------------------------------------------
/tests/lib/fixtures/valid-typescript.ts:
--------------------------------------------------------------------------------
1 | declare module 'country-emoji' {}
2 | var a: Element['nodeName'];
3 | var a3: Omit;
4 |
5 | function Button({ t = 'name' }: { t: string }) {}
6 | // var aa: 'abc' = 'abc';
7 | // var a4: 'abc' | 'name' | undefined = 'abc';
8 |
9 | // type T = { name: 'b' };
10 | // var a5: T = { name: 'b' };
11 |
12 | enum T2 {
13 | howard = 1,
14 | 'a b' = 2,
15 | }
16 | var a6 = T2['howard'];
17 |
18 | function Button2({ t = 'name' }: { t: 'name' }) {}
19 |
20 | type Ta = { t?: 'name' | 'abc' };
21 | let ta: Ta = { t: 'abc' };
22 |
23 | const str: 'str' = 'str';
24 |
25 | type Animal = 'dog' | 'cat';
26 | const animal: Animal = 'dog';
27 |
28 | function Button3({ t = 'name' }: Ta) {}
29 |
--------------------------------------------------------------------------------
/tests/lib/fixtures/valid.jsx:
--------------------------------------------------------------------------------
1 | import('hello');
2 |
3 | // don't validate template by default
4 | var aa = `hello world`;
5 |
6 | function bar(a = 'jianhua') { }
7 |
8 | var emojis = "🌼🌺🌸";
9 |
10 | name === 'Android' || name === 'iOS';
11 |
12 | switch (a) {
13 | case 'a':
14 | break;
15 | default:
16 | break;
17 | }
18 |
19 | import name from 'hello';
20 | a.indexOf('ios');
21 | a.includes('ios');
22 |
23 | a.startsWith('ios');
24 | a.endsWith('@gmail.com');
25 | export * from 'hello_export_all';
26 | export { a } from 'hello_export';
27 | document.addEventListener('click', event => {
28 | event.preventDefault();
29 | });
30 | document.removeEventListener('click', event => {
31 | event.preventDefault();
32 | });
33 | window.postMessage('message', '*');
34 | document.getElementById('some-id');
35 | require('hello');
36 | const a = require(['hello']);
37 | const b = require(['hel' + 'lo']);
38 | const c = 1;
39 | const d = '?';
40 | const e = '0123456789!@#$%^&*()_+|~-=`[]{};\':",./<>?';
41 | i18n('hello');
42 | dispatch('hello');
43 | store.dispatch('hello');
44 | store.commit('hello');
45 | i18n.t('hello');
46 |
47 | const f = 'FOO';
48 | const g = `FOO`;
49 | var A_B = `world`;
50 | var A_B = 'world';
51 | var A_B = ['world'];
52 | var z = { ['A_B']: 'hello world' };
53 | var a1 = { [A_B]: 'hello world' };
54 | var a2 = { A_B: 'hello world' };
55 | var a3 = { foo: 123 };
56 | var a4 = { foo: 'FOO' };
57 |
58 | class Form extends Component {
59 | displayName = 'FormContainer';
60 | }
61 |
62 | <>
63 |
64 |
65 | {i18next.t('foo')}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | foo
77 |
78 | bar
79 |
80 | ×&
81 | {import("abc")}
82 | >;
83 |
--------------------------------------------------------------------------------
/tests/lib/helpers/runTest.js:
--------------------------------------------------------------------------------
1 | const RuleTester = require('eslint').RuleTester;
2 | const rule = require('../../../lib/rules/no-literal-string');
3 |
4 | var ruleTester = new RuleTester({
5 | parser: require.resolve('babel-eslint'),
6 | parserOptions: { sourceType: 'module', ecmaFeatures: { jsx: true } },
7 | });
8 |
9 | module.exports = function runText(name, cases) {
10 | ruleTester.run(name, rule, cases);
11 | };
12 |
--------------------------------------------------------------------------------
/tests/lib/helpers/testFile.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | module.exports = function testFile(file) {
5 | return {
6 | code: fs.readFileSync(path.join(__dirname, '../fixtures', file), {
7 | encoding: 'utf8',
8 | }),
9 | options: [{ mode: 'all' }],
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/all.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [testFile('valid.jsx')],
6 | invalid: [
7 | { ...testFile('invalid.jsx'), errors: 15 },
8 | {
9 | code: `export const validationSchema = Yup.object({
10 | email: Yup
11 | .string()
12 | .email('hello')
13 | .required('world'),
14 | })`,
15 | options: [{ mode: 'all' }],
16 | errors: 2,
17 | },
18 | ],
19 | };
20 |
21 | runTest('no-literal-string: mode all', cases);
22 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/callees.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [
6 | {
7 | code: 'new Error("hello")',
8 | options: [{ mode: 'all', callees: { exclude: ['Error'] } }],
9 | },
10 | {
11 | code: 'foo.bar("taa");',
12 | options: [{ mode: 'all', callees: { exclude: ['foo.+'] } }],
13 | },
14 | ],
15 | invalid: [{ ...testFile('invalid.jsx'), errors: 15 }],
16 | };
17 |
18 | runTest('no-literal-string: callees', cases);
19 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/default.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [{ code: 'const a = "absfoo";' }],
6 | invalid: [{ ...testFile('invalid.jsx'), errors: 15 }],
7 | };
8 |
9 | runTest('no-literal-string: default', cases);
10 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/jsx-attributes.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const options = [{ mode: 'all', 'should-validate-template': true }];
5 | const cases = {
6 | valid: [
7 | {
8 | code: '',
9 | options: [{ mode: 'jsx-only', 'jsx-attributes': { exclude: ['foo'] } }],
10 | },
11 | {
12 | code: '',
13 | options: [{ mode: 'all', 'jsx-attributes': { include: ['bar'] } }],
14 | },
15 | ],
16 | invalid: [
17 | {
18 | code: '',
19 | options: [{ mode: 'jsx-only', 'jsx-attributes': { include: ['foo'] } }],
20 | errors: 1,
21 | },
22 | ],
23 | };
24 |
25 | runTest('no-literal-string: jsx-attributes', cases);
26 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/jsx-components.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [
6 | { code: 'arrow' },
7 | {
8 | code: 'arrow',
9 | options: [{ 'jsx-components': { exclude: ['Icon'] } }],
10 | },
11 | {
12 | code: 'arrow',
13 | options: [{ 'jsx-components': { exclude: ['Icon.A'] } }],
14 | },
15 | {
16 | code: '<>hello{i18next.t("KEY")}
>',
17 | options: [{ 'jsx-components': { include: ['p'] } }],
18 | },
19 | ],
20 | invalid: [
21 | {
22 | code: '<>helloworld
>',
23 | options: [{ 'jsx-components': { include: ['span'] } }],
24 | errors: 1,
25 | },
26 | ],
27 | };
28 |
29 | runTest('no-literal-string: jsx-components', cases);
30 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/jsx-only.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 | const cases = {
4 | valid: [
5 | {
6 | code: '',
7 | options: [{ mode: 'jsx-only', 'jsx-attributes': { exclude: ['foo'] } }],
8 | },
9 | {
10 | code: `{[].map((a) => { switch (a) { case 'abc': return null; default: return null; } })}
`,
11 | options: [{ mode: 'jsx-only' }],
12 | },
13 | ],
14 | invalid: [
15 | {
16 | ...testFile('invalid-jsx-only.jsx'),
17 | options: [{ mode: 'jsx-only' }],
18 | errors: 5,
19 | },
20 | ],
21 | };
22 | runTest('no-literal-string: mode jsx-only', cases);
23 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/jsx-text-only.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [
6 | {
7 | ...testFile('valid-jsx-text-only.jsx'),
8 | options: [{ mode: 'jsx-text-only' }],
9 | },
10 | ],
11 | invalid: [{ code: 'hello
', errors: 1 }],
12 | };
13 |
14 | runTest('no-literal-string: mode jsx-text-only', cases);
15 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/object-properties.js:
--------------------------------------------------------------------------------
1 | const runTest = require('../../helpers/runTest');
2 |
3 | const cases = {
4 | valid: [
5 | {
6 | code: 'HomePage.displayName = "HomePage";',
7 | options: [
8 | {
9 | mode: 'all',
10 | 'object-properties': {
11 | include: [],
12 | exclude: ['displayName'],
13 | },
14 | },
15 | ],
16 | },
17 | {
18 | code: 'var a = {foo: "foo"};',
19 | options: [{ mode: 'all', 'object-properties': { exclude: ['foo'] } }],
20 | },
21 | ],
22 | invalid: [],
23 | };
24 |
25 | runTest('no-literal-string: object-properties', cases);
26 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/should-validate-template.js:
--------------------------------------------------------------------------------
1 | const runTest = require('../../helpers/runTest');
2 |
3 | const cases = {
4 | valid: [
5 | {
6 | code: `import(\`hello\`);
7 | var a = \`12345\`
8 | var a = \`\`
9 | `,
10 | options: [{ mode: 'all', 'should-validate-template': true }],
11 | },
12 |
13 | {
14 | code: 'var a = `hello ${abc} world`',
15 | options: [{ mode: 'jsx-only', 'should-validate-template': true }],
16 | },
17 |
18 | {
19 | code: 'const StyledParagaph = styled.p`some css here`',
20 | options: [
21 | {
22 | mode: 'all',
23 | callees: { exclude: ['styled.*'] },
24 | 'should-validate-template': true,
25 | },
26 | ],
27 | },
28 | ],
29 | invalid: [
30 | {
31 | code: 'var a = `hello ${abc} world`',
32 | options: [{ mode: 'all', 'should-validate-template': true }],
33 | errors: 1,
34 | },
35 | {
36 | code: 'var a = "hello world"; <>`abcd`>',
37 | options: [{ mode: 'jsx-only', 'should-validate-template': true }],
38 | errors: 1,
39 | },
40 | ],
41 | };
42 |
43 | runTest('no-literal-string: should-validate-template', cases);
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | "sourceMap": true /* Generates corresponding '.map' file. */,
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | // "outDir": "./", /* Redirect output structure to the directory. */
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true /* Enable all strict type-checking options. */,
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
32 |
33 | /* Additional Checks */
34 | // "noUnusedLocals": true, /* Report errors on unused locals. */
35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
38 | "preserveWatchOutput": true,
39 | "allowUnreachableCode": false,
40 | "resolveJsonModule": true,
41 |
42 | /* Module Resolution Options */
43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
47 | // "typeRoots": [], /* List of folders to include type definitions from. */
48 | "types": [
49 | "node"
50 | ] /* Type declaration files to be included in compilation. */,
51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/typescript.js:
--------------------------------------------------------------------------------
1 | const RuleTester = require('eslint').RuleTester;
2 | const path = require('path');
3 | const rule = require('../../../../lib/rules/no-literal-string');
4 | const testFile = require('../../helpers/testFile');
5 |
6 | const tsTester = new RuleTester({
7 | parser: require.resolve('@typescript-eslint/parser'),
8 | parserOptions: {
9 | sourceType: 'module',
10 | project: path.resolve(__dirname, 'tsconfig.json'),
11 | },
12 | });
13 |
14 | tsTester.run('no-literal-string: typescript', rule, {
15 | valid: [
16 | {
17 | code: 'arrow',
18 | options: [{ 'jsx-components': { exclude: ['Icon'] } }],
19 | filename: 'a.jsx',
20 | },
21 | {
22 | ...testFile('valid-typescript.ts'),
23 | options: [{ mode: 'all' }],
24 | },
25 | {
26 | code: '',
27 | options: [{ mode: 'all' }],
28 | filename: 'a.tsx',
29 | },
30 | ],
31 | invalid: [
32 | {
33 | code: 'let a:string; a="hello"',
34 | options: [{ mode: 'all' }],
35 | errors: 1,
36 | },
37 | {
38 | code: 'const a="foobar"',
39 | options: [{ mode: 'all' }],
40 | errors: 1,
41 | },
42 | {
43 | code: '<>foo123>',
44 | filename: 'a.tsx',
45 | errors: 1,
46 | },
47 | {
48 | code: '<>foo999>',
49 | filename: 'a.tsx',
50 | options: [{ markupOnly: true, message: 'Some error message' }],
51 | errors: [{ message: 'Some error message: <>foo999>' }],
52 | },
53 | {
54 | code: ``,
55 | filename: 'a.tsx',
56 | errors: 1,
57 | },
58 | {
59 | code: "var a: {type: string} = {type: 'bb'}",
60 | options: [{ mode: 'all' }],
61 | errors: 1,
62 | },
63 | ],
64 | });
65 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/vue.js:
--------------------------------------------------------------------------------
1 | const RuleTester = require('eslint').RuleTester;
2 | const rule = require('../../../../lib/rules/no-literal-string');
3 |
4 | const vueTester = new RuleTester({
5 | parser: require.resolve('vue-eslint-parser'),
6 | parserOptions: {
7 | sourceType: 'module',
8 | },
9 | });
10 |
11 | vueTester.run('no-literal-string: vue', rule, {
12 | valid: [
13 | {
14 | code: '{{ i18next.t("abc") }}',
15 | options: [{ mode: 'all' }],
16 | },
17 | {
18 | code:
19 | '',
20 | options: [
21 | {
22 | framework: 'vue',
23 | mode: 'vue-template-only',
24 | 'jsx-attributes': { exclude: ['string-prop'] },
25 | },
26 | ],
27 | errors: 0,
28 | },
29 | ],
30 | invalid: [
31 | {
32 | code: '{{ a("abc") }}',
33 | options: [{ mode: 'all' }],
34 | errors: 1,
35 | },
36 | {
37 | code: 'abc',
38 | errors: 1,
39 | },
40 | {
41 | code: '{{"hello"}}',
42 | options: [{ mode: 'all' }],
43 | errors: 1,
44 | },
45 | {
46 | code:
47 | '',
48 | options: [{ mode: 'all' }],
49 | errors: 1,
50 | },
51 | {
52 | code:
53 | '{{ "this is a string literal in mustaches" }}
',
54 | options: [{ framework: 'vue', mode: 'vue-template-only' }],
55 | errors: 1,
56 | },
57 | ],
58 | });
59 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-literal-string/words.js:
--------------------------------------------------------------------------------
1 | const testFile = require('../../helpers/testFile');
2 | const runTest = require('../../helpers/runTest');
3 |
4 | const cases = {
5 | valid: [
6 | {
7 | code: 'const a = "absfoo";',
8 | options: [{ mode: 'all', words: { exclude: ['.*foo.*'] } }],
9 | },
10 | {
11 | code: 'const a = "fooabc";',
12 | options: [{ mode: 'all', words: { exclude: ['^foo.*'] } }],
13 | },
14 | ],
15 | invalid: [
16 | {
17 | code: 'const a = "afoo";',
18 | options: [{ mode: 'all', words: { exclude: ['^foo'] } }],
19 | errors: 1,
20 | },
21 | ],
22 | };
23 |
24 | runTest('no-literal-string: words', cases);
25 |
--------------------------------------------------------------------------------
/v5.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-i18next
2 |
3 | ESLint plugin for i18n
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install eslint-plugin-i18next --save-dev
9 | ```
10 |
11 | ## Usage
12 |
13 | Add `i18next` to the plugins section of your `.eslintrc` configuration file.
14 |
15 | ```json
16 | {
17 | "plugins": ["i18next"]
18 | }
19 | ```
20 |
21 | Then configure the rules you want to use under the rules section.
22 |
23 | ```json
24 | {
25 | "rules": {
26 | "i18next/no-literal-string": 2
27 | }
28 | }
29 | ```
30 |
31 | or
32 |
33 | ```json
34 | {
35 | "extends": ["plugin:i18next/recommended"]
36 | }
37 | ```
38 |
39 | ## Rule `no-literal-string`
40 |
41 | This rule aims to avoid developers to display literal string to users
42 | in those projects which need to support [multi-language](https://www.i18next.com/).
43 |
44 | > Note: Disable auto-fix because key in the call `i18next.t(key)` ussally was not the same as the literal
45 |
46 | ### Rule Details
47 |
48 | **It will find out all literal strings and validate them.**
49 |
50 | Examples of **incorrect** code for this rule:
51 |
52 | ```js
53 | /*eslint i18next/no-literal-string: "error"*/
54 | const a = 'foo';
55 | ```
56 |
57 | Examples of **correct** code for this rule:
58 |
59 | ```js
60 | /*eslint i18next/no-literal-string: "error"*/
61 | // safe to assign string to const variables whose name are UPPER_CASE
62 | var FOO = 'foo';
63 |
64 | // UPPER_CASE properties are valid no matter if they are computed or not
65 | var a = {
66 | BAR: 'bar',
67 | [FOO]: 'foo'
68 | };
69 |
70 | // also safe to use strings themselves are UPPCASE_CASE
71 | var foo = 'FOO';
72 | ```
73 |
74 | #### i18n
75 |
76 | This rule allows to call i18next translate function.
77 |
78 | **Correct** code:
79 |
80 | ```js
81 | /*eslint i18next/no-literal-string: "error"*/
82 | var bar = i18next.t('bar');
83 | var bar2 = i18n.t('bar');
84 | ```
85 |
86 | Maybe you use other internationalization libraries
87 | not [i18next](https://www.i18next.com/). You can use like this:
88 |
89 | ```js
90 | /*eslint i18next/no-literal-string: ["error", { "ignoreCallee": ["yourI18n"] }]*/
91 | const bar = yourI18n('bar');
92 |
93 | // or
94 |
95 | /*eslint i18next/no-literal-string: ["error", { "ignoreCallee": ["yourI18n.method"] }]*/
96 | const bar = yourI18n.method('bar');
97 | ```
98 |
99 | #### HTML Markup
100 |
101 | All literal strings in html template are typically mistakes. For JSX example:
102 |
103 | ```HTML
104 | foo
105 | ```
106 |
107 | They should be translated by [i18next translation api](https://www.i18next.com/):
108 |
109 | ```HTML
110 | {i18next.t('foo')}
111 | ```
112 |
113 | Same for [Vue template](https://vuejs.org/v2/guide/syntax.html):
114 |
115 | ```HTML
116 |
117 |
118 | foo
119 |
120 |
121 |
122 |
123 | {{ i18next.t('foo') }}
124 |
125 | ```
126 |
127 | It would allow most reasonable usages of string that would rarely be shown to user, like following examples.
128 |
129 | Click on them to see details.
130 |
131 |
132 |
133 | react-i18next
134 |
135 |
136 | This plugin are compatible with [react-i18next](https://react.i18next.com/)
137 |
138 | ```tsx
139 | // correct
140 |
141 | bar
142 |
143 | ```
144 |
145 |
146 |
147 |
148 |
149 | Redux/Vuex
150 |
151 |
152 | This rule also works with those state managers like
153 | [Redux](https://redux.js.org/) and [Vuex](https://vuex.vuejs.org/).
154 |
155 | **Correct** code:
156 |
157 | ```js
158 | var bar = store.dispatch('bar');
159 | var bar2 = store.commit('bar');
160 | ```
161 |
162 |
163 |
164 |
165 |
166 | Typescript
167 |
168 |
169 | This plugin would not complain on those reasonable usages of string.
170 |
171 | The following cases are considered as **correct**:
172 |
173 | ```typescript
174 | var a: Type['member'];
175 | var a: Omit;
176 | enum E {
177 | A = 1
178 | }
179 | var a = E['A'];
180 | var a: { t: 'button' } = { t: 'button' };
181 | var a: 'abc' | 'name' = 'abc';
182 | ```
183 |
184 | We require type information to work properly, so you need to add some options in your `.eslintrc`:
185 |
186 | ```js
187 | "parserOptions": {
188 | // path of your tsconfig.json
189 | "project": "./tsconfig.json"
190 | }
191 | ```
192 |
193 | See
194 | [here](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage)
195 | for more deteils.
196 |
197 |
198 |
199 |
200 |
201 |
202 | Import/Export
203 |
204 |
205 | The following cases are **allowed**:
206 |
207 | ```typescript
208 | import mod from 'm';
209 | import('mod');
210 | require('mod');
211 |
212 | export { named } from 'm';
213 | export * from 'm';
214 | ```
215 |
216 |
217 |
218 |
219 |
220 | String Comparison
221 |
222 |
223 | String comparison is fine.
224 |
225 | ```typescript
226 | // correct
227 | name === 'Android' || name === 'iOS';
228 | ```
229 |
230 |
231 |
232 |
233 |
234 | SwithCase
235 |
236 |
237 | Skip switchcase statement:
238 |
239 | ```typescript
240 | // correct
241 | switch (type) {
242 | case 'foo':
243 | break;
244 | case 'bar':
245 | break;
246 | }
247 | ```
248 |
249 |
250 |
251 | ### Options
252 |
253 | ### markupOnly
254 |
255 | If `markupOnly` option turn on, only JSX text and strings used as JSX attributes will be validated.
256 |
257 | JSX text:
258 |
259 | ```jsx
260 | /*eslint i18next/no-literal-string: ["error", { "markupOnly": true }]*/
261 | // incorrect
262 | hello world
263 | {"hello world"}
264 | ```
265 |
266 | Strings as JSX attribute:
267 |
268 | ```jsx
269 | /*eslint i18next/no-literal-string: ["error", { "markupOnly": true }]*/
270 | // incorrect
271 |
272 |
273 | ```
274 |
275 | ### onlyAttribute
276 |
277 | Only check the `JSX` attributes that listed in this option. **This option would turn on `markupOnly`.**
278 |
279 | ```jsx
280 | // correct
281 | const foo = 'bar';
282 |
283 |
284 | // incorrect
285 | foo
286 |
287 | /*eslint i18next/no-literal-string: ["error", {"onlyAttribute": ["foo"]}]*/
288 | // incorrect
289 |
290 | ```
291 |
292 | #### ignore
293 |
294 | The `ignore` option specifies exceptions not to check for
295 | literal strings that match one of regexp paterns.
296 |
297 | Examples of correct code for the `{ "ignore": ['foo'] }` option:
298 |
299 | ```js
300 | /*eslint i18next/no-literal-string: ["error", {"ignore": ["foo"]}]*/
301 | const a = 'afoo';
302 | ```
303 |
304 | #### ignoreCallee
305 |
306 | THe `ignoreCallee` option speficies exceptions not check for
307 | function calls whose names match one of regexp patterns.
308 |
309 | Examples of correct code for the `{ "ignoreCallee": ["foo"] }` option:
310 |
311 | ```js
312 | /*eslint i18next/no-literal-string: ["error", { "ignoreCallee": ["foo"] }]*/
313 | const bar = foo('bar');
314 | ```
315 |
316 | #### ignoreAttribute
317 |
318 | The `ignoreAttribute` option specifies exceptions not to check for JSX attributes that match one of ignored attributes.
319 |
320 | Examples of correct code for the `{ "ignoreAttribute": ["foo"] }` option:
321 |
322 | ```jsx
323 | /*eslint i18next/no-literal-string: ["error", { "ignoreAttribute": ["foo"] }]*/
324 | const element = ;
325 | ```
326 |
327 | #### ignoreProperty
328 |
329 | The `ignoreProperty` option specifies exceptions not to check for object properties that match one of ignored properties.
330 |
331 | Examples of correct code for the `{ "ignoreProperty": ["foo"] }` option:
332 |
333 | ```jsx
334 | /*eslint i18next/no-literal-string: ["error", { "ignoreProperty": ["foo"] }]*/
335 | const a = { foo: 'bar' };
336 | ```
337 |
338 | #### ignoreComponent
339 |
340 | The `ignoreComponent` option specifies exceptions not to check for string literals inside a list of named components. It includes `` per default.
341 |
342 | Examples of correct code for the `{ "ignoreComponent": ["Icon"] }` option:
343 |
344 | ```jsx
345 | /*eslint i18next/no-literal-string: ["error", { "ignoreComponent": ["Icon"] }]*/
346 | arrow
347 | ```
348 |
349 | #### validateTemplate
350 |
351 | Indicate whether to validate [template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) or not. Default `false`
352 |
353 | ```js
354 | /*eslint i18next/no-literal-string: ["error", { "validateTemplate": true }]*/
355 | // incorrect
356 | var foo = `hello world`;
357 | ```
358 |
--------------------------------------------------------------------------------