-
Notifications
You must be signed in to change notification settings - Fork 835
Expand file tree
/
Copy pathindex.ts
More file actions
202 lines (182 loc) · 6.59 KB
/
index.ts
File metadata and controls
202 lines (182 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import { Command } from "interactive-commander";
import { exec } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import os from "os";
import setup from "./setup";
import plan from "./plan";
import execute from "./execute";
import watch from "./watch";
import { CmdRunContext, flagsSchema } from "./_types";
import frozen from "./frozen";
import { applyRunExitCode } from "./exit-code";
import {
renderClear,
renderSpacer,
renderBanner,
renderHero,
pauseIfDebug,
renderSummary,
} from "../../utils/ui";
import trackEvent, { UserIdentity } from "../../utils/observability";
import { determineUserIdentity } from "./_utils";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function playSound(type: "success" | "failure") {
const platform = os.platform();
return new Promise<void>((resolve) => {
const assetDir = path.join(__dirname, "../assets");
const soundFiles = [path.join(assetDir, `${type}.mp3`)];
let command = "";
if (platform === "linux") {
command = soundFiles
.map(
(file) =>
`mpg123 -q "${file}" 2>/dev/null || aplay "${file}" 2>/dev/null`,
)
.join(" || ");
} else if (platform === "darwin") {
command = soundFiles.map((file) => `afplay "${file}"`).join(" || ");
} else if (platform === "win32") {
command = `powershell -c "try { (New-Object Media.SoundPlayer '${soundFiles[1]}').PlaySync() } catch { Start-Process -FilePath '${soundFiles[0]}' -WindowStyle Hidden -Wait }"`;
} else {
command = soundFiles
.map(
(file) =>
`aplay "${file}" 2>/dev/null || afplay "${file}" 2>/dev/null`,
)
.join(" || ");
}
exec(command, () => {
resolve();
});
setTimeout(resolve, 3000);
});
}
export default new Command()
.command("run")
.description("Run localization pipeline")
.helpOption("-h, --help", "Show help")
.option(
"--source-locale <source-locale>",
"Override the source locale from i18n.json for this run",
)
.option(
"--target-locale <target-locale>",
"Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--bucket <bucket>",
"Limit processing to specific bucket types defined in i18n.json (e.g., json, yaml, android). Repeat the flag to include multiple bucket types. Defaults to all configured buckets",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--file <file>",
"Filter bucket path pattern values by substring match. Examples: messages.json or locale/. Repeat to add multiple filters",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--key <key>",
"Filter keys by prefix matching on dot-separated paths. Example: auth.login to match all keys starting with auth.login. Repeat for multiple patterns",
(val: string, prev: string[]) =>
prev ? [...prev, encodeURIComponent(val)] : [encodeURIComponent(val)],
)
.option(
"--force",
"Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
)
.option(
"--frozen",
"Validate translations are up-to-date without making changes - fails if source files, target files, or lockfile are out of sync. Ideal for CI/CD to ensure translation consistency before deployment",
)
.option(
"--api-key <api-key>",
"Override API key from settings or environment variables",
)
.option("--debug", "Pause before processing to allow attaching a debugger.")
.option(
"--concurrency <concurrency>",
"Number of translation jobs to run concurrently. Higher values can speed up large translation batches but may increase memory usage. Defaults to 10 (maximum 10)",
(val: string) => parseInt(val),
)
.option(
"--watch",
"Watch source locale files continuously and retranslate automatically when files change",
)
.option(
"--debounce <milliseconds>",
"Delay in milliseconds after file changes before retranslating in watch mode. Defaults to 5000",
(val: string) => parseInt(val),
)
.option(
"--sound",
"Play audio feedback when translations complete (success or failure sounds)",
)
.option(
"--pseudo",
"Enable pseudo-localization mode: automatically pseudo-translates all extracted strings with accented characters and visual markers without calling any external API. Useful for testing UI internationalization readiness",
)
.option(
"--batch-size <number>",
"Number of translations to process in a single batch (not applicable when using lingo.dev provider)",
(val: string) => parseInt(val),
)
.action(async (args) => {
let userIdentity: UserIdentity = null;
try {
const ctx: CmdRunContext = {
flags: flagsSchema.parse(args),
config: null,
results: new Map(),
tasks: [],
localizer: null,
};
await pauseIfDebug(ctx.flags.debug);
await renderClear();
await renderSpacer();
await renderBanner();
await renderHero();
await renderSpacer();
await setup(ctx);
userIdentity = await determineUserIdentity(ctx);
await trackEvent(userIdentity, "cmd.run.start", {
config: ctx.config,
flags: ctx.flags,
});
await renderSpacer();
await plan(ctx);
await renderSpacer();
await frozen(ctx);
await renderSpacer();
await execute(ctx);
await renderSpacer();
await renderSummary(ctx.results);
await renderSpacer();
const hasErrors = applyRunExitCode(ctx.results);
// Play sound after main tasks complete if sound flag is enabled
if (ctx.flags.sound) {
await playSound(hasErrors ? "failure" : "success");
}
// If watch mode is enabled, start watching for changes
if (ctx.flags.watch) {
await watch(ctx);
}
await trackEvent(userIdentity, "cmd.run.success", {
config: ctx.config,
flags: ctx.flags,
});
await new Promise((resolve) => setTimeout(resolve, 50));
} catch (error: any) {
await trackEvent(userIdentity, "cmd.run.error", {
flags: args,
error: error.message,
authenticated: !!userIdentity,
});
await new Promise((resolve) => setTimeout(resolve, 50));
// Play sad sound if sound flag is enabled
if (args.sound) {
await playSound("failure");
}
throw error;
}
});