diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 8b014c8dc..9b9214710 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -2,10 +2,12 @@ * AWOOOI Web ESLint Configuration * ================================ * Extends @awoooi/eslint-config/react + * Wave 3: i18n enforcement via eslint-plugin-i18next */ module.exports = { extends: ['@awoooi/eslint-config/react', 'next/core-web-vitals'], + plugins: ['i18next'], parserOptions: { project: './tsconfig.json', tsconfigRootDir: __dirname, @@ -17,8 +19,26 @@ module.exports = { // Allow console for debugging (TODO: integrate unified logger) 'no-console': 'off', - // i18n enforcement - no hardcoded strings in JSX - // (Custom rule would require eslint-plugin-i18n-json setup) + // Wave 3: i18n enforcement - 禁止 JSX 中的硬編碼字串 + // 階段一: warn 模式 (2026-03-31) + // 階段二: error 模式 (待統帥批准) + 'i18next/no-literal-string': ['warn', { + // 允許技術識別符 (服務名/API 路徑) + markupOnly: true, + // 忽略特定屬性 + ignoreAttribute: [ + 'data-testid', + 'className', + 'href', + 'src', + 'alt', + 'name', + 'type', + 'placeholder', // 部分 placeholder 可能需要 i18n,視情況調整 + ], + // 忽略特定函數調用 + ignoreCallee: ['console.log', 'console.error', 'console.warn', 't', 'tRisk', 'tBlast'], + }], // TypeScript strict rules '@typescript-eslint/no-explicit-any': 'warn', diff --git a/apps/web/package.json b/apps/web/package.json index 827bdd9e7..12aae9d09 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,6 +34,7 @@ "autoprefixer": "^10.4.0", "eslint": "^8.57.0", "eslint-config-next": "^14.1.0", + "eslint-plugin-i18next": "^6.1.3", "playwright": "^1.58.2", "postcss": "^8.4.0", "tailwindcss": "^3.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f63dbd24..641ca4165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: eslint-config-next: specifier: ^14.1.0 version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-i18next: + specifier: ^6.1.3 + version: 6.1.3 playwright: specifier: ^1.58.2 version: 1.58.2 @@ -2293,6 +2296,10 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-i18next@6.1.3: + resolution: {integrity: sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw==} + engines: {node: '>=18.10.0'} + eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} @@ -2884,6 +2891,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3378,6 +3388,10 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requireindex@1.1.0: + resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} + engines: {node: '>=0.10.5'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -6173,6 +6187,11 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-i18next@6.1.3: + dependencies: + lodash: 4.17.23 + requireindex: 1.1.0 + eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 @@ -6846,6 +6865,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.23: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -7325,6 +7346,8 @@ snapshots: transitivePeerDependencies: - supports-color + requireindex@1.1.0: {} + reselect@5.1.1: {} resolve-from@4.0.0: {}