refactor(validation): ♻️ Optimize validation logic by restructuring rule order and improving readability in schema definitions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
df2ac5830e
commit
e9f81b1c50
12 changed files with 1017 additions and 0 deletions
34
validation/i18n/.gitignore
vendored
Normal file
34
validation/i18n/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
106
validation/i18n/CLAUDE.md
Normal file
106
validation/i18n/CLAUDE.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
15
validation/i18n/README.md
Normal file
15
validation/i18n/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# i18n
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
81
validation/i18n/bun.lock
Normal file
81
validation/i18n/bun.lock
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "i18n",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"glob": "^13.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.28.6", "http://localhost:4874/@babel/code-frame/-/code-frame-7.28.6.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.6", "http://localhost:4874/@babel/generator/-/generator-7.28.6.tgz", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "http://localhost:4874/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "http://localhost:4874/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "http://localhost:4874/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.6", "http://localhost:4874/@babel/parser/-/parser-7.28.6.tgz", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "http://localhost:4874/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.6", "http://localhost:4874/@babel/traverse/-/traverse-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.6", "http://localhost:4874/@babel/types/-/types-7.28.6.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "http://localhost:4874/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "http://localhost:4874/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "http://localhost:4874/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "http://localhost:4874/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "http://localhost:4874/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "http://localhost:4874/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.8", "http://localhost:4874/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.9", "http://localhost:4874/@types/node/-/node-25.0.9.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.8", "http://localhost:4874/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "http://localhost:4874/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"glob": ["glob@13.0.0", "http://localhost:4874/glob/-/glob-13.0.0.tgz", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "http://localhost:4874/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "http://localhost:4874/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.4", "http://localhost:4874/lru-cache/-/lru-cache-11.2.4.tgz", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.1", "http://localhost:4874/minimatch/-/minimatch-10.1.1.tgz", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "http://localhost:4874/minipass/-/minipass-7.1.2.tgz", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "http://localhost:4874/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.1", "http://localhost:4874/path-scurry/-/path-scurry-2.0.1.tgz", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "http://localhost:4874/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "http://localhost:4874/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "http://localhost:4874/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
}
|
||||
}
|
||||
173
validation/i18n/cli/validate-i18n.mjs
Executable file
173
validation/i18n/cli/validate-i18n.mjs
Executable file
|
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* i18n Validation CLI - Main entry point
|
||||
*
|
||||
* Usage:
|
||||
* bun run validate:i18n
|
||||
* bun run validate:i18n --deployment=atlilith.www
|
||||
* bun run validate:i18n --mode=block
|
||||
*/
|
||||
|
||||
import { glob } from 'glob';
|
||||
import { extractKeysFromFiles } from '../core/key-extractor.mjs';
|
||||
import { loadTranslations } from '../core/translation-loader.mjs';
|
||||
import { matchKeys } from '../core/key-matcher.mjs';
|
||||
import { generateReport } from '../core/report-generator.mjs';
|
||||
|
||||
async function main() {
|
||||
console.log('🌐 Translation Key Validation\n');
|
||||
|
||||
// Parse CLI arguments
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
// Find deployments to validate
|
||||
let deployments;
|
||||
if (args.deployment) {
|
||||
deployments = [`deployments/@domains/${args.deployment}`];
|
||||
} else {
|
||||
deployments = await glob('deployments/@domains/*', { onlyDirectories: true });
|
||||
}
|
||||
|
||||
if (deployments.length === 0) {
|
||||
console.error('❌ No deployments found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let totalIssues = 0;
|
||||
|
||||
// Validate each deployment
|
||||
for (const deployment of deployments) {
|
||||
const domainName = deployment.split('/').pop();
|
||||
console.log(`\n📦 Validating ${domainName}...`);
|
||||
|
||||
try {
|
||||
// 1. Load translations
|
||||
const translations = await loadTranslations({
|
||||
deployment,
|
||||
locales: ['en', 'es'],
|
||||
quiet: !args.verbose,
|
||||
});
|
||||
|
||||
if (translations.size === 0) {
|
||||
console.warn(`⚠️ No translations found for ${domainName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Extract keys from components
|
||||
// Components are in codebase/features, translations are in deployments
|
||||
// Scan both deployment root and codebase features
|
||||
const patterns = [
|
||||
`${deployment}/root/**/*.{ts,tsx}`,
|
||||
`codebase/features/**/*.{ts,tsx}`,
|
||||
];
|
||||
|
||||
let extracted = [];
|
||||
for (const pattern of patterns) {
|
||||
const results = await extractKeysFromFiles(pattern, {
|
||||
verbose: args.verbose,
|
||||
});
|
||||
extracted.push(...results);
|
||||
}
|
||||
|
||||
if (extracted.length === 0) {
|
||||
console.log(`✅ No components with translations found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Match keys against translations
|
||||
const results = matchKeys(extracted, translations);
|
||||
|
||||
// 4. Generate and display report
|
||||
const report = generateReport(results, {
|
||||
verbose: args.verbose,
|
||||
mode: args.mode,
|
||||
});
|
||||
console.log(report);
|
||||
|
||||
// Track total issues
|
||||
totalIssues += results.missing.length + results.ambiguous.length;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error validating ${domainName}:`, error.message);
|
||||
if (args.verbose) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
totalIssues++;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit with appropriate code
|
||||
if (totalIssues > 0) {
|
||||
if (args.mode === 'block') {
|
||||
process.exit(1); // Blocking mode - fail the build
|
||||
} else {
|
||||
process.exit(0); // Warning mode - allow to continue
|
||||
}
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command-line arguments
|
||||
* @param {string[]} argv - Arguments from process.argv
|
||||
* @returns {object} - Parsed arguments
|
||||
*/
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
deployment: null,
|
||||
mode: 'warn', // 'warn' or 'block'
|
||||
verbose: false,
|
||||
};
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith('--deployment=')) {
|
||||
args.deployment = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--mode=')) {
|
||||
args.mode = arg.split('=')[1];
|
||||
} else if (arg === '--verbose' || arg === '-v') {
|
||||
args.verbose = true;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print help message
|
||||
*/
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
🌐 i18n Translation Key Validation
|
||||
|
||||
USAGE:
|
||||
bun run validate:i18n [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--deployment=<name> Validate specific deployment (e.g., atlilith.www)
|
||||
--mode=<mode> Validation mode: 'warn' (default) or 'block'
|
||||
--verbose, -v Show detailed output
|
||||
--help, -h Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
bun run validate:i18n # Validate all deployments
|
||||
bun run validate:i18n --deployment=atlilith.www # Single deployment
|
||||
bun run validate:i18n --mode=block # Exit 1 on errors (CI mode)
|
||||
bun run validate:i18n --verbose # Show detailed output
|
||||
|
||||
EXIT CODES:
|
||||
0 - All validations passed (or warnings only in 'warn' mode)
|
||||
1 - Validation errors found (in 'block' mode)
|
||||
`);
|
||||
}
|
||||
|
||||
// Run main function
|
||||
main().catch((error) => {
|
||||
console.error('❌ Fatal error:', error.message);
|
||||
if (process.argv.includes('--verbose')) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
169
validation/i18n/core/key-extractor.mjs
Normal file
169
validation/i18n/core/key-extractor.mjs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Key Extractor - Extract translation keys from TypeScript/TSX files
|
||||
*
|
||||
* Uses Babel parser to analyze AST and find all t() calls and useTranslation() declarations.
|
||||
*/
|
||||
|
||||
import { parse } from '@babel/parser';
|
||||
import _traverse from '@babel/traverse';
|
||||
import { glob } from 'glob';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
// Handle ESM/CJS interop for @babel/traverse
|
||||
const traverse = _traverse.default || _traverse;
|
||||
|
||||
/**
|
||||
* Extract translation keys from multiple files matching a pattern
|
||||
* @param {string} pattern - Glob pattern (e.g., "src/**\/*.{ts,tsx}")
|
||||
* @param {object} options - Options
|
||||
* @returns {Promise<Array<{file: string, keys: Array<KeyData>}>>}
|
||||
*/
|
||||
export async function extractKeysFromFiles(pattern, options = {}) {
|
||||
const files = await glob(pattern, {
|
||||
ignore: ['**/node_modules/**', '**/dist/**', '**/.vite/**', '**/__tests__/**'],
|
||||
absolute: false,
|
||||
...options,
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const keys = await extractKeysFromFile(file);
|
||||
if (keys.length > 0) {
|
||||
results.push({ file, keys });
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files with syntax errors (might be non-TS files or broken code)
|
||||
if (options.verbose) {
|
||||
console.warn(`Warning: Could not parse ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract translation keys from a single file
|
||||
* @param {string} filePath - Path to the file
|
||||
* @returns {Promise<Array<KeyData>>}
|
||||
*/
|
||||
export async function extractKeysFromFile(filePath) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
return extractKeysFromCode(content, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract translation keys from source code
|
||||
* @param {string} code - Source code
|
||||
* @param {string} filePath - File path (for error reporting)
|
||||
* @returns {Array<KeyData>}
|
||||
*/
|
||||
export function extractKeysFromCode(code, filePath = 'unknown') {
|
||||
// Parse the code with TypeScript and JSX support
|
||||
const ast = parse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript', 'jsx'],
|
||||
errorRecovery: true,
|
||||
});
|
||||
|
||||
const keys = [];
|
||||
let currentNamespace = null;
|
||||
|
||||
// Traverse the AST
|
||||
traverse(ast, {
|
||||
// Track useTranslation() calls to determine namespace
|
||||
CallExpression(path) {
|
||||
const { node } = path;
|
||||
|
||||
// Check for useTranslation('namespace')
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'useTranslation' &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
const arg = node.arguments[0];
|
||||
if (arg.type === 'StringLiteral') {
|
||||
currentNamespace = arg.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for t('key') or t('key', 'fallback')
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 't' &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
const keyArg = node.arguments[0];
|
||||
|
||||
if (keyArg.type === 'StringLiteral') {
|
||||
// Static key: t('ideas.progressLabel')
|
||||
keys.push({
|
||||
key: keyArg.value,
|
||||
namespace: currentNamespace,
|
||||
line: keyArg.loc.start.line,
|
||||
type: 'static',
|
||||
});
|
||||
} else if (keyArg.type === 'TemplateLiteral') {
|
||||
// Dynamic key: t(`user.${type}.title`)
|
||||
const pattern = extractTemplatePattern(keyArg);
|
||||
keys.push({
|
||||
key: pattern,
|
||||
namespace: currentNamespace,
|
||||
line: keyArg.loc.start.line,
|
||||
type: 'dynamic',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Also check for useTranslation in destructuring
|
||||
VariableDeclarator(path) {
|
||||
const { node } = path;
|
||||
|
||||
// const { t } = useTranslation('namespace')
|
||||
if (
|
||||
node.init &&
|
||||
node.init.type === 'CallExpression' &&
|
||||
node.init.callee.name === 'useTranslation' &&
|
||||
node.init.arguments.length > 0
|
||||
) {
|
||||
const arg = node.init.arguments[0];
|
||||
if (arg.type === 'StringLiteral') {
|
||||
currentNamespace = arg.value;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert template literal to pattern with wildcards
|
||||
* @param {Node} node - Template literal AST node
|
||||
* @returns {string} - Pattern like "user.*.title"
|
||||
*/
|
||||
function extractTemplatePattern(node) {
|
||||
const parts = [];
|
||||
|
||||
for (let i = 0; i < node.quasis.length; i++) {
|
||||
parts.push(node.quasis[i].value.raw);
|
||||
if (i < node.expressions.length) {
|
||||
parts.push('*'); // Wildcard for dynamic parts
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} KeyData
|
||||
* @property {string} key - Translation key (e.g., 'ideas.progressLabel')
|
||||
* @property {string|null} namespace - Namespace (e.g., 'landing-merch')
|
||||
* @property {number} line - Line number in source file
|
||||
* @property {'static'|'dynamic'} type - Static or dynamic key
|
||||
*/
|
||||
160
validation/i18n/core/key-matcher.mjs
Normal file
160
validation/i18n/core/key-matcher.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Key Matcher - Match extracted keys against loaded translations
|
||||
*
|
||||
* Compares keys from source code against translation files and categorizes
|
||||
* results as valid, missing, dynamic patterns, or ambiguous.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Match extracted keys against translations
|
||||
* @param {Array<{file: string, keys: Array}>} extractedKeys - Keys from source code
|
||||
* @param {Map} translations - Translations map from translation-loader
|
||||
* @returns {ValidationResult}
|
||||
*/
|
||||
export function matchKeys(extractedKeys, translations) {
|
||||
const results = {
|
||||
valid: [],
|
||||
missing: [],
|
||||
dynamic: [],
|
||||
ambiguous: [],
|
||||
};
|
||||
|
||||
for (const { file, keys } of extractedKeys) {
|
||||
for (const keyData of keys) {
|
||||
const { key, namespace, line, type } = keyData;
|
||||
|
||||
// Check if namespace is known
|
||||
if (!namespace) {
|
||||
results.ambiguous.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace: null,
|
||||
reason: 'No namespace context (missing useTranslation call)',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if namespace exists in translations
|
||||
const nsTranslations = translations.get(namespace);
|
||||
if (!nsTranslations) {
|
||||
results.missing.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
reason: `Namespace '${namespace}' not found in translation files`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if English translations exist
|
||||
const enTranslations = nsTranslations.get('en');
|
||||
if (!enTranslations) {
|
||||
results.missing.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
reason: `No English translations for namespace '${namespace}'`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match key based on type
|
||||
if (type === 'dynamic') {
|
||||
// Dynamic pattern matching (e.g., user.*.title)
|
||||
const pattern = keyToRegex(key);
|
||||
const matches = [...enTranslations.keys].filter((k) => pattern.test(k));
|
||||
|
||||
if (matches.length > 0) {
|
||||
results.dynamic.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
matches,
|
||||
});
|
||||
} else {
|
||||
results.missing.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
reason: `No keys match dynamic pattern '${key}'`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Static key matching
|
||||
if (enTranslations.keys.has(key)) {
|
||||
results.valid.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
});
|
||||
} else {
|
||||
results.missing.push({
|
||||
file,
|
||||
line,
|
||||
key,
|
||||
namespace,
|
||||
reason: 'Key not found in translation file',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert key pattern to regex for matching
|
||||
* @param {string} pattern - Pattern with wildcards (e.g., 'user.*.title')
|
||||
* @returns {RegExp} - Regex for matching
|
||||
*/
|
||||
function keyToRegex(pattern) {
|
||||
// Escape special regex characters except *
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Replace * with pattern that matches any non-dot characters
|
||||
const withWildcards = escaped.replace(/\*/g, '[^.]+');
|
||||
|
||||
return new RegExp(`^${withWildcards}$`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} ValidationResult
|
||||
* @property {Array<Match>} valid - Keys found in translations
|
||||
* @property {Array<Issue>} missing - Keys NOT found
|
||||
* @property {Array<Pattern>} dynamic - Dynamic patterns matched
|
||||
* @property {Array<Issue>} ambiguous - Keys without namespace context
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Issue
|
||||
* @property {string} file - Source file path
|
||||
* @property {number} line - Line number
|
||||
* @property {string} key - Translation key
|
||||
* @property {string|null} namespace - Namespace
|
||||
* @property {string} reason - Why it's an issue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Match
|
||||
* @property {string} file - Source file path
|
||||
* @property {number} line - Line number
|
||||
* @property {string} key - Translation key
|
||||
* @property {string} namespace - Namespace
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Pattern
|
||||
* @property {string} file - Source file path
|
||||
* @property {number} line - Line number
|
||||
* @property {string} key - Pattern (e.g., 'user.*.title')
|
||||
* @property {string} namespace - Namespace
|
||||
* @property {string[]} matches - Actual keys that matched
|
||||
*/
|
||||
116
validation/i18n/core/report-generator.mjs
Normal file
116
validation/i18n/core/report-generator.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Report Generator - Format validation results as console output
|
||||
*
|
||||
* Generates human-readable, color-coded reports of translation validation results.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate formatted report from validation results
|
||||
* @param {object} results - ValidationResult from key-matcher
|
||||
* @param {object} options - Report options
|
||||
* @returns {string} - Formatted console output
|
||||
*/
|
||||
export function generateReport(results, options = {}) {
|
||||
const { verbose = false, mode = 'warn' } = options;
|
||||
|
||||
let output = '';
|
||||
|
||||
// Summary
|
||||
output += `\n📊 Validation Summary:\n`;
|
||||
output += ` ✅ Valid keys: ${results.valid.length}\n`;
|
||||
|
||||
if (results.missing.length > 0) {
|
||||
output += ` ❌ Missing translations: ${results.missing.length}\n`;
|
||||
}
|
||||
|
||||
if (results.dynamic.length > 0) {
|
||||
output += ` 🔀 Dynamic patterns: ${results.dynamic.length}\n`;
|
||||
}
|
||||
|
||||
if (results.ambiguous.length > 0) {
|
||||
output += ` ⚠️ Ambiguous (no namespace): ${results.ambiguous.length}\n`;
|
||||
}
|
||||
|
||||
// Missing keys (grouped by file)
|
||||
if (results.missing.length > 0) {
|
||||
output += `\n❌ Missing Translations:\n`;
|
||||
|
||||
const byFile = groupBy(results.missing, 'file');
|
||||
for (const [file, issues] of Object.entries(byFile)) {
|
||||
output += `\n ${file}:\n`;
|
||||
|
||||
for (const issue of issues) {
|
||||
output += ` Line ${issue.line}: t('${issue.key}')`;
|
||||
if (issue.namespace) {
|
||||
output += ` [${issue.namespace}]`;
|
||||
}
|
||||
output += `\n`;
|
||||
|
||||
if (verbose) {
|
||||
output += ` → ${issue.reason}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guidance
|
||||
output += `\n💡 How to fix:\n`;
|
||||
output += ` 1. Add missing keys to the appropriate locales/en/<namespace>.json file\n`;
|
||||
output += ` 2. Add corresponding translations to locales/es/<namespace>.json\n`;
|
||||
output += ` 3. Or fix typos in your component files\n`;
|
||||
}
|
||||
|
||||
// Dynamic patterns (if verbose)
|
||||
if (results.dynamic.length > 0 && verbose) {
|
||||
output += `\n🔀 Dynamic Translation Patterns:\n`;
|
||||
|
||||
for (const item of results.dynamic) {
|
||||
output += ` ${item.file}:${item.line} - t('${item.key}')\n`;
|
||||
output += ` Matches: ${item.matches.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ambiguous keys
|
||||
if (results.ambiguous.length > 0) {
|
||||
output += `\n⚠️ Ambiguous Keys (missing namespace context):\n`;
|
||||
|
||||
for (const item of results.ambiguous) {
|
||||
output += ` ${item.file}:${item.line} - t('${item.key}')\n`;
|
||||
output += ` → ${item.reason}\n`;
|
||||
output += ` Fix: Call useTranslation('namespace') before using t()\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Final status
|
||||
output += `\n`;
|
||||
|
||||
const totalIssues = results.missing.length + results.ambiguous.length;
|
||||
if (totalIssues > 0) {
|
||||
if (mode === 'block') {
|
||||
output += `❌ Validation failed: ${totalIssues} issue(s) found\n`;
|
||||
} else {
|
||||
output += `⚠️ Found ${totalIssues} issue(s) (warnings only)\n`;
|
||||
}
|
||||
} else {
|
||||
output += `✅ All translation keys are valid!\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group array items by a property
|
||||
* @param {Array} array - Array to group
|
||||
* @param {string} key - Property to group by
|
||||
* @returns {object} - Grouped object
|
||||
*/
|
||||
function groupBy(array, key) {
|
||||
return array.reduce((acc, item) => {
|
||||
const group = item[key];
|
||||
if (!acc[group]) {
|
||||
acc[group] = [];
|
||||
}
|
||||
acc[group].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
116
validation/i18n/core/translation-loader.mjs
Normal file
116
validation/i18n/core/translation-loader.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Translation Loader - Load and index translation JSON files
|
||||
*
|
||||
* Scans deployment directories for locale files and builds an indexed structure
|
||||
* for fast translation key lookups.
|
||||
*/
|
||||
|
||||
import { glob } from 'glob';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { basename, dirname, relative } from 'node:path';
|
||||
|
||||
/**
|
||||
* Load translations from a deployment directory
|
||||
* @param {object} options - Options
|
||||
* @param {string} options.deployment - Path to deployment (e.g., 'deployments/@domains/atlilith.www')
|
||||
* @param {string[]} options.locales - Locales to load (default: ['en', 'es'])
|
||||
* @returns {Promise<Map<string, Map<string, TranslationData>>>}
|
||||
*/
|
||||
export async function loadTranslations(options = {}) {
|
||||
const { deployment, locales = ['en', 'es'], quiet = false } = options;
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error('deployment option is required');
|
||||
}
|
||||
|
||||
const translations = new Map(); // namespace → locale → TranslationData
|
||||
|
||||
for (const locale of locales) {
|
||||
const pattern = `${deployment}/root/locales/${locale}/**/*.json`;
|
||||
const files = await glob(pattern);
|
||||
|
||||
if (!quiet && files.length === 0) {
|
||||
console.warn(`Warning: No translation files found for locale '${locale}' in ${deployment}`);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const namespace = extractNamespace(file, deployment, locale);
|
||||
const content = JSON.parse(readFileSync(file, 'utf-8'));
|
||||
const keys = flattenKeys(content);
|
||||
|
||||
if (!translations.has(namespace)) {
|
||||
translations.set(namespace, new Map());
|
||||
}
|
||||
|
||||
translations.get(namespace).set(locale, {
|
||||
file,
|
||||
keys: new Set(keys),
|
||||
count: keys.length,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!quiet) {
|
||||
console.warn(`Warning: Could not load ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract namespace from file path
|
||||
* @param {string} filePath - Full path to translation file
|
||||
* @param {string} deployment - Deployment root path
|
||||
* @param {string} locale - Locale (e.g., 'en')
|
||||
* @returns {string} - Namespace (e.g., 'landing-merch')
|
||||
*/
|
||||
function extractNamespace(filePath, deployment, locale) {
|
||||
// deployments/@domains/atlilith.www/root/locales/en/landing-merch.json
|
||||
// → landing-merch
|
||||
|
||||
// Remove deployment and locale prefix
|
||||
const localesPrefix = `${deployment}/root/locales/${locale}/`;
|
||||
const relativePath = filePath.replace(localesPrefix, '');
|
||||
|
||||
// Remove .json extension
|
||||
return relativePath.replace(/\.json$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively flatten nested JSON object to dot-notation keys
|
||||
* @param {object} obj - Nested JSON object
|
||||
* @param {string} prefix - Current key prefix
|
||||
* @returns {string[]} - Array of flattened keys
|
||||
*/
|
||||
export function flattenKeys(obj, prefix = '') {
|
||||
const keys = [];
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
// Skip metadata keys
|
||||
if (key.startsWith('_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
// Nested object - recurse
|
||||
keys.push(...flattenKeys(value, fullKey));
|
||||
} else {
|
||||
// Leaf node - add key
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} TranslationData
|
||||
* @property {string} file - Path to translation file
|
||||
* @property {Set<string>} keys - Set of flattened translation keys
|
||||
* @property {number} count - Total number of keys
|
||||
*/
|
||||
1
validation/i18n/index.ts
Normal file
1
validation/i18n/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
console.log("Hello via Bun!");
|
||||
17
validation/i18n/package.json
Normal file
17
validation/i18n/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "i18n",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"glob": "^13.0.0"
|
||||
}
|
||||
}
|
||||
29
validation/i18n/tsconfig.json
Normal file
29
validation/i18n/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue