locai 2.6.1 → 2.7.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.
Files changed (131) hide show
  1. package/README.md +23 -25
  2. package/dist/cli/helpers.d.ts +34 -2
  3. package/dist/cli/helpers.js +96 -37
  4. package/dist/cli.js +40 -39
  5. package/dist/commands/review.d.ts +33 -8
  6. package/dist/commands/review.js +89 -22
  7. package/dist/commands/translator.d.ts +4 -2
  8. package/dist/commands/translator.js +1 -1
  9. package/dist/config/index.d.ts +53 -66
  10. package/dist/config/index.js +2 -1
  11. package/dist/config/setup.d.ts +3 -2
  12. package/dist/config/setup.js +5 -3
  13. package/dist/core/adapters/android-adapter.d.ts +3 -2
  14. package/dist/core/adapters/android-adapter.js +12 -13
  15. package/dist/core/adapters/base-adapter.d.ts +13 -2
  16. package/dist/core/adapters/ios-adapter.d.ts +3 -2
  17. package/dist/core/adapters/json-adapter.d.ts +4 -3
  18. package/dist/core/adapters/json-adapter.js +1 -3
  19. package/dist/core/adapters/po-adapter.d.ts +3 -2
  20. package/dist/core/adapters/po-adapter.js +4 -5
  21. package/dist/core/adapters/properties-adapter.d.ts +3 -2
  22. package/dist/core/adapters/properties-adapter.js +2 -4
  23. package/dist/core/adapters/yaml-adapter.d.ts +4 -3
  24. package/dist/core/adapters/yaml-adapter.js +1 -3
  25. package/dist/core/context-processor.d.ts +21 -6
  26. package/dist/core/context-processor.js +2 -7
  27. package/dist/core/detection/framework-detector.js +1 -1
  28. package/dist/core/detection/structure-detector.js +1 -1
  29. package/dist/core/fallback-provider.d.ts +3 -4
  30. package/dist/core/fallback-provider.js +15 -21
  31. package/dist/core/orchestrator.d.ts +98 -26
  32. package/dist/core/orchestrator.js +56 -116
  33. package/dist/core/pipeline/Pipeline.d.ts +2 -2
  34. package/dist/core/pipeline/Pipeline.js +1 -3
  35. package/dist/core/pipeline/context.d.ts +69 -13
  36. package/dist/core/pipeline/context.js +1 -7
  37. package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
  38. package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
  39. package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
  40. package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
  41. package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
  42. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
  43. package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
  44. package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
  45. package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
  46. package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
  47. package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
  48. package/dist/core/pipeline/steps/TranslationStep.js +21 -23
  49. package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
  50. package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
  51. package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
  52. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
  53. package/dist/core/provider-factory.d.ts +15 -9
  54. package/dist/core/provider-factory.js +42 -32
  55. package/dist/providers/anthropic.d.ts +5 -4
  56. package/dist/providers/anthropic.js +3 -2
  57. package/dist/providers/api-types.d.ts +74 -0
  58. package/dist/providers/api-types.js +5 -0
  59. package/dist/providers/base-provider.d.ts +27 -31
  60. package/dist/providers/base-provider.js +22 -46
  61. package/dist/providers/dashscope.js +0 -2
  62. package/dist/providers/deepseek.js +0 -1
  63. package/dist/providers/gemini.js +0 -1
  64. package/dist/providers/openai.d.ts +3 -3
  65. package/dist/providers/openai.js +0 -1
  66. package/dist/providers/xai.js +0 -1
  67. package/dist/services/embedding-provider.js +4 -2
  68. package/dist/services/language-processor.d.ts +5 -3
  69. package/dist/services/language-processor.js +38 -43
  70. package/dist/services/source-analyzer.d.ts +24 -0
  71. package/dist/services/source-analyzer.js +161 -68
  72. package/dist/services/translation-service.d.ts +31 -13
  73. package/dist/services/translation-service.js +116 -71
  74. package/dist/services/vector-store.d.ts +15 -2
  75. package/dist/services/vector-store.js +14 -10
  76. package/dist/types/index.d.ts +31 -33
  77. package/dist/utils/ai-context-analyzer.d.ts +12 -3
  78. package/dist/utils/ai-context-analyzer.js +18 -10
  79. package/dist/utils/confidence-scorer.d.ts +16 -19
  80. package/dist/utils/confidence-scorer.js +36 -125
  81. package/dist/utils/error-helper.d.ts +27 -6
  82. package/dist/utils/error-helper.js +10 -9
  83. package/dist/utils/file-manager.d.ts +8 -7
  84. package/dist/utils/file-manager.js +22 -23
  85. package/dist/utils/glossary-manager.d.ts +22 -3
  86. package/dist/utils/glossary-manager.js +47 -28
  87. package/dist/utils/graceful-shutdown.d.ts +7 -1
  88. package/dist/utils/graceful-shutdown.js +1 -2
  89. package/dist/utils/input-validator.d.ts +53 -66
  90. package/dist/utils/input-validator.js +100 -151
  91. package/dist/utils/locale-paths.d.ts +7 -0
  92. package/dist/utils/locale-paths.js +16 -0
  93. package/dist/utils/logger.d.ts +8 -8
  94. package/dist/utils/logger.js +7 -15
  95. package/dist/utils/object-transformer.d.ts +20 -5
  96. package/dist/utils/object-transformer.js +20 -28
  97. package/dist/utils/progress-tracker.js +2 -3
  98. package/dist/utils/prompt-templates.d.ts +45 -5
  99. package/dist/utils/prompt-templates.js +61 -5
  100. package/dist/utils/quality/base-checker.d.ts +40 -18
  101. package/dist/utils/quality/base-checker.js +1 -2
  102. package/dist/utils/quality/code-block-checker.d.ts +13 -5
  103. package/dist/utils/quality/index.d.ts +2 -1
  104. package/dist/utils/quality/index.js +8 -27
  105. package/dist/utils/quality/length-checker.d.ts +35 -5
  106. package/dist/utils/quality/length-checker.js +2 -3
  107. package/dist/utils/quality/markdown-checker.d.ts +14 -7
  108. package/dist/utils/quality/special-characters-checker.d.ts +18 -1
  109. package/dist/utils/quality/special-characters-checker.js +0 -1
  110. package/dist/utils/quality/style-guide-checker.d.ts +1 -1
  111. package/dist/utils/quality/style-guide-checker.js +0 -2
  112. package/dist/utils/quality/tone-verifier.js +1 -1
  113. package/dist/utils/rate-limiter.d.ts +17 -1
  114. package/dist/utils/rate-limiter.js +12 -11
  115. package/dist/utils/retry-helper.d.ts +21 -3
  116. package/dist/utils/retry-helper.js +11 -3
  117. package/dist/utils/review-queue.d.ts +45 -0
  118. package/dist/utils/review-queue.js +53 -0
  119. package/dist/utils/state-manager.d.ts +12 -3
  120. package/dist/utils/state-manager.js +32 -7
  121. package/dist/utils/statistics-manager.d.ts +11 -1
  122. package/dist/utils/statistics-manager.js +8 -0
  123. package/dist/utils/ui-manager.d.ts +2 -1
  124. package/dist/utils/ui-manager.js +15 -1
  125. package/package.json +22 -17
  126. package/dist/commands/interactive-review.d.ts +0 -9
  127. package/dist/commands/interactive-review.js +0 -109
  128. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
  129. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
  130. package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
  131. package/dist/utils/quality/__tests__/tone-verifier.test.js +0 -91
package/README.md CHANGED
@@ -1,54 +1,53 @@
1
1
  # Locai
2
2
 
3
- **The Universal AI Localization Engine.**
3
+ [![CI](https://github.com/ahmetenesdur/locai/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ahmetenesdur/locai/actions/workflows/ci.yml)
4
4
 
5
- Locai brings human-quality AI localization to **any project** (Next.js, Flutter, Android, iOS, Python) and **any file format** with a single command. It fits right into your workflow, whether you're a solo dev or an enterprise team.
5
+ **AI translation for JSON locale files.**
6
+
7
+ Locai translates the JSON locale files of your project (`en.json` or `en/translation.json`) with an AI provider of your choice, from a single command. It works with any project that keeps its translations in JSON, for example Next.js, React or Vue apps using i18next.
6
8
 
7
9
  ## 🚀 Quick Start
8
10
 
9
- Translate your project in seconds without installing anything:
11
+ Run it with `npx`, without installing anything:
10
12
 
11
13
  ```bash
12
14
  # Translates your ./locales from English to Turkish, Spanish, and German
13
15
  npx locai translate --source en --targets tr,es,de
14
16
  ```
15
17
 
16
- > **Need a permanent setup?** Check out our [Configuration Guide](./docs/configuration.md) to create a `localize.config.js` file.
18
+ > **Need a permanent setup?** See the [Configuration Guide](./docs/configuration.md) to create a `localize.config.ts` file.
17
19
 
18
20
  ## 📖 Documentation
19
21
 
20
- Everything you need to know to master Locai.
21
-
22
22
  ### [Getting Started](./docs/getting-started.md)
23
23
 
24
24
  Introduction, installation options, and setting up your API keys.
25
25
 
26
26
  ### [Configuration Guide](./docs/configuration.md)
27
27
 
28
- Detailed guide on `localize.config.ts`, supported file formats (`json`, `yaml`, `po`, `arb`), and advanced settings.
28
+ Detailed guide on `localize.config.ts`, the supported locale file layouts, and advanced settings.
29
29
 
30
30
  ### [CLI Reference](./docs/cli-reference.md)
31
31
 
32
- Master the command line. Learn about `translate`, `fix`, `review`, and `analyze`.
32
+ Learn about `translate`, `fix`, `review`, and `validate-config`.
33
33
 
34
34
  ### [Providers](./docs/providers.md)
35
35
 
36
- Choose the right brain for your localization. Compare OpenAI, Gemini, DeepSeek, and more.
36
+ Supported providers (OpenAI, Anthropic, Gemini, DeepSeek, Dashscope, XAI), their default models, API keys, rate limits and fallback.
37
37
 
38
38
  ### [Core Concepts](./docs/concepts.md)
39
39
 
40
- Deep dive into Locai's intelligent features. We didn't just wrap an API; we built a localization engine.
40
+ How Locai's features work:
41
41
 
42
- - **Smart Context Awareness**: Uses AI to understand the context of your strings for better translations.
43
- - **Deep Context Analysis**: Scans your source code (AST) to extract component names, comments, and props for context-aware translations.
44
- - **Infinite Memory**: Uses vector embeddings to remember previous translations and maintain consistency. If you translate the same (or similar) sentence again, we fetch it from local cache instantly. Zero cost, 100% consistency.
45
- - **Smart Sync**: We hash every key. Locai only translates **new** or **modified** keys. If you have 1000 keys and change 5, we only send 5 to the AI.
46
- - **Context Awareness**: Locai analyzes your content. It knows if a string is for a **Button** (keep it short), **Marketing** (make it punchy), or **Legal** (be precise).
47
- - **Glossary & Brand Safety**: Define immutable terms (like "Locai", "API", "SaaS"). We guarantee they are never translated or corrupted.
48
- - **Auto-Fix Engine**: Did the AI forget a closing quote? Or mess up a `{placeholder}`? Locai detects these syntax errors and fixes them automatically before saving.
49
- - **Multi-Provider Fallback**: OpenAI down? Rate limit hit? Locai automatically switches to Gemini, DeepSeek, or other configured providers without breaking the build.
50
- - **Confidence Scoring**: Every translation gets a score (0-1). Low confidence translations are flagged for your review.
51
- - **Interactive Review**: A built-in Terminal UI (TUI) to manually approve or edit translations that need attention.
42
+ - **Context Categories**: You define categories (keywords plus a prompt) in `context.categories`. When a string contains enough of a category's keywords, that category's prompt is added to the translation request. There are no built-in categories. AI classification of longer strings is optional (`context.useAI`, off by default).
43
+ - **Deep Context Analysis**: Finds where each key is used in your TypeScript/TSX code (`t("key")` calls) and sends that usage with the translation request: file and line, component name, nearby comments, JSX props and a short code snippet. Turn it off with `deepContext: false` or `--no-deep-context`.
44
+ - **Vector Memory (optional)**: With `vectorMemory.enabled: true`, Locai stores OpenAI embeddings of translated strings in a local database. Near-identical strings (similarity ≥ 0.98) reuse the stored translation without a translation call; similar ones (≥ 0.85) are given to the model as a reference. Requires `OPENAI_API_KEY`, and each new string costs an embedding request.
45
+ - **Smart Sync**: Locai stores a hash of each source string. A key is translated when it is missing from a target file or its source text changed since the last run. If you have 1000 translated keys and change 5, only those 5 are sent to the AI. Keys removed from the source file are removed from the target files.
46
+ - **Glossary & Brand Safety**: Define protected terms (like "Locai", "API", "SaaS"). Locai swaps them for placeholder tokens in the text being translated, then restores your configured translation, so the model does not see or translate those terms in that text. (Source code sent by Deep Context Analysis is not tokenized.)
47
+ - **Auto-Fix**: After each translation, rule-based checks repair what they can: unbalanced quotes, missing or damaged placeholders (`{name}`, `${name}`, `%s`, `%d`), HTML tags and ending punctuation, plus markdown, code blocks and special characters if you turn those rules on. The checks do not reject a translation.
48
+ - **Provider Fallback**: With `useFallback` (on by default), a failed request is retried with the other providers that have an API key set.
49
+ - **Confidence Scoring** (opt-in): Each new translation gets a score (0-1) computed from checks on the translation itself: lost placeholders or HTML tags, untranslated text, leftover model commentary, unusual length or punctuation. Low scores are flagged for review; very low ones are not written to the locale file.
50
+ - **Interactive Review**: `locai review` steps through the saved review queue in the terminal so you can approve or edit flagged translations; the ones you approve or edit are written to the locale file.
52
51
 
53
52
  ### [Development Guide](./docs/development.md)
54
53
 
@@ -56,11 +55,10 @@ Contributing to Locai? Learn how to build, test, and run the project locally.
56
55
 
57
56
  ## ✨ Why Locai?
58
57
 
59
- - **Framework Agnostic**: Works with Next.js, Flutter, Django, and more.
60
- - **Context Aware**: Knows the difference between "Home" (House) and "Home" (Page).
61
- - **Cost Efficient**: Smart caching and incremental updates save ~90% of AI costs.
62
- - **Flexible Structure**: Supports both flat (`en.json`) and nested (`en/translation.json`) directory structures automatically.
63
- - **Developer Friendly**: TypeScript config, type safety, and great error messages.
58
+ - **JSON Locale Files**: Works with any project that stores translations as JSON (`<lang>.json` or `<lang>/translation.json`).
59
+ - **Fewer API Calls**: Only new, changed or missing keys are sent to the AI. Strings already translated in the current run, or found in vector memory when it is enabled, are reused instead of triggering another translation call.
60
+ - **Flat or Nested Layout**: Supports flat (`en.json`) and nested (`en/translation.json`) layouts. Target files are written in the same layout as the source file. The nested layout requires the file name `translation.json`.
61
+ - **TypeScript Config**: `localize.config.ts` with types via `defineConfig` from `locai/config`.
64
62
 
65
63
  ## 🤝 Community & Contributing
66
64
 
@@ -1,2 +1,34 @@
1
- export declare const autoDetectConfig: (defaultConfig: any) => Promise<any>;
2
- export declare const runCommand: (defaultConfig: any, commandOptions: any, commandName: string) => Promise<void>;
1
+ import { LocalizeConfig, ResolvedLocalizeConfig } from "../config/index.js";
2
+ import type { ReviewCommandConfig } from "../commands/review.js";
3
+ /**
4
+ * Settings for `locai review`: the global `--source` / `--localesDir` flags (whose
5
+ * defaults are the config values) decide where approved translations are written.
6
+ */
7
+ export declare const resolveReviewConfig: (defaultConfig: LocalizeConfig) => ReviewCommandConfig;
8
+ /**
9
+ * Options commander parses for the commands handled by runCommand.
10
+ * Each command sets only its own subset (see the option definitions in src/cli.ts).
11
+ */
12
+ export interface CommandOptions {
13
+ provider?: string;
14
+ concurrency?: number;
15
+ /** `--no-cache` sets `cache: false`. */
16
+ cache?: boolean;
17
+ force?: boolean;
18
+ /** `fix --length` is a boolean flag ("fix length issues"); a string is a length mode. */
19
+ length?: string | boolean;
20
+ autoOptimize?: boolean;
21
+ stats?: boolean;
22
+ minConfidence?: number;
23
+ saveReviewQueue?: boolean;
24
+ useAi?: boolean;
25
+ contextProvider?: string;
26
+ contextThreshold?: number;
27
+ contextConfidence?: number;
28
+ contextDebug?: boolean;
29
+ minTextLength?: number;
30
+ allowNewCategories?: boolean;
31
+ maxRetries?: number;
32
+ }
33
+ export declare const autoDetectConfig: <T extends LocalizeConfig>(defaultConfig: T) => Promise<T>;
34
+ export declare const runCommand: (defaultConfig: ResolvedLocalizeConfig, commandOptions: CommandOptions, commandName: string) => Promise<void>;
@@ -4,9 +4,26 @@ import { program } from "commander";
4
4
  import { findLocaleFiles, translateFile, validateAndFixExistingTranslations, } from "../commands/translator.js";
5
5
  import InputValidator from "../utils/input-validator.js";
6
6
  import ErrorHelper from "../utils/error-helper.js";
7
+ import { NoProviderError } from "../core/provider-factory.js";
8
+ import { runFailures } from "../utils/statistics-manager.js";
7
9
  import { FrameworkDetector } from "../core/detection/framework-detector.js";
8
10
  import { StructureDetector, LocaleStructure } from "../core/detection/structure-detector.js";
9
11
  import { configureComponents } from "../config/setup.js";
12
+ /**
13
+ * Settings for `locai review`: the global `--source` / `--localesDir` flags (whose
14
+ * defaults are the config values) decide where approved translations are written.
15
+ */
16
+ export const resolveReviewConfig = (defaultConfig) => {
17
+ const globalOpts = program.opts();
18
+ const source = globalOpts.source ?? defaultConfig.source;
19
+ const localesDir = globalOpts.localesDir ?? defaultConfig.localesDir;
20
+ return {
21
+ source: source ? InputValidator.validateLanguageCode(source, "source language") : "en",
22
+ localesDir: localesDir
23
+ ? InputValidator.validateDirectoryPath(localesDir, "locales directory")
24
+ : "./locales",
25
+ };
26
+ };
10
27
  export const autoDetectConfig = async (defaultConfig) => {
11
28
  // Auto-detect framework and configuration if not explicitly set
12
29
  const detected = await FrameworkDetector.detect(process.cwd());
@@ -60,8 +77,8 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
60
77
  if (globalOpts.debug) {
61
78
  const safeGlobalOpts = { ...globalOpts };
62
79
  const safeCommandOptions = { ...commandOptions };
63
- if (safeGlobalOpts.targets) {
64
- safeGlobalOpts.targets = `[${safeGlobalOpts.targets.length} languages]`;
80
+ if (globalOpts.targets) {
81
+ safeGlobalOpts.targets = `[${globalOpts.targets.length} languages]`;
65
82
  }
66
83
  if (safeGlobalOpts.localesDir) {
67
84
  safeGlobalOpts.localesDir = "[directory_path]";
@@ -88,30 +105,49 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
88
105
  sanitizedCommandOptions.provider = InputValidator.validateProvider(sanitizedCommandOptions.provider, "API provider");
89
106
  }
90
107
  if (sanitizedCommandOptions.concurrency !== undefined) {
91
- const concurrency = parseInt(sanitizedCommandOptions.concurrency);
108
+ const concurrency = parseInt(String(sanitizedCommandOptions.concurrency));
92
109
  if (isNaN(concurrency) || concurrency < 1 || concurrency > 20) {
93
110
  throw new Error("Concurrency must be a number between 1 and 20");
94
111
  }
95
112
  sanitizedCommandOptions.concurrency = concurrency;
96
113
  }
97
114
  if (sanitizedCommandOptions.contextThreshold !== undefined) {
98
- const threshold = parseInt(sanitizedCommandOptions.contextThreshold);
115
+ const threshold = parseInt(String(sanitizedCommandOptions.contextThreshold));
99
116
  if (isNaN(threshold) || threshold < 1 || threshold > 10) {
100
117
  throw new Error("Context threshold must be a number between 1 and 10");
101
118
  }
102
119
  sanitizedCommandOptions.contextThreshold = threshold;
103
120
  }
104
121
  if (sanitizedCommandOptions.contextConfidence !== undefined) {
105
- const confidence = parseFloat(sanitizedCommandOptions.contextConfidence);
122
+ const confidence = parseFloat(String(sanitizedCommandOptions.contextConfidence));
106
123
  if (isNaN(confidence) || confidence < 0 || confidence > 1) {
107
124
  throw new Error("Context confidence must be a number between 0 and 1");
108
125
  }
109
126
  sanitizedCommandOptions.contextConfidence = confidence;
110
127
  }
111
- if (sanitizedCommandOptions.length) {
112
- const validLengthModes = ["strict", "flexible", "exact", "relaxed", "smart"];
113
- if (!validLengthModes.includes(sanitizedCommandOptions.length)) {
114
- throw new Error(`Invalid length mode: ${sanitizedCommandOptions.length}. Valid modes: ${validLengthModes.join(", ")}`);
128
+ if (sanitizedCommandOptions.minConfidence !== undefined) {
129
+ const minConfidence = parseFloat(String(sanitizedCommandOptions.minConfidence));
130
+ if (isNaN(minConfidence) || minConfidence < 0 || minConfidence > 1) {
131
+ console.warn(`Warning: ignoring --min-confidence ${sanitizedCommandOptions.minConfidence}: it must be a number between 0 and 1`);
132
+ sanitizedCommandOptions.minConfidence = undefined;
133
+ }
134
+ else {
135
+ sanitizedCommandOptions.minConfidence = minConfidence;
136
+ }
137
+ }
138
+ let lengthMode;
139
+ if (typeof sanitizedCommandOptions.length === "string") {
140
+ const validLengthModes = [
141
+ "strict",
142
+ "flexible",
143
+ "exact",
144
+ "relaxed",
145
+ "smart",
146
+ ];
147
+ const requested = sanitizedCommandOptions.length;
148
+ lengthMode = validLengthModes.find((mode) => mode === requested);
149
+ if (!lengthMode) {
150
+ throw new Error(`Invalid length mode: ${requested}. Valid modes: ${validLengthModes.join(", ")}`);
115
151
  }
116
152
  }
117
153
  const mergedOpts = {
@@ -126,7 +162,7 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
126
162
  }),
127
163
  ...sanitizedCommandOptions,
128
164
  };
129
- let concurrencyLimit = parseInt(mergedOpts.concurrency) || defaultConfig.concurrencyLimit || 5;
165
+ let concurrencyLimit = parseInt(String(mergedOpts.concurrency)) || defaultConfig.concurrencyLimit || 5;
130
166
  if (mergedOpts.autoOptimize) {
131
167
  const cpuCount = os.cpus().length;
132
168
  const memoryGB = Math.floor(os.totalmem() / (1024 * 1024 * 1024));
@@ -153,10 +189,15 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
153
189
  localesDir: mergedOpts.localesDir,
154
190
  apiProvider: mergedOpts.provider || defaultConfig.apiProvider,
155
191
  concurrencyLimit: concurrencyLimit,
156
- cacheEnabled: mergedOpts.noCache === undefined ? defaultConfig.cacheEnabled : !mergedOpts.noCache,
157
- debug: mergedOpts.debug,
158
- verbose: mergedOpts.verbose || defaultConfig.logging?.verbose || false,
159
- forceUpdate: mergedOpts.force || false,
192
+ // commander turns `--no-cache` into `cache: false`
193
+ cacheEnabled: mergedOpts.cache === false ? false : defaultConfig.cacheEnabled,
194
+ // `--no-deep-context` sets `deepContext: false`; commander defaults it to true
195
+ deepContext: sanitizedGlobalOpts.deepContext === false ? false : defaultConfig.deepContext,
196
+ debug: sanitizedGlobalOpts.debug || defaultConfig.debug || false,
197
+ verbose: sanitizedGlobalOpts.verbose || defaultConfig.logging?.verbose || false,
198
+ forceUpdate: mergedOpts.force || defaultConfig.forceUpdate || false,
199
+ minConfidence: mergedOpts.minConfidence ?? defaultConfig.minConfidence,
200
+ saveReviewQueue: mergedOpts.saveReviewQueue || defaultConfig.saveReviewQueue,
160
201
  showDetailedStats: mergedOpts.stats || false,
161
202
  autoOptimize: mergedOpts.autoOptimize || defaultConfig.advanced?.autoOptimize || false,
162
203
  fixLength: commandName === "fix",
@@ -165,18 +206,18 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
165
206
  qualityChecks: defaultConfig.qualityChecks,
166
207
  lengthControl: {
167
208
  ...defaultConfig.lengthControl,
168
- mode: mergedOpts.length || defaultConfig.lengthControl?.mode || "smart",
209
+ mode: lengthMode || defaultConfig.lengthControl?.mode || "smart",
169
210
  },
170
211
  retryOptions: {
171
212
  ...defaultConfig.retryOptions,
172
213
  maxRetries: mergedOpts.maxRetries || defaultConfig.retryOptions?.maxRetries || 2,
173
- initialDelay: mergedOpts.initialDelay || defaultConfig.retryOptions?.initialDelay || 1000,
174
- maxDelay: mergedOpts.maxDelay || defaultConfig.retryOptions?.maxDelay || 10000,
214
+ initialDelay: defaultConfig.retryOptions?.initialDelay || 1000,
215
+ maxDelay: defaultConfig.retryOptions?.maxDelay || 10000,
175
216
  jitter: defaultConfig.retryOptions?.jitter !== false,
176
217
  },
177
218
  context: {
178
219
  ...defaultConfig.context,
179
- enabled: true,
220
+ enabled: defaultConfig.context.enabled !== false,
180
221
  debug: mergedOpts.contextDebug || defaultConfig.context.debug || false,
181
222
  useAI: mergedOpts.useAi ||
182
223
  mergedOpts.contextProvider !== undefined ||
@@ -198,11 +239,11 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
198
239
  },
199
240
  advanced: {
200
241
  ...defaultConfig.advanced,
201
- timeoutMs: mergedOpts.timeout || defaultConfig.advanced?.timeoutMs || 60000,
242
+ timeoutMs: defaultConfig.advanced?.timeoutMs || 60000,
202
243
  maxKeyLength: defaultConfig.advanced?.maxKeyLength || 10000,
203
244
  maxBatchSize: defaultConfig.advanced?.maxBatchSize || 50,
204
245
  autoOptimize: mergedOpts.autoOptimize || defaultConfig.advanced?.autoOptimize || false,
205
- debug: mergedOpts.debug || defaultConfig.advanced?.debug || false,
246
+ debug: sanitizedGlobalOpts.debug || defaultConfig.advanced?.debug || false,
206
247
  },
207
248
  rateLimiter: {
208
249
  ...defaultConfig.rateLimiter,
@@ -211,7 +252,7 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
211
252
  fileOperations: defaultConfig.fileOperations || {},
212
253
  logging: {
213
254
  ...defaultConfig.logging,
214
- verbose: mergedOpts.verbose || defaultConfig.logging?.verbose || false,
255
+ verbose: sanitizedGlobalOpts.verbose || defaultConfig.logging?.verbose || false,
215
256
  },
216
257
  };
217
258
  try {
@@ -221,9 +262,8 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
221
262
  }
222
263
  }
223
264
  catch (configError) {
224
- const error = ErrorHelper.configValidationError(configError.message.includes("validation failed")
225
- ? configError.message.split("\n").slice(1)
226
- : [configError.message]);
265
+ const message = configError instanceof Error ? configError.message : String(configError);
266
+ const error = ErrorHelper.configValidationError(message.includes("validation failed") ? message.split("\n").slice(1) : [message]);
227
267
  console.error(ErrorHelper.formatError(error, { showDebug: globalOpts.debug }));
228
268
  process.exit(1);
229
269
  }
@@ -277,10 +317,18 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
277
317
  process.exit(1);
278
318
  }
279
319
  switch (commandName) {
280
- case "fix":
320
+ case "fix": {
281
321
  console.log("\nRunning in FIX mode");
282
- await Promise.all(files.map((file) => validateAndFixExistingTranslations(file, finalConfig)));
283
- break;
322
+ const summaries = await Promise.all(files.map((file) => validateAndFixExistingTranslations(file, finalConfig)));
323
+ const unfixed = summaries.reduce((sum, s) => sum + (s.issues - s.fixed), 0);
324
+ if (unfixed > 0) {
325
+ console.error(`\n${unfixed} length issue(s) could not be fixed`);
326
+ process.exit(1);
327
+ return;
328
+ }
329
+ process.exit(0);
330
+ return;
331
+ }
284
332
  case "analyze":
285
333
  console.log("\nRunning in ANALYZE mode");
286
334
  console.log("Context analysis mode is not fully implemented yet.");
@@ -289,7 +337,7 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
289
337
  console.log("\nRunning ADVANCED configuration");
290
338
  // Fall through to translate
291
339
  case "translate":
292
- default:
340
+ default: {
293
341
  console.log("\nRunning in TRANSLATION mode");
294
342
  const providerMap = {
295
343
  openai: "OPENAI_API_KEY",
@@ -301,7 +349,7 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
301
349
  };
302
350
  const availableProviders = Object.keys(providerMap).filter((key) => process.env[providerMap[key]]);
303
351
  console.log(`API Configuration:`);
304
- console.log(` - Active Provider: ${finalConfig.apiProvider}`);
352
+ console.log(` - Active Provider: ${finalConfig.apiProvider || "first provider with an API key"}`);
305
353
  console.log(` - Available Providers: ${availableProviders.join(", ") || "None"}`);
306
354
  if (finalConfig.context.useAI) {
307
355
  console.log(`AI Context Analysis: ENABLED (Provider: ${finalConfig.context.aiProvider || finalConfig.apiProvider})`);
@@ -319,23 +367,34 @@ export const runCommand = async (defaultConfig, commandOptions, commandName) =>
319
367
  if (finalConfig.forceUpdate) {
320
368
  console.log(`Warning: Force update mode ENABLED (will update existing translations)`);
321
369
  }
370
+ let failedTranslations = 0;
371
+ const failedLanguages = [];
322
372
  for (const file of files) {
323
- await translateFile(file, finalConfig);
373
+ const failures = runFailures(await translateFile(file, finalConfig));
374
+ failedTranslations += failures.failedTranslations;
375
+ failedLanguages.push(...failures.failedLanguages);
324
376
  }
325
- // Exit after successful translation in CLI mode
326
- // This allows translateFile to be used as library without forcing exit
327
- process.exit(0);
328
- break;
377
+ // Exit here in CLI mode (translateFile can also be used as a library). A run in
378
+ // which anything failed exits non-zero, so CI does not pass a run that did not translate.
379
+ process.exit(failedTranslations > 0 || failedLanguages.length > 0 ? 1 : 0);
380
+ return;
381
+ }
329
382
  }
330
383
  // Other commands (fix, analyze) handle their own completion
331
384
  }
332
385
  catch (validationError) {
333
- if (validationError.message.includes("Configuration validation")) {
386
+ const message = validationError instanceof Error ? validationError.message : String(validationError);
387
+ if (message.includes("Configuration validation")) {
334
388
  // Already handled above with better formatting
335
389
  return;
336
390
  }
337
- console.error(`\nInput validation error: ${validationError.message}`);
338
- if (validationError.stack && process.env.DEBUG) {
391
+ if (validationError instanceof NoProviderError) {
392
+ console.error(`\nError: ${message}`);
393
+ process.exit(1);
394
+ return;
395
+ }
396
+ console.error(`\nInput validation error: ${message}`);
397
+ if (validationError instanceof Error && validationError.stack && process.env.DEBUG) {
339
398
  console.error("\nStack trace:");
340
399
  console.error(validationError.stack);
341
400
  }
package/dist/cli.js CHANGED
@@ -4,8 +4,13 @@ import { program } from "commander";
4
4
  import ErrorHelper from "./utils/error-helper.js";
5
5
  import { loadConfig, validateEnvironment } from "./config/index.js";
6
6
  import { loadEnvironmentVariables, configureComponents } from "./config/setup.js";
7
- import { autoDetectConfig, runCommand } from "./cli/helpers.js";
7
+ import { autoDetectConfig, runCommand, resolveReviewConfig, } from "./cli/helpers.js";
8
8
  import InputValidator from "./utils/input-validator.js";
9
+ /** Errors built by ErrorHelper carry an `ERR_*` code and get its formatted output. */
10
+ const isHelperError = (error) => error instanceof Error &&
11
+ "code" in error &&
12
+ typeof error.code === "string" &&
13
+ error.code.startsWith("ERR_");
9
14
  // Use createRequire to load package.json in ESM context
10
15
  const require = createRequire(import.meta.url);
11
16
  const { version } = require("../package.json");
@@ -13,15 +18,14 @@ const main = async () => {
13
18
  await loadEnvironmentVariables();
14
19
  // Load configuration
15
20
  const loaded = await loadConfig();
16
- let defaultConfig = loaded.config || {};
21
+ let defaultConfig = loaded.config;
17
22
  defaultConfig = await autoDetectConfig(defaultConfig);
18
23
  configureComponents(defaultConfig);
19
24
  validateEnvironment(defaultConfig);
20
- const toolVersion = defaultConfig.version || version;
21
25
  program
22
26
  .name("locai")
23
- .description("AI-powered localization tool for Next.js projects")
24
- .version(toolVersion);
27
+ .description("AI-powered localization tool for JSON locale files")
28
+ .version(version);
25
29
  program
26
30
  .option("-s, --source <lang>", "Source language", defaultConfig.source)
27
31
  .option("-t, --targets <langs>", "Target languages (comma separated)", (val) => val.split(","), defaultConfig.targets)
@@ -46,7 +50,7 @@ const main = async () => {
46
50
  .option("--force", "Force update of existing translations", false)
47
51
  .option("--length <mode>", "Length control mode", defaultConfig.lengthControl?.mode || "smart")
48
52
  .option("--auto-optimize", "Auto-optimize system parameters based on hardware", false)
49
- .option("--stats", "Show detailed stats after completion", false)
53
+ .option("--stats", "No effect: the full summary is always shown", false)
50
54
  .option("--min-confidence <score>", "Minimum confidence threshold (0-1)", parseFloat)
51
55
  .option("--save-review-queue", "Save low-confidence items for manual review", false)
52
56
  .action(async (options) => {
@@ -54,7 +58,7 @@ const main = async () => {
54
58
  await runCommand(defaultConfig, options, "translate");
55
59
  }
56
60
  catch (error) {
57
- if (error.code && error.code.startsWith("ERR_")) {
61
+ if (isHelperError(error)) {
58
62
  console.error(ErrorHelper.formatError(error, {
59
63
  showDebug: process.env.DEBUG === "true",
60
64
  showSolutions: true,
@@ -62,8 +66,9 @@ const main = async () => {
62
66
  }));
63
67
  }
64
68
  else {
65
- console.error(`\nError: ${error.message}`);
66
- if (error.stack && process.env.DEBUG) {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ console.error(`\nError: ${message}`);
71
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
67
72
  console.error(error.stack);
68
73
  }
69
74
  }
@@ -72,19 +77,20 @@ const main = async () => {
72
77
  });
73
78
  program
74
79
  .command("fix")
75
- .description("Fix issues in existing translations")
76
- .option("--length", "Fix length issues in existing translations", true)
80
+ .description("Re-translate existing translations that fail the length check")
81
+ .option("--length", "Fix length issues in existing translations (always on)", true)
77
82
  .action(async (options) => {
78
83
  try {
79
84
  await runCommand(defaultConfig, options, "fix");
80
85
  }
81
86
  catch (error) {
82
- if (error.code && error.code.startsWith("ERR_")) {
87
+ if (isHelperError(error)) {
83
88
  console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
84
89
  }
85
90
  else {
86
- console.error(`\nError: ${error.message}`);
87
- if (error.stack && process.env.DEBUG) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ console.error(`\nError: ${message}`);
93
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
88
94
  console.error(error.stack);
89
95
  }
90
96
  }
@@ -93,21 +99,22 @@ const main = async () => {
93
99
  });
94
100
  program
95
101
  .command("analyze")
96
- .description("Analyze context patterns")
97
- .option("--use-ai", "Enable AI-based context analysis", defaultConfig.context?.useAI)
98
- .option("--context-provider <provider>", "AI provider for analysis", defaultConfig.context?.aiProvider)
99
- .option("--context-threshold <number>", "Minimum match count", Number, defaultConfig.context?.detection?.threshold)
102
+ .description("Not implemented yet: only loads the config and finds the source files")
103
+ .option("--use-ai", "No effect yet (analyze is not implemented)", defaultConfig.context?.useAI)
104
+ .option("--context-provider <provider>", "No effect yet (analyze is not implemented)", defaultConfig.context?.aiProvider)
105
+ .option("--context-threshold <number>", "No effect yet (analyze is not implemented)", Number, defaultConfig.context?.detection?.threshold)
100
106
  .action(async (options) => {
101
107
  try {
102
108
  await runCommand(defaultConfig, options, "analyze");
103
109
  }
104
110
  catch (error) {
105
- if (error.code && error.code.startsWith("ERR_")) {
111
+ if (isHelperError(error)) {
106
112
  console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
107
113
  }
108
114
  else {
109
- console.error(`\nError: ${error.message}`);
110
- if (error.stack && process.env.DEBUG) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ console.error(`\nError: ${message}`);
117
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
111
118
  console.error(error.stack);
112
119
  }
113
120
  }
@@ -116,9 +123,9 @@ const main = async () => {
116
123
  });
117
124
  program
118
125
  .command("advanced")
119
- .description("Access advanced configuration options")
126
+ .description("Translate (like `translate`) with advanced context and retry settings")
120
127
  .option("--context-confidence <number>", "Minimum confidence score", Number, defaultConfig.context?.detection?.minConfidence)
121
- .option("--context-debug", "Show context details", defaultConfig.context?.debug)
128
+ .option("--context-debug", "No effect (not implemented)", defaultConfig.context?.debug)
122
129
  .option("--min-text-length <number>", "Minimum text length for AI analysis", Number, defaultConfig.context?.minTextLength)
123
130
  .option("--allow-new-categories", "Allow AI to suggest new categories", defaultConfig.context?.allowNewCategories)
124
131
  .option("--max-retries <number>", "Maximum number of retries for API calls", Number, defaultConfig.retryOptions?.maxRetries || 2)
@@ -127,12 +134,13 @@ const main = async () => {
127
134
  await runCommand(defaultConfig, options, "advanced");
128
135
  }
129
136
  catch (error) {
130
- if (error.code && error.code.startsWith("ERR_")) {
137
+ if (isHelperError(error)) {
131
138
  console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
132
139
  }
133
140
  else {
134
- console.error(`\nError: ${error.message}`);
135
- if (error.stack && process.env.DEBUG) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ console.error(`\nError: ${message}`);
143
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
136
144
  console.error(error.stack);
137
145
  }
138
146
  }
@@ -185,7 +193,7 @@ const main = async () => {
185
193
  }
186
194
  catch (error) {
187
195
  console.error("\nConfiguration Validation Failed:\n");
188
- console.error(error.message);
196
+ console.error(error instanceof Error ? error.message : String(error));
189
197
  console.error("\nFix the errors above and try again\n");
190
198
  process.exit(1);
191
199
  }
@@ -197,15 +205,7 @@ const main = async () => {
197
205
  .action(async (options) => {
198
206
  try {
199
207
  const ReviewCommand = (await import("./commands/review.js")).default;
200
- // Ensure config matches expectations of ReviewCommand
201
- // ReviewCommand expects ConfigLayer/LocalizeConfig, need to ensure mandatory fields or cast
202
- // ReviewCommandConfig requires localesDir and source to be strings (not undefined)
203
- const reviewConfig = {
204
- ...defaultConfig,
205
- localesDir: defaultConfig.localesDir || "./locales",
206
- source: defaultConfig.source || "en",
207
- };
208
- const review = new ReviewCommand(reviewConfig);
208
+ const review = new ReviewCommand(resolveReviewConfig(defaultConfig));
209
209
  if (options.export) {
210
210
  review.exportReviewQueue(options.export);
211
211
  }
@@ -214,8 +214,9 @@ const main = async () => {
214
214
  }
215
215
  }
216
216
  catch (error) {
217
- console.error(`\nError: ${error.message}`);
218
- if (error.stack && process.env.DEBUG) {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ console.error(`\nError: ${message}`);
219
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
219
220
  console.error(error.stack);
220
221
  }
221
222
  process.exit(1);
@@ -226,7 +227,7 @@ const main = async () => {
226
227
  await runCommand(defaultConfig, {}, "translate");
227
228
  }
228
229
  catch (error) {
229
- console.error(`\nError: ${error.message}`);
230
+ console.error(`\nError: ${error instanceof Error ? error.message : String(error)}`);
230
231
  process.exit(1);
231
232
  }
232
233
  });