i18ntk 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +157 -15
- package/SECURITY.md +14 -8
- package/main/i18ntk-backup.js +305 -62
- package/main/i18ntk-scanner.js +188 -49
- package/main/i18ntk-sizing.js +223 -29
- package/main/i18ntk-usage.js +203 -3
- package/main/i18ntk-validate.js +107 -3
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +2 -2
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +240 -50
- package/ui-locales/en.json +1 -1
- package/utils/translate/protection.js +147 -6
- package/utils/watch-locales.js +183 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
|
|
5
5
|
"readmeFilename": "README.md",
|
|
6
6
|
"keywords": [
|
|
@@ -161,5 +161,5 @@
|
|
|
161
161
|
"access": "public"
|
|
162
162
|
},
|
|
163
163
|
"preferGlobal": true,
|
|
164
|
-
"readme": "# i18ntk v3.3.0\n\nA i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n\n\n[](https://www.npmjs.com/package/i18ntk)\n[](https://www.npmjs.com/package/i18ntk)\n[](https://nodejs.org)\n[](https://www.npmjs.com/package/i18ntk)\n[](LICENSE)\n[](https://socket.dev/npm/package/i18ntk/overview/3.3.0)\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 3.3.0\n\n- **SECURITY**: Eliminated all 21 dynamic `require()` calls flagged by Socket.dev; 20 converted to static string literals, 1 gated with `SecurityUtils.validatePath`.\n- **AUTO TRANSLATE**: Added provider selection for Google, DeepL, and LibreTranslate.\n- **FIX**: `i18ntk-complete` now fills missing target-language keys from English values with language prefixes instead of `NOT_TRANSLATED`.\n- **DOCS**: SECURITY.md updated with Socket.dev analysis disclaimer explaining expected alerts for a CLI/i18n toolkit.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\ni18ntk --command=debug\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n`n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` (or legacy `i18ntk-backup`) directly for backup operations.\n\n## Common Options\n\nMost commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--dry-run`\n- `--help`\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate (Beta)\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\"BrandName\", \"PRODUCT_CODE\", \"API\"],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate Beta` to edit defaults for placeholder mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nIn 3.1.2, warning types are more specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nruntime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(runtime.t('common.hello'));\nruntime.setLanguage('fr');\nconsole.log(runtime.getLanguage());\nconsole.log(runtime.getAvailableLanguages());\nruntime.refresh('fr');\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"3.3.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"de\", \"es\", \"fr\", \"ru\"],\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 6,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v3.2.0](./docs/migration-guide-v3.2.0.md)\n- [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)\n- [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
|
|
164
|
+
"readme": "# i18ntk v4.0.0\n\nA i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n\n\n[](https://www.npmjs.com/package/i18ntk)\n[](https://www.npmjs.com/package/i18ntk)\n[](https://nodejs.org)\n[](https://www.npmjs.com/package/i18ntk)\n[](LICENSE)\n[](https://socket.dev/npm/package/i18ntk/overview/4.0.0)\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 4.0.0\n\n- **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.\n- **WATCH**: `watchLocales()` now returns an EventEmitter-compatible watcher with debounced `change`/`add`/`unlink`/`error` events and SHA-256 hash tracking.\n- **USAGE**: `--cleanup` and `--dry-run-delete` flags identify dead translation keys with confidence scores.\n- **VALIDATOR**: `--enforce-key-style` enforces dot.notation, snake_case, camelCase, kebab-case, or flat naming conventions.\n- **SCANNER**: `--source-language` supports multi-language hardcoded text detection with 12+ language profiles.\n- **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.\n- **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.\n- **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.\n- **FIX**: `initRuntime()` now returns independent instances with isolated language and cache state.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\ni18ntk --command=debug\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n`n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` (or legacy `i18ntk-backup`) directly for backup operations.\n\n## Common Options\n\nMost commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--dry-run`\n- `--help`\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate (Beta)\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\n \"BrandName\",\n \"PRODUCT_CODE\",\n { \"value\": \"OK\", \"context\": \"after:Click|Press|Tap\" },\n { \"value\": \"API\", \"context\": \"standalone\" }\n ],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n - **Plain strings**: masked everywhere (backward compatible).\n - **Context objects**: masked only in specific contexts (`after:word`, `before:word`, `standalone`, `surrounded:left,right`).\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate Beta` to edit defaults for placeholder mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nIn 3.1.2, warning types are more specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n### Expansion Prediction (New in 4.0.0)\n\nPredict UI layout overflow risk by analyzing per-key character-count expansion across languages:\n\n```bash\ni18ntk-sizing --source-dir ./locales --predict-expansion --output-report\n```\n\nExpansion ratios are classified into risk tiers:\n\n- **Safe** (<30% expansion): no UI impact expected\n- **Warning** (30–50%): may overflow in tight layouts — test on target languages\n- **Critical** (>50%): high risk of truncation — review UI element sizing\n\nThe report includes a built-in language-pair expansion reference table (EN→DE +35%, EN→RU +50%, EN→JA −40%, etc.) and lists the top-30 most-expanded keys.\n\n## Scanner: Multi-Language Detection (New in 4.0.0)\n\n`i18ntk-scanner` now supports detecting hardcoded text in multiple source languages beyond English:\n\n```bash\ni18ntk-scanner --source-dir ./src --source-language de\ni18ntk-scanner --source-dir ./src --source-language ja --output-report\n```\n\nSupported language profiles (12+): English, German, French, Spanish, Japanese, Chinese, Russian, Korean, Arabic, Hindi, and more. Each profile includes language-specific character ranges, stopword lists for false-positive filtering, and transliteration rules for key generation.\n\n## Usage: Dead Key Detection (New in 4.0.0)\n\n`i18ntk-usage` can identify translation keys that are defined but never referenced in source code:\n\n```bash\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete\n```\n\nEach dead key receives a confidence score (0.0–1.0) factoring:\n- Dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score\n- Key appears in source code comments or JSDoc — medium score\n- Parent file recently modified (<30 days) — medium score\n- No references found anywhere — high score (>0.8)\n\nThe `--dry-run-delete` flag writes a `.dead-keys.json` report for review before any destructive action.\n\n## Validator: Key Naming Conventions (New in 4.0.0)\n\nEnforce consistent translation key naming across your project:\n\n```bash\ni18ntk-validate --enforce-key-style\n```\n\nConfigure the expected style in `.i18ntk-config`:\n\n```json\n{\n \"keyStyle\": \"dot.notation\"\n}\n```\n\nSupported styles: `dot.notation`, `snake_case`, `camelCase`, `kebab-case`, `flat`. Violations are reported as warnings with suggested canonical forms.\n\n## Watch: Hot Reload (New in 4.0.0)\n\n`utils/watch-locales.js` now provides debounced file watching with EventEmitter support:\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => {\n console.log('Locale changed:', filePath);\n});\n\nwatcher.on('add', (filePath) => {\n console.log('Locale added:', filePath);\n});\n\n// Later:\nwatcher.stop();\n```\n\nFeatures: 300ms debounce (configurable), SHA-256 hash tracking to skip no-change saves, and a maximum of 50 watched directories.\n\n### Migration\n\nThe `watchLocales` return value gained EventEmitter methods in v4.0.0. Existing stop-function usage still works:\n\n```js\nconst stop = watchLocales('./locales', onChange);\n```\n\nCan be updated to:\n\n```js\nconst watcher = watchLocales('./locales');\nwatcher.on('change', onChange);\nwatcher.stop();\n```\n\nPassing a callback as the second argument is still supported — it auto-subscribes to `change` and `add` events.\n\n## Backup: Incremental Mode (New in 4.0.0)\n\nCreate differential backups that only include changed files:\n\n```bash\ni18ntk-backup create ./locales --incremental\n```\n\nIncremental backups store SHA-256 hashes per file and a parent-chain reference. Restoring an incremental backup automatically chains from the oldest full backup through each incremental diff in order. Chain depth is capped at 10 increments. Use `verify` to validate the hash chain.\n\n## Runtime: Lazy Loading (New in 4.0.0)\n\nReduce memory usage by deferring locale file loads until first key access:\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n lazy: true\n});\n\nconsole.log(i18n.t('common.hello')); // loads common.json on first access\n```\n\nWhen `lazy: true`, the runtime builds a key-to-file manifest on first access and loads individual files on demand. Files are loaded once and cached. If the manifest is missing or incomplete, the runtime falls back to full eager loading for that language. Manifest size is capped at 100KB with path containment validation.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getLanguage());\nconsole.log(i18n.getAvailableLanguages());\ni18n.refresh('fr');\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"4.0.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"de\", \"es\", \"fr\", \"ru\"],\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 6,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v3.2.0](./docs/migration-guide-v3.2.0.md)\n- [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)\n- [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
|
|
165
165
|
}
|
package/runtime/i18ntk.d.ts
CHANGED
|
@@ -471,11 +471,11 @@ export interface BasicI18nRuntime {
|
|
|
471
471
|
*/
|
|
472
472
|
getAvailableLanguages(): string[];
|
|
473
473
|
|
|
474
|
-
/**
|
|
475
|
-
* Refresh translations
|
|
476
|
-
*/
|
|
477
|
-
refresh(): void;
|
|
478
|
-
}
|
|
474
|
+
/**
|
|
475
|
+
* Refresh translations
|
|
476
|
+
*/
|
|
477
|
+
refresh(language?: string): void;
|
|
478
|
+
}
|
|
479
479
|
|
|
480
480
|
/**
|
|
481
481
|
* Initialize the enhanced i18ntk runtime (async, returns full I18nRuntime)
|
|
@@ -486,16 +486,22 @@ export declare function initI18nRuntime(config: I18nConfig): Promise<I18nRuntime
|
|
|
486
486
|
* Initialize the basic lightweight runtime (synchronous)
|
|
487
487
|
* This is the default export from 'i18ntk/runtime'
|
|
488
488
|
*/
|
|
489
|
-
export declare function initRuntime(options: {
|
|
490
|
-
baseDir: string;
|
|
491
|
-
language?: string;
|
|
492
|
-
fallbackLanguage?: string;
|
|
493
|
-
keySeparator?: string;
|
|
494
|
-
preload?: boolean;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
489
|
+
export declare function initRuntime(options: {
|
|
490
|
+
baseDir: string;
|
|
491
|
+
language?: string;
|
|
492
|
+
fallbackLanguage?: string;
|
|
493
|
+
keySeparator?: string;
|
|
494
|
+
preload?: boolean;
|
|
495
|
+
lazy?: boolean;
|
|
496
|
+
}): BasicI18nRuntime;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Build the lightweight runtime lazy-loading key manifest for a locale path.
|
|
500
|
+
*/
|
|
501
|
+
export declare function loadKeyManifest(baseDir?: string): Map<string, string>;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Type guards (runtime checks, not exported by actual code)
|
|
499
505
|
*/
|
|
500
506
|
export declare function isI18nRuntime(obj: any): obj is I18nRuntime;
|
|
501
507
|
export declare function isBasicI18nRuntime(obj: any): obj is BasicI18nRuntime;
|
|
@@ -538,4 +544,4 @@ declare global {
|
|
|
538
544
|
/**
|
|
539
545
|
* Export all interfaces and types
|
|
540
546
|
*/
|
|
541
|
-
export * from './enhanced';
|
|
547
|
+
export * from './enhanced';
|
package/runtime/index.d.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
export interface InitOptions {
|
|
5
5
|
baseDir?: string;
|
|
6
6
|
language?: string;
|
|
7
|
-
fallbackLanguage?: string;
|
|
8
|
-
keySeparator?: string;
|
|
9
|
-
preload?: boolean;
|
|
10
|
-
|
|
7
|
+
fallbackLanguage?: string;
|
|
8
|
+
keySeparator?: string;
|
|
9
|
+
preload?: boolean;
|
|
10
|
+
lazy?: boolean;
|
|
11
|
+
}
|
|
11
12
|
|
|
12
13
|
export type TranslateParams = Record<string, unknown>;
|
|
13
14
|
|
|
@@ -24,6 +25,7 @@ export function initRuntime(options?: InitOptions): {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export function setLanguage(lang: string): void;
|
|
27
|
-
export function getLanguage(): string;
|
|
28
|
-
export function getAvailableLanguages(): string[];
|
|
29
|
-
export function refresh(lang?: string): void;
|
|
28
|
+
export function getLanguage(): string;
|
|
29
|
+
export function getAvailableLanguages(): string[];
|
|
30
|
+
export function refresh(lang?: string): void;
|
|
31
|
+
export function loadKeyManifest(baseDir?: string): Map<string, string>;
|
package/runtime/index.js
CHANGED
|
@@ -11,13 +11,32 @@ const { envManager } = require('../utils/env-manager');
|
|
|
11
11
|
let configManager = null;
|
|
12
12
|
try { configManager = require('../utils/config-manager'); } catch (_) { /* optional */ }
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function createState(options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
baseDir: resolveBaseDir(options.baseDir),
|
|
17
|
+
language: options.language || 'en',
|
|
18
|
+
fallbackLanguage: options.fallbackLanguage || 'en',
|
|
19
|
+
keySeparator: options.keySeparator || '.',
|
|
20
|
+
cache: new Map(),
|
|
21
|
+
lazy: options.lazy === true,
|
|
22
|
+
keyManifest: new Map(),
|
|
23
|
+
loadedFiles: new Set(),
|
|
24
|
+
eagerLoadedLanguages: new Set(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const singletonState = {
|
|
15
29
|
baseDir: null, // absolute path to locales dir (e.g., ./locales)
|
|
16
30
|
language: 'en',
|
|
17
31
|
fallbackLanguage: 'en',
|
|
18
|
-
keySeparator: '.',
|
|
19
|
-
cache: new Map(), // lang -> merged translations object
|
|
20
|
-
|
|
32
|
+
keySeparator: '.',
|
|
33
|
+
cache: new Map(), // lang -> merged translations object
|
|
34
|
+
lazy: false,
|
|
35
|
+
keyManifest: new Map(), // lang -> Map: keyName -> filePath
|
|
36
|
+
loadedFiles: new Set(), // tracks loaded files in lazy mode
|
|
37
|
+
eagerLoadedLanguages: new Set(),
|
|
38
|
+
};
|
|
39
|
+
let singletonInitialized = false;
|
|
21
40
|
|
|
22
41
|
// --- Utilities ---
|
|
23
42
|
function stripBOMAndComments(s) {
|
|
@@ -98,12 +117,12 @@ function resolveBaseDir(explicitBaseDir) {
|
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
function listJsonFilesRecursively(dir) {
|
|
120
|
+
function listJsonFilesRecursively(dir, baseDir = dir) {
|
|
102
121
|
const results = [];
|
|
103
122
|
const stack = [dir];
|
|
104
123
|
while (stack.length) {
|
|
105
124
|
const d = stack.pop();
|
|
106
|
-
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
125
|
+
if (!SecurityUtils.safeExistsSync(d, baseDir)) continue;
|
|
107
126
|
try {
|
|
108
127
|
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
109
128
|
const full = path.join(d, entry.name);
|
|
@@ -120,6 +139,86 @@ function listJsonFilesRecursively(dir) {
|
|
|
120
139
|
return results;
|
|
121
140
|
}
|
|
122
141
|
|
|
142
|
+
function loadKeyManifestFromDir(baseDir) {
|
|
143
|
+
const validatedBase = SecurityUtils.validatePath(baseDir, path.dirname(baseDir));
|
|
144
|
+
const baseStat = SecurityUtils.safeStatSync(validatedBase, path.dirname(validatedBase));
|
|
145
|
+
const baseRoot = baseStat && baseStat.isFile() ? path.dirname(validatedBase) : validatedBase;
|
|
146
|
+
const files = baseStat && baseStat.isFile() ? [validatedBase] : listJsonFilesRecursively(validatedBase, validatedBase);
|
|
147
|
+
const manifest = new Map();
|
|
148
|
+
const MAX_SIZE = 100 * 1024;
|
|
149
|
+
let currentSize = 0;
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
let validated;
|
|
153
|
+
try { validated = SecurityUtils.validatePath(file, baseRoot); } catch (_) { continue; }
|
|
154
|
+
const rel = path.relative(baseRoot, validated);
|
|
155
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const data = readJsonSafe(validated);
|
|
159
|
+
if (!data || typeof data !== 'object') continue;
|
|
160
|
+
|
|
161
|
+
for (const key of Object.keys(data)) {
|
|
162
|
+
if (manifest.has(key)) continue;
|
|
163
|
+
|
|
164
|
+
const entrySize = JSON.stringify(key).length + JSON.stringify(validated).length + 5;
|
|
165
|
+
if (currentSize + entrySize > MAX_SIZE) break;
|
|
166
|
+
|
|
167
|
+
manifest.set(key, validated);
|
|
168
|
+
currentSize += entrySize;
|
|
169
|
+
}
|
|
170
|
+
} catch (_) { continue; }
|
|
171
|
+
|
|
172
|
+
if (currentSize >= MAX_SIZE) break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return manifest;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getLanguagePath(baseDir, lang) {
|
|
179
|
+
const langDir = path.join(baseDir, lang);
|
|
180
|
+
const langFile = path.join(baseDir, `${lang}.json`);
|
|
181
|
+
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
182
|
+
if (langDirStat && langDirStat.isDirectory()) return langDir;
|
|
183
|
+
const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
|
|
184
|
+
if (langFileStat && langFileStat.isFile()) return langFile;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getManifestForLanguage(runtimeState, lang) {
|
|
189
|
+
if (!runtimeState.keyManifest) runtimeState.keyManifest = new Map();
|
|
190
|
+
if (runtimeState.keyManifest.has(lang)) return runtimeState.keyManifest.get(lang);
|
|
191
|
+
|
|
192
|
+
const languagePath = getLanguagePath(runtimeState.baseDir, lang);
|
|
193
|
+
const manifest = languagePath ? loadKeyManifestFromDir(languagePath) : new Map();
|
|
194
|
+
runtimeState.keyManifest.set(lang, manifest);
|
|
195
|
+
return manifest;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadFileLazy(runtimeState, filePath, lang) {
|
|
199
|
+
const baseDir = runtimeState.baseDir;
|
|
200
|
+
if (!baseDir) throw new Error('baseDir not initialized');
|
|
201
|
+
|
|
202
|
+
let validatedPath;
|
|
203
|
+
try { validatedPath = SecurityUtils.validatePath(filePath, baseDir); } catch (e) {
|
|
204
|
+
throw new Error(`Invalid file path for lazy load: ${e.message}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rel = path.relative(baseDir, validatedPath);
|
|
208
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
209
|
+
throw new Error(`File outside base directory: ${filePath}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const data = readJsonSafe(validatedPath);
|
|
213
|
+
if (data && typeof data === 'object') {
|
|
214
|
+
let cacheEntry = runtimeState.cache.get(lang);
|
|
215
|
+
if (!cacheEntry) cacheEntry = {};
|
|
216
|
+
deepMerge(cacheEntry, data);
|
|
217
|
+
runtimeState.cache.set(lang, cacheEntry);
|
|
218
|
+
}
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
|
|
123
222
|
function readLanguageFromBase(baseDir, lang) {
|
|
124
223
|
const merged = {};
|
|
125
224
|
const langFile = path.join(baseDir, `${lang}.json`);
|
|
@@ -128,7 +227,7 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
128
227
|
// Prefer folder if exists, otherwise single file
|
|
129
228
|
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
130
229
|
if (langDirStat && langDirStat.isDirectory()) {
|
|
131
|
-
const files = listJsonFilesRecursively(langDir);
|
|
230
|
+
const files = listJsonFilesRecursively(langDir, langDir);
|
|
132
231
|
for (const file of files) {
|
|
133
232
|
try {
|
|
134
233
|
const data = readJsonSafe(file);
|
|
@@ -150,10 +249,19 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
150
249
|
return merged;
|
|
151
250
|
}
|
|
152
251
|
|
|
153
|
-
function
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
252
|
+
function getTranslationsForState(runtimeState, lang) {
|
|
253
|
+
if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
|
|
254
|
+
|
|
255
|
+
if (runtimeState.lazy) {
|
|
256
|
+
if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
|
|
257
|
+
runtimeState.cache.set(lang, {});
|
|
258
|
+
getManifestForLanguage(runtimeState, lang);
|
|
259
|
+
return runtimeState.cache.get(lang);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
|
|
263
|
+
const data = readLanguageFromBase(runtimeState.baseDir, lang);
|
|
264
|
+
runtimeState.cache.set(lang, data);
|
|
157
265
|
return data;
|
|
158
266
|
}
|
|
159
267
|
|
|
@@ -165,15 +273,38 @@ function interpolate(template, params) {
|
|
|
165
273
|
}
|
|
166
274
|
|
|
167
275
|
// Resolve a dotted key path from an object
|
|
168
|
-
function resolveKey(obj, key, sep = '.') {
|
|
169
|
-
if (!obj || typeof obj !== 'object') return undefined;
|
|
170
|
-
if (!key || typeof key !== 'string') return undefined;
|
|
276
|
+
function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
|
|
277
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
278
|
+
if (!key || typeof key !== 'string') return undefined;
|
|
171
279
|
const parts = key.split(sep);
|
|
172
280
|
let cur = obj;
|
|
173
281
|
for (const p of parts) {
|
|
174
282
|
if (cur && Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
175
283
|
cur = cur[p];
|
|
176
284
|
} else {
|
|
285
|
+
if (runtimeState && lang && runtimeState.lazy) {
|
|
286
|
+
const manifest = getManifestForLanguage(runtimeState, lang);
|
|
287
|
+
for (let i = parts.length; i > 0; i--) {
|
|
288
|
+
const prefix = parts.slice(0, i).join(sep);
|
|
289
|
+
const filePath = manifest.get(prefix);
|
|
290
|
+
const loadedFileKey = `${lang}\0${filePath}`;
|
|
291
|
+
if (filePath && !runtimeState.loadedFiles.has(loadedFileKey)) {
|
|
292
|
+
loadFileLazy(runtimeState, filePath, lang);
|
|
293
|
+
runtimeState.loadedFiles.add(loadedFileKey);
|
|
294
|
+
const langData = runtimeState.cache.get(lang);
|
|
295
|
+
return resolveKey(langData, key, sep, runtimeState, lang);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!runtimeState.eagerLoadedLanguages.has(lang)) {
|
|
299
|
+
const fullData = readLanguageFromBase(runtimeState.baseDir, lang);
|
|
300
|
+
const langData = runtimeState.cache.get(lang) || {};
|
|
301
|
+
deepMerge(langData, fullData);
|
|
302
|
+
runtimeState.cache.set(lang, langData);
|
|
303
|
+
runtimeState.eagerLoadedLanguages.add(lang);
|
|
304
|
+
return resolveKey(langData, key, sep, runtimeState, lang);
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
177
308
|
return undefined;
|
|
178
309
|
}
|
|
179
310
|
}
|
|
@@ -182,62 +313,110 @@ function resolveKey(obj, key, sep = '.') {
|
|
|
182
313
|
|
|
183
314
|
// --- Public API ---
|
|
184
315
|
function initRuntime(options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
316
|
+
const runtimeState = createState(options);
|
|
317
|
+
preload(runtimeState, options.preload);
|
|
318
|
+
|
|
319
|
+
if (!singletonInitialized) {
|
|
320
|
+
singletonState.baseDir = runtimeState.baseDir;
|
|
321
|
+
singletonState.language = runtimeState.language;
|
|
322
|
+
singletonState.fallbackLanguage = runtimeState.fallbackLanguage;
|
|
323
|
+
singletonState.keySeparator = runtimeState.keySeparator;
|
|
324
|
+
singletonState.lazy = runtimeState.lazy;
|
|
325
|
+
singletonState.keyManifest = new Map();
|
|
326
|
+
singletonState.loadedFiles = new Set();
|
|
327
|
+
singletonState.eagerLoadedLanguages = new Set();
|
|
328
|
+
singletonState.cache.clear();
|
|
329
|
+
preload(singletonState, options.preload);
|
|
330
|
+
singletonInitialized = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return createRuntime(runtimeState);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function preload(runtimeState, shouldPreload) {
|
|
337
|
+
if (shouldPreload === true) {
|
|
338
|
+
if (runtimeState.lazy) {
|
|
339
|
+
getManifestForLanguage(runtimeState, runtimeState.language);
|
|
340
|
+
if (!runtimeState.cache.has(runtimeState.language)) {
|
|
341
|
+
runtimeState.cache.set(runtimeState.language, {});
|
|
342
|
+
}
|
|
343
|
+
if (runtimeState.fallbackLanguage && runtimeState.fallbackLanguage !== runtimeState.language) {
|
|
344
|
+
getManifestForLanguage(runtimeState, runtimeState.fallbackLanguage);
|
|
345
|
+
if (!runtimeState.cache.has(runtimeState.fallbackLanguage)) {
|
|
346
|
+
runtimeState.cache.set(runtimeState.fallbackLanguage, {});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
getTranslationsForState(runtimeState, runtimeState.language);
|
|
351
|
+
if (runtimeState.fallbackLanguage && runtimeState.fallbackLanguage !== runtimeState.language) {
|
|
352
|
+
getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
|
|
353
|
+
}
|
|
195
354
|
}
|
|
196
355
|
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createRuntime(runtimeState) {
|
|
359
|
+
const runtimeTranslate = (key, params) => translateWithState(runtimeState, key, params);
|
|
197
360
|
return {
|
|
198
|
-
t:
|
|
199
|
-
translate,
|
|
200
|
-
setLanguage,
|
|
201
|
-
getLanguage,
|
|
202
|
-
getAvailableLanguages,
|
|
203
|
-
refresh,
|
|
361
|
+
t: runtimeTranslate,
|
|
362
|
+
translate: runtimeTranslate,
|
|
363
|
+
setLanguage: (lang) => setLanguageForState(runtimeState, lang),
|
|
364
|
+
getLanguage: () => getLanguageForState(runtimeState),
|
|
365
|
+
getAvailableLanguages: () => getAvailableLanguagesForState(runtimeState),
|
|
366
|
+
refresh: (lang) => refreshForState(runtimeState, lang),
|
|
204
367
|
};
|
|
205
368
|
}
|
|
206
369
|
|
|
207
370
|
function translate(key, params = {}) {
|
|
208
|
-
|
|
209
|
-
|
|
371
|
+
return translateWithState(singletonState, key, params);
|
|
372
|
+
}
|
|
210
373
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
374
|
+
function translateWithState(runtimeState, key, params = {}) {
|
|
375
|
+
const langData = getTranslationsForState(runtimeState, runtimeState.language);
|
|
376
|
+
let value = resolveKey(langData, key, runtimeState.keySeparator, runtimeState, runtimeState.language);
|
|
377
|
+
|
|
378
|
+
if (typeof value === 'undefined' && runtimeState.fallbackLanguage) {
|
|
379
|
+
const fbData = getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
|
|
380
|
+
value = resolveKey(fbData, key, runtimeState.keySeparator, runtimeState, runtimeState.fallbackLanguage);
|
|
381
|
+
}
|
|
215
382
|
|
|
216
383
|
if (typeof value === 'string') return interpolate(value, params);
|
|
217
384
|
return typeof value === 'undefined' ? key : value;
|
|
218
385
|
}
|
|
219
386
|
|
|
220
387
|
function setLanguage(lang) {
|
|
388
|
+
setLanguageForState(singletonState, lang);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function setLanguageForState(runtimeState, lang) {
|
|
221
392
|
if (!lang || typeof lang !== 'string') return;
|
|
222
|
-
|
|
393
|
+
runtimeState.language = lang;
|
|
223
394
|
}
|
|
224
395
|
|
|
225
396
|
function getLanguage() {
|
|
226
|
-
return
|
|
397
|
+
return getLanguageForState(singletonState);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getLanguageForState(runtimeState) {
|
|
401
|
+
return runtimeState.language;
|
|
227
402
|
}
|
|
228
403
|
|
|
229
404
|
function getAvailableLanguages() {
|
|
405
|
+
return getAvailableLanguagesForState(singletonState);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getAvailableLanguagesForState(runtimeState) {
|
|
230
409
|
const langs = new Set();
|
|
231
|
-
if (!
|
|
232
|
-
if (!SecurityUtils.safeExistsSync(
|
|
410
|
+
if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
|
|
411
|
+
if (!SecurityUtils.safeExistsSync(runtimeState.baseDir, path.dirname(runtimeState.baseDir))) return ['en'];
|
|
233
412
|
try {
|
|
234
|
-
for (const entry of fs.readdirSync(
|
|
413
|
+
for (const entry of fs.readdirSync(runtimeState.baseDir, { withFileTypes: true })) {
|
|
235
414
|
if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
236
415
|
langs.add(entry.name.replace(/\.json$/i, ''));
|
|
237
416
|
} else if (entry.isDirectory()) {
|
|
238
417
|
const lang = entry.name;
|
|
239
|
-
const idx = path.join(
|
|
240
|
-
if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
|
|
418
|
+
const idx = path.join(runtimeState.baseDir, lang, `${lang}.json`);
|
|
419
|
+
if (SecurityUtils.safeExistsSync(idx, path.dirname(idx))) langs.add(lang);
|
|
241
420
|
else langs.add(lang); // be permissive
|
|
242
421
|
}
|
|
243
422
|
}
|
|
@@ -248,13 +427,23 @@ function getAvailableLanguages() {
|
|
|
248
427
|
return Array.from(langs.size ? langs : new Set(['en']));
|
|
249
428
|
}
|
|
250
429
|
|
|
251
|
-
function refresh(lang
|
|
252
|
-
|
|
253
|
-
if (lang !== state.fallbackLanguage && state.cache.has(state.fallbackLanguage)) {
|
|
254
|
-
// do nothing; keep or clear on demand
|
|
255
|
-
}
|
|
430
|
+
function refresh(lang) {
|
|
431
|
+
refreshForState(singletonState, lang);
|
|
256
432
|
}
|
|
257
433
|
|
|
434
|
+
function refreshForState(runtimeState, lang = runtimeState.language) {
|
|
435
|
+
if (runtimeState.cache.has(lang)) runtimeState.cache.delete(lang);
|
|
436
|
+
if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
|
|
437
|
+
if (runtimeState.loadedFiles) {
|
|
438
|
+
for (const fileKey of Array.from(runtimeState.loadedFiles)) {
|
|
439
|
+
if (fileKey.startsWith(`${lang}\0`)) runtimeState.loadedFiles.delete(fileKey);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (lang !== runtimeState.fallbackLanguage && runtimeState.cache.has(runtimeState.fallbackLanguage)) {
|
|
443
|
+
// do nothing; keep or clear on demand
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
258
447
|
module.exports = {
|
|
259
448
|
initRuntime,
|
|
260
449
|
translate,
|
|
@@ -262,5 +451,6 @@ module.exports = {
|
|
|
262
451
|
setLanguage,
|
|
263
452
|
getLanguage,
|
|
264
453
|
getAvailableLanguages,
|
|
265
|
-
refresh,
|
|
266
|
-
|
|
454
|
+
refresh,
|
|
455
|
+
loadKeyManifest: (baseDir) => loadKeyManifestFromDir(baseDir || singletonState.baseDir),
|
|
456
|
+
};
|
package/ui-locales/en.json
CHANGED
|
@@ -1209,7 +1209,7 @@
|
|
|
1209
1209
|
},
|
|
1210
1210
|
"operations": {
|
|
1211
1211
|
"completed": "✅ Operation completed successfully!",
|
|
1212
|
-
"deleteReportsTitle": "🗑️ Delete Reports &
|
|
1212
|
+
"deleteReportsTitle": "🗑️ Delete Reports, Logs & Cache",
|
|
1213
1213
|
"scanningForFiles": "🔍 Scanning for files to delete...",
|
|
1214
1214
|
"noFilesFoundToDelete": "✅ No files found to delete.",
|
|
1215
1215
|
"availableDirectories": "📁 Available directories:",
|