@verbatra/sdk 0.6.3 → 0.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 +7 -7
- package/dist/index.cjs +767 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -16
- package/dist/index.d.ts +178 -16
- package/dist/index.js +767 -139
- package/dist/index.js.map +1 -1
- package/package.json +16 -9
package/dist/index.cjs
CHANGED
|
@@ -152,6 +152,18 @@ function defineConfig(config) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// src/errors.ts
|
|
155
|
+
function errorMessage(error) {
|
|
156
|
+
return error instanceof Error ? error.message : String(error);
|
|
157
|
+
}
|
|
158
|
+
function stringCode(error) {
|
|
159
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") {
|
|
160
|
+
return error.code;
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
function describeError(error, fallbackCode) {
|
|
165
|
+
return { code: stringCode(error) ?? fallbackCode, message: errorMessage(error) };
|
|
166
|
+
}
|
|
155
167
|
var SdkError = class extends Error {
|
|
156
168
|
/** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
|
|
157
169
|
code;
|
|
@@ -276,6 +288,9 @@ var defaultFs = {
|
|
|
276
288
|
createExclusive: (path, data) => createExclusive(path, data),
|
|
277
289
|
deleteFile: async (path) => {
|
|
278
290
|
await promises.rm(path, { force: true });
|
|
291
|
+
},
|
|
292
|
+
mkdir: async (path) => {
|
|
293
|
+
await promises.mkdir(path, { recursive: true });
|
|
279
294
|
}
|
|
280
295
|
};
|
|
281
296
|
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
@@ -514,9 +529,109 @@ function assessValueDegeneracy(sourceValue, candidateValue) {
|
|
|
514
529
|
}
|
|
515
530
|
return { degenerate: hasRunawayRepetition(candidate) };
|
|
516
531
|
}
|
|
532
|
+
|
|
533
|
+
// src/locale-path/pattern.ts
|
|
517
534
|
var LOCALE_TOKEN = "{locale}";
|
|
518
|
-
|
|
519
|
-
|
|
535
|
+
var SEPARATOR = /[/\\]/;
|
|
536
|
+
function tokenOccupiesWholeSegments(pattern) {
|
|
537
|
+
return pattern.split(SEPARATOR).every((segment) => segment === LOCALE_TOKEN || !segment.includes(LOCALE_TOKEN));
|
|
538
|
+
}
|
|
539
|
+
function expandPattern(pattern, spelling) {
|
|
540
|
+
return pattern.replaceAll(LOCALE_TOKEN, () => spelling);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/locale-path/tag.ts
|
|
544
|
+
var LANGUAGE = /^[a-z]{2,3}$/;
|
|
545
|
+
var SCRIPT = /^[a-z]{4}$/;
|
|
546
|
+
var REGION = /^([a-z]{2}|[0-9]{3})$/;
|
|
547
|
+
var NUMERIC_REGION = /^[0-9]{3}$/;
|
|
548
|
+
var VARIANT = /^([a-z0-9]{5,8}|[0-9][a-z0-9]{3})$/;
|
|
549
|
+
function subtagIf(subtags, index, pattern) {
|
|
550
|
+
const subtag = subtags[index];
|
|
551
|
+
return subtag !== void 0 && pattern.test(subtag) ? subtag : void 0;
|
|
552
|
+
}
|
|
553
|
+
function parseLocaleTag(locale) {
|
|
554
|
+
const subtags = locale.toLowerCase().split("-");
|
|
555
|
+
const language = subtags[0];
|
|
556
|
+
if (language === void 0 || !LANGUAGE.test(language)) {
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
559
|
+
let index = 1;
|
|
560
|
+
const script = subtagIf(subtags, index, SCRIPT);
|
|
561
|
+
index += script === void 0 ? 0 : 1;
|
|
562
|
+
const region = subtagIf(subtags, index, REGION);
|
|
563
|
+
index += region === void 0 ? 0 : 1;
|
|
564
|
+
const variants = subtags.slice(index);
|
|
565
|
+
if (!variants.every((variant) => VARIANT.test(variant))) {
|
|
566
|
+
return void 0;
|
|
567
|
+
}
|
|
568
|
+
return { language, script, region, variants };
|
|
569
|
+
}
|
|
570
|
+
function conventionalSubtags(tag) {
|
|
571
|
+
const subtags = [tag.language];
|
|
572
|
+
if (tag.script !== void 0) {
|
|
573
|
+
subtags.push(tag.script.charAt(0).toUpperCase() + tag.script.slice(1));
|
|
574
|
+
}
|
|
575
|
+
if (tag.region !== void 0) {
|
|
576
|
+
subtags.push(tag.region.toUpperCase());
|
|
577
|
+
}
|
|
578
|
+
subtags.push(...tag.variants);
|
|
579
|
+
return subtags;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/locale-path/android.ts
|
|
583
|
+
var SOURCE_SEGMENT = "values";
|
|
584
|
+
var MAX_BCP47_SUBTAGS = 4;
|
|
585
|
+
var UI_MODE_LANGUAGE_COLLISION = "car";
|
|
586
|
+
function needsBcp47(tag, subtagCount) {
|
|
587
|
+
return tag.script !== void 0 || tag.variants.length > 0 || tag.region !== void 0 && NUMERIC_REGION.test(tag.region) || tag.language === UI_MODE_LANGUAGE_COLLISION || subtagCount >= 3;
|
|
588
|
+
}
|
|
589
|
+
function legacySegment(tag) {
|
|
590
|
+
return tag.region === void 0 ? `${SOURCE_SEGMENT}-${tag.language}` : `${SOURCE_SEGMENT}-${tag.language}-r${tag.region.toUpperCase()}`;
|
|
591
|
+
}
|
|
592
|
+
function androidSegment(locale, isSourceLocale) {
|
|
593
|
+
if (isSourceLocale) {
|
|
594
|
+
return SOURCE_SEGMENT;
|
|
595
|
+
}
|
|
596
|
+
const tag = parseLocaleTag(locale);
|
|
597
|
+
if (tag === void 0) {
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
const subtags = conventionalSubtags(tag);
|
|
601
|
+
if (!needsBcp47(tag, subtags.length)) {
|
|
602
|
+
return legacySegment(tag);
|
|
603
|
+
}
|
|
604
|
+
if (subtags.length > MAX_BCP47_SUBTAGS) {
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
return `${SOURCE_SEGMENT}-b+${subtags.join("+")}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/locale-path/style.ts
|
|
611
|
+
var LOCALE_STYLES = ["literal", "posix", "android"];
|
|
612
|
+
var SEGMENT_STYLES = /* @__PURE__ */ new Set(["android"]);
|
|
613
|
+
var UNSAFE_IN_SEGMENT = /[/\\\0]/;
|
|
614
|
+
function isSegmentStyle(style) {
|
|
615
|
+
return SEGMENT_STYLES.has(style);
|
|
616
|
+
}
|
|
617
|
+
function posixSpelling(locale) {
|
|
618
|
+
const tag = parseLocaleTag(locale);
|
|
619
|
+
if (tag === void 0 || tag.script !== void 0 || tag.variants.length > 0) {
|
|
620
|
+
return void 0;
|
|
621
|
+
}
|
|
622
|
+
if (tag.region !== void 0 && NUMERIC_REGION.test(tag.region)) {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
return locale.replaceAll("-", "_");
|
|
626
|
+
}
|
|
627
|
+
function spellLocale(locale, style, isSourceLocale) {
|
|
628
|
+
if (style === "android") {
|
|
629
|
+
return androidSegment(locale, isSourceLocale);
|
|
630
|
+
}
|
|
631
|
+
return style === "posix" ? posixSpelling(locale) : locale;
|
|
632
|
+
}
|
|
633
|
+
function isSafeSpelling(spelling) {
|
|
634
|
+
return spelling.length > 0 && !UNSAFE_IN_SEGMENT.test(spelling) && spelling !== "." && spelling !== "..";
|
|
520
635
|
}
|
|
521
636
|
function checkBatchIntegrity(inputs, extract, compare) {
|
|
522
637
|
const integrity = /* @__PURE__ */ new Map();
|
|
@@ -689,7 +804,7 @@ function deriveJsonSchema(schema) {
|
|
|
689
804
|
result[key] = value;
|
|
690
805
|
}
|
|
691
806
|
}
|
|
692
|
-
return result;
|
|
807
|
+
return { ...result, type: "object" };
|
|
693
808
|
}
|
|
694
809
|
function partitionTranslations(translations, requested) {
|
|
695
810
|
const firstSeen = /* @__PURE__ */ new Map();
|
|
@@ -809,6 +924,8 @@ function assertNotTruncated(truncated) {
|
|
|
809
924
|
var RATE_LIMITED_STATUS = 429;
|
|
810
925
|
var TIMEOUT_STATUS = 408;
|
|
811
926
|
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
927
|
+
var SERVER_OUTAGE_MIN_STATUS = 500;
|
|
928
|
+
var SERVER_OUTAGE_MAX_STATUS = 599;
|
|
812
929
|
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
813
930
|
"RateLimitError",
|
|
814
931
|
"TooManyRequestsError"
|
|
@@ -830,6 +947,9 @@ function getErrorStatus(error) {
|
|
|
830
947
|
const status = error.status;
|
|
831
948
|
return typeof status === "number" ? status : void 0;
|
|
832
949
|
}
|
|
950
|
+
function isServerOutageStatus(status) {
|
|
951
|
+
return status !== void 0 && status >= SERVER_OUTAGE_MIN_STATUS && status <= SERVER_OUTAGE_MAX_STATUS;
|
|
952
|
+
}
|
|
833
953
|
function readClassName(error) {
|
|
834
954
|
if (typeof error !== "object" || error === null) {
|
|
835
955
|
return void 0;
|
|
@@ -847,6 +967,9 @@ function classifyProviderError(error) {
|
|
|
847
967
|
if (status === TIMEOUT_STATUS) {
|
|
848
968
|
return "TIMEOUT";
|
|
849
969
|
}
|
|
970
|
+
if (isServerOutageStatus(status)) {
|
|
971
|
+
return "PROVIDER_UNAVAILABLE";
|
|
972
|
+
}
|
|
850
973
|
const className = readClassName(error);
|
|
851
974
|
if (className !== void 0) {
|
|
852
975
|
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
@@ -878,10 +1001,12 @@ var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
|
878
1001
|
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
879
1002
|
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
880
1003
|
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
1004
|
+
var PROVIDER_UNAVAILABLE_MESSAGE = "The translation provider is currently unavailable.";
|
|
881
1005
|
var MESSAGE_BY_CODE = {
|
|
882
1006
|
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
883
1007
|
TIMEOUT: TIMEOUT_MESSAGE,
|
|
884
1008
|
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
1009
|
+
PROVIDER_UNAVAILABLE: PROVIDER_UNAVAILABLE_MESSAGE,
|
|
885
1010
|
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
886
1011
|
};
|
|
887
1012
|
async function guardProviderCall(call, signal) {
|
|
@@ -962,14 +1087,14 @@ function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
|
962
1087
|
}
|
|
963
1088
|
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
964
1089
|
}
|
|
1090
|
+
function toMutableRequest(request) {
|
|
1091
|
+
return request;
|
|
1092
|
+
}
|
|
965
1093
|
function createDefaultClient() {
|
|
966
1094
|
const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
967
1095
|
return {
|
|
968
1096
|
messages: {
|
|
969
|
-
create: async (body, options) => await sdk.messages.create(
|
|
970
|
-
body,
|
|
971
|
-
options
|
|
972
|
-
)
|
|
1097
|
+
create: async (body, options) => await sdk.messages.create(toMutableRequest(body), options)
|
|
973
1098
|
}
|
|
974
1099
|
};
|
|
975
1100
|
}
|
|
@@ -1387,7 +1512,7 @@ function createDefaultClient3() {
|
|
|
1387
1512
|
models: {
|
|
1388
1513
|
generateContent: (request) => withGeminiRetry(
|
|
1389
1514
|
async () => await ai.models.generateContent(
|
|
1390
|
-
request
|
|
1515
|
+
toMutableRequest(request)
|
|
1391
1516
|
),
|
|
1392
1517
|
request.config.abortSignal
|
|
1393
1518
|
)
|
|
@@ -1551,7 +1676,7 @@ function createDefaultClient4() {
|
|
|
1551
1676
|
chat: {
|
|
1552
1677
|
completions: {
|
|
1553
1678
|
create: async (body, options) => await sdk.chat.completions.create(
|
|
1554
|
-
body,
|
|
1679
|
+
toMutableRequest(body),
|
|
1555
1680
|
options
|
|
1556
1681
|
)
|
|
1557
1682
|
}
|
|
@@ -1752,7 +1877,7 @@ function createDefaultClient5(config) {
|
|
|
1752
1877
|
chat: {
|
|
1753
1878
|
completions: {
|
|
1754
1879
|
create: async (body, options) => await sdk.chat.completions.create(
|
|
1755
|
-
body,
|
|
1880
|
+
toMutableRequest(body),
|
|
1756
1881
|
options
|
|
1757
1882
|
)
|
|
1758
1883
|
}
|
|
@@ -1834,7 +1959,13 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
1834
1959
|
targetLocales: zod.z.array(zod.z.string().min(1)).min(1),
|
|
1835
1960
|
format: supportedFormatSchema,
|
|
1836
1961
|
files: zod.z.strictObject({
|
|
1837
|
-
pattern: zod.z.string().min(1)
|
|
1962
|
+
pattern: zod.z.string().min(1),
|
|
1963
|
+
/**
|
|
1964
|
+
* How the `{locale}` token in `pattern` is spelled for each locale. Absent means `"literal"`,
|
|
1965
|
+
* the configured tag verbatim, which is what every layout except Android and the POSIX
|
|
1966
|
+
* underscore layouts needs. See `LocaleStyle` for what each style produces.
|
|
1967
|
+
*/
|
|
1968
|
+
localeStyle: zod.z.enum(LOCALE_STYLES).optional()
|
|
1838
1969
|
}),
|
|
1839
1970
|
provider: providerConfigSchema,
|
|
1840
1971
|
/**
|
|
@@ -1855,8 +1986,9 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
1855
1986
|
* Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
|
|
1856
1987
|
* translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
|
|
1857
1988
|
* requires but the source does not supply (for example Polish few/many). The per-run
|
|
1858
|
-
* `generatePlurals` option on `translate` overrides this.
|
|
1859
|
-
*
|
|
1989
|
+
* `generatePlurals` option on `translate` overrides this. A plural form the target file already holds
|
|
1990
|
+
* is adopted, never generated over. Unsupported cases (DeepL, non-i18next, an unknown language) fall
|
|
1991
|
+
* back to the per-locale plural warning.
|
|
1860
1992
|
*/
|
|
1861
1993
|
generatePlurals: zod.z.boolean().optional(),
|
|
1862
1994
|
/**
|
|
@@ -1949,7 +2081,7 @@ async function loadExplicitWithMeta(explorer, configPath, cwd, fs$1) {
|
|
|
1949
2081
|
try {
|
|
1950
2082
|
result = await explorer.load(resolved);
|
|
1951
2083
|
} catch (error) {
|
|
1952
|
-
const detail =
|
|
2084
|
+
const detail = errorMessage(error);
|
|
1953
2085
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
1954
2086
|
}
|
|
1955
2087
|
const parsed = parseConfig(result?.config);
|
|
@@ -1974,7 +2106,7 @@ async function loadConfigWithMeta(options = {}) {
|
|
|
1974
2106
|
try {
|
|
1975
2107
|
result = await explorer.search(options.cwd);
|
|
1976
2108
|
} catch (error) {
|
|
1977
|
-
const detail =
|
|
2109
|
+
const detail = errorMessage(error);
|
|
1978
2110
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
1979
2111
|
}
|
|
1980
2112
|
if (result === null || result.isEmpty === true) {
|
|
@@ -1991,6 +2123,63 @@ async function loadConfig(options = {}) {
|
|
|
1991
2123
|
const { config } = await loadConfigWithMeta(options);
|
|
1992
2124
|
return config;
|
|
1993
2125
|
}
|
|
2126
|
+
var DEFAULT_LOCALE_STYLE = "literal";
|
|
2127
|
+
function validatePattern(pattern, style) {
|
|
2128
|
+
if (!pattern.includes(LOCALE_TOKEN)) {
|
|
2129
|
+
throw new SdkError(
|
|
2130
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2131
|
+
`The files pattern "${pattern}" must contain the ${LOCALE_TOKEN} token.`
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
if (isSegmentStyle(style) && !tokenOccupiesWholeSegments(pattern)) {
|
|
2135
|
+
throw new SdkError(
|
|
2136
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2137
|
+
`The "${style}" locale style expands ${LOCALE_TOKEN} to a whole path segment, so the token must stand alone between separators, but the pattern "${pattern}" embeds it in a segment.`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
function safeSpelling(locale, style, sourceLocale) {
|
|
2142
|
+
const spelling = spellLocale(locale, style, locale === sourceLocale);
|
|
2143
|
+
if (spelling === void 0) {
|
|
2144
|
+
throw new SdkError(
|
|
2145
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2146
|
+
`The "${style}" locale style has no valid path spelling for the locale "${locale}".`
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
if (!isSafeSpelling(spelling)) {
|
|
2150
|
+
throw new SdkError(
|
|
2151
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2152
|
+
`The locale "${locale}" expands to "${spelling}" under the "${style}" locale style, which is not a single path segment.`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
return spelling;
|
|
2156
|
+
}
|
|
2157
|
+
function buildForwardMap(config, pathFor) {
|
|
2158
|
+
const forward = /* @__PURE__ */ new Map();
|
|
2159
|
+
for (const locale of [config.sourceLocale, ...config.targetLocales]) {
|
|
2160
|
+
const path = pathFor(locale);
|
|
2161
|
+
const claimed = forward.get(path);
|
|
2162
|
+
if (claimed !== void 0 && claimed !== locale) {
|
|
2163
|
+
throw new SdkError(
|
|
2164
|
+
"LOCALE_PATH_COLLISION",
|
|
2165
|
+
`The locales "${claimed}" and "${locale}" both resolve to ${path}.`
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
forward.set(path, locale);
|
|
2169
|
+
}
|
|
2170
|
+
return forward;
|
|
2171
|
+
}
|
|
2172
|
+
function createLocalePathResolver(cwd, config) {
|
|
2173
|
+
const style = config.files.localeStyle ?? DEFAULT_LOCALE_STYLE;
|
|
2174
|
+
const pattern = config.files.pattern;
|
|
2175
|
+
validatePattern(pattern, style);
|
|
2176
|
+
const pathFor = (locale) => path.resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
|
|
2177
|
+
const forward = buildForwardMap(config, pathFor);
|
|
2178
|
+
return {
|
|
2179
|
+
pathFor,
|
|
2180
|
+
localeFor: (absolutePath) => forward.get(path.resolve(cwd, absolutePath))
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
1994
2183
|
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
1995
2184
|
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
1996
2185
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
@@ -2059,15 +2248,15 @@ async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
|
|
|
2059
2248
|
if (await fs.createExclusive(path, lockPayload())) {
|
|
2060
2249
|
return;
|
|
2061
2250
|
}
|
|
2251
|
+
if (notify !== void 0) {
|
|
2252
|
+
await notify();
|
|
2253
|
+
}
|
|
2062
2254
|
if (Date.now() >= deadline) {
|
|
2063
2255
|
throw new SdkError(
|
|
2064
2256
|
"LOCK_CONTENDED",
|
|
2065
2257
|
`Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
|
|
2066
2258
|
);
|
|
2067
2259
|
}
|
|
2068
|
-
if (notify !== void 0) {
|
|
2069
|
-
await notify();
|
|
2070
|
-
}
|
|
2071
2260
|
const jitter = Math.random() * pollIntervalMs;
|
|
2072
2261
|
await sleep(pollIntervalMs + jitter);
|
|
2073
2262
|
}
|
|
@@ -2657,6 +2846,9 @@ async function fsyncDirBestEffort(path) {
|
|
|
2657
2846
|
}
|
|
2658
2847
|
}
|
|
2659
2848
|
var nodeOps = {
|
|
2849
|
+
mkdir: async (path) => {
|
|
2850
|
+
await promises.mkdir(path, { recursive: true });
|
|
2851
|
+
},
|
|
2660
2852
|
writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
|
|
2661
2853
|
fsyncFile: (path) => fsyncPath(path),
|
|
2662
2854
|
rename: (from, to) => promises.rename(from, to),
|
|
@@ -2673,8 +2865,10 @@ function tempFileName2(path$1) {
|
|
|
2673
2865
|
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
2674
2866
|
}
|
|
2675
2867
|
async function atomicWriteFile(path$1, data, ops = nodeOps) {
|
|
2868
|
+
const directory = path.dirname(path$1);
|
|
2676
2869
|
const tmp = tempFileName2(path$1);
|
|
2677
2870
|
try {
|
|
2871
|
+
await ops.mkdir(directory);
|
|
2678
2872
|
await ops.writeFile(tmp, data);
|
|
2679
2873
|
await ops.fsyncFile(tmp);
|
|
2680
2874
|
await ops.rename(tmp, path$1);
|
|
@@ -2683,7 +2877,7 @@ async function atomicWriteFile(path$1, data, ops = nodeOps) {
|
|
|
2683
2877
|
throw error;
|
|
2684
2878
|
}
|
|
2685
2879
|
try {
|
|
2686
|
-
await ops.fsyncDir(
|
|
2880
|
+
await ops.fsyncDir(directory);
|
|
2687
2881
|
} catch {
|
|
2688
2882
|
}
|
|
2689
2883
|
}
|
|
@@ -4007,7 +4201,7 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
4007
4201
|
|
|
4008
4202
|
// src/flow/read-target.ts
|
|
4009
4203
|
async function readTargetResource(input) {
|
|
4010
|
-
const path =
|
|
4204
|
+
const path = input.resolver.pathFor(input.locale);
|
|
4011
4205
|
if (!await input.fs.fileExists(path)) {
|
|
4012
4206
|
return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
|
|
4013
4207
|
}
|
|
@@ -4034,7 +4228,7 @@ function selectLocales(config, requested) {
|
|
|
4034
4228
|
|
|
4035
4229
|
// src/flow/source.ts
|
|
4036
4230
|
async function readSource(config, cwd, fs, adapter) {
|
|
4037
|
-
const sourcePath =
|
|
4231
|
+
const sourcePath = createLocalePathResolver(cwd, config).pathFor(config.sourceLocale);
|
|
4038
4232
|
if (!await fs.fileExists(sourcePath)) {
|
|
4039
4233
|
throw new SdkError(
|
|
4040
4234
|
"SOURCE_UNREADABLE",
|
|
@@ -4044,7 +4238,7 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
4044
4238
|
try {
|
|
4045
4239
|
return await adapter.read(sourcePath, config.sourceLocale);
|
|
4046
4240
|
} catch (error) {
|
|
4047
|
-
const detail =
|
|
4241
|
+
const detail = errorMessage(error);
|
|
4048
4242
|
throw new SdkError(
|
|
4049
4243
|
"SOURCE_INVALID",
|
|
4050
4244
|
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
@@ -4055,8 +4249,7 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
4055
4249
|
// src/flow/diff-locales.ts
|
|
4056
4250
|
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
4057
4251
|
return readTargetResource({
|
|
4058
|
-
cwd,
|
|
4059
|
-
filesPattern: config.files.pattern,
|
|
4252
|
+
resolver: createLocalePathResolver(cwd, config),
|
|
4060
4253
|
format: config.format,
|
|
4061
4254
|
locale,
|
|
4062
4255
|
adapter,
|
|
@@ -4177,7 +4370,7 @@ async function editEntry(input, deps = {}) {
|
|
|
4177
4370
|
}
|
|
4178
4371
|
const merged = new Map(target.entries);
|
|
4179
4372
|
merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
|
|
4180
|
-
const path =
|
|
4373
|
+
const path = createLocalePathResolver(cwd, config).pathFor(locale);
|
|
4181
4374
|
await adapter.write(
|
|
4182
4375
|
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
4183
4376
|
path
|
|
@@ -4344,7 +4537,7 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
4344
4537
|
try {
|
|
4345
4538
|
return createProvider(config);
|
|
4346
4539
|
} catch (error) {
|
|
4347
|
-
const detail =
|
|
4540
|
+
const detail = errorMessage(error);
|
|
4348
4541
|
throw new SdkError(
|
|
4349
4542
|
"PROVIDER_CONSTRUCTION_FAILED",
|
|
4350
4543
|
`Failed to construct provider "${config.id}": ${detail}`
|
|
@@ -4400,7 +4593,7 @@ async function retranslateEntry(input, deps = {}) {
|
|
|
4400
4593
|
}
|
|
4401
4594
|
const merged = new Map(target.entries);
|
|
4402
4595
|
merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
|
|
4403
|
-
const path =
|
|
4596
|
+
const path = createLocalePathResolver(cwd, config).pathFor(locale);
|
|
4404
4597
|
await adapter.write(
|
|
4405
4598
|
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
4406
4599
|
path
|
|
@@ -4567,13 +4760,6 @@ function budgetExceededNotice(tracker) {
|
|
|
4567
4760
|
}
|
|
4568
4761
|
|
|
4569
4762
|
// src/flow/locale-failure.ts
|
|
4570
|
-
function describeError(error) {
|
|
4571
|
-
if (error instanceof Error) {
|
|
4572
|
-
const code = error.code;
|
|
4573
|
-
return { code: typeof code === "string" ? code : "LOCALE_FAILED", message: error.message };
|
|
4574
|
-
}
|
|
4575
|
-
return { code: "LOCALE_FAILED", message: String(error) };
|
|
4576
|
-
}
|
|
4577
4763
|
function failureSummary(locale, error) {
|
|
4578
4764
|
return {
|
|
4579
4765
|
locale,
|
|
@@ -4593,7 +4779,7 @@ function failureSummary(locale, error) {
|
|
|
4593
4779
|
unfilled: [],
|
|
4594
4780
|
malformedRows: [],
|
|
4595
4781
|
duplicateKeys: [],
|
|
4596
|
-
error: describeError(error)
|
|
4782
|
+
error: describeError(error, "LOCALE_FAILED")
|
|
4597
4783
|
};
|
|
4598
4784
|
}
|
|
4599
4785
|
function deriveLocaleStatus(parts) {
|
|
@@ -4826,6 +5012,9 @@ function syntheticEntry(item) {
|
|
|
4826
5012
|
meaning: `CLDR plural category "${item.category}"`
|
|
4827
5013
|
};
|
|
4828
5014
|
}
|
|
5015
|
+
function isAdopted(item, targetKeys, baseline) {
|
|
5016
|
+
return targetKeys.has(item.targetKey) && !baseline.has(item.targetKey);
|
|
5017
|
+
}
|
|
4829
5018
|
function staleItems(items, baseline) {
|
|
4830
5019
|
return items.filter((item) => {
|
|
4831
5020
|
const hash = generatedLockHash(item.governingEntries, item.category);
|
|
@@ -4845,7 +5034,10 @@ function buildRequest2(context, entries) {
|
|
|
4845
5034
|
}
|
|
4846
5035
|
async function generatePluralForms(context) {
|
|
4847
5036
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
4848
|
-
const
|
|
5037
|
+
const candidates = plan.items.filter(
|
|
5038
|
+
(item) => !isAdopted(item, context.targetKeys, context.baseline)
|
|
5039
|
+
);
|
|
5040
|
+
const stale = staleItems(candidates, context.baseline);
|
|
4849
5041
|
if (stale.length === 0) {
|
|
4850
5042
|
return EMPTY_RESULT;
|
|
4851
5043
|
}
|
|
@@ -5027,10 +5219,15 @@ function buildRequest3(params, entries) {
|
|
|
5027
5219
|
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
5028
5220
|
};
|
|
5029
5221
|
}
|
|
5222
|
+
async function shouldWriteTarget(params, path, changed) {
|
|
5223
|
+
if (changed.accepted > 0 || changed.pruned > 0 || changed.generated > 0) {
|
|
5224
|
+
return true;
|
|
5225
|
+
}
|
|
5226
|
+
return !await params.fs.fileExists(path);
|
|
5227
|
+
}
|
|
5030
5228
|
async function runLocale(params) {
|
|
5031
5229
|
const target = await readTargetResource({
|
|
5032
|
-
|
|
5033
|
-
filesPattern: params.filesPattern,
|
|
5230
|
+
resolver: params.resolver,
|
|
5034
5231
|
format: params.format,
|
|
5035
5232
|
locale: params.targetLocale,
|
|
5036
5233
|
adapter: params.adapter,
|
|
@@ -5107,20 +5304,27 @@ async function runLocale(params) {
|
|
|
5107
5304
|
merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
|
|
5108
5305
|
}
|
|
5109
5306
|
}
|
|
5110
|
-
const generation = await runGeneration(params, provider);
|
|
5307
|
+
const generation = await runGeneration(params, provider, new Set(target.entries.keys()));
|
|
5111
5308
|
for (const form of generation.accepted) {
|
|
5112
5309
|
merged.set(form.targetKey, { ...form.entry, namespace: target.namespace });
|
|
5113
5310
|
}
|
|
5114
|
-
const path =
|
|
5115
|
-
await params
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5311
|
+
const path = params.resolver.pathFor(params.targetLocale);
|
|
5312
|
+
const writeNeeded = await shouldWriteTarget(params, path, {
|
|
5313
|
+
accepted: accepted.size,
|
|
5314
|
+
pruned: pruned.length,
|
|
5315
|
+
generated: generation.accepted.length
|
|
5316
|
+
});
|
|
5317
|
+
if (writeNeeded) {
|
|
5318
|
+
await params.adapter.write(
|
|
5319
|
+
{
|
|
5320
|
+
locale: params.targetLocale,
|
|
5321
|
+
namespace: target.namespace,
|
|
5322
|
+
format: params.format,
|
|
5323
|
+
entries: merged
|
|
5324
|
+
},
|
|
5325
|
+
path
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5124
5328
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
5125
5329
|
const notices = [
|
|
5126
5330
|
...pluralNotices,
|
|
@@ -5167,7 +5371,7 @@ var NO_GENERATION_RESULT = {
|
|
|
5167
5371
|
usage: void 0,
|
|
5168
5372
|
tripped: false
|
|
5169
5373
|
};
|
|
5170
|
-
async function runGeneration(params, provider) {
|
|
5374
|
+
async function runGeneration(params, provider, targetKeys) {
|
|
5171
5375
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
5172
5376
|
return NO_GENERATION_RESULT;
|
|
5173
5377
|
}
|
|
@@ -5181,6 +5385,7 @@ async function runGeneration(params, provider) {
|
|
|
5181
5385
|
glossary: params.glossary,
|
|
5182
5386
|
tone: params.tone,
|
|
5183
5387
|
baseline: params.baseline,
|
|
5388
|
+
targetKeys,
|
|
5184
5389
|
maxBatchSize: params.maxBatchSize,
|
|
5185
5390
|
budget: params.budget
|
|
5186
5391
|
});
|
|
@@ -5423,7 +5628,7 @@ function buildLocaleRunParams(context, targetLocale, baseline) {
|
|
|
5423
5628
|
adapter: context.adapter,
|
|
5424
5629
|
provider: context.provider,
|
|
5425
5630
|
cwd: context.cwd,
|
|
5426
|
-
|
|
5631
|
+
resolver: context.resolver,
|
|
5427
5632
|
sourceLocale: context.config.sourceLocale,
|
|
5428
5633
|
targetLocale,
|
|
5429
5634
|
format: context.config.format,
|
|
@@ -5577,6 +5782,7 @@ async function translate2(input, deps = {}) {
|
|
|
5577
5782
|
config.maxTokens,
|
|
5578
5783
|
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
5579
5784
|
);
|
|
5785
|
+
const resolver = createLocalePathResolver(cwd, config);
|
|
5580
5786
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
5581
5787
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
5582
5788
|
const source = await readSource(config, cwd, fs, adapter);
|
|
@@ -5587,6 +5793,7 @@ async function translate2(input, deps = {}) {
|
|
|
5587
5793
|
provider,
|
|
5588
5794
|
cwd,
|
|
5589
5795
|
config,
|
|
5796
|
+
resolver,
|
|
5590
5797
|
prune,
|
|
5591
5798
|
generatePlurals,
|
|
5592
5799
|
maxBatchSize,
|
|
@@ -5627,6 +5834,123 @@ var ExchangeError = class extends Error {
|
|
|
5627
5834
|
this.code = code;
|
|
5628
5835
|
}
|
|
5629
5836
|
};
|
|
5837
|
+
var DELIMITER = {
|
|
5838
|
+
csv: ",",
|
|
5839
|
+
tsv: " "
|
|
5840
|
+
};
|
|
5841
|
+
var FORBIDDEN_FILE_NAME_CHARS = /[\\/:*?"<>|\p{Cc}]/u;
|
|
5842
|
+
function assertPlainFileLocale(locale) {
|
|
5843
|
+
if (locale.length === 0 || locale === "." || locale === "..") {
|
|
5844
|
+
throw new ExchangeError(
|
|
5845
|
+
"WORKBOOK_INVALID",
|
|
5846
|
+
`The locale "${locale}" cannot be an interchange file name: it must name a file, not a directory.`
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5849
|
+
if (FORBIDDEN_FILE_NAME_CHARS.test(locale)) {
|
|
5850
|
+
throw new ExchangeError(
|
|
5851
|
+
"WORKBOOK_INVALID",
|
|
5852
|
+
`The locale "${locale}" cannot be an interchange file name: it must not contain a path separator, a control character, or any of : * ? " < > | .`
|
|
5853
|
+
);
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
function delimitedFileName(locale, format) {
|
|
5857
|
+
assertPlainFileLocale(locale);
|
|
5858
|
+
return `${locale}.${format}`;
|
|
5859
|
+
}
|
|
5860
|
+
var COLUMN = {
|
|
5861
|
+
key: 1,
|
|
5862
|
+
source: 2,
|
|
5863
|
+
current: 3,
|
|
5864
|
+
status: 4,
|
|
5865
|
+
translation: 5,
|
|
5866
|
+
sourceHash: 6,
|
|
5867
|
+
context: 7,
|
|
5868
|
+
reviewStatus: 8,
|
|
5869
|
+
reviewReasons: 9
|
|
5870
|
+
};
|
|
5871
|
+
var HEADERS = [
|
|
5872
|
+
"Key",
|
|
5873
|
+
"Source",
|
|
5874
|
+
"Current translation",
|
|
5875
|
+
"Status",
|
|
5876
|
+
"Translation",
|
|
5877
|
+
"Source hash",
|
|
5878
|
+
"Context",
|
|
5879
|
+
"Review status",
|
|
5880
|
+
"Review reasons"
|
|
5881
|
+
];
|
|
5882
|
+
var HEADER_ROW = 1;
|
|
5883
|
+
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
5884
|
+
var rowSchema = zod.z.object({
|
|
5885
|
+
key: zod.z.string().min(1),
|
|
5886
|
+
source: zod.z.string(),
|
|
5887
|
+
currentTarget: zod.z.string(),
|
|
5888
|
+
status: zod.z.enum(["new", "changed", "unchanged"]),
|
|
5889
|
+
sourceHash: zod.z.string(),
|
|
5890
|
+
translation: zod.z.string(),
|
|
5891
|
+
context: zod.z.string(),
|
|
5892
|
+
reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
|
|
5893
|
+
reviewReasons: zod.z.string().catch("")
|
|
5894
|
+
});
|
|
5895
|
+
var MALFORMED_ROW_COLUMN = "Status";
|
|
5896
|
+
function cellAt(cells, column) {
|
|
5897
|
+
return cells[column - 1] ?? "";
|
|
5898
|
+
}
|
|
5899
|
+
function parseRowCells(cells) {
|
|
5900
|
+
const result = rowSchema.safeParse({
|
|
5901
|
+
key: cellAt(cells, COLUMN.key),
|
|
5902
|
+
source: cellAt(cells, COLUMN.source),
|
|
5903
|
+
currentTarget: cellAt(cells, COLUMN.current),
|
|
5904
|
+
status: cellAt(cells, COLUMN.status),
|
|
5905
|
+
sourceHash: cellAt(cells, COLUMN.sourceHash),
|
|
5906
|
+
translation: cellAt(cells, COLUMN.translation).trim(),
|
|
5907
|
+
context: cellAt(cells, COLUMN.context),
|
|
5908
|
+
reviewStatus: cellAt(cells, COLUMN.reviewStatus),
|
|
5909
|
+
reviewReasons: cellAt(cells, COLUMN.reviewReasons)
|
|
5910
|
+
});
|
|
5911
|
+
if (!result.success) {
|
|
5912
|
+
return { ok: false, column: MALFORMED_ROW_COLUMN };
|
|
5913
|
+
}
|
|
5914
|
+
return { ok: true, row: result.data };
|
|
5915
|
+
}
|
|
5916
|
+
function rowCells(row) {
|
|
5917
|
+
const cells = new Array(HEADERS.length).fill("");
|
|
5918
|
+
cells[COLUMN.key - 1] = row.key;
|
|
5919
|
+
cells[COLUMN.source - 1] = row.source;
|
|
5920
|
+
cells[COLUMN.current - 1] = row.currentTarget;
|
|
5921
|
+
cells[COLUMN.status - 1] = row.status;
|
|
5922
|
+
cells[COLUMN.translation - 1] = row.translation;
|
|
5923
|
+
cells[COLUMN.sourceHash - 1] = row.sourceHash;
|
|
5924
|
+
cells[COLUMN.context - 1] = row.context;
|
|
5925
|
+
cells[COLUMN.reviewStatus - 1] = row.reviewStatus;
|
|
5926
|
+
cells[COLUMN.reviewReasons - 1] = row.reviewReasons;
|
|
5927
|
+
return cells;
|
|
5928
|
+
}
|
|
5929
|
+
var QUOTE = '"';
|
|
5930
|
+
var ESCAPED_QUOTE = '""';
|
|
5931
|
+
var LINE_BREAK = "\n";
|
|
5932
|
+
var UTF8_BOM = "\uFEFF";
|
|
5933
|
+
function needsQuoting(value, delimiter) {
|
|
5934
|
+
return value.includes(delimiter) || value.includes(QUOTE) || value.includes("\n") || value.includes("\r") || value !== value.trim();
|
|
5935
|
+
}
|
|
5936
|
+
function encodeField(value, delimiter) {
|
|
5937
|
+
if (!needsQuoting(value, delimiter)) {
|
|
5938
|
+
return value;
|
|
5939
|
+
}
|
|
5940
|
+
return `${QUOTE}${value.replaceAll(QUOTE, ESCAPED_QUOTE)}${QUOTE}`;
|
|
5941
|
+
}
|
|
5942
|
+
function encodeRecord(fields, delimiter) {
|
|
5943
|
+
return fields.map((field) => encodeField(field, delimiter)).join(delimiter);
|
|
5944
|
+
}
|
|
5945
|
+
function buildDelimited(sheet, format) {
|
|
5946
|
+
const delimiter = DELIMITER[format];
|
|
5947
|
+
const records = [encodeRecord(HEADERS, delimiter)];
|
|
5948
|
+
for (const row of sheet.rows) {
|
|
5949
|
+
records.push(encodeRecord(rowCells(row), delimiter));
|
|
5950
|
+
}
|
|
5951
|
+
const text = `${records.join(LINE_BREAK)}${LINE_BREAK}`;
|
|
5952
|
+
return format === "csv" ? `${UTF8_BOM}${text}` : text;
|
|
5953
|
+
}
|
|
5630
5954
|
var INSTRUCTIONS_LINES = [
|
|
5631
5955
|
"How to use this workbook",
|
|
5632
5956
|
"",
|
|
@@ -5655,30 +5979,6 @@ var INSTRUCTIONS_LINES = [
|
|
|
5655
5979
|
"a value with a broken placeholder, invalid ICU, or a source that changed since export is",
|
|
5656
5980
|
"withheld (not written) and reported so you can fix and resubmit it."
|
|
5657
5981
|
];
|
|
5658
|
-
var COLUMN = {
|
|
5659
|
-
key: 1,
|
|
5660
|
-
source: 2,
|
|
5661
|
-
current: 3,
|
|
5662
|
-
status: 4,
|
|
5663
|
-
translation: 5,
|
|
5664
|
-
sourceHash: 6,
|
|
5665
|
-
context: 7,
|
|
5666
|
-
reviewStatus: 8,
|
|
5667
|
-
reviewReasons: 9
|
|
5668
|
-
};
|
|
5669
|
-
var HEADERS = [
|
|
5670
|
-
"Key",
|
|
5671
|
-
"Source",
|
|
5672
|
-
"Current translation",
|
|
5673
|
-
"Status",
|
|
5674
|
-
"Translation",
|
|
5675
|
-
"Source hash",
|
|
5676
|
-
"Context",
|
|
5677
|
-
"Review status",
|
|
5678
|
-
"Review reasons"
|
|
5679
|
-
];
|
|
5680
|
-
var HEADER_ROW = 1;
|
|
5681
|
-
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
5682
5982
|
var READ_ONLY_FILL = {
|
|
5683
5983
|
type: "pattern",
|
|
5684
5984
|
pattern: "solid",
|
|
@@ -5838,6 +6138,12 @@ async function buildWorkbook(model) {
|
|
|
5838
6138
|
const protectedBytes = await protectWorkbookStructure(serialized);
|
|
5839
6139
|
return Uint8Array.prototype.slice.call(protectedBytes);
|
|
5840
6140
|
}
|
|
6141
|
+
var DEFAULT_DELIMITED_LIMITS = {
|
|
6142
|
+
maxInputBytes: 32 * 1024 * 1024,
|
|
6143
|
+
maxRowsPerFile: 1e5,
|
|
6144
|
+
maxFieldsPerRow: 64,
|
|
6145
|
+
maxFieldLength: 1024 * 1024
|
|
6146
|
+
};
|
|
5841
6147
|
var DEFAULT_WORKBOOK_LIMITS = {
|
|
5842
6148
|
maxDecompressedBytes: 64 * 1024 * 1024,
|
|
5843
6149
|
maxEntryCount: 1024,
|
|
@@ -5845,6 +6151,205 @@ var DEFAULT_WORKBOOK_LIMITS = {
|
|
|
5845
6151
|
maxRowsPerSheet: 1e5,
|
|
5846
6152
|
maxCellsPerRow: 64
|
|
5847
6153
|
};
|
|
6154
|
+
var QUOTE2 = '"';
|
|
6155
|
+
var HEADER_RECORD = 1;
|
|
6156
|
+
function scanQuotedField(text, start) {
|
|
6157
|
+
const parts = [];
|
|
6158
|
+
let cursor = start;
|
|
6159
|
+
for (; ; ) {
|
|
6160
|
+
const quote = text.indexOf(QUOTE2, cursor);
|
|
6161
|
+
if (quote === -1) {
|
|
6162
|
+
parts.push(text.slice(cursor));
|
|
6163
|
+
return { value: parts.join(""), next: text.length };
|
|
6164
|
+
}
|
|
6165
|
+
parts.push(text.slice(cursor, quote));
|
|
6166
|
+
if (text[quote + 1] !== QUOTE2) {
|
|
6167
|
+
return { value: parts.join(""), next: quote + 1 };
|
|
6168
|
+
}
|
|
6169
|
+
parts.push(QUOTE2);
|
|
6170
|
+
cursor = quote + 2;
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
function scanPlainField(text, start, delimiter) {
|
|
6174
|
+
let cursor = start;
|
|
6175
|
+
while (cursor < text.length) {
|
|
6176
|
+
const char = text[cursor];
|
|
6177
|
+
if (char === delimiter || char === "\n" || char === "\r") {
|
|
6178
|
+
break;
|
|
6179
|
+
}
|
|
6180
|
+
cursor += 1;
|
|
6181
|
+
}
|
|
6182
|
+
return { value: text.slice(start, cursor), next: cursor };
|
|
6183
|
+
}
|
|
6184
|
+
function scanField(text, start, delimiter) {
|
|
6185
|
+
if (text[start] !== QUOTE2) {
|
|
6186
|
+
return scanPlainField(text, start, delimiter);
|
|
6187
|
+
}
|
|
6188
|
+
const quoted = scanQuotedField(text, start + 1);
|
|
6189
|
+
const trailing = scanPlainField(text, quoted.next, delimiter);
|
|
6190
|
+
return { value: `${quoted.value}${trailing.value}`, next: trailing.next };
|
|
6191
|
+
}
|
|
6192
|
+
function countLineBreaks(value) {
|
|
6193
|
+
let breaks = 0;
|
|
6194
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
6195
|
+
const char = value[index];
|
|
6196
|
+
if (char === "\n") {
|
|
6197
|
+
breaks += 1;
|
|
6198
|
+
} else if (char === "\r") {
|
|
6199
|
+
breaks += 1;
|
|
6200
|
+
if (value[index + 1] === "\n") {
|
|
6201
|
+
index += 1;
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
return breaks;
|
|
6206
|
+
}
|
|
6207
|
+
function scanRecord(text, start, delimiter, limits) {
|
|
6208
|
+
const fields = [];
|
|
6209
|
+
let breaks = 0;
|
|
6210
|
+
let cursor = start;
|
|
6211
|
+
for (; ; ) {
|
|
6212
|
+
const field = scanField(text, cursor, delimiter);
|
|
6213
|
+
assertFieldLength(field.value, limits);
|
|
6214
|
+
fields.push(field.value);
|
|
6215
|
+
breaks += countLineBreaks(field.value);
|
|
6216
|
+
assertFieldCount(fields.length, limits);
|
|
6217
|
+
cursor = field.next;
|
|
6218
|
+
if (text[cursor] !== delimiter) {
|
|
6219
|
+
return { fields, breaks, next: cursor };
|
|
6220
|
+
}
|
|
6221
|
+
cursor += 1;
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
function consumeLineBreak(text, cursor) {
|
|
6225
|
+
if (text[cursor] === "\r") {
|
|
6226
|
+
return text[cursor + 1] === "\n" ? cursor + 2 : cursor + 1;
|
|
6227
|
+
}
|
|
6228
|
+
return text[cursor] === "\n" ? cursor + 1 : cursor;
|
|
6229
|
+
}
|
|
6230
|
+
function scanRecords(text, delimiter, limits) {
|
|
6231
|
+
const records = [];
|
|
6232
|
+
let cursor = 0;
|
|
6233
|
+
let line = 1;
|
|
6234
|
+
while (cursor < text.length) {
|
|
6235
|
+
const record = scanRecord(text, cursor, delimiter, limits);
|
|
6236
|
+
records.push({ fields: record.fields, line });
|
|
6237
|
+
assertRecordCount(records.length, limits);
|
|
6238
|
+
const next = consumeLineBreak(text, record.next);
|
|
6239
|
+
if (next === record.next) {
|
|
6240
|
+
break;
|
|
6241
|
+
}
|
|
6242
|
+
line += record.breaks + 1;
|
|
6243
|
+
cursor = next;
|
|
6244
|
+
}
|
|
6245
|
+
return records;
|
|
6246
|
+
}
|
|
6247
|
+
function assertInputBytes(text, limits) {
|
|
6248
|
+
if (Buffer.byteLength(text, "utf8") > limits.maxInputBytes) {
|
|
6249
|
+
throw new ExchangeError(
|
|
6250
|
+
"WORKBOOK_INVALID",
|
|
6251
|
+
`The interchange file is larger than the maximum of ${limits.maxInputBytes} bytes.`
|
|
6252
|
+
);
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
function assertFieldLength(value, limits) {
|
|
6256
|
+
if (value.length > limits.maxFieldLength) {
|
|
6257
|
+
throw new ExchangeError(
|
|
6258
|
+
"WORKBOOK_INVALID",
|
|
6259
|
+
`The interchange file has a field longer than the maximum of ${limits.maxFieldLength} characters.`
|
|
6260
|
+
);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
function assertFieldCount(count, limits) {
|
|
6264
|
+
if (count > limits.maxFieldsPerRow) {
|
|
6265
|
+
throw new ExchangeError(
|
|
6266
|
+
"WORKBOOK_INVALID",
|
|
6267
|
+
`The interchange file has a row with more than the maximum of ${limits.maxFieldsPerRow} fields.`
|
|
6268
|
+
);
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
function assertRecordCount(count, limits) {
|
|
6272
|
+
if (count - HEADER_RECORD > limits.maxRowsPerFile) {
|
|
6273
|
+
throw new ExchangeError(
|
|
6274
|
+
"WORKBOOK_INVALID",
|
|
6275
|
+
`The interchange file has more than the maximum of ${limits.maxRowsPerFile} rows.`
|
|
6276
|
+
);
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
function assertHeaderRecord(records) {
|
|
6280
|
+
const header = records[0]?.fields;
|
|
6281
|
+
if (header === void 0 || header.length !== HEADERS.length) {
|
|
6282
|
+
throw new ExchangeError(
|
|
6283
|
+
"WORKBOOK_INVALID",
|
|
6284
|
+
`The interchange file does not start with the expected header line of ${HEADERS.length} columns.`
|
|
6285
|
+
);
|
|
6286
|
+
}
|
|
6287
|
+
for (const [index, label] of HEADERS.entries()) {
|
|
6288
|
+
if (header[index] !== label) {
|
|
6289
|
+
throw new ExchangeError(
|
|
6290
|
+
"WORKBOOK_INVALID",
|
|
6291
|
+
`The interchange file's header line does not match the expected columns (column ${index + 1} should be "${label}").`
|
|
6292
|
+
);
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
function fieldCountColumn(fieldCount) {
|
|
6297
|
+
const label = HEADERS.at(Math.min(fieldCount, HEADERS.length - 1));
|
|
6298
|
+
if (label === void 0) {
|
|
6299
|
+
return MALFORMED_ROW_COLUMN;
|
|
6300
|
+
}
|
|
6301
|
+
return label;
|
|
6302
|
+
}
|
|
6303
|
+
function isBlankRecord(fields) {
|
|
6304
|
+
return fields.length === 1 && fields[0] === "";
|
|
6305
|
+
}
|
|
6306
|
+
function readRecord(fields, locale, at, into) {
|
|
6307
|
+
const { row, line } = at;
|
|
6308
|
+
if (isBlankRecord(fields)) {
|
|
6309
|
+
return;
|
|
6310
|
+
}
|
|
6311
|
+
if (fields.length !== HEADERS.length) {
|
|
6312
|
+
into.malformed.push({ locale, row, line, column: fieldCountColumn(fields.length) });
|
|
6313
|
+
return;
|
|
6314
|
+
}
|
|
6315
|
+
if (fields[COLUMN.key - 1] === "") {
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
const outcome = parseRowCells(fields);
|
|
6319
|
+
if (!outcome.ok) {
|
|
6320
|
+
into.malformed.push({ locale, row, line, column: outcome.column });
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
if (into.seenKeys.has(outcome.row.key)) {
|
|
6324
|
+
into.duplicates.push({ locale, key: outcome.row.key, row, line });
|
|
6325
|
+
return;
|
|
6326
|
+
}
|
|
6327
|
+
into.seenKeys.add(outcome.row.key);
|
|
6328
|
+
into.rows.push(outcome.row);
|
|
6329
|
+
}
|
|
6330
|
+
function readDelimited(input, options = {}) {
|
|
6331
|
+
const limits = options.limits ?? DEFAULT_DELIMITED_LIMITS;
|
|
6332
|
+
assertInputBytes(input.text, limits);
|
|
6333
|
+
const text = input.text.startsWith(UTF8_BOM) ? input.text.slice(UTF8_BOM.length) : input.text;
|
|
6334
|
+
const records = scanRecords(text, DELIMITER[input.format], limits);
|
|
6335
|
+
assertHeaderRecord(records);
|
|
6336
|
+
const into = {
|
|
6337
|
+
rows: [],
|
|
6338
|
+
malformed: [],
|
|
6339
|
+
duplicates: [],
|
|
6340
|
+
seenKeys: /* @__PURE__ */ new Set()
|
|
6341
|
+
};
|
|
6342
|
+
for (const [index, record] of records.entries()) {
|
|
6343
|
+
if (index >= HEADER_RECORD) {
|
|
6344
|
+
readRecord(record.fields, input.locale, { row: index + 1, line: record.line }, into);
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
return {
|
|
6348
|
+
sheets: [{ locale: input.locale, rows: into.rows }],
|
|
6349
|
+
malformedRows: into.malformed,
|
|
6350
|
+
duplicateKeys: into.duplicates
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
5848
6353
|
function assertNoDoctype2(name, xml) {
|
|
5849
6354
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
5850
6355
|
throw new ExchangeError(
|
|
@@ -5921,17 +6426,6 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
5921
6426
|
assertNoDoctype2(file.name, content);
|
|
5922
6427
|
}
|
|
5923
6428
|
}
|
|
5924
|
-
var rowSchema = zod.z.object({
|
|
5925
|
-
key: zod.z.string().min(1),
|
|
5926
|
-
source: zod.z.string(),
|
|
5927
|
-
currentTarget: zod.z.string(),
|
|
5928
|
-
status: zod.z.enum(["new", "changed", "unchanged"]),
|
|
5929
|
-
sourceHash: zod.z.string(),
|
|
5930
|
-
translation: zod.z.string(),
|
|
5931
|
-
context: zod.z.string(),
|
|
5932
|
-
reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
|
|
5933
|
-
reviewReasons: zod.z.string().catch("")
|
|
5934
|
-
});
|
|
5935
6429
|
function cellString(cell) {
|
|
5936
6430
|
const value = cell.value;
|
|
5937
6431
|
if (value === null || value === void 0) {
|
|
@@ -5956,24 +6450,8 @@ function assertHeader(sheet) {
|
|
|
5956
6450
|
);
|
|
5957
6451
|
}
|
|
5958
6452
|
}
|
|
5959
|
-
var MALFORMED_ROW_COLUMN = "Status";
|
|
5960
6453
|
function parseRow(row) {
|
|
5961
|
-
|
|
5962
|
-
key: cellString(row.getCell(COLUMN.key)),
|
|
5963
|
-
source: cellString(row.getCell(COLUMN.source)),
|
|
5964
|
-
currentTarget: cellString(row.getCell(COLUMN.current)),
|
|
5965
|
-
status: cellString(row.getCell(COLUMN.status)),
|
|
5966
|
-
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
5967
|
-
translation: cellString(row.getCell(COLUMN.translation)).trim(),
|
|
5968
|
-
context: cellString(row.getCell(COLUMN.context)),
|
|
5969
|
-
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
5970
|
-
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
5971
|
-
};
|
|
5972
|
-
const result = rowSchema.safeParse(candidate);
|
|
5973
|
-
if (!result.success) {
|
|
5974
|
-
return { ok: false, column: MALFORMED_ROW_COLUMN };
|
|
5975
|
-
}
|
|
5976
|
-
return { ok: true, row: result.data };
|
|
6454
|
+
return parseRowCells(HEADERS.map((_, index) => cellString(row.getCell(index + 1))));
|
|
5977
6455
|
}
|
|
5978
6456
|
function readDataSheet(sheet, limits) {
|
|
5979
6457
|
assertHeader(sheet);
|
|
@@ -6047,8 +6525,52 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
6047
6525
|
return { sheets, malformedRows, duplicateKeys };
|
|
6048
6526
|
}
|
|
6049
6527
|
|
|
6528
|
+
// src/flow/workbook/exchange-format.ts
|
|
6529
|
+
function isDelimitedFormat(format) {
|
|
6530
|
+
return format === "csv" || format === "tsv";
|
|
6531
|
+
}
|
|
6532
|
+
var MANIFEST_VERSION = 1;
|
|
6533
|
+
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
6534
|
+
function exportManifestFileName(format) {
|
|
6535
|
+
return `.verbatra-export-${format}.json`;
|
|
6536
|
+
}
|
|
6537
|
+
var manifestSchema = zod.z.object({
|
|
6538
|
+
version: zod.z.number().int().positive(),
|
|
6539
|
+
format: zod.z.enum(["csv", "tsv"]),
|
|
6540
|
+
locales: zod.z.array(zod.z.string())
|
|
6541
|
+
});
|
|
6542
|
+
async function writeExportManifest(fs, directory, format, locales) {
|
|
6543
|
+
const manifest = { version: MANIFEST_VERSION, format, locales: [...locales] };
|
|
6544
|
+
await fs.writeFile(
|
|
6545
|
+
path.join(directory, exportManifestFileName(format)),
|
|
6546
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
6547
|
+
`
|
|
6548
|
+
);
|
|
6549
|
+
}
|
|
6550
|
+
async function readExportedLocales(fs, directory, format) {
|
|
6551
|
+
const read = await fs.readFileBounded(
|
|
6552
|
+
path.join(directory, exportManifestFileName(format)),
|
|
6553
|
+
MAX_MANIFEST_BYTES
|
|
6554
|
+
);
|
|
6555
|
+
if (read.kind !== "ok") {
|
|
6556
|
+
return void 0;
|
|
6557
|
+
}
|
|
6558
|
+
let parsed;
|
|
6559
|
+
try {
|
|
6560
|
+
parsed = JSON.parse(read.content);
|
|
6561
|
+
} catch {
|
|
6562
|
+
return void 0;
|
|
6563
|
+
}
|
|
6564
|
+
const result = manifestSchema.safeParse(parsed);
|
|
6565
|
+
if (!result.success || result.data.version !== MANIFEST_VERSION || result.data.format !== format) {
|
|
6566
|
+
return void 0;
|
|
6567
|
+
}
|
|
6568
|
+
return new Set(result.data.locales);
|
|
6569
|
+
}
|
|
6570
|
+
|
|
6050
6571
|
// src/flow/workbook/export-workbook.ts
|
|
6051
6572
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
6573
|
+
var DEFAULT_DELIMITED_PATH = "verbatra-translations";
|
|
6052
6574
|
function reasonLabel(reason) {
|
|
6053
6575
|
return reason.toLowerCase().replace(/_/g, "-");
|
|
6054
6576
|
}
|
|
@@ -6112,6 +6634,21 @@ function buildRows(source, target, baseline, includeUnchanged, adapter, glossary
|
|
|
6112
6634
|
}
|
|
6113
6635
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
6114
6636
|
}
|
|
6637
|
+
async function writeDelimitedFiles(fs, directory, format, sheets) {
|
|
6638
|
+
await fs.mkdir?.(directory);
|
|
6639
|
+
for (const sheet of sheets) {
|
|
6640
|
+
await fs.writeFile(
|
|
6641
|
+
path.join(directory, delimitedFileName(sheet.locale, format)),
|
|
6642
|
+
buildDelimited(sheet, format)
|
|
6643
|
+
);
|
|
6644
|
+
}
|
|
6645
|
+
await writeExportManifest(
|
|
6646
|
+
fs,
|
|
6647
|
+
directory,
|
|
6648
|
+
format,
|
|
6649
|
+
sheets.map((sheet) => sheet.locale)
|
|
6650
|
+
);
|
|
6651
|
+
}
|
|
6115
6652
|
async function exportWorkbook(input, deps = {}) {
|
|
6116
6653
|
const config = input.config;
|
|
6117
6654
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -6134,10 +6671,17 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
6134
6671
|
return { locale, rows };
|
|
6135
6672
|
})
|
|
6136
6673
|
);
|
|
6137
|
-
const
|
|
6138
|
-
const
|
|
6139
|
-
|
|
6140
|
-
|
|
6674
|
+
const format = input.format ?? "xlsx";
|
|
6675
|
+
const path$1 = path.resolve(
|
|
6676
|
+
cwd,
|
|
6677
|
+
input.out ?? (isDelimitedFormat(format) ? DEFAULT_DELIMITED_PATH : DEFAULT_WORKBOOK_PATH)
|
|
6678
|
+
);
|
|
6679
|
+
if (isDelimitedFormat(format)) {
|
|
6680
|
+
await writeDelimitedFiles(fs, path$1, format, sheets);
|
|
6681
|
+
} else {
|
|
6682
|
+
const model = { sheets };
|
|
6683
|
+
await fs.writeBytes(path$1, await buildWorkbook(model));
|
|
6684
|
+
}
|
|
6141
6685
|
return {
|
|
6142
6686
|
path: path$1,
|
|
6143
6687
|
locales: sheets.map((sheet) => ({ locale: sheet.locale, rows: sheet.rows.length }))
|
|
@@ -6266,6 +6810,7 @@ function importLocale(params) {
|
|
|
6266
6810
|
|
|
6267
6811
|
// src/flow/workbook/import-workbook.ts
|
|
6268
6812
|
var MAX_WORKBOOK_FILE_BYTES = 64 * 1024 * 1024;
|
|
6813
|
+
var MAX_DELIMITED_FILE_BYTES = 32 * 1024 * 1024;
|
|
6269
6814
|
async function readWorkbookBytes(path, fs) {
|
|
6270
6815
|
const read = await fs.readBytesBounded(path, MAX_WORKBOOK_FILE_BYTES);
|
|
6271
6816
|
if (read.kind === "missing") {
|
|
@@ -6279,6 +6824,75 @@ async function readWorkbookBytes(path, fs) {
|
|
|
6279
6824
|
}
|
|
6280
6825
|
return read.bytes;
|
|
6281
6826
|
}
|
|
6827
|
+
async function readDelimitedText(path, fs) {
|
|
6828
|
+
const read = await fs.readFileBounded(path, MAX_DELIMITED_FILE_BYTES);
|
|
6829
|
+
if (read.kind === "missing") {
|
|
6830
|
+
return void 0;
|
|
6831
|
+
}
|
|
6832
|
+
if (read.kind === "too-large") {
|
|
6833
|
+
throw new SdkError(
|
|
6834
|
+
"SOURCE_INVALID",
|
|
6835
|
+
`The interchange file at ${path} exceeds the maximum allowed size of ${MAX_DELIMITED_FILE_BYTES} bytes.`
|
|
6836
|
+
);
|
|
6837
|
+
}
|
|
6838
|
+
return read.content;
|
|
6839
|
+
}
|
|
6840
|
+
async function collectDelimitedSources(path$1, config, fs, format) {
|
|
6841
|
+
const single = await readDelimitedText(path$1, fs);
|
|
6842
|
+
if (single !== void 0) {
|
|
6843
|
+
return { sources: [{ locale: path.basename(path$1, `.${format}`), text: single }], staleLocales: [] };
|
|
6844
|
+
}
|
|
6845
|
+
const exported = await readExportedLocales(fs, path$1, format);
|
|
6846
|
+
const sources = [];
|
|
6847
|
+
const staleLocales = [];
|
|
6848
|
+
for (const locale of config.targetLocales) {
|
|
6849
|
+
const text = await readDelimitedText(path.join(path$1, delimitedFileName(locale, format)), fs);
|
|
6850
|
+
if (text === void 0) {
|
|
6851
|
+
continue;
|
|
6852
|
+
}
|
|
6853
|
+
if (exported !== void 0 && !exported.has(locale)) {
|
|
6854
|
+
staleLocales.push(locale);
|
|
6855
|
+
continue;
|
|
6856
|
+
}
|
|
6857
|
+
sources.push({ locale, text });
|
|
6858
|
+
}
|
|
6859
|
+
if (sources.length === 0 && staleLocales.length === 0) {
|
|
6860
|
+
throw new SdkError(
|
|
6861
|
+
"SOURCE_UNREADABLE",
|
|
6862
|
+
`No ${format} file was found at ${path$1}, and it holds no <locale>.${format} file for any configured target locale.`
|
|
6863
|
+
);
|
|
6864
|
+
}
|
|
6865
|
+
return { sources, staleLocales };
|
|
6866
|
+
}
|
|
6867
|
+
function parseDelimitedSources(sources, format) {
|
|
6868
|
+
const sheets = [];
|
|
6869
|
+
const malformedRows = [];
|
|
6870
|
+
const duplicateKeys = [];
|
|
6871
|
+
for (const source of sources) {
|
|
6872
|
+
const data = readDelimited({ text: source.text, locale: source.locale, format });
|
|
6873
|
+
sheets.push(...data.sheets);
|
|
6874
|
+
malformedRows.push(...data.malformedRows);
|
|
6875
|
+
duplicateKeys.push(...data.duplicateKeys);
|
|
6876
|
+
}
|
|
6877
|
+
return { sheets, malformedRows, duplicateKeys };
|
|
6878
|
+
}
|
|
6879
|
+
async function readImportData(path, config, fs, format) {
|
|
6880
|
+
try {
|
|
6881
|
+
if (isDelimitedFormat(format)) {
|
|
6882
|
+
const handoff = await collectDelimitedSources(path, config, fs, format);
|
|
6883
|
+
return {
|
|
6884
|
+
data: parseDelimitedSources(handoff.sources, format),
|
|
6885
|
+
staleLocales: handoff.staleLocales
|
|
6886
|
+
};
|
|
6887
|
+
}
|
|
6888
|
+
return { data: await readWorkbook(await readWorkbookBytes(path, fs)), staleLocales: [] };
|
|
6889
|
+
} catch (error) {
|
|
6890
|
+
if (error instanceof SdkError) {
|
|
6891
|
+
throw error;
|
|
6892
|
+
}
|
|
6893
|
+
throw new SdkError("SOURCE_INVALID", errorMessage(error));
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6282
6896
|
function mergeAccepted(target, accepted) {
|
|
6283
6897
|
const merged = new Map(target.entries);
|
|
6284
6898
|
for (const [key, { value, source }] of accepted) {
|
|
@@ -6319,18 +6933,43 @@ function computeLockEntries2(source, merged, baseline, accepted) {
|
|
|
6319
6933
|
}
|
|
6320
6934
|
var MissingSheetError = class extends Error {
|
|
6321
6935
|
code = "WORKBOOK_SHEET_MISSING";
|
|
6322
|
-
constructor(locale) {
|
|
6936
|
+
constructor(locale, format) {
|
|
6323
6937
|
super(
|
|
6324
|
-
`The workbook has no sheet (tab) for the configured target locale "${locale}". The tab may have been renamed, deleted, or reordered out of the workbook.`
|
|
6938
|
+
isDelimitedFormat(format) ? `The handoff has no "${delimitedFileName(locale, format)}" file for the configured target locale "${locale}". The file may have been renamed, deleted, or left out of the directory.` : `The workbook has no sheet (tab) for the configured target locale "${locale}". The tab may have been renamed, deleted, or reordered out of the workbook.`
|
|
6325
6939
|
);
|
|
6326
6940
|
this.name = "MissingSheetError";
|
|
6327
6941
|
}
|
|
6328
6942
|
};
|
|
6943
|
+
var StaleHandoffFileError = class extends Error {
|
|
6944
|
+
code = "HANDOFF_FILE_STALE";
|
|
6945
|
+
constructor(locale, format) {
|
|
6946
|
+
super(
|
|
6947
|
+
`The file "${delimitedFileName(locale, format)}" is left over from an earlier export that included the target locale "${locale}"; the most recent export into this directory did not. Its rows were not applied, because they reflect that earlier run. Re-export the locale to refresh the file, or delete it.`
|
|
6948
|
+
);
|
|
6949
|
+
this.name = "StaleHandoffFileError";
|
|
6950
|
+
}
|
|
6951
|
+
};
|
|
6952
|
+
function absentLocaleFailures(config, sheets, format, staleLocales) {
|
|
6953
|
+
const present = new Set(sheets.map((sheet) => sheet.locale));
|
|
6954
|
+
const stale = new Set(staleLocales);
|
|
6955
|
+
const failures = [];
|
|
6956
|
+
for (const locale of config.targetLocales) {
|
|
6957
|
+
if (present.has(locale)) {
|
|
6958
|
+
continue;
|
|
6959
|
+
}
|
|
6960
|
+
const error = stale.has(locale) && isDelimitedFormat(format) ? new StaleHandoffFileError(locale, format) : new MissingSheetError(locale, format);
|
|
6961
|
+
failures.push(failureSummary(locale, error));
|
|
6962
|
+
}
|
|
6963
|
+
return failures;
|
|
6964
|
+
}
|
|
6965
|
+
function lineOf(reported) {
|
|
6966
|
+
return reported.line === void 0 ? {} : { line: reported.line };
|
|
6967
|
+
}
|
|
6329
6968
|
async function runSheet(ctx, sheet, lock) {
|
|
6330
6969
|
if (!ctx.config.targetLocales.includes(sheet.locale)) {
|
|
6331
6970
|
throw new SdkError(
|
|
6332
6971
|
"CONFIG_INVALID",
|
|
6333
|
-
`The workbook has a sheet named "${sheet.locale}", which is not a configured target locale. It may be a renamed, added, or reordered tab; leave every language tab named exactly as exported.`
|
|
6972
|
+
isDelimitedFormat(ctx.format) ? `The handoff has a file named "${sheet.locale}.${ctx.format}", whose locale is not a configured target locale. Name every interchange file exactly as it was exported.` : `The workbook has a sheet named "${sheet.locale}", which is not a configured target locale. It may be a renamed, added, or reordered tab; leave every language tab named exactly as exported.`
|
|
6334
6973
|
);
|
|
6335
6974
|
}
|
|
6336
6975
|
const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
@@ -6342,15 +6981,15 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
6342
6981
|
baseline,
|
|
6343
6982
|
adapter: ctx.adapter,
|
|
6344
6983
|
sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
|
|
6345
|
-
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column })),
|
|
6346
|
-
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row }))
|
|
6984
|
+
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...lineOf(problem) })),
|
|
6985
|
+
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...lineOf(duplicate) }))
|
|
6347
6986
|
});
|
|
6348
6987
|
if (ctx.dryRun) {
|
|
6349
6988
|
return { summary, lockEntries: {}, cacheAdditions: {} };
|
|
6350
6989
|
}
|
|
6351
6990
|
const merged = mergeAccepted(target, accepted);
|
|
6352
6991
|
if (accepted.size > 0) {
|
|
6353
|
-
const path =
|
|
6992
|
+
const path = createLocalePathResolver(ctx.cwd, ctx.config).pathFor(sheet.locale);
|
|
6354
6993
|
await ctx.adapter.write(
|
|
6355
6994
|
{
|
|
6356
6995
|
locale: sheet.locale,
|
|
@@ -6374,14 +7013,13 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6374
7013
|
const fs = deps.fs ?? defaultFs;
|
|
6375
7014
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
6376
7015
|
const source = await readSource(config, cwd, fs, adapter);
|
|
6377
|
-
const
|
|
6378
|
-
const
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
}
|
|
7016
|
+
const format = input.format ?? "xlsx";
|
|
7017
|
+
const { data, staleLocales } = await readImportData(
|
|
7018
|
+
path.resolve(cwd, input.workbook),
|
|
7019
|
+
config,
|
|
7020
|
+
fs,
|
|
7021
|
+
format
|
|
7022
|
+
);
|
|
6385
7023
|
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
6386
7024
|
const ctx = {
|
|
6387
7025
|
config,
|
|
@@ -6392,7 +7030,8 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6392
7030
|
sourceInvalidIcuKeys: source.invalidIcuKeys,
|
|
6393
7031
|
dryRun,
|
|
6394
7032
|
malformedRows: data.malformedRows,
|
|
6395
|
-
duplicateKeys: data.duplicateKeys
|
|
7033
|
+
duplicateKeys: data.duplicateKeys,
|
|
7034
|
+
format
|
|
6396
7035
|
};
|
|
6397
7036
|
const summaries = [];
|
|
6398
7037
|
const cacheAdditions = /* @__PURE__ */ new Map();
|
|
@@ -6417,12 +7056,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6417
7056
|
summaries.push(failureSummary(sheet.locale, error));
|
|
6418
7057
|
}
|
|
6419
7058
|
}
|
|
6420
|
-
|
|
6421
|
-
for (const locale of config.targetLocales) {
|
|
6422
|
-
if (!presentLocales.has(locale)) {
|
|
6423
|
-
summaries.push(failureSummary(locale, new MissingSheetError(locale)));
|
|
6424
|
-
}
|
|
6425
|
-
}
|
|
7059
|
+
summaries.push(...absentLocaleFailures(config, data.sheets, format, staleLocales));
|
|
6426
7060
|
if (!dryRun) {
|
|
6427
7061
|
await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
|
|
6428
7062
|
}
|
|
@@ -6466,19 +7100,13 @@ function defaultRunTranslate(deps) {
|
|
|
6466
7100
|
|
|
6467
7101
|
// src/watch/watch.ts
|
|
6468
7102
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
6469
|
-
function describeError2(error) {
|
|
6470
|
-
if (error instanceof Error) {
|
|
6471
|
-
const code = error.code;
|
|
6472
|
-
return { code: typeof code === "string" ? code : "WATCH_RUN_FAILED", message: error.message };
|
|
6473
|
-
}
|
|
6474
|
-
return { code: "WATCH_RUN_FAILED", message: String(error) };
|
|
6475
|
-
}
|
|
6476
7103
|
async function watch(input, deps = {}) {
|
|
6477
7104
|
const cwd = input.cwd ?? process.cwd();
|
|
6478
7105
|
const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6479
7106
|
const fs = deps.fs ?? defaultFs;
|
|
6480
7107
|
resolveRunConcurrency(input.concurrency, false, input.config);
|
|
6481
|
-
const
|
|
7108
|
+
const resolver = createLocalePathResolver(cwd, input.config);
|
|
7109
|
+
const sourcePath = resolver.pathFor(input.config.sourceLocale);
|
|
6482
7110
|
if (!await fs.fileExists(sourcePath)) {
|
|
6483
7111
|
throw new SdkError(
|
|
6484
7112
|
"SOURCE_UNREADABLE",
|
|
@@ -6504,7 +7132,7 @@ async function watch(input, deps = {}) {
|
|
|
6504
7132
|
try {
|
|
6505
7133
|
input.onRun({ status: "succeeded", summary: await runTranslate(runInput) });
|
|
6506
7134
|
} catch (error) {
|
|
6507
|
-
input.onRun({ status: "failed", error:
|
|
7135
|
+
input.onRun({ status: "failed", error: describeError(error, "WATCH_RUN_FAILED") });
|
|
6508
7136
|
}
|
|
6509
7137
|
}
|
|
6510
7138
|
function startRun() {
|
|
@@ -6562,10 +7190,12 @@ async function watch(input, deps = {}) {
|
|
|
6562
7190
|
}
|
|
6563
7191
|
|
|
6564
7192
|
exports.CACHE_FILE_NAME = CACHE_FILE_NAME;
|
|
7193
|
+
exports.DEFAULT_DELIMITED_PATH = DEFAULT_DELIMITED_PATH;
|
|
6565
7194
|
exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
|
|
6566
7195
|
exports.LOCK_FILE_NAME = LOCK_FILE_NAME;
|
|
6567
7196
|
exports.SdkError = SdkError;
|
|
6568
7197
|
exports.check = check;
|
|
7198
|
+
exports.createLocalePathResolver = createLocalePathResolver;
|
|
6569
7199
|
exports.defineConfig = defineConfig;
|
|
6570
7200
|
exports.diff = diff;
|
|
6571
7201
|
exports.diffLocaleSnapshots = diffLocaleSnapshots;
|