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.
- package/README.md +23 -25
- package/dist/cli/helpers.d.ts +34 -2
- package/dist/cli/helpers.js +96 -37
- package/dist/cli.js +40 -39
- package/dist/commands/review.d.ts +33 -8
- package/dist/commands/review.js +89 -22
- package/dist/commands/translator.d.ts +4 -2
- package/dist/commands/translator.js +1 -1
- package/dist/config/index.d.ts +53 -66
- package/dist/config/index.js +2 -1
- package/dist/config/setup.d.ts +3 -2
- package/dist/config/setup.js +5 -3
- package/dist/core/adapters/android-adapter.d.ts +3 -2
- package/dist/core/adapters/android-adapter.js +12 -13
- package/dist/core/adapters/base-adapter.d.ts +13 -2
- package/dist/core/adapters/ios-adapter.d.ts +3 -2
- package/dist/core/adapters/json-adapter.d.ts +4 -3
- package/dist/core/adapters/json-adapter.js +1 -3
- package/dist/core/adapters/po-adapter.d.ts +3 -2
- package/dist/core/adapters/po-adapter.js +4 -5
- package/dist/core/adapters/properties-adapter.d.ts +3 -2
- package/dist/core/adapters/properties-adapter.js +2 -4
- package/dist/core/adapters/yaml-adapter.d.ts +4 -3
- package/dist/core/adapters/yaml-adapter.js +1 -3
- package/dist/core/context-processor.d.ts +21 -6
- package/dist/core/context-processor.js +2 -7
- package/dist/core/detection/framework-detector.js +1 -1
- package/dist/core/detection/structure-detector.js +1 -1
- package/dist/core/fallback-provider.d.ts +3 -4
- package/dist/core/fallback-provider.js +15 -21
- package/dist/core/orchestrator.d.ts +98 -26
- package/dist/core/orchestrator.js +56 -116
- package/dist/core/pipeline/Pipeline.d.ts +2 -2
- package/dist/core/pipeline/Pipeline.js +1 -3
- package/dist/core/pipeline/context.d.ts +69 -13
- package/dist/core/pipeline/context.js +1 -7
- package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
- package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
- package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
- package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
- package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
- package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
- package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
- package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
- package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
- package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
- package/dist/core/pipeline/steps/TranslationStep.js +21 -23
- package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
- package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
- package/dist/core/provider-factory.d.ts +15 -9
- package/dist/core/provider-factory.js +42 -32
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/api-types.d.ts +74 -0
- package/dist/providers/api-types.js +5 -0
- package/dist/providers/base-provider.d.ts +27 -31
- package/dist/providers/base-provider.js +22 -46
- package/dist/providers/dashscope.js +0 -2
- package/dist/providers/deepseek.js +0 -1
- package/dist/providers/gemini.js +0 -1
- package/dist/providers/openai.d.ts +3 -3
- package/dist/providers/openai.js +0 -1
- package/dist/providers/xai.js +0 -1
- package/dist/services/embedding-provider.js +4 -2
- package/dist/services/language-processor.d.ts +5 -3
- package/dist/services/language-processor.js +38 -43
- package/dist/services/source-analyzer.d.ts +24 -0
- package/dist/services/source-analyzer.js +161 -68
- package/dist/services/translation-service.d.ts +31 -13
- package/dist/services/translation-service.js +116 -71
- package/dist/services/vector-store.d.ts +15 -2
- package/dist/services/vector-store.js +14 -10
- package/dist/types/index.d.ts +31 -33
- package/dist/utils/ai-context-analyzer.d.ts +12 -3
- package/dist/utils/ai-context-analyzer.js +18 -10
- package/dist/utils/confidence-scorer.d.ts +16 -19
- package/dist/utils/confidence-scorer.js +36 -125
- package/dist/utils/error-helper.d.ts +27 -6
- package/dist/utils/error-helper.js +10 -9
- package/dist/utils/file-manager.d.ts +8 -7
- package/dist/utils/file-manager.js +22 -23
- package/dist/utils/glossary-manager.d.ts +22 -3
- package/dist/utils/glossary-manager.js +47 -28
- package/dist/utils/graceful-shutdown.d.ts +7 -1
- package/dist/utils/graceful-shutdown.js +1 -2
- package/dist/utils/input-validator.d.ts +53 -66
- package/dist/utils/input-validator.js +100 -151
- package/dist/utils/locale-paths.d.ts +7 -0
- package/dist/utils/locale-paths.js +16 -0
- package/dist/utils/logger.d.ts +8 -8
- package/dist/utils/logger.js +7 -15
- package/dist/utils/object-transformer.d.ts +20 -5
- package/dist/utils/object-transformer.js +20 -28
- package/dist/utils/progress-tracker.js +2 -3
- package/dist/utils/prompt-templates.d.ts +45 -5
- package/dist/utils/prompt-templates.js +61 -5
- package/dist/utils/quality/base-checker.d.ts +40 -18
- package/dist/utils/quality/base-checker.js +1 -2
- package/dist/utils/quality/code-block-checker.d.ts +13 -5
- package/dist/utils/quality/index.d.ts +2 -1
- package/dist/utils/quality/index.js +8 -27
- package/dist/utils/quality/length-checker.d.ts +35 -5
- package/dist/utils/quality/length-checker.js +2 -3
- package/dist/utils/quality/markdown-checker.d.ts +14 -7
- package/dist/utils/quality/special-characters-checker.d.ts +18 -1
- package/dist/utils/quality/special-characters-checker.js +0 -1
- package/dist/utils/quality/style-guide-checker.d.ts +1 -1
- package/dist/utils/quality/style-guide-checker.js +0 -2
- package/dist/utils/quality/tone-verifier.js +1 -1
- package/dist/utils/rate-limiter.d.ts +17 -1
- package/dist/utils/rate-limiter.js +12 -11
- package/dist/utils/retry-helper.d.ts +21 -3
- package/dist/utils/retry-helper.js +11 -3
- package/dist/utils/review-queue.d.ts +45 -0
- package/dist/utils/review-queue.js +53 -0
- package/dist/utils/state-manager.d.ts +12 -3
- package/dist/utils/state-manager.js +32 -7
- package/dist/utils/statistics-manager.d.ts +11 -1
- package/dist/utils/statistics-manager.js +8 -0
- package/dist/utils/ui-manager.d.ts +2 -1
- package/dist/utils/ui-manager.js +15 -1
- package/package.json +22 -17
- package/dist/commands/interactive-review.d.ts +0 -9
- package/dist/commands/interactive-review.js +0 -109
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
- package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
- 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
|
-
|
|
3
|
+
[](https://github.com/ahmetenesdur/locai/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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?**
|
|
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
|
|
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
|
-
|
|
32
|
+
Learn about `translate`, `fix`, `review`, and `validate-config`.
|
|
33
33
|
|
|
34
34
|
### [Providers](./docs/providers.md)
|
|
35
35
|
|
|
36
|
-
|
|
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
|
-
|
|
40
|
+
How Locai's features work:
|
|
41
41
|
|
|
42
|
-
- **
|
|
43
|
-
- **Deep Context Analysis**:
|
|
44
|
-
- **
|
|
45
|
-
- **Smart Sync**:
|
|
46
|
-
- **
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
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
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
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
|
|
package/dist/cli/helpers.d.ts
CHANGED
|
@@ -1,2 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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>;
|
package/dist/cli/helpers.js
CHANGED
|
@@ -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 (
|
|
64
|
-
safeGlobalOpts.targets = `[${
|
|
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.
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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:
|
|
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:
|
|
174
|
-
maxDelay:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
//
|
|
327
|
-
process.exit(0);
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
|
24
|
-
.version(
|
|
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", "
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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("
|
|
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
|
|
87
|
+
if (isHelperError(error)) {
|
|
83
88
|
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
84
89
|
}
|
|
85
90
|
else {
|
|
86
|
-
|
|
87
|
-
|
|
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("
|
|
97
|
-
.option("--use-ai", "
|
|
98
|
-
.option("--context-provider <provider>", "
|
|
99
|
-
.option("--context-threshold <number>", "
|
|
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
|
|
111
|
+
if (isHelperError(error)) {
|
|
106
112
|
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
107
113
|
}
|
|
108
114
|
else {
|
|
109
|
-
|
|
110
|
-
|
|
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("
|
|
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", "
|
|
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
|
|
137
|
+
if (isHelperError(error)) {
|
|
131
138
|
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
132
139
|
}
|
|
133
140
|
else {
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
});
|