@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.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from 'fs';
|
|
|
4
4
|
import { cosmiconfig } from 'cosmiconfig';
|
|
5
5
|
import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
|
-
import { rm, access,
|
|
7
|
+
import { mkdir, rm, access, open, writeFile, rename } from 'fs/promises';
|
|
8
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
9
9
|
import * as deepl from 'deepl-node';
|
|
10
10
|
import { createRequire } from 'module';
|
|
@@ -122,6 +122,18 @@ function defineConfig(config) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// src/errors.ts
|
|
125
|
+
function errorMessage(error) {
|
|
126
|
+
return error instanceof Error ? error.message : String(error);
|
|
127
|
+
}
|
|
128
|
+
function stringCode(error) {
|
|
129
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") {
|
|
130
|
+
return error.code;
|
|
131
|
+
}
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
function describeError(error, fallbackCode) {
|
|
135
|
+
return { code: stringCode(error) ?? fallbackCode, message: errorMessage(error) };
|
|
136
|
+
}
|
|
125
137
|
var SdkError = class extends Error {
|
|
126
138
|
/** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
|
|
127
139
|
code;
|
|
@@ -246,6 +258,9 @@ var defaultFs = {
|
|
|
246
258
|
createExclusive: (path, data) => createExclusive(path, data),
|
|
247
259
|
deleteFile: async (path) => {
|
|
248
260
|
await rm(path, { force: true });
|
|
261
|
+
},
|
|
262
|
+
mkdir: async (path) => {
|
|
263
|
+
await mkdir(path, { recursive: true });
|
|
249
264
|
}
|
|
250
265
|
};
|
|
251
266
|
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
@@ -484,9 +499,109 @@ function assessValueDegeneracy(sourceValue, candidateValue) {
|
|
|
484
499
|
}
|
|
485
500
|
return { degenerate: hasRunawayRepetition(candidate) };
|
|
486
501
|
}
|
|
502
|
+
|
|
503
|
+
// src/locale-path/pattern.ts
|
|
487
504
|
var LOCALE_TOKEN = "{locale}";
|
|
488
|
-
|
|
489
|
-
|
|
505
|
+
var SEPARATOR = /[/\\]/;
|
|
506
|
+
function tokenOccupiesWholeSegments(pattern) {
|
|
507
|
+
return pattern.split(SEPARATOR).every((segment) => segment === LOCALE_TOKEN || !segment.includes(LOCALE_TOKEN));
|
|
508
|
+
}
|
|
509
|
+
function expandPattern(pattern, spelling) {
|
|
510
|
+
return pattern.replaceAll(LOCALE_TOKEN, () => spelling);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/locale-path/tag.ts
|
|
514
|
+
var LANGUAGE = /^[a-z]{2,3}$/;
|
|
515
|
+
var SCRIPT = /^[a-z]{4}$/;
|
|
516
|
+
var REGION = /^([a-z]{2}|[0-9]{3})$/;
|
|
517
|
+
var NUMERIC_REGION = /^[0-9]{3}$/;
|
|
518
|
+
var VARIANT = /^([a-z0-9]{5,8}|[0-9][a-z0-9]{3})$/;
|
|
519
|
+
function subtagIf(subtags, index, pattern) {
|
|
520
|
+
const subtag = subtags[index];
|
|
521
|
+
return subtag !== void 0 && pattern.test(subtag) ? subtag : void 0;
|
|
522
|
+
}
|
|
523
|
+
function parseLocaleTag(locale) {
|
|
524
|
+
const subtags = locale.toLowerCase().split("-");
|
|
525
|
+
const language = subtags[0];
|
|
526
|
+
if (language === void 0 || !LANGUAGE.test(language)) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
let index = 1;
|
|
530
|
+
const script = subtagIf(subtags, index, SCRIPT);
|
|
531
|
+
index += script === void 0 ? 0 : 1;
|
|
532
|
+
const region = subtagIf(subtags, index, REGION);
|
|
533
|
+
index += region === void 0 ? 0 : 1;
|
|
534
|
+
const variants = subtags.slice(index);
|
|
535
|
+
if (!variants.every((variant) => VARIANT.test(variant))) {
|
|
536
|
+
return void 0;
|
|
537
|
+
}
|
|
538
|
+
return { language, script, region, variants };
|
|
539
|
+
}
|
|
540
|
+
function conventionalSubtags(tag) {
|
|
541
|
+
const subtags = [tag.language];
|
|
542
|
+
if (tag.script !== void 0) {
|
|
543
|
+
subtags.push(tag.script.charAt(0).toUpperCase() + tag.script.slice(1));
|
|
544
|
+
}
|
|
545
|
+
if (tag.region !== void 0) {
|
|
546
|
+
subtags.push(tag.region.toUpperCase());
|
|
547
|
+
}
|
|
548
|
+
subtags.push(...tag.variants);
|
|
549
|
+
return subtags;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/locale-path/android.ts
|
|
553
|
+
var SOURCE_SEGMENT = "values";
|
|
554
|
+
var MAX_BCP47_SUBTAGS = 4;
|
|
555
|
+
var UI_MODE_LANGUAGE_COLLISION = "car";
|
|
556
|
+
function needsBcp47(tag, subtagCount) {
|
|
557
|
+
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;
|
|
558
|
+
}
|
|
559
|
+
function legacySegment(tag) {
|
|
560
|
+
return tag.region === void 0 ? `${SOURCE_SEGMENT}-${tag.language}` : `${SOURCE_SEGMENT}-${tag.language}-r${tag.region.toUpperCase()}`;
|
|
561
|
+
}
|
|
562
|
+
function androidSegment(locale, isSourceLocale) {
|
|
563
|
+
if (isSourceLocale) {
|
|
564
|
+
return SOURCE_SEGMENT;
|
|
565
|
+
}
|
|
566
|
+
const tag = parseLocaleTag(locale);
|
|
567
|
+
if (tag === void 0) {
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
const subtags = conventionalSubtags(tag);
|
|
571
|
+
if (!needsBcp47(tag, subtags.length)) {
|
|
572
|
+
return legacySegment(tag);
|
|
573
|
+
}
|
|
574
|
+
if (subtags.length > MAX_BCP47_SUBTAGS) {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
return `${SOURCE_SEGMENT}-b+${subtags.join("+")}`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/locale-path/style.ts
|
|
581
|
+
var LOCALE_STYLES = ["literal", "posix", "android"];
|
|
582
|
+
var SEGMENT_STYLES = /* @__PURE__ */ new Set(["android"]);
|
|
583
|
+
var UNSAFE_IN_SEGMENT = /[/\\\0]/;
|
|
584
|
+
function isSegmentStyle(style) {
|
|
585
|
+
return SEGMENT_STYLES.has(style);
|
|
586
|
+
}
|
|
587
|
+
function posixSpelling(locale) {
|
|
588
|
+
const tag = parseLocaleTag(locale);
|
|
589
|
+
if (tag === void 0 || tag.script !== void 0 || tag.variants.length > 0) {
|
|
590
|
+
return void 0;
|
|
591
|
+
}
|
|
592
|
+
if (tag.region !== void 0 && NUMERIC_REGION.test(tag.region)) {
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
return locale.replaceAll("-", "_");
|
|
596
|
+
}
|
|
597
|
+
function spellLocale(locale, style, isSourceLocale) {
|
|
598
|
+
if (style === "android") {
|
|
599
|
+
return androidSegment(locale, isSourceLocale);
|
|
600
|
+
}
|
|
601
|
+
return style === "posix" ? posixSpelling(locale) : locale;
|
|
602
|
+
}
|
|
603
|
+
function isSafeSpelling(spelling) {
|
|
604
|
+
return spelling.length > 0 && !UNSAFE_IN_SEGMENT.test(spelling) && spelling !== "." && spelling !== "..";
|
|
490
605
|
}
|
|
491
606
|
function checkBatchIntegrity(inputs, extract, compare) {
|
|
492
607
|
const integrity = /* @__PURE__ */ new Map();
|
|
@@ -659,7 +774,7 @@ function deriveJsonSchema(schema) {
|
|
|
659
774
|
result[key] = value;
|
|
660
775
|
}
|
|
661
776
|
}
|
|
662
|
-
return result;
|
|
777
|
+
return { ...result, type: "object" };
|
|
663
778
|
}
|
|
664
779
|
function partitionTranslations(translations, requested) {
|
|
665
780
|
const firstSeen = /* @__PURE__ */ new Map();
|
|
@@ -779,6 +894,8 @@ function assertNotTruncated(truncated) {
|
|
|
779
894
|
var RATE_LIMITED_STATUS = 429;
|
|
780
895
|
var TIMEOUT_STATUS = 408;
|
|
781
896
|
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
897
|
+
var SERVER_OUTAGE_MIN_STATUS = 500;
|
|
898
|
+
var SERVER_OUTAGE_MAX_STATUS = 599;
|
|
782
899
|
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
783
900
|
"RateLimitError",
|
|
784
901
|
"TooManyRequestsError"
|
|
@@ -800,6 +917,9 @@ function getErrorStatus(error) {
|
|
|
800
917
|
const status = error.status;
|
|
801
918
|
return typeof status === "number" ? status : void 0;
|
|
802
919
|
}
|
|
920
|
+
function isServerOutageStatus(status) {
|
|
921
|
+
return status !== void 0 && status >= SERVER_OUTAGE_MIN_STATUS && status <= SERVER_OUTAGE_MAX_STATUS;
|
|
922
|
+
}
|
|
803
923
|
function readClassName(error) {
|
|
804
924
|
if (typeof error !== "object" || error === null) {
|
|
805
925
|
return void 0;
|
|
@@ -817,6 +937,9 @@ function classifyProviderError(error) {
|
|
|
817
937
|
if (status === TIMEOUT_STATUS) {
|
|
818
938
|
return "TIMEOUT";
|
|
819
939
|
}
|
|
940
|
+
if (isServerOutageStatus(status)) {
|
|
941
|
+
return "PROVIDER_UNAVAILABLE";
|
|
942
|
+
}
|
|
820
943
|
const className = readClassName(error);
|
|
821
944
|
if (className !== void 0) {
|
|
822
945
|
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
@@ -848,10 +971,12 @@ var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
|
848
971
|
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
849
972
|
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
850
973
|
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
974
|
+
var PROVIDER_UNAVAILABLE_MESSAGE = "The translation provider is currently unavailable.";
|
|
851
975
|
var MESSAGE_BY_CODE = {
|
|
852
976
|
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
853
977
|
TIMEOUT: TIMEOUT_MESSAGE,
|
|
854
978
|
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
979
|
+
PROVIDER_UNAVAILABLE: PROVIDER_UNAVAILABLE_MESSAGE,
|
|
855
980
|
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
856
981
|
};
|
|
857
982
|
async function guardProviderCall(call, signal) {
|
|
@@ -932,14 +1057,14 @@ function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
|
932
1057
|
}
|
|
933
1058
|
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
934
1059
|
}
|
|
1060
|
+
function toMutableRequest(request) {
|
|
1061
|
+
return request;
|
|
1062
|
+
}
|
|
935
1063
|
function createDefaultClient() {
|
|
936
1064
|
const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
937
1065
|
return {
|
|
938
1066
|
messages: {
|
|
939
|
-
create: async (body, options) => await sdk.messages.create(
|
|
940
|
-
body,
|
|
941
|
-
options
|
|
942
|
-
)
|
|
1067
|
+
create: async (body, options) => await sdk.messages.create(toMutableRequest(body), options)
|
|
943
1068
|
}
|
|
944
1069
|
};
|
|
945
1070
|
}
|
|
@@ -1357,7 +1482,7 @@ function createDefaultClient3() {
|
|
|
1357
1482
|
models: {
|
|
1358
1483
|
generateContent: (request) => withGeminiRetry(
|
|
1359
1484
|
async () => await ai.models.generateContent(
|
|
1360
|
-
request
|
|
1485
|
+
toMutableRequest(request)
|
|
1361
1486
|
),
|
|
1362
1487
|
request.config.abortSignal
|
|
1363
1488
|
)
|
|
@@ -1521,7 +1646,7 @@ function createDefaultClient4() {
|
|
|
1521
1646
|
chat: {
|
|
1522
1647
|
completions: {
|
|
1523
1648
|
create: async (body, options) => await sdk.chat.completions.create(
|
|
1524
|
-
body,
|
|
1649
|
+
toMutableRequest(body),
|
|
1525
1650
|
options
|
|
1526
1651
|
)
|
|
1527
1652
|
}
|
|
@@ -1722,7 +1847,7 @@ function createDefaultClient5(config) {
|
|
|
1722
1847
|
chat: {
|
|
1723
1848
|
completions: {
|
|
1724
1849
|
create: async (body, options) => await sdk.chat.completions.create(
|
|
1725
|
-
body,
|
|
1850
|
+
toMutableRequest(body),
|
|
1726
1851
|
options
|
|
1727
1852
|
)
|
|
1728
1853
|
}
|
|
@@ -1804,7 +1929,13 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1804
1929
|
targetLocales: z.array(z.string().min(1)).min(1),
|
|
1805
1930
|
format: supportedFormatSchema,
|
|
1806
1931
|
files: z.strictObject({
|
|
1807
|
-
pattern: z.string().min(1)
|
|
1932
|
+
pattern: z.string().min(1),
|
|
1933
|
+
/**
|
|
1934
|
+
* How the `{locale}` token in `pattern` is spelled for each locale. Absent means `"literal"`,
|
|
1935
|
+
* the configured tag verbatim, which is what every layout except Android and the POSIX
|
|
1936
|
+
* underscore layouts needs. See `LocaleStyle` for what each style produces.
|
|
1937
|
+
*/
|
|
1938
|
+
localeStyle: z.enum(LOCALE_STYLES).optional()
|
|
1808
1939
|
}),
|
|
1809
1940
|
provider: providerConfigSchema,
|
|
1810
1941
|
/**
|
|
@@ -1825,8 +1956,9 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1825
1956
|
* Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
|
|
1826
1957
|
* translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
|
|
1827
1958
|
* requires but the source does not supply (for example Polish few/many). The per-run
|
|
1828
|
-
* `generatePlurals` option on `translate` overrides this.
|
|
1829
|
-
*
|
|
1959
|
+
* `generatePlurals` option on `translate` overrides this. A plural form the target file already holds
|
|
1960
|
+
* is adopted, never generated over. Unsupported cases (DeepL, non-i18next, an unknown language) fall
|
|
1961
|
+
* back to the per-locale plural warning.
|
|
1830
1962
|
*/
|
|
1831
1963
|
generatePlurals: z.boolean().optional(),
|
|
1832
1964
|
/**
|
|
@@ -1919,7 +2051,7 @@ async function loadExplicitWithMeta(explorer, configPath, cwd, fs) {
|
|
|
1919
2051
|
try {
|
|
1920
2052
|
result = await explorer.load(resolved);
|
|
1921
2053
|
} catch (error) {
|
|
1922
|
-
const detail =
|
|
2054
|
+
const detail = errorMessage(error);
|
|
1923
2055
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
1924
2056
|
}
|
|
1925
2057
|
const parsed = parseConfig(result?.config);
|
|
@@ -1944,7 +2076,7 @@ async function loadConfigWithMeta(options = {}) {
|
|
|
1944
2076
|
try {
|
|
1945
2077
|
result = await explorer.search(options.cwd);
|
|
1946
2078
|
} catch (error) {
|
|
1947
|
-
const detail =
|
|
2079
|
+
const detail = errorMessage(error);
|
|
1948
2080
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
1949
2081
|
}
|
|
1950
2082
|
if (result === null || result.isEmpty === true) {
|
|
@@ -1961,6 +2093,63 @@ async function loadConfig(options = {}) {
|
|
|
1961
2093
|
const { config } = await loadConfigWithMeta(options);
|
|
1962
2094
|
return config;
|
|
1963
2095
|
}
|
|
2096
|
+
var DEFAULT_LOCALE_STYLE = "literal";
|
|
2097
|
+
function validatePattern(pattern, style) {
|
|
2098
|
+
if (!pattern.includes(LOCALE_TOKEN)) {
|
|
2099
|
+
throw new SdkError(
|
|
2100
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2101
|
+
`The files pattern "${pattern}" must contain the ${LOCALE_TOKEN} token.`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
if (isSegmentStyle(style) && !tokenOccupiesWholeSegments(pattern)) {
|
|
2105
|
+
throw new SdkError(
|
|
2106
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2107
|
+
`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.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
function safeSpelling(locale, style, sourceLocale) {
|
|
2112
|
+
const spelling = spellLocale(locale, style, locale === sourceLocale);
|
|
2113
|
+
if (spelling === void 0) {
|
|
2114
|
+
throw new SdkError(
|
|
2115
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2116
|
+
`The "${style}" locale style has no valid path spelling for the locale "${locale}".`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
if (!isSafeSpelling(spelling)) {
|
|
2120
|
+
throw new SdkError(
|
|
2121
|
+
"LOCALE_LAYOUT_INVALID",
|
|
2122
|
+
`The locale "${locale}" expands to "${spelling}" under the "${style}" locale style, which is not a single path segment.`
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
return spelling;
|
|
2126
|
+
}
|
|
2127
|
+
function buildForwardMap(config, pathFor) {
|
|
2128
|
+
const forward = /* @__PURE__ */ new Map();
|
|
2129
|
+
for (const locale of [config.sourceLocale, ...config.targetLocales]) {
|
|
2130
|
+
const path = pathFor(locale);
|
|
2131
|
+
const claimed = forward.get(path);
|
|
2132
|
+
if (claimed !== void 0 && claimed !== locale) {
|
|
2133
|
+
throw new SdkError(
|
|
2134
|
+
"LOCALE_PATH_COLLISION",
|
|
2135
|
+
`The locales "${claimed}" and "${locale}" both resolve to ${path}.`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
forward.set(path, locale);
|
|
2139
|
+
}
|
|
2140
|
+
return forward;
|
|
2141
|
+
}
|
|
2142
|
+
function createLocalePathResolver(cwd, config) {
|
|
2143
|
+
const style = config.files.localeStyle ?? DEFAULT_LOCALE_STYLE;
|
|
2144
|
+
const pattern = config.files.pattern;
|
|
2145
|
+
validatePattern(pattern, style);
|
|
2146
|
+
const pathFor = (locale) => resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
|
|
2147
|
+
const forward = buildForwardMap(config, pathFor);
|
|
2148
|
+
return {
|
|
2149
|
+
pathFor,
|
|
2150
|
+
localeFor: (absolutePath) => forward.get(resolve(cwd, absolutePath))
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
1964
2153
|
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
1965
2154
|
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
1966
2155
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
@@ -2029,15 +2218,15 @@ async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
|
|
|
2029
2218
|
if (await fs.createExclusive(path, lockPayload())) {
|
|
2030
2219
|
return;
|
|
2031
2220
|
}
|
|
2221
|
+
if (notify !== void 0) {
|
|
2222
|
+
await notify();
|
|
2223
|
+
}
|
|
2032
2224
|
if (Date.now() >= deadline) {
|
|
2033
2225
|
throw new SdkError(
|
|
2034
2226
|
"LOCK_CONTENDED",
|
|
2035
2227
|
`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.`
|
|
2036
2228
|
);
|
|
2037
2229
|
}
|
|
2038
|
-
if (notify !== void 0) {
|
|
2039
|
-
await notify();
|
|
2040
|
-
}
|
|
2041
2230
|
const jitter = Math.random() * pollIntervalMs;
|
|
2042
2231
|
await sleep(pollIntervalMs + jitter);
|
|
2043
2232
|
}
|
|
@@ -2627,6 +2816,9 @@ async function fsyncDirBestEffort(path) {
|
|
|
2627
2816
|
}
|
|
2628
2817
|
}
|
|
2629
2818
|
var nodeOps = {
|
|
2819
|
+
mkdir: async (path) => {
|
|
2820
|
+
await mkdir(path, { recursive: true });
|
|
2821
|
+
},
|
|
2630
2822
|
writeFile: (path, data) => writeFile(path, data, "utf8"),
|
|
2631
2823
|
fsyncFile: (path) => fsyncPath(path),
|
|
2632
2824
|
rename: (from, to) => rename(from, to),
|
|
@@ -2643,8 +2835,10 @@ function tempFileName2(path) {
|
|
|
2643
2835
|
return join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`);
|
|
2644
2836
|
}
|
|
2645
2837
|
async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
2838
|
+
const directory = dirname(path);
|
|
2646
2839
|
const tmp = tempFileName2(path);
|
|
2647
2840
|
try {
|
|
2841
|
+
await ops.mkdir(directory);
|
|
2648
2842
|
await ops.writeFile(tmp, data);
|
|
2649
2843
|
await ops.fsyncFile(tmp);
|
|
2650
2844
|
await ops.rename(tmp, path);
|
|
@@ -2653,7 +2847,7 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
2653
2847
|
throw error;
|
|
2654
2848
|
}
|
|
2655
2849
|
try {
|
|
2656
|
-
await ops.fsyncDir(
|
|
2850
|
+
await ops.fsyncDir(directory);
|
|
2657
2851
|
} catch {
|
|
2658
2852
|
}
|
|
2659
2853
|
}
|
|
@@ -3977,7 +4171,7 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
3977
4171
|
|
|
3978
4172
|
// src/flow/read-target.ts
|
|
3979
4173
|
async function readTargetResource(input) {
|
|
3980
|
-
const path =
|
|
4174
|
+
const path = input.resolver.pathFor(input.locale);
|
|
3981
4175
|
if (!await input.fs.fileExists(path)) {
|
|
3982
4176
|
return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
|
|
3983
4177
|
}
|
|
@@ -4004,7 +4198,7 @@ function selectLocales(config, requested) {
|
|
|
4004
4198
|
|
|
4005
4199
|
// src/flow/source.ts
|
|
4006
4200
|
async function readSource(config, cwd, fs, adapter) {
|
|
4007
|
-
const sourcePath =
|
|
4201
|
+
const sourcePath = createLocalePathResolver(cwd, config).pathFor(config.sourceLocale);
|
|
4008
4202
|
if (!await fs.fileExists(sourcePath)) {
|
|
4009
4203
|
throw new SdkError(
|
|
4010
4204
|
"SOURCE_UNREADABLE",
|
|
@@ -4014,7 +4208,7 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
4014
4208
|
try {
|
|
4015
4209
|
return await adapter.read(sourcePath, config.sourceLocale);
|
|
4016
4210
|
} catch (error) {
|
|
4017
|
-
const detail =
|
|
4211
|
+
const detail = errorMessage(error);
|
|
4018
4212
|
throw new SdkError(
|
|
4019
4213
|
"SOURCE_INVALID",
|
|
4020
4214
|
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
@@ -4025,8 +4219,7 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
4025
4219
|
// src/flow/diff-locales.ts
|
|
4026
4220
|
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
4027
4221
|
return readTargetResource({
|
|
4028
|
-
cwd,
|
|
4029
|
-
filesPattern: config.files.pattern,
|
|
4222
|
+
resolver: createLocalePathResolver(cwd, config),
|
|
4030
4223
|
format: config.format,
|
|
4031
4224
|
locale,
|
|
4032
4225
|
adapter,
|
|
@@ -4147,7 +4340,7 @@ async function editEntry(input, deps = {}) {
|
|
|
4147
4340
|
}
|
|
4148
4341
|
const merged = new Map(target.entries);
|
|
4149
4342
|
merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
|
|
4150
|
-
const path =
|
|
4343
|
+
const path = createLocalePathResolver(cwd, config).pathFor(locale);
|
|
4151
4344
|
await adapter.write(
|
|
4152
4345
|
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
4153
4346
|
path
|
|
@@ -4314,7 +4507,7 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
4314
4507
|
try {
|
|
4315
4508
|
return createProvider(config);
|
|
4316
4509
|
} catch (error) {
|
|
4317
|
-
const detail =
|
|
4510
|
+
const detail = errorMessage(error);
|
|
4318
4511
|
throw new SdkError(
|
|
4319
4512
|
"PROVIDER_CONSTRUCTION_FAILED",
|
|
4320
4513
|
`Failed to construct provider "${config.id}": ${detail}`
|
|
@@ -4370,7 +4563,7 @@ async function retranslateEntry(input, deps = {}) {
|
|
|
4370
4563
|
}
|
|
4371
4564
|
const merged = new Map(target.entries);
|
|
4372
4565
|
merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
|
|
4373
|
-
const path =
|
|
4566
|
+
const path = createLocalePathResolver(cwd, config).pathFor(locale);
|
|
4374
4567
|
await adapter.write(
|
|
4375
4568
|
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
4376
4569
|
path
|
|
@@ -4537,13 +4730,6 @@ function budgetExceededNotice(tracker) {
|
|
|
4537
4730
|
}
|
|
4538
4731
|
|
|
4539
4732
|
// src/flow/locale-failure.ts
|
|
4540
|
-
function describeError(error) {
|
|
4541
|
-
if (error instanceof Error) {
|
|
4542
|
-
const code = error.code;
|
|
4543
|
-
return { code: typeof code === "string" ? code : "LOCALE_FAILED", message: error.message };
|
|
4544
|
-
}
|
|
4545
|
-
return { code: "LOCALE_FAILED", message: String(error) };
|
|
4546
|
-
}
|
|
4547
4733
|
function failureSummary(locale, error) {
|
|
4548
4734
|
return {
|
|
4549
4735
|
locale,
|
|
@@ -4563,7 +4749,7 @@ function failureSummary(locale, error) {
|
|
|
4563
4749
|
unfilled: [],
|
|
4564
4750
|
malformedRows: [],
|
|
4565
4751
|
duplicateKeys: [],
|
|
4566
|
-
error: describeError(error)
|
|
4752
|
+
error: describeError(error, "LOCALE_FAILED")
|
|
4567
4753
|
};
|
|
4568
4754
|
}
|
|
4569
4755
|
function deriveLocaleStatus(parts) {
|
|
@@ -4796,6 +4982,9 @@ function syntheticEntry(item) {
|
|
|
4796
4982
|
meaning: `CLDR plural category "${item.category}"`
|
|
4797
4983
|
};
|
|
4798
4984
|
}
|
|
4985
|
+
function isAdopted(item, targetKeys, baseline) {
|
|
4986
|
+
return targetKeys.has(item.targetKey) && !baseline.has(item.targetKey);
|
|
4987
|
+
}
|
|
4799
4988
|
function staleItems(items, baseline) {
|
|
4800
4989
|
return items.filter((item) => {
|
|
4801
4990
|
const hash = generatedLockHash(item.governingEntries, item.category);
|
|
@@ -4815,7 +5004,10 @@ function buildRequest2(context, entries) {
|
|
|
4815
5004
|
}
|
|
4816
5005
|
async function generatePluralForms(context) {
|
|
4817
5006
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
4818
|
-
const
|
|
5007
|
+
const candidates = plan.items.filter(
|
|
5008
|
+
(item) => !isAdopted(item, context.targetKeys, context.baseline)
|
|
5009
|
+
);
|
|
5010
|
+
const stale = staleItems(candidates, context.baseline);
|
|
4819
5011
|
if (stale.length === 0) {
|
|
4820
5012
|
return EMPTY_RESULT;
|
|
4821
5013
|
}
|
|
@@ -4997,10 +5189,15 @@ function buildRequest3(params, entries) {
|
|
|
4997
5189
|
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
4998
5190
|
};
|
|
4999
5191
|
}
|
|
5192
|
+
async function shouldWriteTarget(params, path, changed) {
|
|
5193
|
+
if (changed.accepted > 0 || changed.pruned > 0 || changed.generated > 0) {
|
|
5194
|
+
return true;
|
|
5195
|
+
}
|
|
5196
|
+
return !await params.fs.fileExists(path);
|
|
5197
|
+
}
|
|
5000
5198
|
async function runLocale(params) {
|
|
5001
5199
|
const target = await readTargetResource({
|
|
5002
|
-
|
|
5003
|
-
filesPattern: params.filesPattern,
|
|
5200
|
+
resolver: params.resolver,
|
|
5004
5201
|
format: params.format,
|
|
5005
5202
|
locale: params.targetLocale,
|
|
5006
5203
|
adapter: params.adapter,
|
|
@@ -5077,20 +5274,27 @@ async function runLocale(params) {
|
|
|
5077
5274
|
merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
|
|
5078
5275
|
}
|
|
5079
5276
|
}
|
|
5080
|
-
const generation = await runGeneration(params, provider);
|
|
5277
|
+
const generation = await runGeneration(params, provider, new Set(target.entries.keys()));
|
|
5081
5278
|
for (const form of generation.accepted) {
|
|
5082
5279
|
merged.set(form.targetKey, { ...form.entry, namespace: target.namespace });
|
|
5083
5280
|
}
|
|
5084
|
-
const path =
|
|
5085
|
-
await params
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5281
|
+
const path = params.resolver.pathFor(params.targetLocale);
|
|
5282
|
+
const writeNeeded = await shouldWriteTarget(params, path, {
|
|
5283
|
+
accepted: accepted.size,
|
|
5284
|
+
pruned: pruned.length,
|
|
5285
|
+
generated: generation.accepted.length
|
|
5286
|
+
});
|
|
5287
|
+
if (writeNeeded) {
|
|
5288
|
+
await params.adapter.write(
|
|
5289
|
+
{
|
|
5290
|
+
locale: params.targetLocale,
|
|
5291
|
+
namespace: target.namespace,
|
|
5292
|
+
format: params.format,
|
|
5293
|
+
entries: merged
|
|
5294
|
+
},
|
|
5295
|
+
path
|
|
5296
|
+
);
|
|
5297
|
+
}
|
|
5094
5298
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
5095
5299
|
const notices = [
|
|
5096
5300
|
...pluralNotices,
|
|
@@ -5137,7 +5341,7 @@ var NO_GENERATION_RESULT = {
|
|
|
5137
5341
|
usage: void 0,
|
|
5138
5342
|
tripped: false
|
|
5139
5343
|
};
|
|
5140
|
-
async function runGeneration(params, provider) {
|
|
5344
|
+
async function runGeneration(params, provider, targetKeys) {
|
|
5141
5345
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
5142
5346
|
return NO_GENERATION_RESULT;
|
|
5143
5347
|
}
|
|
@@ -5151,6 +5355,7 @@ async function runGeneration(params, provider) {
|
|
|
5151
5355
|
glossary: params.glossary,
|
|
5152
5356
|
tone: params.tone,
|
|
5153
5357
|
baseline: params.baseline,
|
|
5358
|
+
targetKeys,
|
|
5154
5359
|
maxBatchSize: params.maxBatchSize,
|
|
5155
5360
|
budget: params.budget
|
|
5156
5361
|
});
|
|
@@ -5393,7 +5598,7 @@ function buildLocaleRunParams(context, targetLocale, baseline) {
|
|
|
5393
5598
|
adapter: context.adapter,
|
|
5394
5599
|
provider: context.provider,
|
|
5395
5600
|
cwd: context.cwd,
|
|
5396
|
-
|
|
5601
|
+
resolver: context.resolver,
|
|
5397
5602
|
sourceLocale: context.config.sourceLocale,
|
|
5398
5603
|
targetLocale,
|
|
5399
5604
|
format: context.config.format,
|
|
@@ -5547,6 +5752,7 @@ async function translate2(input, deps = {}) {
|
|
|
5547
5752
|
config.maxTokens,
|
|
5548
5753
|
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
5549
5754
|
);
|
|
5755
|
+
const resolver = createLocalePathResolver(cwd, config);
|
|
5550
5756
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
5551
5757
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
5552
5758
|
const source = await readSource(config, cwd, fs, adapter);
|
|
@@ -5557,6 +5763,7 @@ async function translate2(input, deps = {}) {
|
|
|
5557
5763
|
provider,
|
|
5558
5764
|
cwd,
|
|
5559
5765
|
config,
|
|
5766
|
+
resolver,
|
|
5560
5767
|
prune,
|
|
5561
5768
|
generatePlurals,
|
|
5562
5769
|
maxBatchSize,
|
|
@@ -5597,6 +5804,123 @@ var ExchangeError = class extends Error {
|
|
|
5597
5804
|
this.code = code;
|
|
5598
5805
|
}
|
|
5599
5806
|
};
|
|
5807
|
+
var DELIMITER = {
|
|
5808
|
+
csv: ",",
|
|
5809
|
+
tsv: " "
|
|
5810
|
+
};
|
|
5811
|
+
var FORBIDDEN_FILE_NAME_CHARS = /[\\/:*?"<>|\p{Cc}]/u;
|
|
5812
|
+
function assertPlainFileLocale(locale) {
|
|
5813
|
+
if (locale.length === 0 || locale === "." || locale === "..") {
|
|
5814
|
+
throw new ExchangeError(
|
|
5815
|
+
"WORKBOOK_INVALID",
|
|
5816
|
+
`The locale "${locale}" cannot be an interchange file name: it must name a file, not a directory.`
|
|
5817
|
+
);
|
|
5818
|
+
}
|
|
5819
|
+
if (FORBIDDEN_FILE_NAME_CHARS.test(locale)) {
|
|
5820
|
+
throw new ExchangeError(
|
|
5821
|
+
"WORKBOOK_INVALID",
|
|
5822
|
+
`The locale "${locale}" cannot be an interchange file name: it must not contain a path separator, a control character, or any of : * ? " < > | .`
|
|
5823
|
+
);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
function delimitedFileName(locale, format) {
|
|
5827
|
+
assertPlainFileLocale(locale);
|
|
5828
|
+
return `${locale}.${format}`;
|
|
5829
|
+
}
|
|
5830
|
+
var COLUMN = {
|
|
5831
|
+
key: 1,
|
|
5832
|
+
source: 2,
|
|
5833
|
+
current: 3,
|
|
5834
|
+
status: 4,
|
|
5835
|
+
translation: 5,
|
|
5836
|
+
sourceHash: 6,
|
|
5837
|
+
context: 7,
|
|
5838
|
+
reviewStatus: 8,
|
|
5839
|
+
reviewReasons: 9
|
|
5840
|
+
};
|
|
5841
|
+
var HEADERS = [
|
|
5842
|
+
"Key",
|
|
5843
|
+
"Source",
|
|
5844
|
+
"Current translation",
|
|
5845
|
+
"Status",
|
|
5846
|
+
"Translation",
|
|
5847
|
+
"Source hash",
|
|
5848
|
+
"Context",
|
|
5849
|
+
"Review status",
|
|
5850
|
+
"Review reasons"
|
|
5851
|
+
];
|
|
5852
|
+
var HEADER_ROW = 1;
|
|
5853
|
+
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
5854
|
+
var rowSchema = z.object({
|
|
5855
|
+
key: z.string().min(1),
|
|
5856
|
+
source: z.string(),
|
|
5857
|
+
currentTarget: z.string(),
|
|
5858
|
+
status: z.enum(["new", "changed", "unchanged"]),
|
|
5859
|
+
sourceHash: z.string(),
|
|
5860
|
+
translation: z.string(),
|
|
5861
|
+
context: z.string(),
|
|
5862
|
+
reviewStatus: z.enum(["ok", "review"]).catch("ok"),
|
|
5863
|
+
reviewReasons: z.string().catch("")
|
|
5864
|
+
});
|
|
5865
|
+
var MALFORMED_ROW_COLUMN = "Status";
|
|
5866
|
+
function cellAt(cells, column) {
|
|
5867
|
+
return cells[column - 1] ?? "";
|
|
5868
|
+
}
|
|
5869
|
+
function parseRowCells(cells) {
|
|
5870
|
+
const result = rowSchema.safeParse({
|
|
5871
|
+
key: cellAt(cells, COLUMN.key),
|
|
5872
|
+
source: cellAt(cells, COLUMN.source),
|
|
5873
|
+
currentTarget: cellAt(cells, COLUMN.current),
|
|
5874
|
+
status: cellAt(cells, COLUMN.status),
|
|
5875
|
+
sourceHash: cellAt(cells, COLUMN.sourceHash),
|
|
5876
|
+
translation: cellAt(cells, COLUMN.translation).trim(),
|
|
5877
|
+
context: cellAt(cells, COLUMN.context),
|
|
5878
|
+
reviewStatus: cellAt(cells, COLUMN.reviewStatus),
|
|
5879
|
+
reviewReasons: cellAt(cells, COLUMN.reviewReasons)
|
|
5880
|
+
});
|
|
5881
|
+
if (!result.success) {
|
|
5882
|
+
return { ok: false, column: MALFORMED_ROW_COLUMN };
|
|
5883
|
+
}
|
|
5884
|
+
return { ok: true, row: result.data };
|
|
5885
|
+
}
|
|
5886
|
+
function rowCells(row) {
|
|
5887
|
+
const cells = new Array(HEADERS.length).fill("");
|
|
5888
|
+
cells[COLUMN.key - 1] = row.key;
|
|
5889
|
+
cells[COLUMN.source - 1] = row.source;
|
|
5890
|
+
cells[COLUMN.current - 1] = row.currentTarget;
|
|
5891
|
+
cells[COLUMN.status - 1] = row.status;
|
|
5892
|
+
cells[COLUMN.translation - 1] = row.translation;
|
|
5893
|
+
cells[COLUMN.sourceHash - 1] = row.sourceHash;
|
|
5894
|
+
cells[COLUMN.context - 1] = row.context;
|
|
5895
|
+
cells[COLUMN.reviewStatus - 1] = row.reviewStatus;
|
|
5896
|
+
cells[COLUMN.reviewReasons - 1] = row.reviewReasons;
|
|
5897
|
+
return cells;
|
|
5898
|
+
}
|
|
5899
|
+
var QUOTE = '"';
|
|
5900
|
+
var ESCAPED_QUOTE = '""';
|
|
5901
|
+
var LINE_BREAK = "\n";
|
|
5902
|
+
var UTF8_BOM = "\uFEFF";
|
|
5903
|
+
function needsQuoting(value, delimiter) {
|
|
5904
|
+
return value.includes(delimiter) || value.includes(QUOTE) || value.includes("\n") || value.includes("\r") || value !== value.trim();
|
|
5905
|
+
}
|
|
5906
|
+
function encodeField(value, delimiter) {
|
|
5907
|
+
if (!needsQuoting(value, delimiter)) {
|
|
5908
|
+
return value;
|
|
5909
|
+
}
|
|
5910
|
+
return `${QUOTE}${value.replaceAll(QUOTE, ESCAPED_QUOTE)}${QUOTE}`;
|
|
5911
|
+
}
|
|
5912
|
+
function encodeRecord(fields, delimiter) {
|
|
5913
|
+
return fields.map((field) => encodeField(field, delimiter)).join(delimiter);
|
|
5914
|
+
}
|
|
5915
|
+
function buildDelimited(sheet, format) {
|
|
5916
|
+
const delimiter = DELIMITER[format];
|
|
5917
|
+
const records = [encodeRecord(HEADERS, delimiter)];
|
|
5918
|
+
for (const row of sheet.rows) {
|
|
5919
|
+
records.push(encodeRecord(rowCells(row), delimiter));
|
|
5920
|
+
}
|
|
5921
|
+
const text = `${records.join(LINE_BREAK)}${LINE_BREAK}`;
|
|
5922
|
+
return format === "csv" ? `${UTF8_BOM}${text}` : text;
|
|
5923
|
+
}
|
|
5600
5924
|
var INSTRUCTIONS_LINES = [
|
|
5601
5925
|
"How to use this workbook",
|
|
5602
5926
|
"",
|
|
@@ -5625,30 +5949,6 @@ var INSTRUCTIONS_LINES = [
|
|
|
5625
5949
|
"a value with a broken placeholder, invalid ICU, or a source that changed since export is",
|
|
5626
5950
|
"withheld (not written) and reported so you can fix and resubmit it."
|
|
5627
5951
|
];
|
|
5628
|
-
var COLUMN = {
|
|
5629
|
-
key: 1,
|
|
5630
|
-
source: 2,
|
|
5631
|
-
current: 3,
|
|
5632
|
-
status: 4,
|
|
5633
|
-
translation: 5,
|
|
5634
|
-
sourceHash: 6,
|
|
5635
|
-
context: 7,
|
|
5636
|
-
reviewStatus: 8,
|
|
5637
|
-
reviewReasons: 9
|
|
5638
|
-
};
|
|
5639
|
-
var HEADERS = [
|
|
5640
|
-
"Key",
|
|
5641
|
-
"Source",
|
|
5642
|
-
"Current translation",
|
|
5643
|
-
"Status",
|
|
5644
|
-
"Translation",
|
|
5645
|
-
"Source hash",
|
|
5646
|
-
"Context",
|
|
5647
|
-
"Review status",
|
|
5648
|
-
"Review reasons"
|
|
5649
|
-
];
|
|
5650
|
-
var HEADER_ROW = 1;
|
|
5651
|
-
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
5652
5952
|
var READ_ONLY_FILL = {
|
|
5653
5953
|
type: "pattern",
|
|
5654
5954
|
pattern: "solid",
|
|
@@ -5808,6 +6108,12 @@ async function buildWorkbook(model) {
|
|
|
5808
6108
|
const protectedBytes = await protectWorkbookStructure(serialized);
|
|
5809
6109
|
return Uint8Array.prototype.slice.call(protectedBytes);
|
|
5810
6110
|
}
|
|
6111
|
+
var DEFAULT_DELIMITED_LIMITS = {
|
|
6112
|
+
maxInputBytes: 32 * 1024 * 1024,
|
|
6113
|
+
maxRowsPerFile: 1e5,
|
|
6114
|
+
maxFieldsPerRow: 64,
|
|
6115
|
+
maxFieldLength: 1024 * 1024
|
|
6116
|
+
};
|
|
5811
6117
|
var DEFAULT_WORKBOOK_LIMITS = {
|
|
5812
6118
|
maxDecompressedBytes: 64 * 1024 * 1024,
|
|
5813
6119
|
maxEntryCount: 1024,
|
|
@@ -5815,6 +6121,205 @@ var DEFAULT_WORKBOOK_LIMITS = {
|
|
|
5815
6121
|
maxRowsPerSheet: 1e5,
|
|
5816
6122
|
maxCellsPerRow: 64
|
|
5817
6123
|
};
|
|
6124
|
+
var QUOTE2 = '"';
|
|
6125
|
+
var HEADER_RECORD = 1;
|
|
6126
|
+
function scanQuotedField(text, start) {
|
|
6127
|
+
const parts = [];
|
|
6128
|
+
let cursor = start;
|
|
6129
|
+
for (; ; ) {
|
|
6130
|
+
const quote = text.indexOf(QUOTE2, cursor);
|
|
6131
|
+
if (quote === -1) {
|
|
6132
|
+
parts.push(text.slice(cursor));
|
|
6133
|
+
return { value: parts.join(""), next: text.length };
|
|
6134
|
+
}
|
|
6135
|
+
parts.push(text.slice(cursor, quote));
|
|
6136
|
+
if (text[quote + 1] !== QUOTE2) {
|
|
6137
|
+
return { value: parts.join(""), next: quote + 1 };
|
|
6138
|
+
}
|
|
6139
|
+
parts.push(QUOTE2);
|
|
6140
|
+
cursor = quote + 2;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
function scanPlainField(text, start, delimiter) {
|
|
6144
|
+
let cursor = start;
|
|
6145
|
+
while (cursor < text.length) {
|
|
6146
|
+
const char = text[cursor];
|
|
6147
|
+
if (char === delimiter || char === "\n" || char === "\r") {
|
|
6148
|
+
break;
|
|
6149
|
+
}
|
|
6150
|
+
cursor += 1;
|
|
6151
|
+
}
|
|
6152
|
+
return { value: text.slice(start, cursor), next: cursor };
|
|
6153
|
+
}
|
|
6154
|
+
function scanField(text, start, delimiter) {
|
|
6155
|
+
if (text[start] !== QUOTE2) {
|
|
6156
|
+
return scanPlainField(text, start, delimiter);
|
|
6157
|
+
}
|
|
6158
|
+
const quoted = scanQuotedField(text, start + 1);
|
|
6159
|
+
const trailing = scanPlainField(text, quoted.next, delimiter);
|
|
6160
|
+
return { value: `${quoted.value}${trailing.value}`, next: trailing.next };
|
|
6161
|
+
}
|
|
6162
|
+
function countLineBreaks(value) {
|
|
6163
|
+
let breaks = 0;
|
|
6164
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
6165
|
+
const char = value[index];
|
|
6166
|
+
if (char === "\n") {
|
|
6167
|
+
breaks += 1;
|
|
6168
|
+
} else if (char === "\r") {
|
|
6169
|
+
breaks += 1;
|
|
6170
|
+
if (value[index + 1] === "\n") {
|
|
6171
|
+
index += 1;
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
return breaks;
|
|
6176
|
+
}
|
|
6177
|
+
function scanRecord(text, start, delimiter, limits) {
|
|
6178
|
+
const fields = [];
|
|
6179
|
+
let breaks = 0;
|
|
6180
|
+
let cursor = start;
|
|
6181
|
+
for (; ; ) {
|
|
6182
|
+
const field = scanField(text, cursor, delimiter);
|
|
6183
|
+
assertFieldLength(field.value, limits);
|
|
6184
|
+
fields.push(field.value);
|
|
6185
|
+
breaks += countLineBreaks(field.value);
|
|
6186
|
+
assertFieldCount(fields.length, limits);
|
|
6187
|
+
cursor = field.next;
|
|
6188
|
+
if (text[cursor] !== delimiter) {
|
|
6189
|
+
return { fields, breaks, next: cursor };
|
|
6190
|
+
}
|
|
6191
|
+
cursor += 1;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
function consumeLineBreak(text, cursor) {
|
|
6195
|
+
if (text[cursor] === "\r") {
|
|
6196
|
+
return text[cursor + 1] === "\n" ? cursor + 2 : cursor + 1;
|
|
6197
|
+
}
|
|
6198
|
+
return text[cursor] === "\n" ? cursor + 1 : cursor;
|
|
6199
|
+
}
|
|
6200
|
+
function scanRecords(text, delimiter, limits) {
|
|
6201
|
+
const records = [];
|
|
6202
|
+
let cursor = 0;
|
|
6203
|
+
let line = 1;
|
|
6204
|
+
while (cursor < text.length) {
|
|
6205
|
+
const record = scanRecord(text, cursor, delimiter, limits);
|
|
6206
|
+
records.push({ fields: record.fields, line });
|
|
6207
|
+
assertRecordCount(records.length, limits);
|
|
6208
|
+
const next = consumeLineBreak(text, record.next);
|
|
6209
|
+
if (next === record.next) {
|
|
6210
|
+
break;
|
|
6211
|
+
}
|
|
6212
|
+
line += record.breaks + 1;
|
|
6213
|
+
cursor = next;
|
|
6214
|
+
}
|
|
6215
|
+
return records;
|
|
6216
|
+
}
|
|
6217
|
+
function assertInputBytes(text, limits) {
|
|
6218
|
+
if (Buffer.byteLength(text, "utf8") > limits.maxInputBytes) {
|
|
6219
|
+
throw new ExchangeError(
|
|
6220
|
+
"WORKBOOK_INVALID",
|
|
6221
|
+
`The interchange file is larger than the maximum of ${limits.maxInputBytes} bytes.`
|
|
6222
|
+
);
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
function assertFieldLength(value, limits) {
|
|
6226
|
+
if (value.length > limits.maxFieldLength) {
|
|
6227
|
+
throw new ExchangeError(
|
|
6228
|
+
"WORKBOOK_INVALID",
|
|
6229
|
+
`The interchange file has a field longer than the maximum of ${limits.maxFieldLength} characters.`
|
|
6230
|
+
);
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
function assertFieldCount(count, limits) {
|
|
6234
|
+
if (count > limits.maxFieldsPerRow) {
|
|
6235
|
+
throw new ExchangeError(
|
|
6236
|
+
"WORKBOOK_INVALID",
|
|
6237
|
+
`The interchange file has a row with more than the maximum of ${limits.maxFieldsPerRow} fields.`
|
|
6238
|
+
);
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
function assertRecordCount(count, limits) {
|
|
6242
|
+
if (count - HEADER_RECORD > limits.maxRowsPerFile) {
|
|
6243
|
+
throw new ExchangeError(
|
|
6244
|
+
"WORKBOOK_INVALID",
|
|
6245
|
+
`The interchange file has more than the maximum of ${limits.maxRowsPerFile} rows.`
|
|
6246
|
+
);
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
function assertHeaderRecord(records) {
|
|
6250
|
+
const header = records[0]?.fields;
|
|
6251
|
+
if (header === void 0 || header.length !== HEADERS.length) {
|
|
6252
|
+
throw new ExchangeError(
|
|
6253
|
+
"WORKBOOK_INVALID",
|
|
6254
|
+
`The interchange file does not start with the expected header line of ${HEADERS.length} columns.`
|
|
6255
|
+
);
|
|
6256
|
+
}
|
|
6257
|
+
for (const [index, label] of HEADERS.entries()) {
|
|
6258
|
+
if (header[index] !== label) {
|
|
6259
|
+
throw new ExchangeError(
|
|
6260
|
+
"WORKBOOK_INVALID",
|
|
6261
|
+
`The interchange file's header line does not match the expected columns (column ${index + 1} should be "${label}").`
|
|
6262
|
+
);
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
function fieldCountColumn(fieldCount) {
|
|
6267
|
+
const label = HEADERS.at(Math.min(fieldCount, HEADERS.length - 1));
|
|
6268
|
+
if (label === void 0) {
|
|
6269
|
+
return MALFORMED_ROW_COLUMN;
|
|
6270
|
+
}
|
|
6271
|
+
return label;
|
|
6272
|
+
}
|
|
6273
|
+
function isBlankRecord(fields) {
|
|
6274
|
+
return fields.length === 1 && fields[0] === "";
|
|
6275
|
+
}
|
|
6276
|
+
function readRecord(fields, locale, at, into) {
|
|
6277
|
+
const { row, line } = at;
|
|
6278
|
+
if (isBlankRecord(fields)) {
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
if (fields.length !== HEADERS.length) {
|
|
6282
|
+
into.malformed.push({ locale, row, line, column: fieldCountColumn(fields.length) });
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
6285
|
+
if (fields[COLUMN.key - 1] === "") {
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
const outcome = parseRowCells(fields);
|
|
6289
|
+
if (!outcome.ok) {
|
|
6290
|
+
into.malformed.push({ locale, row, line, column: outcome.column });
|
|
6291
|
+
return;
|
|
6292
|
+
}
|
|
6293
|
+
if (into.seenKeys.has(outcome.row.key)) {
|
|
6294
|
+
into.duplicates.push({ locale, key: outcome.row.key, row, line });
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
into.seenKeys.add(outcome.row.key);
|
|
6298
|
+
into.rows.push(outcome.row);
|
|
6299
|
+
}
|
|
6300
|
+
function readDelimited(input, options = {}) {
|
|
6301
|
+
const limits = options.limits ?? DEFAULT_DELIMITED_LIMITS;
|
|
6302
|
+
assertInputBytes(input.text, limits);
|
|
6303
|
+
const text = input.text.startsWith(UTF8_BOM) ? input.text.slice(UTF8_BOM.length) : input.text;
|
|
6304
|
+
const records = scanRecords(text, DELIMITER[input.format], limits);
|
|
6305
|
+
assertHeaderRecord(records);
|
|
6306
|
+
const into = {
|
|
6307
|
+
rows: [],
|
|
6308
|
+
malformed: [],
|
|
6309
|
+
duplicates: [],
|
|
6310
|
+
seenKeys: /* @__PURE__ */ new Set()
|
|
6311
|
+
};
|
|
6312
|
+
for (const [index, record] of records.entries()) {
|
|
6313
|
+
if (index >= HEADER_RECORD) {
|
|
6314
|
+
readRecord(record.fields, input.locale, { row: index + 1, line: record.line }, into);
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
return {
|
|
6318
|
+
sheets: [{ locale: input.locale, rows: into.rows }],
|
|
6319
|
+
malformedRows: into.malformed,
|
|
6320
|
+
duplicateKeys: into.duplicates
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
5818
6323
|
function assertNoDoctype2(name, xml) {
|
|
5819
6324
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
5820
6325
|
throw new ExchangeError(
|
|
@@ -5891,17 +6396,6 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
5891
6396
|
assertNoDoctype2(file.name, content);
|
|
5892
6397
|
}
|
|
5893
6398
|
}
|
|
5894
|
-
var rowSchema = z.object({
|
|
5895
|
-
key: z.string().min(1),
|
|
5896
|
-
source: z.string(),
|
|
5897
|
-
currentTarget: z.string(),
|
|
5898
|
-
status: z.enum(["new", "changed", "unchanged"]),
|
|
5899
|
-
sourceHash: z.string(),
|
|
5900
|
-
translation: z.string(),
|
|
5901
|
-
context: z.string(),
|
|
5902
|
-
reviewStatus: z.enum(["ok", "review"]).catch("ok"),
|
|
5903
|
-
reviewReasons: z.string().catch("")
|
|
5904
|
-
});
|
|
5905
6399
|
function cellString(cell) {
|
|
5906
6400
|
const value = cell.value;
|
|
5907
6401
|
if (value === null || value === void 0) {
|
|
@@ -5926,24 +6420,8 @@ function assertHeader(sheet) {
|
|
|
5926
6420
|
);
|
|
5927
6421
|
}
|
|
5928
6422
|
}
|
|
5929
|
-
var MALFORMED_ROW_COLUMN = "Status";
|
|
5930
6423
|
function parseRow(row) {
|
|
5931
|
-
|
|
5932
|
-
key: cellString(row.getCell(COLUMN.key)),
|
|
5933
|
-
source: cellString(row.getCell(COLUMN.source)),
|
|
5934
|
-
currentTarget: cellString(row.getCell(COLUMN.current)),
|
|
5935
|
-
status: cellString(row.getCell(COLUMN.status)),
|
|
5936
|
-
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
5937
|
-
translation: cellString(row.getCell(COLUMN.translation)).trim(),
|
|
5938
|
-
context: cellString(row.getCell(COLUMN.context)),
|
|
5939
|
-
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
5940
|
-
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
5941
|
-
};
|
|
5942
|
-
const result = rowSchema.safeParse(candidate);
|
|
5943
|
-
if (!result.success) {
|
|
5944
|
-
return { ok: false, column: MALFORMED_ROW_COLUMN };
|
|
5945
|
-
}
|
|
5946
|
-
return { ok: true, row: result.data };
|
|
6424
|
+
return parseRowCells(HEADERS.map((_, index) => cellString(row.getCell(index + 1))));
|
|
5947
6425
|
}
|
|
5948
6426
|
function readDataSheet(sheet, limits) {
|
|
5949
6427
|
assertHeader(sheet);
|
|
@@ -6017,8 +6495,52 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
6017
6495
|
return { sheets, malformedRows, duplicateKeys };
|
|
6018
6496
|
}
|
|
6019
6497
|
|
|
6498
|
+
// src/flow/workbook/exchange-format.ts
|
|
6499
|
+
function isDelimitedFormat(format) {
|
|
6500
|
+
return format === "csv" || format === "tsv";
|
|
6501
|
+
}
|
|
6502
|
+
var MANIFEST_VERSION = 1;
|
|
6503
|
+
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
6504
|
+
function exportManifestFileName(format) {
|
|
6505
|
+
return `.verbatra-export-${format}.json`;
|
|
6506
|
+
}
|
|
6507
|
+
var manifestSchema = z.object({
|
|
6508
|
+
version: z.number().int().positive(),
|
|
6509
|
+
format: z.enum(["csv", "tsv"]),
|
|
6510
|
+
locales: z.array(z.string())
|
|
6511
|
+
});
|
|
6512
|
+
async function writeExportManifest(fs, directory, format, locales) {
|
|
6513
|
+
const manifest = { version: MANIFEST_VERSION, format, locales: [...locales] };
|
|
6514
|
+
await fs.writeFile(
|
|
6515
|
+
join(directory, exportManifestFileName(format)),
|
|
6516
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
6517
|
+
`
|
|
6518
|
+
);
|
|
6519
|
+
}
|
|
6520
|
+
async function readExportedLocales(fs, directory, format) {
|
|
6521
|
+
const read = await fs.readFileBounded(
|
|
6522
|
+
join(directory, exportManifestFileName(format)),
|
|
6523
|
+
MAX_MANIFEST_BYTES
|
|
6524
|
+
);
|
|
6525
|
+
if (read.kind !== "ok") {
|
|
6526
|
+
return void 0;
|
|
6527
|
+
}
|
|
6528
|
+
let parsed;
|
|
6529
|
+
try {
|
|
6530
|
+
parsed = JSON.parse(read.content);
|
|
6531
|
+
} catch {
|
|
6532
|
+
return void 0;
|
|
6533
|
+
}
|
|
6534
|
+
const result = manifestSchema.safeParse(parsed);
|
|
6535
|
+
if (!result.success || result.data.version !== MANIFEST_VERSION || result.data.format !== format) {
|
|
6536
|
+
return void 0;
|
|
6537
|
+
}
|
|
6538
|
+
return new Set(result.data.locales);
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6020
6541
|
// src/flow/workbook/export-workbook.ts
|
|
6021
6542
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
6543
|
+
var DEFAULT_DELIMITED_PATH = "verbatra-translations";
|
|
6022
6544
|
function reasonLabel(reason) {
|
|
6023
6545
|
return reason.toLowerCase().replace(/_/g, "-");
|
|
6024
6546
|
}
|
|
@@ -6082,6 +6604,21 @@ function buildRows(source, target, baseline, includeUnchanged, adapter, glossary
|
|
|
6082
6604
|
}
|
|
6083
6605
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
6084
6606
|
}
|
|
6607
|
+
async function writeDelimitedFiles(fs, directory, format, sheets) {
|
|
6608
|
+
await fs.mkdir?.(directory);
|
|
6609
|
+
for (const sheet of sheets) {
|
|
6610
|
+
await fs.writeFile(
|
|
6611
|
+
join(directory, delimitedFileName(sheet.locale, format)),
|
|
6612
|
+
buildDelimited(sheet, format)
|
|
6613
|
+
);
|
|
6614
|
+
}
|
|
6615
|
+
await writeExportManifest(
|
|
6616
|
+
fs,
|
|
6617
|
+
directory,
|
|
6618
|
+
format,
|
|
6619
|
+
sheets.map((sheet) => sheet.locale)
|
|
6620
|
+
);
|
|
6621
|
+
}
|
|
6085
6622
|
async function exportWorkbook(input, deps = {}) {
|
|
6086
6623
|
const config = input.config;
|
|
6087
6624
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -6104,10 +6641,17 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
6104
6641
|
return { locale, rows };
|
|
6105
6642
|
})
|
|
6106
6643
|
);
|
|
6107
|
-
const
|
|
6108
|
-
const
|
|
6109
|
-
|
|
6110
|
-
|
|
6644
|
+
const format = input.format ?? "xlsx";
|
|
6645
|
+
const path = resolve(
|
|
6646
|
+
cwd,
|
|
6647
|
+
input.out ?? (isDelimitedFormat(format) ? DEFAULT_DELIMITED_PATH : DEFAULT_WORKBOOK_PATH)
|
|
6648
|
+
);
|
|
6649
|
+
if (isDelimitedFormat(format)) {
|
|
6650
|
+
await writeDelimitedFiles(fs, path, format, sheets);
|
|
6651
|
+
} else {
|
|
6652
|
+
const model = { sheets };
|
|
6653
|
+
await fs.writeBytes(path, await buildWorkbook(model));
|
|
6654
|
+
}
|
|
6111
6655
|
return {
|
|
6112
6656
|
path,
|
|
6113
6657
|
locales: sheets.map((sheet) => ({ locale: sheet.locale, rows: sheet.rows.length }))
|
|
@@ -6236,6 +6780,7 @@ function importLocale(params) {
|
|
|
6236
6780
|
|
|
6237
6781
|
// src/flow/workbook/import-workbook.ts
|
|
6238
6782
|
var MAX_WORKBOOK_FILE_BYTES = 64 * 1024 * 1024;
|
|
6783
|
+
var MAX_DELIMITED_FILE_BYTES = 32 * 1024 * 1024;
|
|
6239
6784
|
async function readWorkbookBytes(path, fs) {
|
|
6240
6785
|
const read = await fs.readBytesBounded(path, MAX_WORKBOOK_FILE_BYTES);
|
|
6241
6786
|
if (read.kind === "missing") {
|
|
@@ -6249,6 +6794,75 @@ async function readWorkbookBytes(path, fs) {
|
|
|
6249
6794
|
}
|
|
6250
6795
|
return read.bytes;
|
|
6251
6796
|
}
|
|
6797
|
+
async function readDelimitedText(path, fs) {
|
|
6798
|
+
const read = await fs.readFileBounded(path, MAX_DELIMITED_FILE_BYTES);
|
|
6799
|
+
if (read.kind === "missing") {
|
|
6800
|
+
return void 0;
|
|
6801
|
+
}
|
|
6802
|
+
if (read.kind === "too-large") {
|
|
6803
|
+
throw new SdkError(
|
|
6804
|
+
"SOURCE_INVALID",
|
|
6805
|
+
`The interchange file at ${path} exceeds the maximum allowed size of ${MAX_DELIMITED_FILE_BYTES} bytes.`
|
|
6806
|
+
);
|
|
6807
|
+
}
|
|
6808
|
+
return read.content;
|
|
6809
|
+
}
|
|
6810
|
+
async function collectDelimitedSources(path, config, fs, format) {
|
|
6811
|
+
const single = await readDelimitedText(path, fs);
|
|
6812
|
+
if (single !== void 0) {
|
|
6813
|
+
return { sources: [{ locale: basename(path, `.${format}`), text: single }], staleLocales: [] };
|
|
6814
|
+
}
|
|
6815
|
+
const exported = await readExportedLocales(fs, path, format);
|
|
6816
|
+
const sources = [];
|
|
6817
|
+
const staleLocales = [];
|
|
6818
|
+
for (const locale of config.targetLocales) {
|
|
6819
|
+
const text = await readDelimitedText(join(path, delimitedFileName(locale, format)), fs);
|
|
6820
|
+
if (text === void 0) {
|
|
6821
|
+
continue;
|
|
6822
|
+
}
|
|
6823
|
+
if (exported !== void 0 && !exported.has(locale)) {
|
|
6824
|
+
staleLocales.push(locale);
|
|
6825
|
+
continue;
|
|
6826
|
+
}
|
|
6827
|
+
sources.push({ locale, text });
|
|
6828
|
+
}
|
|
6829
|
+
if (sources.length === 0 && staleLocales.length === 0) {
|
|
6830
|
+
throw new SdkError(
|
|
6831
|
+
"SOURCE_UNREADABLE",
|
|
6832
|
+
`No ${format} file was found at ${path}, and it holds no <locale>.${format} file for any configured target locale.`
|
|
6833
|
+
);
|
|
6834
|
+
}
|
|
6835
|
+
return { sources, staleLocales };
|
|
6836
|
+
}
|
|
6837
|
+
function parseDelimitedSources(sources, format) {
|
|
6838
|
+
const sheets = [];
|
|
6839
|
+
const malformedRows = [];
|
|
6840
|
+
const duplicateKeys = [];
|
|
6841
|
+
for (const source of sources) {
|
|
6842
|
+
const data = readDelimited({ text: source.text, locale: source.locale, format });
|
|
6843
|
+
sheets.push(...data.sheets);
|
|
6844
|
+
malformedRows.push(...data.malformedRows);
|
|
6845
|
+
duplicateKeys.push(...data.duplicateKeys);
|
|
6846
|
+
}
|
|
6847
|
+
return { sheets, malformedRows, duplicateKeys };
|
|
6848
|
+
}
|
|
6849
|
+
async function readImportData(path, config, fs, format) {
|
|
6850
|
+
try {
|
|
6851
|
+
if (isDelimitedFormat(format)) {
|
|
6852
|
+
const handoff = await collectDelimitedSources(path, config, fs, format);
|
|
6853
|
+
return {
|
|
6854
|
+
data: parseDelimitedSources(handoff.sources, format),
|
|
6855
|
+
staleLocales: handoff.staleLocales
|
|
6856
|
+
};
|
|
6857
|
+
}
|
|
6858
|
+
return { data: await readWorkbook(await readWorkbookBytes(path, fs)), staleLocales: [] };
|
|
6859
|
+
} catch (error) {
|
|
6860
|
+
if (error instanceof SdkError) {
|
|
6861
|
+
throw error;
|
|
6862
|
+
}
|
|
6863
|
+
throw new SdkError("SOURCE_INVALID", errorMessage(error));
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6252
6866
|
function mergeAccepted(target, accepted) {
|
|
6253
6867
|
const merged = new Map(target.entries);
|
|
6254
6868
|
for (const [key, { value, source }] of accepted) {
|
|
@@ -6289,18 +6903,43 @@ function computeLockEntries2(source, merged, baseline, accepted) {
|
|
|
6289
6903
|
}
|
|
6290
6904
|
var MissingSheetError = class extends Error {
|
|
6291
6905
|
code = "WORKBOOK_SHEET_MISSING";
|
|
6292
|
-
constructor(locale) {
|
|
6906
|
+
constructor(locale, format) {
|
|
6293
6907
|
super(
|
|
6294
|
-
`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.`
|
|
6908
|
+
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.`
|
|
6295
6909
|
);
|
|
6296
6910
|
this.name = "MissingSheetError";
|
|
6297
6911
|
}
|
|
6298
6912
|
};
|
|
6913
|
+
var StaleHandoffFileError = class extends Error {
|
|
6914
|
+
code = "HANDOFF_FILE_STALE";
|
|
6915
|
+
constructor(locale, format) {
|
|
6916
|
+
super(
|
|
6917
|
+
`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.`
|
|
6918
|
+
);
|
|
6919
|
+
this.name = "StaleHandoffFileError";
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
function absentLocaleFailures(config, sheets, format, staleLocales) {
|
|
6923
|
+
const present = new Set(sheets.map((sheet) => sheet.locale));
|
|
6924
|
+
const stale = new Set(staleLocales);
|
|
6925
|
+
const failures = [];
|
|
6926
|
+
for (const locale of config.targetLocales) {
|
|
6927
|
+
if (present.has(locale)) {
|
|
6928
|
+
continue;
|
|
6929
|
+
}
|
|
6930
|
+
const error = stale.has(locale) && isDelimitedFormat(format) ? new StaleHandoffFileError(locale, format) : new MissingSheetError(locale, format);
|
|
6931
|
+
failures.push(failureSummary(locale, error));
|
|
6932
|
+
}
|
|
6933
|
+
return failures;
|
|
6934
|
+
}
|
|
6935
|
+
function lineOf(reported) {
|
|
6936
|
+
return reported.line === void 0 ? {} : { line: reported.line };
|
|
6937
|
+
}
|
|
6299
6938
|
async function runSheet(ctx, sheet, lock) {
|
|
6300
6939
|
if (!ctx.config.targetLocales.includes(sheet.locale)) {
|
|
6301
6940
|
throw new SdkError(
|
|
6302
6941
|
"CONFIG_INVALID",
|
|
6303
|
-
`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.`
|
|
6942
|
+
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.`
|
|
6304
6943
|
);
|
|
6305
6944
|
}
|
|
6306
6945
|
const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
@@ -6312,15 +6951,15 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
6312
6951
|
baseline,
|
|
6313
6952
|
adapter: ctx.adapter,
|
|
6314
6953
|
sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
|
|
6315
|
-
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column })),
|
|
6316
|
-
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row }))
|
|
6954
|
+
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...lineOf(problem) })),
|
|
6955
|
+
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...lineOf(duplicate) }))
|
|
6317
6956
|
});
|
|
6318
6957
|
if (ctx.dryRun) {
|
|
6319
6958
|
return { summary, lockEntries: {}, cacheAdditions: {} };
|
|
6320
6959
|
}
|
|
6321
6960
|
const merged = mergeAccepted(target, accepted);
|
|
6322
6961
|
if (accepted.size > 0) {
|
|
6323
|
-
const path =
|
|
6962
|
+
const path = createLocalePathResolver(ctx.cwd, ctx.config).pathFor(sheet.locale);
|
|
6324
6963
|
await ctx.adapter.write(
|
|
6325
6964
|
{
|
|
6326
6965
|
locale: sheet.locale,
|
|
@@ -6344,14 +6983,13 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6344
6983
|
const fs = deps.fs ?? defaultFs;
|
|
6345
6984
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
6346
6985
|
const source = await readSource(config, cwd, fs, adapter);
|
|
6347
|
-
const
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
}
|
|
6986
|
+
const format = input.format ?? "xlsx";
|
|
6987
|
+
const { data, staleLocales } = await readImportData(
|
|
6988
|
+
resolve(cwd, input.workbook),
|
|
6989
|
+
config,
|
|
6990
|
+
fs,
|
|
6991
|
+
format
|
|
6992
|
+
);
|
|
6355
6993
|
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
6356
6994
|
const ctx = {
|
|
6357
6995
|
config,
|
|
@@ -6362,7 +7000,8 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6362
7000
|
sourceInvalidIcuKeys: source.invalidIcuKeys,
|
|
6363
7001
|
dryRun,
|
|
6364
7002
|
malformedRows: data.malformedRows,
|
|
6365
|
-
duplicateKeys: data.duplicateKeys
|
|
7003
|
+
duplicateKeys: data.duplicateKeys,
|
|
7004
|
+
format
|
|
6366
7005
|
};
|
|
6367
7006
|
const summaries = [];
|
|
6368
7007
|
const cacheAdditions = /* @__PURE__ */ new Map();
|
|
@@ -6387,12 +7026,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
6387
7026
|
summaries.push(failureSummary(sheet.locale, error));
|
|
6388
7027
|
}
|
|
6389
7028
|
}
|
|
6390
|
-
|
|
6391
|
-
for (const locale of config.targetLocales) {
|
|
6392
|
-
if (!presentLocales.has(locale)) {
|
|
6393
|
-
summaries.push(failureSummary(locale, new MissingSheetError(locale)));
|
|
6394
|
-
}
|
|
6395
|
-
}
|
|
7029
|
+
summaries.push(...absentLocaleFailures(config, data.sheets, format, staleLocales));
|
|
6396
7030
|
if (!dryRun) {
|
|
6397
7031
|
await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
|
|
6398
7032
|
}
|
|
@@ -6436,19 +7070,13 @@ function defaultRunTranslate(deps) {
|
|
|
6436
7070
|
|
|
6437
7071
|
// src/watch/watch.ts
|
|
6438
7072
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
6439
|
-
function describeError2(error) {
|
|
6440
|
-
if (error instanceof Error) {
|
|
6441
|
-
const code = error.code;
|
|
6442
|
-
return { code: typeof code === "string" ? code : "WATCH_RUN_FAILED", message: error.message };
|
|
6443
|
-
}
|
|
6444
|
-
return { code: "WATCH_RUN_FAILED", message: String(error) };
|
|
6445
|
-
}
|
|
6446
7073
|
async function watch(input, deps = {}) {
|
|
6447
7074
|
const cwd = input.cwd ?? process.cwd();
|
|
6448
7075
|
const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6449
7076
|
const fs = deps.fs ?? defaultFs;
|
|
6450
7077
|
resolveRunConcurrency(input.concurrency, false, input.config);
|
|
6451
|
-
const
|
|
7078
|
+
const resolver = createLocalePathResolver(cwd, input.config);
|
|
7079
|
+
const sourcePath = resolver.pathFor(input.config.sourceLocale);
|
|
6452
7080
|
if (!await fs.fileExists(sourcePath)) {
|
|
6453
7081
|
throw new SdkError(
|
|
6454
7082
|
"SOURCE_UNREADABLE",
|
|
@@ -6474,7 +7102,7 @@ async function watch(input, deps = {}) {
|
|
|
6474
7102
|
try {
|
|
6475
7103
|
input.onRun({ status: "succeeded", summary: await runTranslate(runInput) });
|
|
6476
7104
|
} catch (error) {
|
|
6477
|
-
input.onRun({ status: "failed", error:
|
|
7105
|
+
input.onRun({ status: "failed", error: describeError(error, "WATCH_RUN_FAILED") });
|
|
6478
7106
|
}
|
|
6479
7107
|
}
|
|
6480
7108
|
function startRun() {
|
|
@@ -6531,6 +7159,6 @@ async function watch(input, deps = {}) {
|
|
|
6531
7159
|
return { stop };
|
|
6532
7160
|
}
|
|
6533
7161
|
|
|
6534
|
-
export { CACHE_FILE_NAME, DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
7162
|
+
export { CACHE_FILE_NAME, DEFAULT_DELIMITED_PATH, DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
6535
7163
|
//# sourceMappingURL=index.js.map
|
|
6536
7164
|
//# sourceMappingURL=index.js.map
|