@messagevisor/core 0.0.1 → 0.1.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +8 -0
- package/lib/benchmark/index.d.ts +2 -0
- package/lib/benchmark/index.js +417 -0
- package/lib/benchmark/index.js.map +1 -0
- package/lib/builder/index.d.ts +70 -0
- package/lib/builder/index.js +831 -0
- package/lib/builder/index.js.map +1 -0
- package/lib/cli/index.d.ts +28 -0
- package/lib/cli/index.js +182 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/config/index.d.ts +61 -0
- package/lib/config/index.js +255 -0
- package/lib/config/index.js.map +1 -0
- package/lib/create/index.d.ts +2 -0
- package/lib/create/index.js +405 -0
- package/lib/create/index.js.map +1 -0
- package/lib/datasource/filesystemAdapter.d.ts +44 -0
- package/lib/datasource/filesystemAdapter.js +424 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -0
- package/lib/datasource/index.d.ts +39 -0
- package/lib/datasource/index.js +96 -0
- package/lib/datasource/index.js.map +1 -0
- package/lib/error.d.ts +6 -0
- package/lib/error.js +49 -0
- package/lib/error.js.map +1 -0
- package/lib/evaluate/cli.d.ts +8 -0
- package/lib/evaluate/cli.js +179 -0
- package/lib/evaluate/cli.js.map +1 -0
- package/lib/evaluate/index.d.ts +10 -0
- package/lib/evaluate/index.js +131 -0
- package/lib/evaluate/index.js.map +1 -0
- package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
- package/lib/examples/coerceExampleIsoDates.js +81 -0
- package/lib/examples/coerceExampleIsoDates.js.map +1 -0
- package/lib/examples/index.d.ts +63 -0
- package/lib/examples/index.js +713 -0
- package/lib/examples/index.js.map +1 -0
- package/lib/exporter/index.d.ts +60 -0
- package/lib/exporter/index.js +610 -0
- package/lib/exporter/index.js.map +1 -0
- package/lib/find-duplicates/index.d.ts +41 -0
- package/lib/find-duplicates/index.js +297 -0
- package/lib/find-duplicates/index.js.map +1 -0
- package/lib/generate-code/index.d.ts +11 -0
- package/lib/generate-code/index.js +157 -0
- package/lib/generate-code/index.js.map +1 -0
- package/lib/generate-code/typescript.d.ts +14 -0
- package/lib/generate-code/typescript.js +307 -0
- package/lib/generate-code/typescript.js.map +1 -0
- package/lib/importer/index.d.ts +64 -0
- package/lib/importer/index.js +1092 -0
- package/lib/importer/index.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/info/index.d.ts +17 -0
- package/lib/info/index.js +132 -0
- package/lib/info/index.js.map +1 -0
- package/lib/init/index.d.ts +30 -0
- package/lib/init/index.js +348 -0
- package/lib/init/index.js.map +1 -0
- package/lib/lint/index.d.ts +1 -0
- package/lib/lint/index.js +6 -0
- package/lib/lint/index.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +7 -0
- package/lib/linter/attributeSchema.js +36 -0
- package/lib/linter/attributeSchema.js.map +1 -0
- package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
- package/lib/linter/checkLocaleCircularDependency.js +42 -0
- package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +3 -0
- package/lib/linter/conditionSchema.js +283 -0
- package/lib/linter/conditionSchema.js.map +1 -0
- package/lib/linter/formatSchema.d.ts +325 -0
- package/lib/linter/formatSchema.js +165 -0
- package/lib/linter/formatSchema.js.map +1 -0
- package/lib/linter/icuStyleLint.d.ts +6 -0
- package/lib/linter/icuStyleLint.js +226 -0
- package/lib/linter/icuStyleLint.js.map +1 -0
- package/lib/linter/index.d.ts +34 -0
- package/lib/linter/index.js +557 -0
- package/lib/linter/index.js.map +1 -0
- package/lib/linter/localeSchema.d.ts +672 -0
- package/lib/linter/localeSchema.js +50 -0
- package/lib/linter/localeSchema.js.map +1 -0
- package/lib/linter/messageSchema.d.ts +35 -0
- package/lib/linter/messageSchema.js +115 -0
- package/lib/linter/messageSchema.js.map +1 -0
- package/lib/linter/printError.d.ts +8 -0
- package/lib/linter/printError.js +41 -0
- package/lib/linter/printError.js.map +1 -0
- package/lib/linter/schema.d.ts +33 -0
- package/lib/linter/schema.js +192 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +8 -0
- package/lib/linter/segmentSchema.js +18 -0
- package/lib/linter/segmentSchema.js.map +1 -0
- package/lib/linter/targetSchema.d.ts +337 -0
- package/lib/linter/targetSchema.js +39 -0
- package/lib/linter/targetSchema.js.map +1 -0
- package/lib/linter/testSchema.d.ts +71 -0
- package/lib/linter/testSchema.js +165 -0
- package/lib/linter/testSchema.js.map +1 -0
- package/lib/linter/zodHelpers.d.ts +2 -0
- package/lib/linter/zodHelpers.js +15 -0
- package/lib/linter/zodHelpers.js.map +1 -0
- package/lib/list/index.d.ts +8 -0
- package/lib/list/index.js +524 -0
- package/lib/list/index.js.map +1 -0
- package/lib/matrix.d.ts +4 -0
- package/lib/matrix.js +66 -0
- package/lib/matrix.js.map +1 -0
- package/lib/promoter/index.d.ts +65 -0
- package/lib/promoter/index.js +1208 -0
- package/lib/promoter/index.js.map +1 -0
- package/lib/prune/index.d.ts +37 -0
- package/lib/prune/index.js +673 -0
- package/lib/prune/index.js.map +1 -0
- package/lib/sets.d.ts +10 -0
- package/lib/sets.js +120 -0
- package/lib/sets.js.map +1 -0
- package/lib/tester/cliFormat.d.ts +8 -0
- package/lib/tester/cliFormat.js +15 -0
- package/lib/tester/cliFormat.js.map +1 -0
- package/lib/tester/index.d.ts +35 -0
- package/lib/tester/index.js +713 -0
- package/lib/tester/index.js.map +1 -0
- package/lib/tester/matrix.d.ts +14 -0
- package/lib/tester/matrix.js +76 -0
- package/lib/tester/matrix.js.map +1 -0
- package/lib/tester/prettyDuration.d.ts +1 -0
- package/lib/tester/prettyDuration.js +30 -0
- package/lib/tester/prettyDuration.js.map +1 -0
- package/lib/tester/printTestResult.d.ts +2 -0
- package/lib/tester/printTestResult.js +32 -0
- package/lib/tester/printTestResult.js.map +1 -0
- package/lib/tester/types.d.ts +29 -0
- package/lib/tester/types.js +3 -0
- package/lib/tester/types.js.map +1 -0
- package/package.json +41 -13
- package/src/benchmark/index.spec.ts +375 -0
- package/src/benchmark/index.ts +433 -0
- package/src/builder/index.spec.ts +822 -0
- package/src/builder/index.ts +920 -0
- package/src/cli/index.spec.ts +54 -0
- package/src/cli/index.ts +150 -0
- package/src/config/index.spec.ts +70 -0
- package/src/config/index.ts +259 -0
- package/src/create/index.spec.ts +272 -0
- package/src/create/index.ts +295 -0
- package/src/datasource/filesystemAdapter.ts +313 -0
- package/src/datasource/index.ts +135 -0
- package/src/error.ts +33 -0
- package/src/evaluate/cli.spec.ts +368 -0
- package/src/evaluate/cli.ts +130 -0
- package/src/evaluate/index.ts +161 -0
- package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
- package/src/examples/coerceExampleIsoDates.ts +98 -0
- package/src/examples/index.spec.ts +453 -0
- package/src/examples/index.ts +854 -0
- package/src/exporter/index.spec.ts +443 -0
- package/src/exporter/index.ts +643 -0
- package/src/find-duplicates/index.spec.ts +289 -0
- package/src/find-duplicates/index.ts +314 -0
- package/src/generate-code/index.ts +92 -0
- package/src/generate-code/typescript.spec.ts +241 -0
- package/src/generate-code/typescript.ts +284 -0
- package/src/importer/index.spec.ts +1101 -0
- package/src/importer/index.ts +1190 -0
- package/src/index.ts +18 -0
- package/src/info/index.ts +67 -0
- package/src/init/index.spec.ts +279 -0
- package/src/init/index.ts +292 -0
- package/src/lint/index.ts +1 -0
- package/src/linter/attributeSchema.ts +38 -0
- package/src/linter/checkLocaleCircularDependency.ts +51 -0
- package/src/linter/conditionSchema.ts +386 -0
- package/src/linter/formatSchema.ts +170 -0
- package/src/linter/icuStyleLint.ts +312 -0
- package/src/linter/index.spec.ts +824 -0
- package/src/linter/index.ts +460 -0
- package/src/linter/localeSchema.ts +70 -0
- package/src/linter/messageSchema.ts +152 -0
- package/src/linter/printError.ts +52 -0
- package/src/linter/schema.ts +230 -0
- package/src/linter/segmentSchema.ts +15 -0
- package/src/linter/targetSchema.ts +50 -0
- package/src/linter/testSchema.spec.ts +405 -0
- package/src/linter/testSchema.ts +239 -0
- package/src/linter/zodHelpers.ts +16 -0
- package/src/list/index.spec.ts +431 -0
- package/src/list/index.ts +463 -0
- package/src/matrix.ts +69 -0
- package/src/promoter/index.spec.ts +584 -0
- package/src/promoter/index.ts +1267 -0
- package/src/prune/index.spec.ts +418 -0
- package/src/prune/index.ts +693 -0
- package/src/sets.ts +74 -0
- package/src/tester/cliFormat.ts +11 -0
- package/src/tester/featurevisorIntegration.spec.ts +101 -0
- package/src/tester/index.spec.ts +577 -0
- package/src/tester/index.ts +679 -0
- package/src/tester/matrix.ts +106 -0
- package/src/tester/prettyDuration.ts +34 -0
- package/src/tester/printTestResult.ts +40 -0
- package/src/tester/types.ts +32 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import type { Locale, Message, Target, Translation } from "@messagevisor/types";
|
|
5
|
+
|
|
6
|
+
import type { ProjectConfig } from "../config";
|
|
7
|
+
import type { Datasource } from "../datasource";
|
|
8
|
+
import { MessagevisorCLIError, printMessagevisorCLIError } from "../error";
|
|
9
|
+
import { getProjectSetExecutions } from "../sets";
|
|
10
|
+
|
|
11
|
+
export interface ExportProjectOptions {
|
|
12
|
+
set?: string | string[];
|
|
13
|
+
locale?: string | string[];
|
|
14
|
+
target?: string | string[];
|
|
15
|
+
includeMessages?: string | string[];
|
|
16
|
+
excludeMessages?: string | string[];
|
|
17
|
+
excludeOverrides?: boolean;
|
|
18
|
+
withoutDescription?: boolean;
|
|
19
|
+
withoutStatus?: boolean;
|
|
20
|
+
onlyUntranslated?: boolean;
|
|
21
|
+
onlyDirectlyUntranslated?: boolean;
|
|
22
|
+
print?: boolean;
|
|
23
|
+
output?: string;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
delimiter?: string;
|
|
26
|
+
bom?: boolean;
|
|
27
|
+
lineEnding?: "lf" | "crlf";
|
|
28
|
+
now?: Date;
|
|
29
|
+
allowMissingLocales?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TranslationStatus = "direct" | "inherited" | "missing";
|
|
33
|
+
|
|
34
|
+
interface ExportRow {
|
|
35
|
+
set?: string;
|
|
36
|
+
messageKey: string;
|
|
37
|
+
isOverride: boolean;
|
|
38
|
+
messageDescription?: string;
|
|
39
|
+
translations: Record<string, string>;
|
|
40
|
+
statuses: Record<string, TranslationStatus>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ExportProjectResult {
|
|
44
|
+
csv: string;
|
|
45
|
+
filePath?: string;
|
|
46
|
+
rows: ExportRow[];
|
|
47
|
+
locales: string[];
|
|
48
|
+
summary: {
|
|
49
|
+
messageRows: number;
|
|
50
|
+
overrideRows: number;
|
|
51
|
+
totalRows: number;
|
|
52
|
+
locales: string[];
|
|
53
|
+
sets: string[];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toArray(value?: string | string[]): string[] {
|
|
58
|
+
if (typeof value === "undefined") return [];
|
|
59
|
+
return Array.isArray(value) ? value : [value];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function matchesPattern(key: string, patterns?: string[]) {
|
|
63
|
+
if (!patterns || patterns.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return patterns.some((pattern) => {
|
|
68
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
69
|
+
return new RegExp(`^${escaped}$`).test(key);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readAll<T>(
|
|
74
|
+
keys: string[],
|
|
75
|
+
read: (key: string) => Promise<T>,
|
|
76
|
+
): Promise<Record<string, T>> {
|
|
77
|
+
const entries = await Promise.all(keys.map(async (key) => [key, await read(key)] as const));
|
|
78
|
+
return Object.fromEntries(entries);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isAvailable(message: Message) {
|
|
82
|
+
return !message.archived;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveLocaleChain(localeKey: string, locales: Record<string, Locale>) {
|
|
86
|
+
const chain: string[] = [];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
let currentKey: string | undefined = localeKey;
|
|
89
|
+
|
|
90
|
+
while (currentKey && !seen.has(currentKey)) {
|
|
91
|
+
seen.add(currentKey);
|
|
92
|
+
chain.unshift(currentKey);
|
|
93
|
+
currentKey = locales[currentKey]?.inheritTranslationsFrom;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return chain;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveTranslationStatus(
|
|
100
|
+
translations: Record<string, Translation> | undefined,
|
|
101
|
+
localeKey: string,
|
|
102
|
+
locales: Record<string, Locale>,
|
|
103
|
+
): {
|
|
104
|
+
value: string;
|
|
105
|
+
status: TranslationStatus;
|
|
106
|
+
} {
|
|
107
|
+
if (typeof translations?.[localeKey] !== "undefined") {
|
|
108
|
+
return {
|
|
109
|
+
value: translations[localeKey],
|
|
110
|
+
status: "direct",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const candidates = resolveLocaleChain(localeKey, locales).reverse();
|
|
115
|
+
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
if (translations && typeof translations[candidate] !== "undefined") {
|
|
118
|
+
return {
|
|
119
|
+
value: translations[candidate],
|
|
120
|
+
status: "inherited",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
value: "",
|
|
127
|
+
status: "missing",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveTranslation(
|
|
132
|
+
translations: Record<string, Translation> | undefined,
|
|
133
|
+
localeKey: string,
|
|
134
|
+
locales: Record<string, Locale>,
|
|
135
|
+
) {
|
|
136
|
+
const result = resolveTranslationStatus(translations, localeKey, locales);
|
|
137
|
+
|
|
138
|
+
return result.status === "missing" ? undefined : result.value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function csvEscape(value: unknown, delimiter: string) {
|
|
142
|
+
const stringValue = typeof value === "undefined" || value === null ? "" : String(value);
|
|
143
|
+
const needsEscaping =
|
|
144
|
+
stringValue.includes(delimiter) || stringValue.includes('"') || /[\n\r]/.test(stringValue);
|
|
145
|
+
|
|
146
|
+
if (needsEscaping) {
|
|
147
|
+
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return stringValue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function toCsv(
|
|
154
|
+
headers: string[],
|
|
155
|
+
rows: string[][],
|
|
156
|
+
options: {
|
|
157
|
+
delimiter?: string;
|
|
158
|
+
bom?: boolean;
|
|
159
|
+
lineEnding?: "lf" | "crlf";
|
|
160
|
+
} = {},
|
|
161
|
+
) {
|
|
162
|
+
const delimiter = options.delimiter || ",";
|
|
163
|
+
const lineEnding = options.lineEnding === "crlf" ? "\r\n" : "\n";
|
|
164
|
+
const csv = [headers, ...rows]
|
|
165
|
+
.map((row) => row.map((value) => csvEscape(value, delimiter)).join(delimiter))
|
|
166
|
+
.join(lineEnding);
|
|
167
|
+
|
|
168
|
+
return options.bom ? `\uFEFF${csv}` : csv;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatTimestamp(date: Date) {
|
|
172
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
date.getFullYear(),
|
|
176
|
+
pad(date.getMonth() + 1),
|
|
177
|
+
pad(date.getDate()),
|
|
178
|
+
"T",
|
|
179
|
+
pad(date.getHours()),
|
|
180
|
+
pad(date.getMinutes()),
|
|
181
|
+
pad(date.getSeconds()),
|
|
182
|
+
].join("");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function getExportFilePath(
|
|
186
|
+
projectConfig: ProjectConfig,
|
|
187
|
+
options: {
|
|
188
|
+
now?: Date;
|
|
189
|
+
output?: string;
|
|
190
|
+
force?: boolean;
|
|
191
|
+
},
|
|
192
|
+
) {
|
|
193
|
+
if (options.output) {
|
|
194
|
+
const projectRootDirectoryPath = path.dirname(projectConfig.exportsDirectoryPath);
|
|
195
|
+
const filePath = path.isAbsolute(options.output)
|
|
196
|
+
? options.output
|
|
197
|
+
: path.join(projectRootDirectoryPath, options.output);
|
|
198
|
+
|
|
199
|
+
if (!options.force && fs.existsSync(filePath)) {
|
|
200
|
+
throw new MessagevisorCLIError(
|
|
201
|
+
`Export output file already exists: ${filePath}. Pass --force to overwrite.`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
206
|
+
|
|
207
|
+
return filePath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await fs.promises.mkdir(projectConfig.exportsDirectoryPath, { recursive: true });
|
|
211
|
+
|
|
212
|
+
const timestamp = formatTimestamp(options.now || new Date());
|
|
213
|
+
let index = 0;
|
|
214
|
+
|
|
215
|
+
while (true) {
|
|
216
|
+
const suffix = index === 0 ? "" : `-${index}`;
|
|
217
|
+
const filePath = path.join(
|
|
218
|
+
projectConfig.exportsDirectoryPath,
|
|
219
|
+
`messagevisor-export-${timestamp}${suffix}.csv`,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (!fs.existsSync(filePath)) {
|
|
223
|
+
return filePath;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
index++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assertKnownValues(label: string, requested: string[], available: string[]) {
|
|
231
|
+
for (const value of requested) {
|
|
232
|
+
if (!available.includes(value)) {
|
|
233
|
+
throw new MessagevisorCLIError(
|
|
234
|
+
`Unknown ${label} "${value}". Available ${label}s: ${available.join(", ") || "none"}.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function addLocale(locales: string[], locale: string) {
|
|
241
|
+
if (!locales.includes(locale)) {
|
|
242
|
+
locales.push(locale);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function shouldIncludeForUntranslatedFilter(
|
|
247
|
+
translations: Record<string, Translation> | undefined,
|
|
248
|
+
locales: Record<string, Locale>,
|
|
249
|
+
selectedLocales: string[],
|
|
250
|
+
options: ExportProjectOptions,
|
|
251
|
+
) {
|
|
252
|
+
if (!options.onlyUntranslated && !options.onlyDirectlyUntranslated) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return selectedLocales.some((locale) => {
|
|
257
|
+
if (options.onlyDirectlyUntranslated) {
|
|
258
|
+
return typeof translations?.[locale] === "undefined";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return typeof resolveTranslation(translations, locale, locales) === "undefined";
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function collectRows(
|
|
266
|
+
projectConfig: ProjectConfig,
|
|
267
|
+
datasource: Datasource,
|
|
268
|
+
options: ExportProjectOptions,
|
|
269
|
+
set?: string,
|
|
270
|
+
) {
|
|
271
|
+
const requestedLocales = toArray(options.locale);
|
|
272
|
+
const requestedTargets = toArray(options.target);
|
|
273
|
+
const includeMessages = toArray(options.includeMessages);
|
|
274
|
+
const excludeMessages = toArray(options.excludeMessages);
|
|
275
|
+
|
|
276
|
+
const [localeKeys, targetKeys, messageKeys] = await Promise.all([
|
|
277
|
+
datasource.listLocales(),
|
|
278
|
+
datasource.listTargets(),
|
|
279
|
+
datasource.listMessages(),
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
const datasourceLocales = options.allowMissingLocales
|
|
283
|
+
? requestedLocales.filter((locale) => localeKeys.includes(locale))
|
|
284
|
+
: requestedLocales;
|
|
285
|
+
|
|
286
|
+
if (!options.allowMissingLocales) {
|
|
287
|
+
assertKnownValues("locale", requestedLocales, localeKeys);
|
|
288
|
+
}
|
|
289
|
+
assertKnownValues("target", requestedTargets, targetKeys);
|
|
290
|
+
|
|
291
|
+
const [locales, targets, messages] = await Promise.all([
|
|
292
|
+
readAll<Locale>(localeKeys, (key) => datasource.readLocale(key)),
|
|
293
|
+
readAll<Target>(targetKeys, (key) => datasource.readTarget(key)),
|
|
294
|
+
readAll<Message>(messageKeys, (key) => datasource.readMessage(key)),
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const selectedMessageKeys = new Set<string>();
|
|
298
|
+
const selectedLocales: string[] = [];
|
|
299
|
+
|
|
300
|
+
if (requestedTargets.length > 0) {
|
|
301
|
+
for (const targetKey of requestedTargets) {
|
|
302
|
+
const target = targets[targetKey];
|
|
303
|
+
const targetIncludeMessages = target.includeMessages?.length ? target.includeMessages : ["*"];
|
|
304
|
+
const targetExcludeMessages = target.excludeMessages || [];
|
|
305
|
+
const targetLocales = target.locales?.length ? target.locales : localeKeys;
|
|
306
|
+
|
|
307
|
+
for (const locale of targetLocales) {
|
|
308
|
+
if (datasourceLocales.length === 0 || datasourceLocales.includes(locale)) {
|
|
309
|
+
addLocale(selectedLocales, locale);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const messageKey of messageKeys) {
|
|
314
|
+
if (
|
|
315
|
+
matchesPattern(messageKey, targetIncludeMessages) &&
|
|
316
|
+
!matchesPattern(messageKey, targetExcludeMessages)
|
|
317
|
+
) {
|
|
318
|
+
selectedMessageKeys.add(messageKey);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
messageKeys.forEach((key) => selectedMessageKeys.add(key));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (includeMessages.length > 0) {
|
|
327
|
+
for (const messageKey of Array.from(selectedMessageKeys)) {
|
|
328
|
+
if (!matchesPattern(messageKey, includeMessages)) {
|
|
329
|
+
selectedMessageKeys.delete(messageKey);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const messageKey of Array.from(selectedMessageKeys)) {
|
|
335
|
+
if (matchesPattern(messageKey, excludeMessages)) {
|
|
336
|
+
selectedMessageKeys.delete(messageKey);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (selectedLocales.length === 0 && requestedTargets.length === 0) {
|
|
341
|
+
for (const locale of requestedLocales.length > 0 ? datasourceLocales : localeKeys) {
|
|
342
|
+
addLocale(selectedLocales, locale);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const rows: ExportRow[] = [];
|
|
347
|
+
|
|
348
|
+
function createRow(
|
|
349
|
+
messageKey: string,
|
|
350
|
+
description: string | undefined,
|
|
351
|
+
translations: Record<string, Translation> | undefined,
|
|
352
|
+
isOverride: boolean,
|
|
353
|
+
) {
|
|
354
|
+
if (!shouldIncludeForUntranslatedFilter(translations, locales, selectedLocales, options)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
rows.push({
|
|
359
|
+
set,
|
|
360
|
+
messageKey,
|
|
361
|
+
isOverride,
|
|
362
|
+
messageDescription: options.withoutDescription ? undefined : description || "",
|
|
363
|
+
translations: Object.fromEntries(
|
|
364
|
+
selectedLocales.map((locale) => [
|
|
365
|
+
locale,
|
|
366
|
+
resolveTranslationStatus(translations, locale, locales).value,
|
|
367
|
+
]),
|
|
368
|
+
),
|
|
369
|
+
statuses: Object.fromEntries(
|
|
370
|
+
selectedLocales.map((locale) => [
|
|
371
|
+
locale,
|
|
372
|
+
resolveTranslationStatus(translations, locale, locales).status,
|
|
373
|
+
]),
|
|
374
|
+
) as Record<string, TranslationStatus>,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const messageKey of Array.from(selectedMessageKeys).sort()) {
|
|
379
|
+
const message = messages[messageKey];
|
|
380
|
+
|
|
381
|
+
if (!message || !isAvailable(message)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const messageDescription = message.summary ?? message.description;
|
|
386
|
+
|
|
387
|
+
createRow(messageKey, messageDescription, message.translations, false);
|
|
388
|
+
|
|
389
|
+
if (options.excludeOverrides) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const override of message.overrides || []) {
|
|
394
|
+
const overrideDescription = override.summary ?? override.description;
|
|
395
|
+
|
|
396
|
+
createRow(
|
|
397
|
+
`${messageKey}${projectConfig.exportOverrideKeySeparator}${override.key}`,
|
|
398
|
+
overrideDescription,
|
|
399
|
+
override.translations,
|
|
400
|
+
true,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
rows,
|
|
407
|
+
locales: selectedLocales,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function createCsv(
|
|
412
|
+
rows: ExportRow[],
|
|
413
|
+
locales: string[],
|
|
414
|
+
options: ExportProjectOptions,
|
|
415
|
+
withSets: boolean,
|
|
416
|
+
) {
|
|
417
|
+
const localeHeaders = locales.flatMap((locale) =>
|
|
418
|
+
options.withoutStatus ? [locale] : [locale, `${locale}Status`],
|
|
419
|
+
);
|
|
420
|
+
const headers = [
|
|
421
|
+
...(withSets ? ["set"] : []),
|
|
422
|
+
"messageKey",
|
|
423
|
+
...(options.withoutDescription ? [] : ["messageDescription"]),
|
|
424
|
+
...localeHeaders,
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
return toCsv(
|
|
428
|
+
headers,
|
|
429
|
+
rows.map((row) => [
|
|
430
|
+
...(withSets ? [row.set || ""] : []),
|
|
431
|
+
row.messageKey,
|
|
432
|
+
...(options.withoutDescription ? [] : [row.messageDescription || ""]),
|
|
433
|
+
...locales.flatMap((locale) =>
|
|
434
|
+
options.withoutStatus
|
|
435
|
+
? [row.translations[locale] || ""]
|
|
436
|
+
: [row.translations[locale] || "", row.statuses[locale] || "missing"],
|
|
437
|
+
),
|
|
438
|
+
]),
|
|
439
|
+
{
|
|
440
|
+
delimiter: options.delimiter,
|
|
441
|
+
bom: options.bom,
|
|
442
|
+
lineEnding: options.lineEnding,
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function createExportSummary(rows: ExportRow[], locales: string[]) {
|
|
448
|
+
return {
|
|
449
|
+
messageRows: rows.filter((row) => !row.isOverride).length,
|
|
450
|
+
overrideRows: rows.filter((row) => row.isOverride).length,
|
|
451
|
+
totalRows: rows.length,
|
|
452
|
+
locales,
|
|
453
|
+
sets: Array.from(
|
|
454
|
+
new Set(rows.map((row) => row.set).filter((set): set is string => Boolean(set))),
|
|
455
|
+
),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function finishExport(
|
|
460
|
+
projectConfig: ProjectConfig,
|
|
461
|
+
rows: ExportRow[],
|
|
462
|
+
locales: string[],
|
|
463
|
+
options: ExportProjectOptions,
|
|
464
|
+
withSets: boolean,
|
|
465
|
+
): Promise<ExportProjectResult> {
|
|
466
|
+
const csv = createCsv(rows, locales, options, withSets);
|
|
467
|
+
const summary = createExportSummary(rows, locales);
|
|
468
|
+
|
|
469
|
+
if (options.print) {
|
|
470
|
+
return {
|
|
471
|
+
csv,
|
|
472
|
+
rows,
|
|
473
|
+
locales,
|
|
474
|
+
summary,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const filePath = await getExportFilePath(projectConfig, options);
|
|
479
|
+
await fs.promises.writeFile(filePath, csv);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
csv,
|
|
483
|
+
filePath,
|
|
484
|
+
rows,
|
|
485
|
+
locales,
|
|
486
|
+
summary,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function assertExportOptions(options: ExportProjectOptions) {
|
|
491
|
+
if (options.onlyUntranslated && options.onlyDirectlyUntranslated) {
|
|
492
|
+
throw new MessagevisorCLIError(
|
|
493
|
+
"Use either --onlyUntranslated or --onlyDirectlyUntranslated, not both.",
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (typeof options.delimiter !== "undefined" && options.delimiter.length !== 1) {
|
|
498
|
+
throw new MessagevisorCLIError("--delimiter must be a single character.");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (options.print && options.output) {
|
|
502
|
+
throw new MessagevisorCLIError("Use either --print or --output, not both.");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function exportProject(
|
|
507
|
+
projectConfig: ProjectConfig,
|
|
508
|
+
datasource: Datasource,
|
|
509
|
+
options: ExportProjectOptions = {},
|
|
510
|
+
): Promise<ExportProjectResult> {
|
|
511
|
+
assertExportOptions(options);
|
|
512
|
+
|
|
513
|
+
if (!projectConfig.sets && toArray(options.set).length > 0) {
|
|
514
|
+
throw new MessagevisorCLIError("--set can only be used when `sets: true` is configured.");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const collected = await collectRows(projectConfig, datasource, options);
|
|
518
|
+
|
|
519
|
+
return finishExport(projectConfig, collected.rows, collected.locales, options, false);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function exportProjectSets(
|
|
523
|
+
projectConfig: ProjectConfig,
|
|
524
|
+
datasource: Datasource,
|
|
525
|
+
options: ExportProjectOptions = {},
|
|
526
|
+
): Promise<ExportProjectResult> {
|
|
527
|
+
assertExportOptions(options);
|
|
528
|
+
|
|
529
|
+
if (!projectConfig.sets) {
|
|
530
|
+
return exportProject(projectConfig, datasource, options);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const executions = await getProjectSetExecutions(projectConfig, datasource, undefined);
|
|
534
|
+
const requestedSets = toArray(options.set);
|
|
535
|
+
const selectedExecutions =
|
|
536
|
+
requestedSets.length > 0
|
|
537
|
+
? executions.filter((execution) => requestedSets.includes(execution.set))
|
|
538
|
+
: executions;
|
|
539
|
+
|
|
540
|
+
assertKnownValues(
|
|
541
|
+
"set",
|
|
542
|
+
requestedSets,
|
|
543
|
+
executions.map((execution) => execution.set),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const rows: ExportRow[] = [];
|
|
547
|
+
const requestedLocales = toArray(options.locale);
|
|
548
|
+
const locales: string[] = [];
|
|
549
|
+
const availableLocales: string[] = [];
|
|
550
|
+
|
|
551
|
+
for (const execution of selectedExecutions) {
|
|
552
|
+
const localeKeys = await execution.datasource.listLocales();
|
|
553
|
+
localeKeys.forEach((locale) => addLocale(availableLocales, locale));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
assertKnownValues("locale", requestedLocales, availableLocales);
|
|
557
|
+
requestedLocales.forEach((locale) => addLocale(locales, locale));
|
|
558
|
+
|
|
559
|
+
for (const execution of selectedExecutions) {
|
|
560
|
+
const collected = await collectRows(
|
|
561
|
+
projectConfig,
|
|
562
|
+
execution.datasource,
|
|
563
|
+
{
|
|
564
|
+
...options,
|
|
565
|
+
allowMissingLocales: true,
|
|
566
|
+
},
|
|
567
|
+
execution.set,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
rows.push(...collected.rows);
|
|
571
|
+
if (requestedLocales.length === 0) {
|
|
572
|
+
collected.locales.forEach((locale) => addLocale(locales, locale));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return finishExport(projectConfig, rows, locales, options, true);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function printExportResult(result: ExportProjectResult, print: boolean | undefined) {
|
|
580
|
+
if (print) {
|
|
581
|
+
console.log(result.csv);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
console.log(`CSV file generated successfully at ${result.filePath}`);
|
|
586
|
+
console.log(`Rows: ${result.summary.totalRows} total`);
|
|
587
|
+
console.log(`Messages: ${result.summary.messageRows}`);
|
|
588
|
+
console.log(`Overrides: ${result.summary.overrideRows}`);
|
|
589
|
+
console.log(`Locales: ${result.summary.locales.join(", ") || "(none)"}`);
|
|
590
|
+
|
|
591
|
+
if (result.summary.sets.length > 0) {
|
|
592
|
+
console.log(`Sets: ${result.summary.sets.join(", ")}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export const exportPlugin = {
|
|
597
|
+
command: "export",
|
|
598
|
+
handler: async ({ projectConfig, datasource, parsed }: any) => {
|
|
599
|
+
try {
|
|
600
|
+
const result = await exportProjectSets(projectConfig, datasource, {
|
|
601
|
+
set: parsed.set,
|
|
602
|
+
locale: parsed.locale,
|
|
603
|
+
target: parsed.target,
|
|
604
|
+
includeMessages: parsed.includeMessages,
|
|
605
|
+
excludeMessages: parsed.excludeMessages,
|
|
606
|
+
excludeOverrides: parsed.excludeOverrides,
|
|
607
|
+
withoutDescription: parsed.withoutDescription,
|
|
608
|
+
withoutStatus: parsed.withoutStatus,
|
|
609
|
+
onlyUntranslated: parsed.onlyUntranslated,
|
|
610
|
+
onlyDirectlyUntranslated: parsed.onlyDirectlyUntranslated,
|
|
611
|
+
print: parsed.print,
|
|
612
|
+
output: parsed.output,
|
|
613
|
+
force: parsed.force,
|
|
614
|
+
delimiter: parsed.delimiter,
|
|
615
|
+
bom: parsed.bom,
|
|
616
|
+
lineEnding: parsed.lineEnding,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
printExportResult(result, parsed.print);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (printMessagevisorCLIError(error)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
examples: [
|
|
629
|
+
{ command: "export", description: "export translations to CSV" },
|
|
630
|
+
{
|
|
631
|
+
command: "export --locale=en-US --target=web",
|
|
632
|
+
description: "export translations for one locale and target",
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
command: "export --locale=en --locale=nl --target=web",
|
|
636
|
+
description: "export multiple locales, for example source plus target",
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
command: "export --print",
|
|
640
|
+
description: "print exported CSV to the console",
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
};
|