├── .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 | some-image 29 | `, 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: '', 15 | options: [{ mode: 'all' }], 16 | }, 17 | { 18 | code: 19 | '