@tolkbot/cli 0.7.0 → 0.9.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/dist/index.js +1949 -1561
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -105,7 +105,7 @@ var require_path = __commonJS({
|
|
|
105
105
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
106
106
|
exports.convertPosixPathToPattern = exports.convertWindowsPathToPattern = exports.convertPathToPattern = exports.escapePosixPath = exports.escapeWindowsPath = exports.escape = exports.removeLeadingDotSegment = exports.makeAbsolute = exports.unixify = void 0;
|
|
107
107
|
var os3 = __require("os");
|
|
108
|
-
var
|
|
108
|
+
var path12 = __require("path");
|
|
109
109
|
var IS_WINDOWS_PLATFORM = os3.platform() === "win32";
|
|
110
110
|
var LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2;
|
|
111
111
|
var POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g;
|
|
@@ -117,7 +117,7 @@ var require_path = __commonJS({
|
|
|
117
117
|
}
|
|
118
118
|
exports.unixify = unixify;
|
|
119
119
|
function makeAbsolute(cwd, filepath) {
|
|
120
|
-
return
|
|
120
|
+
return path12.resolve(cwd, filepath);
|
|
121
121
|
}
|
|
122
122
|
exports.makeAbsolute = makeAbsolute;
|
|
123
123
|
function removeLeadingDotSegment(entry) {
|
|
@@ -1416,7 +1416,7 @@ var require_braces = __commonJS({
|
|
|
1416
1416
|
var require_constants2 = __commonJS({
|
|
1417
1417
|
"../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/constants.js"(exports, module) {
|
|
1418
1418
|
"use strict";
|
|
1419
|
-
var
|
|
1419
|
+
var path12 = __require("path");
|
|
1420
1420
|
var WIN_SLASH = "\\\\/";
|
|
1421
1421
|
var WIN_NO_SLASH = `[^${WIN_SLASH}]`;
|
|
1422
1422
|
var DEFAULT_MAX_EXTGLOB_RECURSION = 0;
|
|
@@ -1590,7 +1590,7 @@ var require_constants2 = __commonJS({
|
|
|
1590
1590
|
/* | */
|
|
1591
1591
|
CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279,
|
|
1592
1592
|
/* \uFEFF */
|
|
1593
|
-
SEP:
|
|
1593
|
+
SEP: path12.sep,
|
|
1594
1594
|
/**
|
|
1595
1595
|
* Create EXTGLOB_CHARS
|
|
1596
1596
|
*/
|
|
@@ -1617,7 +1617,7 @@ var require_constants2 = __commonJS({
|
|
|
1617
1617
|
var require_utils2 = __commonJS({
|
|
1618
1618
|
"../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/utils.js"(exports) {
|
|
1619
1619
|
"use strict";
|
|
1620
|
-
var
|
|
1620
|
+
var path12 = __require("path");
|
|
1621
1621
|
var win32 = process.platform === "win32";
|
|
1622
1622
|
var {
|
|
1623
1623
|
REGEX_BACKSLASH,
|
|
@@ -1646,7 +1646,7 @@ var require_utils2 = __commonJS({
|
|
|
1646
1646
|
if (options && typeof options.windows === "boolean") {
|
|
1647
1647
|
return options.windows;
|
|
1648
1648
|
}
|
|
1649
|
-
return win32 === true ||
|
|
1649
|
+
return win32 === true || path12.sep === "\\";
|
|
1650
1650
|
};
|
|
1651
1651
|
exports.escapeLast = (input2, char, lastIdx) => {
|
|
1652
1652
|
const idx = input2.lastIndexOf(char, lastIdx);
|
|
@@ -3010,7 +3010,7 @@ var require_parse2 = __commonJS({
|
|
|
3010
3010
|
var require_picomatch = __commonJS({
|
|
3011
3011
|
"../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/picomatch.js"(exports, module) {
|
|
3012
3012
|
"use strict";
|
|
3013
|
-
var
|
|
3013
|
+
var path12 = __require("path");
|
|
3014
3014
|
var scan = require_scan();
|
|
3015
3015
|
var parse4 = require_parse2();
|
|
3016
3016
|
var utils = require_utils2();
|
|
@@ -3095,7 +3095,7 @@ var require_picomatch = __commonJS({
|
|
|
3095
3095
|
};
|
|
3096
3096
|
picomatch.matchBase = (input2, glob, options, posix = utils.isWindows(options)) => {
|
|
3097
3097
|
const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);
|
|
3098
|
-
return regex.test(
|
|
3098
|
+
return regex.test(path12.basename(input2));
|
|
3099
3099
|
};
|
|
3100
3100
|
picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str);
|
|
3101
3101
|
picomatch.parse = (pattern, options) => {
|
|
@@ -3322,7 +3322,7 @@ var require_pattern = __commonJS({
|
|
|
3322
3322
|
"use strict";
|
|
3323
3323
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3324
3324
|
exports.isAbsolute = exports.partitionAbsoluteAndRelative = exports.removeDuplicateSlashes = exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.isPatternRelatedToParentDirectory = exports.getPatternsOutsideCurrentDirectory = exports.getPatternsInsideCurrentDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0;
|
|
3325
|
-
var
|
|
3325
|
+
var path12 = __require("path");
|
|
3326
3326
|
var globParent = require_glob_parent();
|
|
3327
3327
|
var micromatch = require_micromatch();
|
|
3328
3328
|
var GLOBSTAR = "**";
|
|
@@ -3417,7 +3417,7 @@ var require_pattern = __commonJS({
|
|
|
3417
3417
|
}
|
|
3418
3418
|
exports.endsWithSlashGlobStar = endsWithSlashGlobStar;
|
|
3419
3419
|
function isAffectDepthOfReadingPattern(pattern) {
|
|
3420
|
-
const basename =
|
|
3420
|
+
const basename = path12.basename(pattern);
|
|
3421
3421
|
return endsWithSlashGlobStar(pattern) || isStaticPattern(basename);
|
|
3422
3422
|
}
|
|
3423
3423
|
exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern;
|
|
@@ -3475,7 +3475,7 @@ var require_pattern = __commonJS({
|
|
|
3475
3475
|
}
|
|
3476
3476
|
exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
|
|
3477
3477
|
function isAbsolute(pattern) {
|
|
3478
|
-
return
|
|
3478
|
+
return path12.isAbsolute(pattern);
|
|
3479
3479
|
}
|
|
3480
3480
|
exports.isAbsolute = isAbsolute;
|
|
3481
3481
|
}
|
|
@@ -3652,8 +3652,8 @@ var require_utils3 = __commonJS({
|
|
|
3652
3652
|
exports.errno = errno;
|
|
3653
3653
|
var fs10 = require_fs();
|
|
3654
3654
|
exports.fs = fs10;
|
|
3655
|
-
var
|
|
3656
|
-
exports.path =
|
|
3655
|
+
var path12 = require_path();
|
|
3656
|
+
exports.path = path12;
|
|
3657
3657
|
var pattern = require_pattern();
|
|
3658
3658
|
exports.pattern = pattern;
|
|
3659
3659
|
var stream = require_stream();
|
|
@@ -3765,8 +3765,8 @@ var require_async = __commonJS({
|
|
|
3765
3765
|
"use strict";
|
|
3766
3766
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3767
3767
|
exports.read = void 0;
|
|
3768
|
-
function read(
|
|
3769
|
-
settings.fs.lstat(
|
|
3768
|
+
function read(path12, settings, callback) {
|
|
3769
|
+
settings.fs.lstat(path12, (lstatError, lstat) => {
|
|
3770
3770
|
if (lstatError !== null) {
|
|
3771
3771
|
callFailureCallback(callback, lstatError);
|
|
3772
3772
|
return;
|
|
@@ -3775,7 +3775,7 @@ var require_async = __commonJS({
|
|
|
3775
3775
|
callSuccessCallback(callback, lstat);
|
|
3776
3776
|
return;
|
|
3777
3777
|
}
|
|
3778
|
-
settings.fs.stat(
|
|
3778
|
+
settings.fs.stat(path12, (statError, stat) => {
|
|
3779
3779
|
if (statError !== null) {
|
|
3780
3780
|
if (settings.throwErrorOnBrokenSymbolicLink) {
|
|
3781
3781
|
callFailureCallback(callback, statError);
|
|
@@ -3807,13 +3807,13 @@ var require_sync = __commonJS({
|
|
|
3807
3807
|
"use strict";
|
|
3808
3808
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3809
3809
|
exports.read = void 0;
|
|
3810
|
-
function read(
|
|
3811
|
-
const lstat = settings.fs.lstatSync(
|
|
3810
|
+
function read(path12, settings) {
|
|
3811
|
+
const lstat = settings.fs.lstatSync(path12);
|
|
3812
3812
|
if (!lstat.isSymbolicLink() || !settings.followSymbolicLink) {
|
|
3813
3813
|
return lstat;
|
|
3814
3814
|
}
|
|
3815
3815
|
try {
|
|
3816
|
-
const stat = settings.fs.statSync(
|
|
3816
|
+
const stat = settings.fs.statSync(path12);
|
|
3817
3817
|
if (settings.markSymbolicLink) {
|
|
3818
3818
|
stat.isSymbolicLink = () => true;
|
|
3819
3819
|
}
|
|
@@ -3884,17 +3884,17 @@ var require_out = __commonJS({
|
|
|
3884
3884
|
var sync = require_sync();
|
|
3885
3885
|
var settings_1 = require_settings();
|
|
3886
3886
|
exports.Settings = settings_1.default;
|
|
3887
|
-
function stat(
|
|
3887
|
+
function stat(path12, optionsOrSettingsOrCallback, callback) {
|
|
3888
3888
|
if (typeof optionsOrSettingsOrCallback === "function") {
|
|
3889
|
-
async.read(
|
|
3889
|
+
async.read(path12, getSettings(), optionsOrSettingsOrCallback);
|
|
3890
3890
|
return;
|
|
3891
3891
|
}
|
|
3892
|
-
async.read(
|
|
3892
|
+
async.read(path12, getSettings(optionsOrSettingsOrCallback), callback);
|
|
3893
3893
|
}
|
|
3894
3894
|
exports.stat = stat;
|
|
3895
|
-
function statSync2(
|
|
3895
|
+
function statSync2(path12, optionsOrSettings) {
|
|
3896
3896
|
const settings = getSettings(optionsOrSettings);
|
|
3897
|
-
return sync.read(
|
|
3897
|
+
return sync.read(path12, settings);
|
|
3898
3898
|
}
|
|
3899
3899
|
exports.statSync = statSync2;
|
|
3900
3900
|
function getSettings(settingsOrOptions = {}) {
|
|
@@ -4112,16 +4112,16 @@ var require_async2 = __commonJS({
|
|
|
4112
4112
|
return;
|
|
4113
4113
|
}
|
|
4114
4114
|
const tasks = names.map((name) => {
|
|
4115
|
-
const
|
|
4115
|
+
const path12 = common.joinPathSegments(directory, name, settings.pathSegmentSeparator);
|
|
4116
4116
|
return (done) => {
|
|
4117
|
-
fsStat.stat(
|
|
4117
|
+
fsStat.stat(path12, settings.fsStatSettings, (error62, stats) => {
|
|
4118
4118
|
if (error62 !== null) {
|
|
4119
4119
|
done(error62);
|
|
4120
4120
|
return;
|
|
4121
4121
|
}
|
|
4122
4122
|
const entry = {
|
|
4123
4123
|
name,
|
|
4124
|
-
path:
|
|
4124
|
+
path: path12,
|
|
4125
4125
|
dirent: utils.fs.createDirentFromStats(name, stats)
|
|
4126
4126
|
};
|
|
4127
4127
|
if (settings.stats) {
|
|
@@ -4239,7 +4239,7 @@ var require_settings2 = __commonJS({
|
|
|
4239
4239
|
"../../node_modules/.pnpm/@nodelib+fs.scandir@2.1.5/node_modules/@nodelib/fs.scandir/out/settings.js"(exports) {
|
|
4240
4240
|
"use strict";
|
|
4241
4241
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4242
|
-
var
|
|
4242
|
+
var path12 = __require("path");
|
|
4243
4243
|
var fsStat = require_out();
|
|
4244
4244
|
var fs10 = require_fs4();
|
|
4245
4245
|
var Settings = class {
|
|
@@ -4247,7 +4247,7 @@ var require_settings2 = __commonJS({
|
|
|
4247
4247
|
this._options = _options;
|
|
4248
4248
|
this.followSymbolicLinks = this._getValue(this._options.followSymbolicLinks, false);
|
|
4249
4249
|
this.fs = fs10.createFileSystemAdapter(this._options.fs);
|
|
4250
|
-
this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator,
|
|
4250
|
+
this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path12.sep);
|
|
4251
4251
|
this.stats = this._getValue(this._options.stats, false);
|
|
4252
4252
|
this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, true);
|
|
4253
4253
|
this.fsStatSettings = new fsStat.Settings({
|
|
@@ -4274,17 +4274,17 @@ var require_out2 = __commonJS({
|
|
|
4274
4274
|
var sync = require_sync2();
|
|
4275
4275
|
var settings_1 = require_settings2();
|
|
4276
4276
|
exports.Settings = settings_1.default;
|
|
4277
|
-
function scandir(
|
|
4277
|
+
function scandir(path12, optionsOrSettingsOrCallback, callback) {
|
|
4278
4278
|
if (typeof optionsOrSettingsOrCallback === "function") {
|
|
4279
|
-
async.read(
|
|
4279
|
+
async.read(path12, getSettings(), optionsOrSettingsOrCallback);
|
|
4280
4280
|
return;
|
|
4281
4281
|
}
|
|
4282
|
-
async.read(
|
|
4282
|
+
async.read(path12, getSettings(optionsOrSettingsOrCallback), callback);
|
|
4283
4283
|
}
|
|
4284
4284
|
exports.scandir = scandir;
|
|
4285
|
-
function scandirSync(
|
|
4285
|
+
function scandirSync(path12, optionsOrSettings) {
|
|
4286
4286
|
const settings = getSettings(optionsOrSettings);
|
|
4287
|
-
return sync.read(
|
|
4287
|
+
return sync.read(path12, settings);
|
|
4288
4288
|
}
|
|
4289
4289
|
exports.scandirSync = scandirSync;
|
|
4290
4290
|
function getSettings(settingsOrOptions = {}) {
|
|
@@ -4932,7 +4932,7 @@ var require_settings3 = __commonJS({
|
|
|
4932
4932
|
"../../node_modules/.pnpm/@nodelib+fs.walk@1.2.8/node_modules/@nodelib/fs.walk/out/settings.js"(exports) {
|
|
4933
4933
|
"use strict";
|
|
4934
4934
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4935
|
-
var
|
|
4935
|
+
var path12 = __require("path");
|
|
4936
4936
|
var fsScandir = require_out2();
|
|
4937
4937
|
var Settings = class {
|
|
4938
4938
|
constructor(_options = {}) {
|
|
@@ -4942,7 +4942,7 @@ var require_settings3 = __commonJS({
|
|
|
4942
4942
|
this.deepFilter = this._getValue(this._options.deepFilter, null);
|
|
4943
4943
|
this.entryFilter = this._getValue(this._options.entryFilter, null);
|
|
4944
4944
|
this.errorFilter = this._getValue(this._options.errorFilter, null);
|
|
4945
|
-
this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator,
|
|
4945
|
+
this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path12.sep);
|
|
4946
4946
|
this.fsScandirSettings = new fsScandir.Settings({
|
|
4947
4947
|
followSymbolicLinks: this._options.followSymbolicLinks,
|
|
4948
4948
|
fs: this._options.fs,
|
|
@@ -5004,7 +5004,7 @@ var require_reader2 = __commonJS({
|
|
|
5004
5004
|
"../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/readers/reader.js"(exports) {
|
|
5005
5005
|
"use strict";
|
|
5006
5006
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5007
|
-
var
|
|
5007
|
+
var path12 = __require("path");
|
|
5008
5008
|
var fsStat = require_out();
|
|
5009
5009
|
var utils = require_utils3();
|
|
5010
5010
|
var Reader = class {
|
|
@@ -5017,7 +5017,7 @@ var require_reader2 = __commonJS({
|
|
|
5017
5017
|
});
|
|
5018
5018
|
}
|
|
5019
5019
|
_getFullEntryPath(filepath) {
|
|
5020
|
-
return
|
|
5020
|
+
return path12.resolve(this._settings.cwd, filepath);
|
|
5021
5021
|
}
|
|
5022
5022
|
_makeEntry(stats, pattern) {
|
|
5023
5023
|
const entry = {
|
|
@@ -5433,7 +5433,7 @@ var require_provider = __commonJS({
|
|
|
5433
5433
|
"../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/providers/provider.js"(exports) {
|
|
5434
5434
|
"use strict";
|
|
5435
5435
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5436
|
-
var
|
|
5436
|
+
var path12 = __require("path");
|
|
5437
5437
|
var deep_1 = require_deep();
|
|
5438
5438
|
var entry_1 = require_entry();
|
|
5439
5439
|
var error_1 = require_error();
|
|
@@ -5447,7 +5447,7 @@ var require_provider = __commonJS({
|
|
|
5447
5447
|
this.entryTransformer = new entry_2.default(this._settings);
|
|
5448
5448
|
}
|
|
5449
5449
|
_getRootDirectory(task) {
|
|
5450
|
-
return
|
|
5450
|
+
return path12.resolve(this._settings.cwd, task.base);
|
|
5451
5451
|
}
|
|
5452
5452
|
_getReaderOptions(task) {
|
|
5453
5453
|
const basePath = task.base === "." ? "" : task.base;
|
|
@@ -5784,10 +5784,10 @@ var require_out4 = __commonJS({
|
|
|
5784
5784
|
});
|
|
5785
5785
|
|
|
5786
5786
|
// src/main.ts
|
|
5787
|
-
import { Command as
|
|
5787
|
+
import { Command as Command10, CommanderError, Option as Option2 } from "commander";
|
|
5788
5788
|
|
|
5789
5789
|
// package.json
|
|
5790
|
-
var version = "0.
|
|
5790
|
+
var version = "0.9.0";
|
|
5791
5791
|
|
|
5792
5792
|
// src/commands/auth/index.ts
|
|
5793
5793
|
import { Command } from "commander";
|
|
@@ -28423,6 +28423,48 @@ function parseTranslations(raw) {
|
|
|
28423
28423
|
return { locale, text };
|
|
28424
28424
|
});
|
|
28425
28425
|
}
|
|
28426
|
+
function pickTermMatch(term, candidates) {
|
|
28427
|
+
const exact = candidates.filter((c) => c.src.trim() === term.trim());
|
|
28428
|
+
const matches = exact.length > 0 ? exact : candidates;
|
|
28429
|
+
const [only, ...rest] = matches;
|
|
28430
|
+
if (!only) {
|
|
28431
|
+
throw new CliError(
|
|
28432
|
+
`No glossary term "${term}".`,
|
|
28433
|
+
"List the terms with 'tolkbot glossary list'.",
|
|
28434
|
+
"not_found"
|
|
28435
|
+
);
|
|
28436
|
+
}
|
|
28437
|
+
if (rest.length > 0) {
|
|
28438
|
+
const lines = matches.map(
|
|
28439
|
+
(c) => ` ${c._id} "${c.src}" (${c.srcLocale}, ${c.projectId === void 0 ? "organization" : "project"})`
|
|
28440
|
+
);
|
|
28441
|
+
throw new CliError(
|
|
28442
|
+
`"${term}" matches ${matches.length} glossary terms:
|
|
28443
|
+
${lines.join("\n")}`,
|
|
28444
|
+
"Pass --id with the one you mean.",
|
|
28445
|
+
"ambiguous_term"
|
|
28446
|
+
);
|
|
28447
|
+
}
|
|
28448
|
+
return only;
|
|
28449
|
+
}
|
|
28450
|
+
async function resolveGlossaryId(client3, context, target) {
|
|
28451
|
+
if (target.id !== void 0 && target.term !== void 0) {
|
|
28452
|
+
throw new UsageError("Pass either a term or --id, not both.");
|
|
28453
|
+
}
|
|
28454
|
+
if (target.id !== void 0) return target.id;
|
|
28455
|
+
if (target.term === void 0) {
|
|
28456
|
+
throw new UsageError(
|
|
28457
|
+
"Name the term, or pass --id.",
|
|
28458
|
+
'For example: tolkbot glossary show "Dashboard"'
|
|
28459
|
+
);
|
|
28460
|
+
}
|
|
28461
|
+
const candidates = await client3.query(api.glossaries.findByTerm, {
|
|
28462
|
+
organizationId: context.organizationId,
|
|
28463
|
+
projectId: context.projectId,
|
|
28464
|
+
term: target.term
|
|
28465
|
+
});
|
|
28466
|
+
return pickTermMatch(target.term, candidates)._id;
|
|
28467
|
+
}
|
|
28426
28468
|
|
|
28427
28469
|
// src/commands/glossary/create.tsx
|
|
28428
28470
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
@@ -28914,25 +28956,27 @@ function RemoveUI({ state }) {
|
|
|
28914
28956
|
async function runGlossaryRemove(opts) {
|
|
28915
28957
|
const run = async () => {
|
|
28916
28958
|
const convexClient = await getAuthenticatedConvexClient();
|
|
28917
|
-
const
|
|
28959
|
+
const context = await getProjectContext(convexClient);
|
|
28960
|
+
const glossaryId = await resolveGlossaryId(convexClient, context, opts);
|
|
28918
28961
|
await convexClient.mutation(api.glossaries.remove, {
|
|
28919
|
-
organizationId:
|
|
28920
|
-
glossaryId
|
|
28962
|
+
organizationId: context.organizationId,
|
|
28963
|
+
glossaryId
|
|
28921
28964
|
});
|
|
28965
|
+
return glossaryId;
|
|
28922
28966
|
};
|
|
28923
28967
|
if (opts.json) {
|
|
28924
|
-
await run();
|
|
28925
|
-
jsonSuccess({ id: opts.id, ok: true });
|
|
28968
|
+
jsonSuccess({ id: await run(), ok: true });
|
|
28926
28969
|
}
|
|
28927
28970
|
const { unmount } = renderTransient(
|
|
28928
28971
|
/* @__PURE__ */ jsx6(RemoveUI, { state: { type: "removing" } })
|
|
28929
28972
|
);
|
|
28973
|
+
let id;
|
|
28930
28974
|
try {
|
|
28931
|
-
await run();
|
|
28975
|
+
id = await run();
|
|
28932
28976
|
} finally {
|
|
28933
28977
|
unmount();
|
|
28934
28978
|
}
|
|
28935
|
-
render7(/* @__PURE__ */ jsx6(RemoveUI, { state: { type: "success", id
|
|
28979
|
+
render7(/* @__PURE__ */ jsx6(RemoveUI, { state: { type: "success", id } })).unmount();
|
|
28936
28980
|
}
|
|
28937
28981
|
|
|
28938
28982
|
// src/commands/glossary/show.tsx
|
|
@@ -28980,11 +29024,12 @@ function GlossaryShowUI({ state }) {
|
|
|
28980
29024
|
] }) })
|
|
28981
29025
|
] });
|
|
28982
29026
|
}
|
|
28983
|
-
async function loadGlossary(
|
|
29027
|
+
async function loadGlossary(target) {
|
|
28984
29028
|
const convexClient = await getAuthenticatedConvexClient();
|
|
28985
|
-
const
|
|
29029
|
+
const context = await getProjectContext(convexClient);
|
|
29030
|
+
const id = await resolveGlossaryId(convexClient, context, target);
|
|
28986
29031
|
const glossary = await convexClient.query(api.glossaries.getById, {
|
|
28987
|
-
organizationId:
|
|
29032
|
+
organizationId: context.organizationId,
|
|
28988
29033
|
glossaryId: id
|
|
28989
29034
|
});
|
|
28990
29035
|
if (!glossary) {
|
|
@@ -28998,7 +29043,7 @@ async function loadGlossary(id) {
|
|
|
28998
29043
|
}
|
|
28999
29044
|
async function runGlossaryShow(opts) {
|
|
29000
29045
|
if (opts.json) {
|
|
29001
|
-
const glossary2 = await loadGlossary(opts
|
|
29046
|
+
const glossary2 = await loadGlossary(opts);
|
|
29002
29047
|
jsonSuccess({
|
|
29003
29048
|
id: glossary2._id,
|
|
29004
29049
|
term: glossary2.src,
|
|
@@ -29013,7 +29058,7 @@ async function runGlossaryShow(opts) {
|
|
|
29013
29058
|
);
|
|
29014
29059
|
let glossary;
|
|
29015
29060
|
try {
|
|
29016
|
-
glossary = await loadGlossary(opts
|
|
29061
|
+
glossary = await loadGlossary(opts);
|
|
29017
29062
|
} finally {
|
|
29018
29063
|
unmount();
|
|
29019
29064
|
}
|
|
@@ -29063,29 +29108,36 @@ async function runGlossaryUpdate(opts) {
|
|
|
29063
29108
|
const update = async () => {
|
|
29064
29109
|
const patch = buildPatch();
|
|
29065
29110
|
const convexClient = await getAuthenticatedConvexClient();
|
|
29066
|
-
const
|
|
29111
|
+
const context = await getProjectContext(convexClient);
|
|
29112
|
+
const glossaryId = await resolveGlossaryId(
|
|
29113
|
+
convexClient,
|
|
29114
|
+
context,
|
|
29115
|
+
opts.target
|
|
29116
|
+
);
|
|
29067
29117
|
await convexClient.mutation(api.glossaries.update, {
|
|
29068
|
-
organizationId:
|
|
29069
|
-
glossaryId
|
|
29118
|
+
organizationId: context.organizationId,
|
|
29119
|
+
glossaryId,
|
|
29070
29120
|
patch
|
|
29071
29121
|
});
|
|
29122
|
+
return glossaryId;
|
|
29072
29123
|
};
|
|
29073
29124
|
if (opts.json) {
|
|
29074
|
-
await update();
|
|
29075
|
-
jsonSuccess({ id: opts.id, ok: true });
|
|
29125
|
+
jsonSuccess({ id: await update(), ok: true });
|
|
29076
29126
|
}
|
|
29077
29127
|
const { unmount } = renderTransient(
|
|
29078
29128
|
/* @__PURE__ */ jsx8(UpdateUI, { state: { type: "updating" } })
|
|
29079
29129
|
);
|
|
29130
|
+
let id;
|
|
29080
29131
|
try {
|
|
29081
|
-
await update();
|
|
29132
|
+
id = await update();
|
|
29082
29133
|
} finally {
|
|
29083
29134
|
unmount();
|
|
29084
29135
|
}
|
|
29085
|
-
render9(/* @__PURE__ */ jsx8(UpdateUI, { state: { type: "success", id
|
|
29136
|
+
render9(/* @__PURE__ */ jsx8(UpdateUI, { state: { type: "success", id } })).unmount();
|
|
29086
29137
|
}
|
|
29087
29138
|
|
|
29088
29139
|
// src/commands/glossary/index.ts
|
|
29140
|
+
var TERM_HELP = "The term, as the glossary spells it (any case). A term that matches more than one entry lists them; pass --id then";
|
|
29089
29141
|
var glossaryCommand = new Command2("glossary").description(
|
|
29090
29142
|
"Manage glossary terms"
|
|
29091
29143
|
);
|
|
@@ -29098,13 +29150,13 @@ glossaryCommand.command("list").description("List glossary terms for the current
|
|
|
29098
29150
|
const parsed = validateOrExit(schema, opts);
|
|
29099
29151
|
await runGlossaryList(parsed);
|
|
29100
29152
|
});
|
|
29101
|
-
glossaryCommand.command("show").description("Show details of a glossary term").
|
|
29153
|
+
glossaryCommand.command("show").description("Show details of a glossary term").argument("[term]", TERM_HELP).option("--id <glossaryId>", "Glossary term ID, instead of the term").option("--json", "Output as JSON (machine-readable)").action(async (term, opts) => {
|
|
29102
29154
|
const schema = z4.object({
|
|
29103
|
-
id: z4.string().min(1),
|
|
29155
|
+
id: z4.string().min(1).optional(),
|
|
29104
29156
|
json: z4.boolean().optional()
|
|
29105
29157
|
});
|
|
29106
29158
|
const parsed = validateOrExit(schema, opts);
|
|
29107
|
-
await runGlossaryShow(parsed);
|
|
29159
|
+
await runGlossaryShow({ ...parsed, term });
|
|
29108
29160
|
});
|
|
29109
29161
|
glossaryCommand.command("create").description("Create a new glossary term").requiredOption("--term <text>", "Source term").requiredOption("--locale <locale>", "Source locale (e.g. en)").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option(
|
|
29110
29162
|
"--translation <locale:text...>",
|
|
@@ -29121,12 +29173,12 @@ glossaryCommand.command("create").description("Create a new glossary term").requ
|
|
|
29121
29173
|
const parsed = validateOrExit(schema, opts);
|
|
29122
29174
|
await runGlossaryCreate(parsed);
|
|
29123
29175
|
});
|
|
29124
|
-
glossaryCommand.command("update").description("Update an existing glossary term").
|
|
29176
|
+
glossaryCommand.command("update").description("Update an existing glossary term").argument("[term]", TERM_HELP).option("--id <glossaryId>", "Glossary term ID, instead of the term").option("--term <text>", "New source term").option("--locale <locale>", "New source locale").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option("--no-do-not-translate", "Unmark do-not-translate").option(
|
|
29125
29177
|
"--translation <locale:text...>",
|
|
29126
29178
|
"Translations in locale:text format (replaces all translations)"
|
|
29127
|
-
).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
29179
|
+
).option("--json", "Output as JSON (machine-readable)").action(async (term, opts) => {
|
|
29128
29180
|
const schema = z4.object({
|
|
29129
|
-
id: z4.string().min(1),
|
|
29181
|
+
id: z4.string().min(1).optional(),
|
|
29130
29182
|
term: z4.string().optional(),
|
|
29131
29183
|
locale: z4.string().optional(),
|
|
29132
29184
|
context: z4.string().optional(),
|
|
@@ -29134,21 +29186,21 @@ glossaryCommand.command("update").description("Update an existing glossary term"
|
|
|
29134
29186
|
translation: z4.array(z4.string()).optional(),
|
|
29135
29187
|
json: z4.boolean().optional()
|
|
29136
29188
|
});
|
|
29137
|
-
const parsed = validateOrExit(schema, opts);
|
|
29138
|
-
await runGlossaryUpdate(parsed);
|
|
29189
|
+
const { id, ...parsed } = validateOrExit(schema, opts);
|
|
29190
|
+
await runGlossaryUpdate({ ...parsed, target: { id, term } });
|
|
29139
29191
|
});
|
|
29140
|
-
glossaryCommand.command("remove").description("Remove a glossary term").
|
|
29192
|
+
glossaryCommand.command("remove").description("Remove a glossary term").argument("[term]", TERM_HELP).option("--id <glossaryId>", "Glossary term ID, instead of the term").option("--json", "Output as JSON (machine-readable)").action(async (term, opts) => {
|
|
29141
29193
|
const schema = z4.object({
|
|
29142
|
-
id: z4.string().min(1),
|
|
29194
|
+
id: z4.string().min(1).optional(),
|
|
29143
29195
|
json: z4.boolean().optional()
|
|
29144
29196
|
});
|
|
29145
29197
|
const parsed = validateOrExit(schema, opts);
|
|
29146
|
-
await runGlossaryRemove(parsed);
|
|
29198
|
+
await runGlossaryRemove({ ...parsed, term });
|
|
29147
29199
|
});
|
|
29148
29200
|
|
|
29149
29201
|
// src/commands/init/index.tsx
|
|
29150
|
-
import { existsSync as
|
|
29151
|
-
import
|
|
29202
|
+
import { existsSync as existsSync5 } from "fs";
|
|
29203
|
+
import path10 from "path";
|
|
29152
29204
|
import { Box as Box10, render as render10, Text as Text10 } from "ink";
|
|
29153
29205
|
|
|
29154
29206
|
// src/lib/telemetry.ts
|
|
@@ -29363,9 +29415,9 @@ function runtimeProperties(cliVersion) {
|
|
|
29363
29415
|
};
|
|
29364
29416
|
}
|
|
29365
29417
|
var activeCommand = null;
|
|
29366
|
-
function beginCommand(
|
|
29418
|
+
function beginCommand(path12, cliVersion) {
|
|
29367
29419
|
activeCommand = {
|
|
29368
|
-
path:
|
|
29420
|
+
path: path12,
|
|
29369
29421
|
startedAt: Date.now(),
|
|
29370
29422
|
cliVersion,
|
|
29371
29423
|
reported: false
|
|
@@ -29384,11 +29436,59 @@ function completeCommand(exitCode) {
|
|
|
29384
29436
|
});
|
|
29385
29437
|
}
|
|
29386
29438
|
|
|
29387
|
-
// src/commands/init/
|
|
29388
|
-
import {
|
|
29389
|
-
import { existsSync as existsSync3 } from "fs";
|
|
29439
|
+
// src/commands/init/agents-file.ts
|
|
29440
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
29390
29441
|
import path8 from "path";
|
|
29391
29442
|
|
|
29443
|
+
// ../shared/src/agents/index.ts
|
|
29444
|
+
var AGENT_DOCS_URL = "https://docs.tolkbot.ai/docs/for-agents";
|
|
29445
|
+
var AGENT_BLOCK = `## Translations
|
|
29446
|
+
|
|
29447
|
+
TolkBot translates this project. Run its commands in the directory that holds \`tolkbot.json\`.
|
|
29448
|
+
|
|
29449
|
+
- Add new strings to the base locale file only. Never write the other locale files by hand.
|
|
29450
|
+
- After changing strings, run \`npx tolkbot status\` and \`npx tolkbot translate --all\`, then \`npx tolkbot validate\` before committing.
|
|
29451
|
+
- To correct a translation, run \`npx tolkbot set <key> --locale <locale> --value "<text>" --learn "<why>"\`. TolkBot keeps the value and learns from the note.
|
|
29452
|
+
- Leave every key alone that \`npx tolkbot status --keys\` lists as held by a person.
|
|
29453
|
+
- More: ${AGENT_DOCS_URL}`;
|
|
29454
|
+
var AGENT_BLOCK_START = "<!-- tolkbot:start -->";
|
|
29455
|
+
var AGENT_BLOCK_END = "<!-- tolkbot:end -->";
|
|
29456
|
+
var MARKED_AGENT_BLOCK = `${AGENT_BLOCK_START}
|
|
29457
|
+
${AGENT_BLOCK}
|
|
29458
|
+
${AGENT_BLOCK_END}`;
|
|
29459
|
+
function withAgentBlock(existing) {
|
|
29460
|
+
if (existing === null) {
|
|
29461
|
+
return {
|
|
29462
|
+
ok: true,
|
|
29463
|
+
change: "created",
|
|
29464
|
+
content: `${MARKED_AGENT_BLOCK}
|
|
29465
|
+
`
|
|
29466
|
+
};
|
|
29467
|
+
}
|
|
29468
|
+
const start = existing.indexOf(AGENT_BLOCK_START);
|
|
29469
|
+
const end = existing.indexOf(AGENT_BLOCK_END);
|
|
29470
|
+
if (start === -1 && end === -1) {
|
|
29471
|
+
const trimmed = existing.replace(/\s+$/, "");
|
|
29472
|
+
const content2 = trimmed === "" ? `${MARKED_AGENT_BLOCK}
|
|
29473
|
+
` : `${trimmed}
|
|
29474
|
+
|
|
29475
|
+
${MARKED_AGENT_BLOCK}
|
|
29476
|
+
`;
|
|
29477
|
+
return { ok: true, change: "added", content: content2 };
|
|
29478
|
+
}
|
|
29479
|
+
if (start === -1 || end === -1 || end < start) {
|
|
29480
|
+
return { ok: false, reason: "unmatched_marker" };
|
|
29481
|
+
}
|
|
29482
|
+
const before = existing.slice(0, start);
|
|
29483
|
+
const after = existing.slice(end + AGENT_BLOCK_END.length);
|
|
29484
|
+
const content = `${before}${MARKED_AGENT_BLOCK}${after}`;
|
|
29485
|
+
return {
|
|
29486
|
+
ok: true,
|
|
29487
|
+
change: content === existing ? "unchanged" : "updated",
|
|
29488
|
+
content
|
|
29489
|
+
};
|
|
29490
|
+
}
|
|
29491
|
+
|
|
29392
29492
|
// src/lib/repository-root.ts
|
|
29393
29493
|
import { execFileSync } from "child_process";
|
|
29394
29494
|
import fs7 from "fs";
|
|
@@ -29429,7 +29529,52 @@ function realpath(p) {
|
|
|
29429
29529
|
}
|
|
29430
29530
|
}
|
|
29431
29531
|
|
|
29532
|
+
// src/commands/init/agents-file.ts
|
|
29533
|
+
var AGENT_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
29534
|
+
function defaultAgentsFile(cwd) {
|
|
29535
|
+
const start = path8.resolve(cwd);
|
|
29536
|
+
const root = gitToplevel(start) ?? start;
|
|
29537
|
+
for (let dir = start; ; dir = path8.dirname(dir)) {
|
|
29538
|
+
for (const name of AGENT_FILE_NAMES) {
|
|
29539
|
+
const candidate = path8.join(dir, name);
|
|
29540
|
+
if (existsSync3(candidate)) return candidate;
|
|
29541
|
+
}
|
|
29542
|
+
if (dir === root || path8.dirname(dir) === dir) break;
|
|
29543
|
+
}
|
|
29544
|
+
return path8.join(root, "AGENTS.md");
|
|
29545
|
+
}
|
|
29546
|
+
function resolveAgentsFile(cwd, requested) {
|
|
29547
|
+
return requested === true ? defaultAgentsFile(cwd) : path8.resolve(cwd, requested);
|
|
29548
|
+
}
|
|
29549
|
+
function writeAgentsFile(file2) {
|
|
29550
|
+
const existing = existsSync3(file2) ? readFileSync3(file2, "utf8") : null;
|
|
29551
|
+
const edit = withAgentBlock(existing);
|
|
29552
|
+
if (!edit.ok) {
|
|
29553
|
+
throw new CliError(
|
|
29554
|
+
`${file2} has a TolkBot block that is missing its start or end marker.`,
|
|
29555
|
+
"Remove the half of the block that is left, or the marker, and run 'tolkbot init --agents-file' again."
|
|
29556
|
+
);
|
|
29557
|
+
}
|
|
29558
|
+
if (edit.change !== "unchanged") writeFileSync2(file2, edit.content);
|
|
29559
|
+
return { path: file2, change: edit.change };
|
|
29560
|
+
}
|
|
29561
|
+
function describeAgentsFile(result, shown) {
|
|
29562
|
+
switch (result.change) {
|
|
29563
|
+
case "created":
|
|
29564
|
+
return `Wrote the agent instructions to ${shown}`;
|
|
29565
|
+
case "added":
|
|
29566
|
+
return `Added the agent instructions to ${shown}`;
|
|
29567
|
+
case "updated":
|
|
29568
|
+
return `Updated the agent instructions in ${shown}`;
|
|
29569
|
+
case "unchanged":
|
|
29570
|
+
return `${shown} already has the agent instructions`;
|
|
29571
|
+
}
|
|
29572
|
+
}
|
|
29573
|
+
|
|
29432
29574
|
// src/commands/init/core.ts
|
|
29575
|
+
import { execSync as execSync2 } from "child_process";
|
|
29576
|
+
import { existsSync as existsSync4 } from "fs";
|
|
29577
|
+
import path9 from "path";
|
|
29433
29578
|
var DETECTION_TIMEOUT_MS = 3 * 6e4;
|
|
29434
29579
|
var DETECTION_POLL_MS = 1e3;
|
|
29435
29580
|
function getRecommendedFramework(projectType) {
|
|
@@ -29536,7 +29681,7 @@ function ambiguousProjectError(candidates) {
|
|
|
29536
29681
|
function configInDirectory(dir) {
|
|
29537
29682
|
try {
|
|
29538
29683
|
const found = readConfig(dir);
|
|
29539
|
-
if (!found ||
|
|
29684
|
+
if (!found || path9.dirname(found.path) !== path9.resolve(dir)) {
|
|
29540
29685
|
return void 0;
|
|
29541
29686
|
}
|
|
29542
29687
|
return {
|
|
@@ -29549,7 +29694,7 @@ function configInDirectory(dir) {
|
|
|
29549
29694
|
}
|
|
29550
29695
|
}
|
|
29551
29696
|
function newProjectName(repository, subpath) {
|
|
29552
|
-
return subpath === "" ? repository :
|
|
29697
|
+
return subpath === "" ? repository : path9.posix.basename(subpath);
|
|
29553
29698
|
}
|
|
29554
29699
|
async function linkRepository(cwd, plan, opts = {}) {
|
|
29555
29700
|
const { owner, name } = resolveRepoIdentity(cwd);
|
|
@@ -29630,14 +29775,14 @@ async function linkRepository(cwd, plan, opts = {}) {
|
|
|
29630
29775
|
() => convexClient.query(api.projects.getProjectForCli, { projectId })
|
|
29631
29776
|
);
|
|
29632
29777
|
requireSourcePattern(project, `${owner}/${name}`);
|
|
29633
|
-
const projectDir =
|
|
29778
|
+
const projectDir = path9.join(
|
|
29634
29779
|
root,
|
|
29635
29780
|
...project.subpath.split("/").filter(Boolean)
|
|
29636
29781
|
);
|
|
29637
|
-
const configDir = opts.existingConfig?.projectId === projectId ?
|
|
29782
|
+
const configDir = opts.existingConfig?.projectId === projectId ? path9.dirname(opts.existingConfig.path) : (
|
|
29638
29783
|
// A checkout without the project's directory (a sparse one)
|
|
29639
29784
|
// keeps the config where init ran.
|
|
29640
|
-
|
|
29785
|
+
existsSync4(projectDir) ? projectDir : cwd
|
|
29641
29786
|
);
|
|
29642
29787
|
return {
|
|
29643
29788
|
projectId,
|
|
@@ -29667,8 +29812,8 @@ function writeProjectConfig(result, framework, opts) {
|
|
|
29667
29812
|
);
|
|
29668
29813
|
}
|
|
29669
29814
|
function displayPath(cwd, file2) {
|
|
29670
|
-
const relative =
|
|
29671
|
-
return relative.startsWith("..") ||
|
|
29815
|
+
const relative = path9.relative(cwd, file2);
|
|
29816
|
+
return relative.startsWith("..") || path9.isAbsolute(relative) ? file2 : relative;
|
|
29672
29817
|
}
|
|
29673
29818
|
function isDetecting(project) {
|
|
29674
29819
|
return project.initializationStatus === "pending" || project.initializationStatus === "initializing";
|
|
@@ -29741,6 +29886,7 @@ function friendlyProjectType(t) {
|
|
|
29741
29886
|
function InitWizard({
|
|
29742
29887
|
force,
|
|
29743
29888
|
project,
|
|
29889
|
+
agentsFile,
|
|
29744
29890
|
onComplete,
|
|
29745
29891
|
onFailure
|
|
29746
29892
|
}) {
|
|
@@ -29755,6 +29901,27 @@ function InitWizard({
|
|
|
29755
29901
|
});
|
|
29756
29902
|
const [error62, setError] = React.useState(null);
|
|
29757
29903
|
const [pendingChoice, setPendingChoice] = React.useState(null);
|
|
29904
|
+
const agentsTarget = React.useMemo(
|
|
29905
|
+
() => resolveAgentsFile(cwd, agentsFile ?? true),
|
|
29906
|
+
[cwd, agentsFile]
|
|
29907
|
+
);
|
|
29908
|
+
function addAgentInstructions() {
|
|
29909
|
+
try {
|
|
29910
|
+
const result = writeAgentsFile(agentsTarget);
|
|
29911
|
+
setSetupData((prev) => ({
|
|
29912
|
+
...prev,
|
|
29913
|
+
agentsLine: describeAgentsFile(
|
|
29914
|
+
result,
|
|
29915
|
+
displayPath(cwd, result.path)
|
|
29916
|
+
)
|
|
29917
|
+
}));
|
|
29918
|
+
} catch (err) {
|
|
29919
|
+
setSetupData((prev) => ({
|
|
29920
|
+
...prev,
|
|
29921
|
+
agentsLine: err instanceof Error ? err.message : String(err)
|
|
29922
|
+
}));
|
|
29923
|
+
}
|
|
29924
|
+
}
|
|
29758
29925
|
React.useEffect(() => {
|
|
29759
29926
|
if (step === "detecting") {
|
|
29760
29927
|
setTimeout(() => {
|
|
@@ -29810,7 +29977,12 @@ function InitWizard({
|
|
|
29810
29977
|
createdProject: result.createdProject,
|
|
29811
29978
|
organizationId: result.organizationId
|
|
29812
29979
|
});
|
|
29813
|
-
|
|
29980
|
+
if (agentsFile) {
|
|
29981
|
+
addAgentInstructions();
|
|
29982
|
+
setStep("done");
|
|
29983
|
+
} else {
|
|
29984
|
+
setStep("agentsOffer");
|
|
29985
|
+
}
|
|
29814
29986
|
} catch (err) {
|
|
29815
29987
|
setError(err instanceof Error ? err.message : String(err));
|
|
29816
29988
|
setStep("done");
|
|
@@ -30045,6 +30217,30 @@ function InitWizard({
|
|
|
30045
30217
|
] })
|
|
30046
30218
|
] }) });
|
|
30047
30219
|
}
|
|
30220
|
+
if (step === "agentsOffer") {
|
|
30221
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
30222
|
+
/* @__PURE__ */ jsxs9(Text9, { children: [
|
|
30223
|
+
"Add TolkBot's instructions for coding agents to",
|
|
30224
|
+
" ",
|
|
30225
|
+
displayPath(cwd, agentsTarget),
|
|
30226
|
+
"?"
|
|
30227
|
+
] }),
|
|
30228
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "They tell an agent to run tolkbot translate and validate instead of writing locale files by hand." }),
|
|
30229
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(
|
|
30230
|
+
Select,
|
|
30231
|
+
{
|
|
30232
|
+
options: [
|
|
30233
|
+
{ label: "Yes", value: "yes" },
|
|
30234
|
+
{ label: "No", value: "no" }
|
|
30235
|
+
],
|
|
30236
|
+
onChange: (value) => {
|
|
30237
|
+
if (value === "yes") addAgentInstructions();
|
|
30238
|
+
setStep("done");
|
|
30239
|
+
}
|
|
30240
|
+
}
|
|
30241
|
+
) })
|
|
30242
|
+
] });
|
|
30243
|
+
}
|
|
30048
30244
|
if (step === "done") {
|
|
30049
30245
|
if (error62) {
|
|
30050
30246
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
@@ -30073,10 +30269,13 @@ function InitWizard({
|
|
|
30073
30269
|
"Files: ",
|
|
30074
30270
|
setupData.link?.sourcePattern
|
|
30075
30271
|
] }) }),
|
|
30076
|
-
setupData.configPath && /* @__PURE__ */
|
|
30077
|
-
|
|
30078
|
-
|
|
30079
|
-
|
|
30272
|
+
setupData.configPath && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
30273
|
+
/* @__PURE__ */ jsxs9(Text9, { children: [
|
|
30274
|
+
"Wrote ",
|
|
30275
|
+
displayPath(cwd, setupData.configPath)
|
|
30276
|
+
] }),
|
|
30277
|
+
setupData.agentsLine && /* @__PURE__ */ jsx9(Text9, { children: setupData.agentsLine })
|
|
30278
|
+
] }),
|
|
30080
30279
|
setupData.shouldInstall && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
30081
30280
|
/* @__PURE__ */ jsx9(Text9, { children: "Next steps:" }),
|
|
30082
30281
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "1. Import i18n in your app - see docs" }),
|
|
@@ -30143,7 +30342,8 @@ function resolvePlan(cwd, opts) {
|
|
|
30143
30342
|
function SummaryUI({
|
|
30144
30343
|
plan,
|
|
30145
30344
|
result,
|
|
30146
|
-
configPath
|
|
30345
|
+
configPath,
|
|
30346
|
+
agentsLine
|
|
30147
30347
|
}) {
|
|
30148
30348
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
|
|
30149
30349
|
/* @__PURE__ */ jsxs10(Box10, { children: [
|
|
@@ -30181,10 +30381,13 @@ function SummaryUI({
|
|
|
30181
30381
|
result.createdProject ? " (created)" : " (existing)"
|
|
30182
30382
|
] })
|
|
30183
30383
|
] }),
|
|
30184
|
-
/* @__PURE__ */
|
|
30185
|
-
|
|
30186
|
-
|
|
30187
|
-
|
|
30384
|
+
/* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
|
|
30385
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
30386
|
+
"Wrote ",
|
|
30387
|
+
configPath
|
|
30388
|
+
] }),
|
|
30389
|
+
agentsLine && /* @__PURE__ */ jsx10(Text10, { children: agentsLine })
|
|
30390
|
+
] })
|
|
30188
30391
|
] });
|
|
30189
30392
|
}
|
|
30190
30393
|
async function runHeadless(cwd, opts) {
|
|
@@ -30208,6 +30411,7 @@ async function runHeadless(cwd, opts) {
|
|
|
30208
30411
|
const configPath = writeProjectConfig(result, plan.framework, {
|
|
30209
30412
|
force: opts.force
|
|
30210
30413
|
});
|
|
30414
|
+
const agents = opts.agentsFile ? writeAgentsFile(resolveAgentsFile(cwd, opts.agentsFile)) : null;
|
|
30211
30415
|
track("cli_init_completed", {
|
|
30212
30416
|
framework: plan.framework,
|
|
30213
30417
|
framework_detected: detectedFramework,
|
|
@@ -30231,7 +30435,8 @@ async function runHeadless(cwd, opts) {
|
|
|
30231
30435
|
baseLocale: result.baseLanguage,
|
|
30232
30436
|
targets: result.targetLanguages,
|
|
30233
30437
|
createdProject: result.createdProject,
|
|
30234
|
-
installed: plan.install
|
|
30438
|
+
installed: plan.install,
|
|
30439
|
+
agentsFile: agents
|
|
30235
30440
|
},
|
|
30236
30441
|
0
|
|
30237
30442
|
);
|
|
@@ -30243,7 +30448,8 @@ async function runHeadless(cwd, opts) {
|
|
|
30243
30448
|
{
|
|
30244
30449
|
plan,
|
|
30245
30450
|
result,
|
|
30246
|
-
configPath: displayPath(cwd, configPath)
|
|
30451
|
+
configPath: displayPath(cwd, configPath),
|
|
30452
|
+
agentsLine: agents ? describeAgentsFile(agents, displayPath(cwd, agents.path)) : null
|
|
30247
30453
|
}
|
|
30248
30454
|
)
|
|
30249
30455
|
).unmount();
|
|
@@ -30266,8 +30472,8 @@ async function requireLogin() {
|
|
|
30266
30472
|
}
|
|
30267
30473
|
function refuseExistingConfig(cwd, force) {
|
|
30268
30474
|
if (force) return;
|
|
30269
|
-
const existing =
|
|
30270
|
-
if (
|
|
30475
|
+
const existing = path10.join(cwd, "tolkbot.json");
|
|
30476
|
+
if (existsSync5(existing)) {
|
|
30271
30477
|
throw new CliError(
|
|
30272
30478
|
`${existing} already exists.`,
|
|
30273
30479
|
"Run 'tolkbot init --force' to link again and refresh it."
|
|
@@ -30292,6 +30498,7 @@ async function runInteractive(opts) {
|
|
|
30292
30498
|
{
|
|
30293
30499
|
force: opts.force,
|
|
30294
30500
|
project: opts.project,
|
|
30501
|
+
agentsFile: opts.agentsFile,
|
|
30295
30502
|
onComplete: (summary) => {
|
|
30296
30503
|
setTelemetryOrganization(summary.organizationId);
|
|
30297
30504
|
track("cli_init_completed", {
|
|
@@ -30388,7 +30595,7 @@ function describeLeftOut(plan) {
|
|
|
30388
30595
|
|
|
30389
30596
|
// src/commands/translate/apply-core.ts
|
|
30390
30597
|
import fs8 from "fs/promises";
|
|
30391
|
-
import
|
|
30598
|
+
import path11 from "path";
|
|
30392
30599
|
|
|
30393
30600
|
// ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_u64.js
|
|
30394
30601
|
var fromNumH = (n) => n / 2 ** 32 | 0;
|
|
@@ -30957,7 +31164,7 @@ async function applyTasks(input2) {
|
|
|
30957
31164
|
}
|
|
30958
31165
|
const baseKeys = new Set(getKeys(source.data));
|
|
30959
31166
|
const target = targetFileFor(sourceFile, dstLocale, files, scope);
|
|
30960
|
-
const targetPath =
|
|
31167
|
+
const targetPath = path11.join(root, ...target.relativePath.split("/"));
|
|
30961
31168
|
const existing = target.existing ? await readTranslationFile(target.existing.absolutePath, format) : void 0;
|
|
30962
31169
|
if (existing?.error !== void 0) {
|
|
30963
31170
|
for (const task of tasks) refuse(task, "target_unreadable");
|
|
@@ -31012,7 +31219,7 @@ async function applyTasks(input2) {
|
|
|
31012
31219
|
async function writeTranslationFile(input2) {
|
|
31013
31220
|
const { targetPath, data } = input2;
|
|
31014
31221
|
const model = input2.existingContent ?? input2.sourceContent;
|
|
31015
|
-
await fs8.mkdir(
|
|
31222
|
+
await fs8.mkdir(path11.dirname(targetPath), { recursive: true });
|
|
31016
31223
|
if (input2.format === "json") {
|
|
31017
31224
|
await fs8.writeFile(
|
|
31018
31225
|
targetPath,
|
|
@@ -31723,10 +31930,13 @@ var ERROR_CODES = {
|
|
|
31723
31930
|
login_failed: EXIT_CODES.failure,
|
|
31724
31931
|
repository_path_claimed: EXIT_CODES.failure,
|
|
31725
31932
|
repository_path_in_use: EXIT_CODES.failure,
|
|
31933
|
+
key_not_found: EXIT_CODES.failure,
|
|
31934
|
+
placeholder_mismatch: EXIT_CODES.failure,
|
|
31726
31935
|
// 2: usage
|
|
31727
31936
|
usage: EXIT_CODES.usage,
|
|
31728
31937
|
unsupported_locale: EXIT_CODES.usage,
|
|
31729
31938
|
ambiguous_project: EXIT_CODES.usage,
|
|
31939
|
+
ambiguous_term: EXIT_CODES.usage,
|
|
31730
31940
|
// 3: not authenticated, or not allowed
|
|
31731
31941
|
not_authenticated: EXIT_CODES.auth,
|
|
31732
31942
|
token_expired: EXIT_CODES.auth,
|
|
@@ -31806,10 +32016,10 @@ function serverMessage(message) {
|
|
|
31806
32016
|
}
|
|
31807
32017
|
function argumentError(message) {
|
|
31808
32018
|
if (!message.includes("ArgumentValidationError")) return null;
|
|
31809
|
-
const
|
|
32019
|
+
const path12 = /Path: \.?(\S+)/.exec(message)?.[1];
|
|
31810
32020
|
const value = /Value: (.+)/.exec(message)?.[1];
|
|
31811
32021
|
return new UsageError(
|
|
31812
|
-
|
|
32022
|
+
path12 && value ? `TolkBot refused ${value} as ${path12}.` : "TolkBot refused the arguments.",
|
|
31813
32023
|
"Check the id or value passed; list ids with the command's list subcommand."
|
|
31814
32024
|
);
|
|
31815
32025
|
}
|
|
@@ -32422,6 +32632,50 @@ Examples:
|
|
|
32422
32632
|
import { Command as Command5 } from "commander";
|
|
32423
32633
|
import { z as z7 } from "zod/v4";
|
|
32424
32634
|
|
|
32635
|
+
// src/commands/learnings/add.ts
|
|
32636
|
+
async function addLearning(ctx, opts) {
|
|
32637
|
+
const { project } = ctx;
|
|
32638
|
+
if (sameLocale(opts.locale, project.baseLanguage)) {
|
|
32639
|
+
throw new UsageError(
|
|
32640
|
+
`${opts.locale} is the project's base language.`,
|
|
32641
|
+
"A learning is about translating into another language: pass that one as --locale."
|
|
32642
|
+
);
|
|
32643
|
+
}
|
|
32644
|
+
if (opts.translation !== void 0 && opts.source === void 0) {
|
|
32645
|
+
throw new UsageError(
|
|
32646
|
+
"--translation needs --source: the text it translates."
|
|
32647
|
+
);
|
|
32648
|
+
}
|
|
32649
|
+
const dstLocale = governanceLocale(project, opts.locale);
|
|
32650
|
+
const id = await ctx.client.mutation(api.learnings.createForProject, {
|
|
32651
|
+
organizationId: project.organizationId,
|
|
32652
|
+
projectId: project._id,
|
|
32653
|
+
srcLocale: project.baseLanguage,
|
|
32654
|
+
dstLocale,
|
|
32655
|
+
sourceText: opts.source ?? "",
|
|
32656
|
+
suggestedTranslation: opts.translation ?? "",
|
|
32657
|
+
reasoning: opts.note,
|
|
32658
|
+
feedbackType: opts.translation !== void 0 ? "incorrect-term" : "other"
|
|
32659
|
+
});
|
|
32660
|
+
return {
|
|
32661
|
+
id,
|
|
32662
|
+
srcLocale: project.baseLanguage,
|
|
32663
|
+
dstLocale,
|
|
32664
|
+
sourceText: opts.source ?? null,
|
|
32665
|
+
suggestedTranslation: opts.translation ?? null,
|
|
32666
|
+
note: opts.note
|
|
32667
|
+
};
|
|
32668
|
+
}
|
|
32669
|
+
async function runLearningsAdd(opts) {
|
|
32670
|
+
const ctx = await loadPlanContext(process.cwd());
|
|
32671
|
+
const learning = await addLearning(ctx, opts);
|
|
32672
|
+
if (opts.json) jsonSuccess(learning);
|
|
32673
|
+
console.log(
|
|
32674
|
+
`\u2713 Learning saved for ${learning.srcLocale} \u2192 ${learning.dstLocale}`
|
|
32675
|
+
);
|
|
32676
|
+
console.log(` ID: ${learning.id}`);
|
|
32677
|
+
}
|
|
32678
|
+
|
|
32425
32679
|
// src/commands/learnings/archive.tsx
|
|
32426
32680
|
import { Spinner as Spinner10 } from "@inkjs/ui";
|
|
32427
32681
|
import { Box as Box13, render as render12, Text as Text13 } from "ink";
|
|
@@ -32504,15 +32758,14 @@ function LearningsListUI({ state }) {
|
|
|
32504
32758
|
] }),
|
|
32505
32759
|
l.status === "archived" && /* @__PURE__ */ jsx14(Text14, { color: "yellow", children: "[archived]" })
|
|
32506
32760
|
] }),
|
|
32507
|
-
/* @__PURE__ */ jsx14(Box14, { paddingLeft: 2, children: /* @__PURE__ */ jsxs14(Text14, { children: [
|
|
32761
|
+
l.sourceText ? /* @__PURE__ */ jsx14(Box14, { paddingLeft: 2, children: /* @__PURE__ */ jsxs14(Text14, { children: [
|
|
32508
32762
|
'"',
|
|
32509
32763
|
l.sourceText,
|
|
32510
|
-
'"
|
|
32511
|
-
l.suggestedTranslation
|
|
32512
|
-
|
|
32513
|
-
] }) }),
|
|
32764
|
+
'"',
|
|
32765
|
+
l.suggestedTranslation ? ` \u2192 "${l.suggestedTranslation}"` : ""
|
|
32766
|
+
] }) }) : /* @__PURE__ */ jsx14(Box14, { paddingLeft: 2, children: /* @__PURE__ */ jsx14(Text14, { children: "Every string" }) }),
|
|
32514
32767
|
l.reasoning && /* @__PURE__ */ jsx14(Box14, { paddingLeft: 2, children: /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
32515
|
-
"
|
|
32768
|
+
"Note: ",
|
|
32516
32769
|
l.reasoning
|
|
32517
32770
|
] }) }),
|
|
32518
32771
|
/* @__PURE__ */ jsx14(Box14, { paddingLeft: 2, children: /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
@@ -32618,9 +32871,9 @@ function LearningShowUI({ state }) {
|
|
|
32618
32871
|
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
32619
32872
|
/* @__PURE__ */ jsxs16(Box16, { gap: 1, children: [
|
|
32620
32873
|
/* @__PURE__ */ jsx16(Text16, { bold: true, children: "Source text:" }),
|
|
32621
|
-
/* @__PURE__ */ jsx16(Text16, { children: learning.sourceText })
|
|
32874
|
+
/* @__PURE__ */ jsx16(Text16, { children: learning.sourceText || "(every string)" })
|
|
32622
32875
|
] }),
|
|
32623
|
-
/* @__PURE__ */ jsxs16(Box16, { gap: 1, children: [
|
|
32876
|
+
learning.suggestedTranslation && /* @__PURE__ */ jsxs16(Box16, { gap: 1, children: [
|
|
32624
32877
|
/* @__PURE__ */ jsx16(Text16, { bold: true, children: "Suggested translation:" }),
|
|
32625
32878
|
/* @__PURE__ */ jsx16(Text16, { children: learning.suggestedTranslation })
|
|
32626
32879
|
] }),
|
|
@@ -32637,7 +32890,7 @@ function LearningShowUI({ state }) {
|
|
|
32637
32890
|
/* @__PURE__ */ jsx16(Text16, { children: learning.feedbackType })
|
|
32638
32891
|
] }),
|
|
32639
32892
|
/* @__PURE__ */ jsxs16(Box16, { gap: 1, children: [
|
|
32640
|
-
/* @__PURE__ */ jsx16(Text16, { bold: true, children: "
|
|
32893
|
+
/* @__PURE__ */ jsx16(Text16, { bold: true, children: "Note:" }),
|
|
32641
32894
|
/* @__PURE__ */ jsx16(Text16, { children: learning.reasoning })
|
|
32642
32895
|
] }),
|
|
32643
32896
|
/* @__PURE__ */ jsxs16(Box16, { gap: 1, children: [
|
|
@@ -32699,7 +32952,7 @@ async function runLearningsShow(opts) {
|
|
|
32699
32952
|
|
|
32700
32953
|
// src/commands/learnings/index.ts
|
|
32701
32954
|
var learningsCommand = new Command5("learnings").description(
|
|
32702
|
-
"
|
|
32955
|
+
"Add, view and manage translation learnings"
|
|
32703
32956
|
);
|
|
32704
32957
|
learningsCommand.command("list").description("List translation learnings for the current project").option("--status <status>", "Filter by status (active or archived)").option("--limit <n>", "Max number of results", (v) => Number(v), 100).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
32705
32958
|
const schema = z7.object({
|
|
@@ -32710,6 +32963,25 @@ learningsCommand.command("list").description("List translation learnings for the
|
|
|
32710
32963
|
const parsed = validateOrExit(schema, opts);
|
|
32711
32964
|
await runLearningsList(parsed);
|
|
32712
32965
|
});
|
|
32966
|
+
learningsCommand.command("add").description(
|
|
32967
|
+
"Tell TolkBot how to translate for this project: a note, optionally about a source text"
|
|
32968
|
+
).requiredOption("--locale <locale>", "Target locale the learning is for").requiredOption(
|
|
32969
|
+
"--note <text>",
|
|
32970
|
+
'What to do and why, e.g. "Address the reader as du, never De"'
|
|
32971
|
+
).option(
|
|
32972
|
+
"--source <text>",
|
|
32973
|
+
"Source text the note is about; without it the note applies to every string"
|
|
32974
|
+
).option("--translation <text>", "The translation to prefer for --source").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
32975
|
+
const schema = z7.object({
|
|
32976
|
+
locale: z7.string().min(1),
|
|
32977
|
+
note: z7.string().trim().min(1),
|
|
32978
|
+
source: z7.string().trim().min(1).optional(),
|
|
32979
|
+
translation: z7.string().trim().min(1).optional(),
|
|
32980
|
+
json: z7.boolean().optional()
|
|
32981
|
+
});
|
|
32982
|
+
const parsed = validateOrExit(schema, opts);
|
|
32983
|
+
await runLearningsAdd(parsed);
|
|
32984
|
+
});
|
|
32713
32985
|
learningsCommand.command("show").description("Show details of a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
32714
32986
|
const schema = z7.object({
|
|
32715
32987
|
id: z7.string().min(1),
|
|
@@ -32735,1583 +33007,1694 @@ learningsCommand.command("remove").description("Permanently remove a learning").
|
|
|
32735
33007
|
await runLearningsRemove(parsed);
|
|
32736
33008
|
});
|
|
32737
33009
|
|
|
32738
|
-
// src/commands/
|
|
33010
|
+
// src/commands/set/index.ts
|
|
32739
33011
|
import { Command as Command6 } from "commander";
|
|
32740
33012
|
import { z as z8 } from "zod/v4";
|
|
32741
33013
|
|
|
32742
|
-
// src/
|
|
32743
|
-
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
|
|
32747
|
-
function fullKey4(namespace, key) {
|
|
32748
|
-
return namespace ? `${namespace}.${key}` : key;
|
|
33014
|
+
// ../ai/src/validators/placeholders.ts
|
|
33015
|
+
var IDENTIFIER = /^[A-Za-z0-9_$][A-Za-z0-9_$.[\]-]*$/;
|
|
33016
|
+
var PRINTF_CONV = "sdiufFeEgGxXocpn%@";
|
|
33017
|
+
function isIdentifier(s) {
|
|
33018
|
+
return IDENTIFIER.test(s);
|
|
32749
33019
|
}
|
|
32750
|
-
function
|
|
32751
|
-
|
|
33020
|
+
function matchBrace(s, open3) {
|
|
33021
|
+
let depth = 0;
|
|
33022
|
+
for (let i = open3; i < s.length; i++) {
|
|
33023
|
+
if (s[i] === "{") depth++;
|
|
33024
|
+
else if (s[i] === "}") {
|
|
33025
|
+
depth--;
|
|
33026
|
+
if (depth === 0) return i;
|
|
33027
|
+
}
|
|
33028
|
+
}
|
|
33029
|
+
return -1;
|
|
32752
33030
|
}
|
|
32753
|
-
function
|
|
32754
|
-
const
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
33031
|
+
function splitTopLevel(s, limit) {
|
|
33032
|
+
const parts = [];
|
|
33033
|
+
let depth = 0;
|
|
33034
|
+
let start = 0;
|
|
33035
|
+
for (let i = 0; i < s.length; i++) {
|
|
33036
|
+
const c = s[i];
|
|
33037
|
+
if (c === "{") depth++;
|
|
33038
|
+
else if (c === "}") depth--;
|
|
33039
|
+
else if (c === "," && depth === 0 && parts.length < limit - 1) {
|
|
33040
|
+
parts.push(s.slice(start, i));
|
|
33041
|
+
start = i + 1;
|
|
33042
|
+
}
|
|
33043
|
+
}
|
|
33044
|
+
parts.push(s.slice(start));
|
|
33045
|
+
return parts;
|
|
32758
33046
|
}
|
|
32759
|
-
function
|
|
32760
|
-
const
|
|
32761
|
-
|
|
32762
|
-
)
|
|
32763
|
-
|
|
32764
|
-
|
|
33047
|
+
function parseBranches(s) {
|
|
33048
|
+
const branches = [];
|
|
33049
|
+
let i = 0;
|
|
33050
|
+
while (i < s.length) {
|
|
33051
|
+
while (i < s.length && /\s/.test(s[i] ?? "")) i++;
|
|
33052
|
+
if (i >= s.length) break;
|
|
33053
|
+
const keyStart = i;
|
|
33054
|
+
while (i < s.length && !/[\s{]/.test(s[i] ?? "")) i++;
|
|
33055
|
+
const keyword = s.slice(keyStart, i);
|
|
33056
|
+
if (!keyword) break;
|
|
33057
|
+
if (keyword.startsWith("offset:")) {
|
|
33058
|
+
branches.push({ keyword, message: "" });
|
|
33059
|
+
continue;
|
|
33060
|
+
}
|
|
33061
|
+
while (i < s.length && /\s/.test(s[i] ?? "")) i++;
|
|
33062
|
+
if (s[i] !== "{") break;
|
|
33063
|
+
const close = matchBrace(s, i);
|
|
33064
|
+
if (close === -1) break;
|
|
33065
|
+
branches.push({ keyword, message: s.slice(i + 1, close) });
|
|
33066
|
+
i = close + 1;
|
|
33067
|
+
}
|
|
33068
|
+
return branches;
|
|
32765
33069
|
}
|
|
32766
|
-
function
|
|
32767
|
-
|
|
32768
|
-
|
|
32769
|
-
|
|
32770
|
-
|
|
32771
|
-
|
|
32772
|
-
)
|
|
32773
|
-
|
|
32774
|
-
|
|
32775
|
-
|
|
32776
|
-
|
|
32777
|
-
|
|
33070
|
+
function parseIcuComplex(token) {
|
|
33071
|
+
return token.complex ?? null;
|
|
33072
|
+
}
|
|
33073
|
+
function readBraceToken(s, i) {
|
|
33074
|
+
if (s[i + 1] === "{") {
|
|
33075
|
+
const close2 = s.indexOf("}}", i + 2);
|
|
33076
|
+
if (close2 !== -1) {
|
|
33077
|
+
const inner2 = s.slice(i + 2, close2);
|
|
33078
|
+
if (!inner2.includes("{") && !inner2.includes("}")) {
|
|
33079
|
+
const name2 = (inner2.split(",")[0] ?? "").trim();
|
|
33080
|
+
if (isIdentifier(name2)) {
|
|
33081
|
+
const rest = inner2.slice(inner2.indexOf(name2) + name2.length);
|
|
33082
|
+
const suffix = rest.trim();
|
|
33083
|
+
return {
|
|
33084
|
+
text: s.slice(i, close2 + 2),
|
|
33085
|
+
kind: "mustache",
|
|
33086
|
+
name: name2,
|
|
33087
|
+
signature: `mustache:${name2}${suffix ? `:${suffix.replace(/\s+/g, " ")}` : ""}`,
|
|
33088
|
+
start: i,
|
|
33089
|
+
end: close2 + 2
|
|
33090
|
+
};
|
|
33091
|
+
}
|
|
33092
|
+
}
|
|
33093
|
+
}
|
|
33094
|
+
}
|
|
33095
|
+
const close = matchBrace(s, i);
|
|
33096
|
+
if (close === -1) return null;
|
|
33097
|
+
const inner = s.slice(i + 1, close);
|
|
33098
|
+
const parts = splitTopLevel(inner, 3);
|
|
33099
|
+
const name = (parts[0] ?? "").trim();
|
|
33100
|
+
if (!isIdentifier(name)) return null;
|
|
33101
|
+
const text = s.slice(i, close + 1);
|
|
33102
|
+
const type = (parts[1] ?? "").trim();
|
|
33103
|
+
if (parts.length === 3 && (type === "plural" || type === "select" || type === "selectordinal")) {
|
|
33104
|
+
const branches = parseBranches(parts[2] ?? "");
|
|
33105
|
+
return {
|
|
33106
|
+
text,
|
|
33107
|
+
kind: "icu-complex",
|
|
33108
|
+
name,
|
|
33109
|
+
complex: { name, type, branches },
|
|
33110
|
+
// Branch MESSAGES are translatable, so they stay out of the
|
|
33111
|
+
// signature; their keywords and nested tokens are compared separately.
|
|
33112
|
+
signature: `icu-complex:${name}:${type}:${branches.map((b) => b.keyword).sort().join("|")}`,
|
|
33113
|
+
start: i,
|
|
33114
|
+
end: close + 1
|
|
33115
|
+
};
|
|
33116
|
+
}
|
|
33117
|
+
const normalized = parts.map((p) => p.trim().replace(/\s+/g, " ")).join(", ");
|
|
32778
33118
|
return {
|
|
32779
|
-
|
|
32780
|
-
|
|
32781
|
-
|
|
32782
|
-
|
|
32783
|
-
|
|
32784
|
-
|
|
32785
|
-
|
|
32786
|
-
|
|
32787
|
-
|
|
32788
|
-
|
|
32789
|
-
|
|
32790
|
-
|
|
32791
|
-
|
|
32792
|
-
|
|
32793
|
-
|
|
32794
|
-
|
|
33119
|
+
text,
|
|
33120
|
+
kind: "icu-simple",
|
|
33121
|
+
name,
|
|
33122
|
+
signature: `icu-simple:${normalized}`,
|
|
33123
|
+
start: i,
|
|
33124
|
+
end: close + 1
|
|
33125
|
+
};
|
|
33126
|
+
}
|
|
33127
|
+
function readPercentToken(s, i) {
|
|
33128
|
+
if (s[i + 1] === "{") {
|
|
33129
|
+
const close = s.indexOf("}", i + 2);
|
|
33130
|
+
if (close !== -1) {
|
|
33131
|
+
const name = s.slice(i + 2, close).trim();
|
|
33132
|
+
if (isIdentifier(name)) {
|
|
33133
|
+
return {
|
|
33134
|
+
text: s.slice(i, close + 1),
|
|
33135
|
+
kind: "ruby",
|
|
33136
|
+
name,
|
|
33137
|
+
signature: `ruby:${name}`,
|
|
33138
|
+
start: i,
|
|
33139
|
+
end: close + 1
|
|
33140
|
+
};
|
|
32795
33141
|
}
|
|
32796
|
-
}
|
|
33142
|
+
}
|
|
33143
|
+
}
|
|
33144
|
+
let j = i + 1;
|
|
33145
|
+
if (j >= s.length || s[j] === " ") return null;
|
|
33146
|
+
const rest = s.slice(j);
|
|
33147
|
+
const argnum = /^(\d+)\$/.exec(rest);
|
|
33148
|
+
if (argnum) j += argnum[0].length;
|
|
33149
|
+
while (j < s.length && "-+0#".includes(s[j] ?? "")) j++;
|
|
33150
|
+
while (j < s.length && /\d/.test(s[j] ?? "")) j++;
|
|
33151
|
+
if (s[j] === ".") {
|
|
33152
|
+
j++;
|
|
33153
|
+
while (j < s.length && /\d/.test(s[j] ?? "")) j++;
|
|
33154
|
+
}
|
|
33155
|
+
const conv = s[j];
|
|
33156
|
+
if (!conv || !PRINTF_CONV.includes(conv)) return null;
|
|
33157
|
+
const text = s.slice(i, j + 1);
|
|
33158
|
+
return {
|
|
33159
|
+
text,
|
|
33160
|
+
kind: "printf",
|
|
33161
|
+
name: argnum?.[1] ?? "",
|
|
33162
|
+
signature: `printf:${text}`,
|
|
33163
|
+
start: i,
|
|
33164
|
+
end: j + 1
|
|
32797
33165
|
};
|
|
32798
33166
|
}
|
|
32799
|
-
|
|
32800
|
-
|
|
33167
|
+
var TAG = /^<\/?[A-Za-z0-9][A-Za-z0-9._-]*\s*\/?>/;
|
|
33168
|
+
function readTagToken(s, i) {
|
|
33169
|
+
const m = TAG.exec(s.slice(i));
|
|
33170
|
+
if (!m) return null;
|
|
33171
|
+
const text = m[0];
|
|
33172
|
+
return {
|
|
33173
|
+
text,
|
|
33174
|
+
kind: "tag",
|
|
33175
|
+
name: text.replace(/[<>/\s]/g, ""),
|
|
33176
|
+
signature: `tag:${text.replace(/\s+/g, "")}`,
|
|
33177
|
+
start: i,
|
|
33178
|
+
end: i + text.length
|
|
33179
|
+
};
|
|
32801
33180
|
}
|
|
32802
|
-
|
|
32803
|
-
|
|
32804
|
-
"
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
32808
|
-
|
|
32809
|
-
|
|
32810
|
-
|
|
32811
|
-
(
|
|
32812
|
-
|
|
33181
|
+
function readNestingToken(s, i) {
|
|
33182
|
+
if (!s.startsWith("$t(", i)) return null;
|
|
33183
|
+
const close = s.indexOf(")", i + 3);
|
|
33184
|
+
if (close === -1) return null;
|
|
33185
|
+
const text = s.slice(i, close + 1);
|
|
33186
|
+
return {
|
|
33187
|
+
text,
|
|
33188
|
+
kind: "nesting",
|
|
33189
|
+
name: s.slice(i + 3, close).trim(),
|
|
33190
|
+
signature: `nesting:${text.replace(/\s+/g, "")}`,
|
|
33191
|
+
start: i,
|
|
33192
|
+
end: close + 1
|
|
33193
|
+
};
|
|
32813
33194
|
}
|
|
32814
|
-
function
|
|
32815
|
-
const
|
|
32816
|
-
|
|
32817
|
-
|
|
32818
|
-
|
|
32819
|
-
|
|
32820
|
-
|
|
32821
|
-
|
|
32822
|
-
|
|
32823
|
-
|
|
32824
|
-
|
|
32825
|
-
|
|
32826
|
-
|
|
32827
|
-
|
|
32828
|
-
|
|
32829
|
-
|
|
32830
|
-
` ${language.keptForPerson.total} changed but kept for a person (${reasons(language.keptForPerson)})`
|
|
32831
|
-
);
|
|
32832
|
-
}
|
|
32833
|
-
if (language.untracked > 0) {
|
|
32834
|
-
lines.push(
|
|
32835
|
-
` ${language.untracked} untracked: TolkBot has no record of them`
|
|
32836
|
-
);
|
|
32837
|
-
}
|
|
32838
|
-
if (language.pullRequest) {
|
|
32839
|
-
lines.push(` Pull request: ${language.pullRequest.url}`);
|
|
33195
|
+
function extractPlaceholders(s) {
|
|
33196
|
+
const tokens = [];
|
|
33197
|
+
let i = 0;
|
|
33198
|
+
while (i < s.length) {
|
|
33199
|
+
const c = s[i];
|
|
33200
|
+
let token = null;
|
|
33201
|
+
if (c === "{") token = readBraceToken(s, i);
|
|
33202
|
+
else if (c === "%") token = readPercentToken(s, i);
|
|
33203
|
+
else if (c === "<") token = readTagToken(s, i);
|
|
33204
|
+
else if (c === "$") token = readNestingToken(s, i);
|
|
33205
|
+
if (token) {
|
|
33206
|
+
tokens.push(token);
|
|
33207
|
+
i = token.end;
|
|
33208
|
+
continue;
|
|
33209
|
+
}
|
|
33210
|
+
i++;
|
|
32840
33211
|
}
|
|
32841
|
-
|
|
32842
|
-
|
|
32843
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
)
|
|
32855
|
-
);
|
|
32856
|
-
list(
|
|
32857
|
-
"Kept for a person",
|
|
32858
|
-
keys.keptForPerson.map(
|
|
32859
|
-
(k) => `${k.key} (${REASON_WORDS[k.reason]})`
|
|
32860
|
-
)
|
|
32861
|
-
);
|
|
32862
|
-
list("Untracked", keys.untracked);
|
|
33212
|
+
return tokens;
|
|
33213
|
+
}
|
|
33214
|
+
function countHashes(s) {
|
|
33215
|
+
return (s.match(/#/g) ?? []).length;
|
|
33216
|
+
}
|
|
33217
|
+
function subtract(a, b) {
|
|
33218
|
+
const pool = /* @__PURE__ */ new Map();
|
|
33219
|
+
for (const t of b) pool.set(t.signature, (pool.get(t.signature) ?? 0) + 1);
|
|
33220
|
+
const out = [];
|
|
33221
|
+
for (const t of a) {
|
|
33222
|
+
const left = pool.get(t.signature) ?? 0;
|
|
33223
|
+
if (left > 0) pool.set(t.signature, left - 1);
|
|
33224
|
+
else out.push(t);
|
|
32863
33225
|
}
|
|
32864
|
-
return
|
|
33226
|
+
return out;
|
|
32865
33227
|
}
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32869
|
-
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
error62.code
|
|
33228
|
+
function compareIcuBranches(srcToken, outToken, problems, missing, extra) {
|
|
33229
|
+
const src = parseIcuComplex(srcToken);
|
|
33230
|
+
const out = parseIcuComplex(outToken);
|
|
33231
|
+
if (!src || !out) return;
|
|
33232
|
+
const outByKeyword = new Map(out.branches.map((b) => [b.keyword, b]));
|
|
33233
|
+
for (const branch of src.branches) {
|
|
33234
|
+
const counterpart = outByKeyword.get(branch.keyword);
|
|
33235
|
+
if (!counterpart) continue;
|
|
33236
|
+
const srcHashes = countHashes(branch.message);
|
|
33237
|
+
const outHashes = countHashes(counterpart.message);
|
|
33238
|
+
if (srcHashes !== outHashes) {
|
|
33239
|
+
problems.push(
|
|
33240
|
+
`{${src.name}} branch "${branch.keyword}" has ${srcHashes} "#" in the source but ${outHashes} in the translation`
|
|
32880
33241
|
);
|
|
33242
|
+
if (srcHashes > outHashes) missing.push("#");
|
|
33243
|
+
else extra.push("#");
|
|
32881
33244
|
}
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
32886
|
-
|
|
32887
|
-
|
|
32888
|
-
|
|
32889
|
-
|
|
32890
|
-
/* @__PURE__ */ jsxs17(Text17, { children: [
|
|
32891
|
-
" Reading the status of ",
|
|
32892
|
-
project.name,
|
|
32893
|
-
"\u2026"
|
|
32894
|
-
] })
|
|
32895
|
-
] })
|
|
32896
|
-
);
|
|
32897
|
-
const languages = [];
|
|
32898
|
-
let credits;
|
|
32899
|
-
try {
|
|
32900
|
-
const pullRequests = locales.length === 0 ? [] : await ctx.client.query(api.prJobs.listByProject, {
|
|
32901
|
-
organizationId: project.organizationId,
|
|
32902
|
-
projectId: project._id
|
|
32903
|
-
});
|
|
32904
|
-
for (const locale of locales) {
|
|
32905
|
-
const plan = await planForLocale(ctx, srcLocale, locale);
|
|
32906
|
-
languages.push(
|
|
32907
|
-
languageStatus(locale, plan, pullRequests, {
|
|
32908
|
-
keys: opts.keys
|
|
32909
|
-
})
|
|
33245
|
+
const nested = compareStrict(branch.message, counterpart.message);
|
|
33246
|
+
if (!nested.ok) {
|
|
33247
|
+
missing.push(...nested.missing);
|
|
33248
|
+
extra.push(...nested.extra);
|
|
33249
|
+
problems.push(
|
|
33250
|
+
...nested.problems.map(
|
|
33251
|
+
(p) => `inside {${src.name}} branch "${branch.keyword}": ${p}`
|
|
33252
|
+
)
|
|
32910
33253
|
);
|
|
32911
33254
|
}
|
|
32912
|
-
credits = await readCreditBalance(ctx);
|
|
32913
|
-
} finally {
|
|
32914
|
-
progress.unmount();
|
|
32915
33255
|
}
|
|
32916
|
-
|
|
32917
|
-
|
|
32918
|
-
|
|
32919
|
-
|
|
32920
|
-
|
|
32921
|
-
|
|
32922
|
-
|
|
32923
|
-
|
|
32924
|
-
|
|
32925
|
-
|
|
32926
|
-
|
|
32927
|
-
};
|
|
32928
|
-
const outOfDate = languages.some(needsTranslation);
|
|
32929
|
-
const exitCode = opts.exitCode && outOfDate ? 1 : 0;
|
|
32930
|
-
if (opts.json) {
|
|
32931
|
-
if (exitCode === 0) jsonSuccess(payload);
|
|
32932
|
-
jsonResult(payload, exitCode);
|
|
32933
|
-
return;
|
|
33256
|
+
}
|
|
33257
|
+
function compareStrict(src, out) {
|
|
33258
|
+
const srcTokens = extractPlaceholders(src);
|
|
33259
|
+
const outTokens = extractPlaceholders(out);
|
|
33260
|
+
const missingTokens = subtract(srcTokens, outTokens);
|
|
33261
|
+
const extraTokens = subtract(outTokens, srcTokens);
|
|
33262
|
+
const missing = missingTokens.map((t) => t.text);
|
|
33263
|
+
const extra = extraTokens.map((t) => t.text);
|
|
33264
|
+
const problems = [];
|
|
33265
|
+
for (const t of missingTokens) {
|
|
33266
|
+
problems.push(`"${t.text}" is missing from the translation`);
|
|
32934
33267
|
}
|
|
32935
|
-
const
|
|
32936
|
-
|
|
32937
|
-
|
|
32938
|
-
`Base language ${p.baseLanguage}, files ${p.pattern}, auto-create pull requests ${p.autoCreatePR ? "on" : "off"}`
|
|
32939
|
-
);
|
|
32940
|
-
console.log(
|
|
32941
|
-
credits === null ? "Credits left: could not be read" : `Credits left: ${credits}`
|
|
32942
|
-
);
|
|
32943
|
-
console.log("");
|
|
32944
|
-
if (languages.length === 0) {
|
|
32945
|
-
console.log(
|
|
32946
|
-
`No target languages configured. Add them in the project settings at ${p.url}/settings.`
|
|
33268
|
+
for (const t of extraTokens) {
|
|
33269
|
+
problems.push(
|
|
33270
|
+
`"${t.text}" appears in the translation but not the source`
|
|
32947
33271
|
);
|
|
32948
33272
|
}
|
|
32949
|
-
|
|
32950
|
-
|
|
33273
|
+
const matchedOut = /* @__PURE__ */ new Map();
|
|
33274
|
+
for (const t of outTokens) {
|
|
33275
|
+
if (t.kind !== "icu-complex") continue;
|
|
33276
|
+
const bucket = matchedOut.get(t.signature) ?? [];
|
|
33277
|
+
bucket.push(t);
|
|
33278
|
+
matchedOut.set(t.signature, bucket);
|
|
32951
33279
|
}
|
|
32952
|
-
|
|
32953
|
-
|
|
32954
|
-
|
|
32955
|
-
)
|
|
33280
|
+
for (const t of srcTokens) {
|
|
33281
|
+
if (t.kind !== "icu-complex") continue;
|
|
33282
|
+
const counterpart = matchedOut.get(t.signature)?.shift();
|
|
33283
|
+
if (counterpart) {
|
|
33284
|
+
compareIcuBranches(t, counterpart, problems, missing, extra);
|
|
33285
|
+
}
|
|
32956
33286
|
}
|
|
32957
|
-
|
|
33287
|
+
return {
|
|
33288
|
+
ok: problems.length === 0,
|
|
33289
|
+
missing,
|
|
33290
|
+
extra,
|
|
33291
|
+
problems,
|
|
33292
|
+
incompletePlurals: []
|
|
33293
|
+
};
|
|
32958
33294
|
}
|
|
32959
|
-
|
|
32960
|
-
|
|
32961
|
-
|
|
32962
|
-
|
|
32963
|
-
|
|
32964
|
-
"--exit-code",
|
|
32965
|
-
"Exit 1 when any language has missing or changed keys"
|
|
32966
|
-
).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
32967
|
-
const schema = z8.object({
|
|
32968
|
-
keys: z8.boolean().optional(),
|
|
32969
|
-
exitCode: z8.boolean().optional(),
|
|
32970
|
-
json: z8.boolean().optional()
|
|
32971
|
-
});
|
|
32972
|
-
await runStatus(validateOrExit(schema, opts));
|
|
32973
|
-
});
|
|
32974
|
-
|
|
32975
|
-
// src/commands/telemetry.ts
|
|
32976
|
-
import { Command as Command7 } from "commander";
|
|
32977
|
-
import { z as z9 } from "zod/v4";
|
|
32978
|
-
var REASONS = {
|
|
32979
|
-
env: `disabled by $${TELEMETRY_ENV_VAR}`,
|
|
32980
|
-
"do-not-track": `disabled by $${DO_NOT_TRACK_ENV_VAR}`,
|
|
32981
|
-
preference: "disabled with `tolkbot telemetry off`",
|
|
32982
|
-
"no-key": "disabled: this build has no telemetry key",
|
|
32983
|
-
default: "enabled"
|
|
32984
|
-
};
|
|
32985
|
-
async function printStatus(json2) {
|
|
32986
|
-
const state = await telemetryState();
|
|
32987
|
-
if (json2) {
|
|
32988
|
-
jsonResult({ ...state, preferenceFile: getPreferencePath() }, 0);
|
|
32989
|
-
return;
|
|
33295
|
+
function pluralArgument(token) {
|
|
33296
|
+
const complex = token.complex;
|
|
33297
|
+
if (token.kind !== "icu-complex" || !complex) return null;
|
|
33298
|
+
if (complex.type !== "plural" && complex.type !== "selectordinal") {
|
|
33299
|
+
return null;
|
|
32990
33300
|
}
|
|
32991
|
-
|
|
32992
|
-
|
|
32993
|
-
|
|
32994
|
-
|
|
32995
|
-
|
|
32996
|
-
|
|
33301
|
+
return {
|
|
33302
|
+
token,
|
|
33303
|
+
name: complex.name,
|
|
33304
|
+
type: complex.type,
|
|
33305
|
+
branches: complex.branches
|
|
33306
|
+
};
|
|
32997
33307
|
}
|
|
32998
|
-
|
|
32999
|
-
|
|
33000
|
-
|
|
33001
|
-
|
|
33002
|
-
|
|
33003
|
-
|
|
33004
|
-
|
|
33005
|
-
);
|
|
33006
|
-
await printStatus(parsed.json);
|
|
33007
|
-
});
|
|
33008
|
-
telemetryCommand.command("off").description("Stop reporting usage from this machine").action(async () => {
|
|
33009
|
-
await setTelemetryPreference(false);
|
|
33010
|
-
console.log(
|
|
33011
|
-
`Telemetry is off. Preference saved to ${getPreferencePath()}.`
|
|
33308
|
+
function isPluralToken(token) {
|
|
33309
|
+
return pluralArgument(token) !== null;
|
|
33310
|
+
}
|
|
33311
|
+
function withoutPlurals(s, plurals) {
|
|
33312
|
+
let out = s;
|
|
33313
|
+
const byPositionDesc = [...plurals].sort(
|
|
33314
|
+
(a, b) => b.token.start - a.token.start
|
|
33012
33315
|
);
|
|
33013
|
-
|
|
33014
|
-
|
|
33015
|
-
await setTelemetryPreference(true);
|
|
33016
|
-
await printStatus(false);
|
|
33017
|
-
});
|
|
33018
|
-
|
|
33019
|
-
// src/commands/translate/index.ts
|
|
33020
|
-
import { Command as Command8 } from "commander";
|
|
33021
|
-
import { z as z10 } from "zod/v4";
|
|
33022
|
-
|
|
33023
|
-
// src/commands/translate/adopt.tsx
|
|
33024
|
-
var STAMP_CHUNK2 = 200;
|
|
33025
|
-
async function adoptLocale(ctx, srcLocale, dstLocale) {
|
|
33026
|
-
const plan = await planForLocale(ctx, srcLocale, dstLocale);
|
|
33027
|
-
const locale = governanceLocale(ctx.project, dstLocale);
|
|
33028
|
-
const byTemplate = /* @__PURE__ */ new Map();
|
|
33029
|
-
for (const key of plan.untrackedKeys) {
|
|
33030
|
-
const group = byTemplate.get(key.pathTemplate) ?? [];
|
|
33031
|
-
group.push(key);
|
|
33032
|
-
byTemplate.set(key.pathTemplate, group);
|
|
33033
|
-
}
|
|
33034
|
-
const outcome = {
|
|
33035
|
-
dstLocale,
|
|
33036
|
-
untracked: plan.untrackedKeys.length,
|
|
33037
|
-
adopted: 0,
|
|
33038
|
-
skipped: 0
|
|
33039
|
-
};
|
|
33040
|
-
for (const [pathTemplate, keys] of byTemplate) {
|
|
33041
|
-
for (let i = 0; i < keys.length; i += STAMP_CHUNK2) {
|
|
33042
|
-
const result = await ctx.client.mutation(
|
|
33043
|
-
api.localeKeyStates.adoptUntrackedForCli,
|
|
33044
|
-
{
|
|
33045
|
-
organizationId: ctx.project.organizationId,
|
|
33046
|
-
projectId: ctx.project._id,
|
|
33047
|
-
locale,
|
|
33048
|
-
pathTemplate,
|
|
33049
|
-
entries: keys.slice(i, i + STAMP_CHUNK2).map((key) => ({
|
|
33050
|
-
key: key.fileKey,
|
|
33051
|
-
namespace: key.namespace,
|
|
33052
|
-
baseValue: key.src,
|
|
33053
|
-
translatedValue: key.dst
|
|
33054
|
-
}))
|
|
33055
|
-
}
|
|
33056
|
-
);
|
|
33057
|
-
outcome.adopted += result.adopted;
|
|
33058
|
-
outcome.skipped += result.skipped;
|
|
33059
|
-
}
|
|
33316
|
+
for (const p of byPositionDesc) {
|
|
33317
|
+
out = out.slice(0, p.token.start) + " " + out.slice(p.token.end);
|
|
33060
33318
|
}
|
|
33061
|
-
return
|
|
33319
|
+
return out;
|
|
33062
33320
|
}
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33066
|
-
|
|
33067
|
-
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
) : new UsageError(
|
|
33074
|
-
"Either --dst <locale> or --all must be provided."
|
|
33075
|
-
);
|
|
33076
|
-
}
|
|
33077
|
-
const outcomes2 = [];
|
|
33078
|
-
for (const dst of locales)
|
|
33079
|
-
outcomes2.push(await adoptLocale(ctx, src, dst));
|
|
33080
|
-
track("cli_translate_completed", {
|
|
33081
|
-
mode: "adopt",
|
|
33082
|
-
key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
|
|
33083
|
-
locale_count: outcomes2.length,
|
|
33084
|
-
job_count: 0,
|
|
33085
|
-
json: opts.json === true
|
|
33086
|
-
});
|
|
33087
|
-
return outcomes2;
|
|
33088
|
-
};
|
|
33089
|
-
if (opts.json) {
|
|
33090
|
-
jsonSuccess({ adopted: await run() });
|
|
33321
|
+
function fixedKeywords(p) {
|
|
33322
|
+
return p.branches.map((b) => b.keyword).filter((k) => !PLURAL_CATEGORIES.has(k)).sort();
|
|
33323
|
+
}
|
|
33324
|
+
function comparePluralPair(src, out, dstLocale, result) {
|
|
33325
|
+
const srcFixed = fixedKeywords(src).join(" ");
|
|
33326
|
+
const outFixed = fixedKeywords(out).join(" ");
|
|
33327
|
+
if (srcFixed !== outFixed) {
|
|
33328
|
+
result.problems.push(
|
|
33329
|
+
`{${src.name}} has branches [${srcFixed}] in the source but [${outFixed}] in the translation`
|
|
33330
|
+
);
|
|
33091
33331
|
}
|
|
33092
|
-
const
|
|
33093
|
-
|
|
33094
|
-
|
|
33095
|
-
|
|
33332
|
+
const pluralType = src.type === "plural" ? "cardinal" : "ordinal";
|
|
33333
|
+
const outKeywords = new Set(out.branches.map((b) => b.keyword));
|
|
33334
|
+
const lacking = requiredPluralCategories(dstLocale, pluralType).filter(
|
|
33335
|
+
(c) => !outKeywords.has(c)
|
|
33336
|
+
);
|
|
33337
|
+
if (lacking.length > 0) {
|
|
33338
|
+
result.incompletePlurals.push(
|
|
33339
|
+
`{${src.name}} has no ${lacking.map((c) => `"${c}"`).join(", ")} branch, which ${dstLocale} needs`
|
|
33096
33340
|
);
|
|
33097
33341
|
}
|
|
33098
|
-
|
|
33099
|
-
|
|
33100
|
-
|
|
33101
|
-
|
|
33102
|
-
|
|
33103
|
-
|
|
33104
|
-
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
33108
|
-
|
|
33109
|
-
|
|
33110
|
-
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33342
|
+
const srcByKeyword = new Map(src.branches.map((b) => [b.keyword, b]));
|
|
33343
|
+
const srcOther = srcByKeyword.get("other");
|
|
33344
|
+
for (const branch of out.branches) {
|
|
33345
|
+
if (branch.keyword.startsWith("offset:")) continue;
|
|
33346
|
+
const counterpart = srcByKeyword.get(branch.keyword) ?? (PLURAL_CATEGORIES.has(branch.keyword) ? srcOther : void 0);
|
|
33347
|
+
if (!counterpart) continue;
|
|
33348
|
+
const srcHashes = countHashes(counterpart.message);
|
|
33349
|
+
const outHashes = countHashes(branch.message);
|
|
33350
|
+
const sayableInWords = branch.keyword.startsWith("=") || isSingleNumberCategory(dstLocale, pluralType, branch.keyword);
|
|
33351
|
+
if (srcHashes !== outHashes && !(sayableInWords && outHashes <= Math.max(srcHashes, 1))) {
|
|
33352
|
+
result.problems.push(
|
|
33353
|
+
`{${src.name}} branch "${branch.keyword}" has ${outHashes} "#" but the source's "${counterpart.keyword}" has ${srcHashes}`
|
|
33354
|
+
);
|
|
33355
|
+
if (srcHashes > outHashes) result.missing.push("#");
|
|
33356
|
+
else result.extra.push("#");
|
|
33357
|
+
}
|
|
33358
|
+
const nested = compareForLocale(
|
|
33359
|
+
counterpart.message,
|
|
33360
|
+
branch.message,
|
|
33361
|
+
dstLocale
|
|
33362
|
+
);
|
|
33363
|
+
result.missing.push(...nested.missing);
|
|
33364
|
+
result.extra.push(...nested.extra);
|
|
33365
|
+
result.problems.push(
|
|
33366
|
+
...nested.problems.map(
|
|
33367
|
+
(p) => `inside {${src.name}} branch "${branch.keyword}": ${p}`
|
|
33368
|
+
)
|
|
33115
33369
|
);
|
|
33370
|
+
result.incompletePlurals.push(...nested.incompletePlurals);
|
|
33116
33371
|
}
|
|
33117
|
-
return localeFiles;
|
|
33118
33372
|
}
|
|
33119
|
-
function
|
|
33120
|
-
|
|
33121
|
-
|
|
33122
|
-
const
|
|
33123
|
-
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33373
|
+
function compareForLocale(src, out, dstLocale) {
|
|
33374
|
+
const srcPlurals = extractPlaceholders(src).map(pluralArgument).filter((p) => p !== null);
|
|
33375
|
+
const outPlurals = extractPlaceholders(out).map(pluralArgument).filter((p) => p !== null);
|
|
33376
|
+
const result = compareStrict(
|
|
33377
|
+
withoutPlurals(src, srcPlurals),
|
|
33378
|
+
withoutPlurals(out, outPlurals)
|
|
33379
|
+
);
|
|
33380
|
+
const unmatched = [...outPlurals];
|
|
33381
|
+
for (const p of srcPlurals) {
|
|
33382
|
+
const i = unmatched.findIndex(
|
|
33383
|
+
(o) => o.name === p.name && o.type === p.type
|
|
33384
|
+
);
|
|
33385
|
+
const counterpart = i === -1 ? void 0 : unmatched.splice(i, 1)[0];
|
|
33386
|
+
if (!counterpart) {
|
|
33387
|
+
result.missing.push(p.token.text);
|
|
33388
|
+
result.problems.push(
|
|
33389
|
+
`"${p.token.text}" is missing from the translation`
|
|
33390
|
+
);
|
|
33391
|
+
continue;
|
|
33127
33392
|
}
|
|
33393
|
+
comparePluralPair(p, counterpart, dstLocale, result);
|
|
33128
33394
|
}
|
|
33129
|
-
|
|
33130
|
-
|
|
33131
|
-
|
|
33132
|
-
|
|
33133
|
-
|
|
33134
|
-
if (key.startsWith(`${fileName}.`)) {
|
|
33135
|
-
return key.slice(fileName.length + 1);
|
|
33395
|
+
for (const o of unmatched) {
|
|
33396
|
+
result.extra.push(o.token.text);
|
|
33397
|
+
result.problems.push(
|
|
33398
|
+
`"${o.token.text}" appears in the translation but not the source`
|
|
33399
|
+
);
|
|
33136
33400
|
}
|
|
33137
|
-
|
|
33401
|
+
result.ok = result.problems.length === 0;
|
|
33402
|
+
return result;
|
|
33138
33403
|
}
|
|
33139
|
-
|
|
33140
|
-
|
|
33141
|
-
import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
33142
|
-
function EditUI({ state }) {
|
|
33143
|
-
if (state.type === "editing") {
|
|
33144
|
-
return /* @__PURE__ */ jsxs18(Box18, { children: [
|
|
33145
|
-
/* @__PURE__ */ jsx18(Spinner15, {}),
|
|
33146
|
-
/* @__PURE__ */ jsx18(Text18, { children: " Editing translation\u2026" })
|
|
33147
|
-
] });
|
|
33148
|
-
}
|
|
33149
|
-
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
|
|
33150
|
-
/* @__PURE__ */ jsxs18(Box18, { children: [
|
|
33151
|
-
/* @__PURE__ */ jsx18(Text18, { color: "green", children: "\u2713" }),
|
|
33152
|
-
/* @__PURE__ */ jsxs18(Text18, { children: [
|
|
33153
|
-
" ",
|
|
33154
|
-
"Updated ",
|
|
33155
|
-
/* @__PURE__ */ jsx18(Text18, { bold: true, children: state.key })
|
|
33156
|
-
] })
|
|
33157
|
-
] }),
|
|
33158
|
-
/* @__PURE__ */ jsx18(Box18, { paddingLeft: 2, children: /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: state.value }) }),
|
|
33159
|
-
/* @__PURE__ */ jsx18(Box18, { marginTop: 1, children: /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
|
|
33160
|
-
"File: ",
|
|
33161
|
-
state.filePath
|
|
33162
|
-
] }) })
|
|
33163
|
-
] });
|
|
33404
|
+
function comparePlaceholders(src, out, options = {}) {
|
|
33405
|
+
return options.dstLocale ? compareForLocale(src, out, options.dstLocale) : compareStrict(src, out);
|
|
33164
33406
|
}
|
|
33165
|
-
|
|
33166
|
-
const
|
|
33167
|
-
|
|
33168
|
-
|
|
33169
|
-
const
|
|
33170
|
-
const
|
|
33171
|
-
|
|
33172
|
-
|
|
33173
|
-
|
|
33174
|
-
|
|
33175
|
-
"no_translation_files"
|
|
33176
|
-
);
|
|
33407
|
+
function repairPlaceholders(src, out, options = {}) {
|
|
33408
|
+
const initial = comparePlaceholders(src, out, options);
|
|
33409
|
+
if (initial.ok) return out;
|
|
33410
|
+
const tokens = (s) => options.dstLocale ? extractPlaceholders(s).filter((t) => !isPluralToken(t)) : extractPlaceholders(s);
|
|
33411
|
+
const srcTokens = tokens(src);
|
|
33412
|
+
const outTokens = tokens(out);
|
|
33413
|
+
const missingTokens = subtract(srcTokens, outTokens);
|
|
33414
|
+
const extraTokens = subtract(outTokens, srcTokens);
|
|
33415
|
+
if (missingTokens.length === 0 || missingTokens.length !== extraTokens.length) {
|
|
33416
|
+
return null;
|
|
33177
33417
|
}
|
|
33178
|
-
const
|
|
33179
|
-
|
|
33180
|
-
|
|
33181
|
-
|
|
33182
|
-
|
|
33418
|
+
const braceKinds = /* @__PURE__ */ new Set([
|
|
33419
|
+
"mustache",
|
|
33420
|
+
"icu-simple",
|
|
33421
|
+
"ruby"
|
|
33422
|
+
]);
|
|
33423
|
+
const unclaimed = [...missingTokens];
|
|
33424
|
+
const rewrites = [];
|
|
33425
|
+
for (const bad of extraTokens) {
|
|
33426
|
+
if (!braceKinds.has(bad.kind)) return null;
|
|
33427
|
+
const idx = unclaimed.findIndex(
|
|
33428
|
+
(m) => braceKinds.has(m.kind) && m.name.toLowerCase() === bad.name.toLowerCase()
|
|
33183
33429
|
);
|
|
33430
|
+
if (idx === -1) return null;
|
|
33431
|
+
const [good] = unclaimed.splice(idx, 1);
|
|
33432
|
+
if (!good) return null;
|
|
33433
|
+
rewrites.push({ start: bad.start, end: bad.end, text: good.text });
|
|
33184
33434
|
}
|
|
33185
|
-
|
|
33186
|
-
|
|
33187
|
-
|
|
33188
|
-
|
|
33189
|
-
|
|
33190
|
-
);
|
|
33191
|
-
const adapter = createAdapter("json");
|
|
33192
|
-
let existingData = {};
|
|
33193
|
-
try {
|
|
33194
|
-
const parsed = await adapter.parse(targetFile);
|
|
33195
|
-
existingData = parsed.data;
|
|
33196
|
-
} catch {
|
|
33435
|
+
if (unclaimed.length > 0) return null;
|
|
33436
|
+
rewrites.sort((a, b) => b.start - a.start);
|
|
33437
|
+
let repaired = out;
|
|
33438
|
+
for (const r of rewrites) {
|
|
33439
|
+
repaired = repaired.slice(0, r.start) + r.text + repaired.slice(r.end);
|
|
33197
33440
|
}
|
|
33198
|
-
|
|
33199
|
-
existingData,
|
|
33200
|
-
keyInFile,
|
|
33201
|
-
opts.value
|
|
33202
|
-
);
|
|
33203
|
-
await adapter.serialize(targetFile, updatedData, { merge: false });
|
|
33204
|
-
const relativePath = path12.relative(baseDir, targetFile);
|
|
33205
|
-
return { filePath: relativePath, key: opts.key, value: opts.value };
|
|
33441
|
+
return comparePlaceholders(src, repaired, options).ok ? repaired : null;
|
|
33206
33442
|
}
|
|
33207
|
-
|
|
33208
|
-
|
|
33209
|
-
|
|
33210
|
-
|
|
33211
|
-
|
|
33212
|
-
key: result2.key,
|
|
33213
|
-
value: result2.value
|
|
33214
|
-
});
|
|
33215
|
-
}
|
|
33216
|
-
const { unmount } = renderTransient(/* @__PURE__ */ jsx18(EditUI, { state: { type: "editing" } }));
|
|
33217
|
-
let result;
|
|
33218
|
-
try {
|
|
33219
|
-
result = await performEdit(opts);
|
|
33220
|
-
} finally {
|
|
33221
|
-
unmount();
|
|
33222
|
-
}
|
|
33223
|
-
render16(/* @__PURE__ */ jsx18(EditUI, { state: { type: "success", ...result } })).unmount();
|
|
33443
|
+
function settlePlaceholders(src, out, options = {}) {
|
|
33444
|
+
const comparison = comparePlaceholders(src, out, options);
|
|
33445
|
+
if (comparison.ok) return { value: out, repaired: false, comparison };
|
|
33446
|
+
const value = repairPlaceholders(src, out, options);
|
|
33447
|
+
return { value, repaired: value !== null, comparison };
|
|
33224
33448
|
}
|
|
33225
33449
|
|
|
33226
|
-
// src/
|
|
33227
|
-
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
|
|
33231
|
-
return
|
|
33232
|
-
...plan.summary,
|
|
33233
|
-
protectedKeys: plan.protectedKeys,
|
|
33234
|
-
heldKeys: plan.heldKeys,
|
|
33235
|
-
untrackedKeys: plan.untrackedKeys.map((k) => k.key)
|
|
33236
|
-
};
|
|
33237
|
-
}
|
|
33238
|
-
function fullKey5(namespace, key) {
|
|
33239
|
-
return namespace ? `${namespace}.${key}` : key;
|
|
33240
|
-
}
|
|
33241
|
-
function characters(text) {
|
|
33242
|
-
return Array.from(text).length;
|
|
33450
|
+
// ../ai/src/utils/term-match.ts
|
|
33451
|
+
function sharedPrefixLength(a, b) {
|
|
33452
|
+
const minLen = Math.min(a.length, b.length);
|
|
33453
|
+
let shared = 0;
|
|
33454
|
+
while (shared < minLen && a[shared] === b[shared]) shared++;
|
|
33455
|
+
return shared;
|
|
33243
33456
|
}
|
|
33244
|
-
|
|
33245
|
-
|
|
33246
|
-
|
|
33247
|
-
|
|
33248
|
-
|
|
33249
|
-
|
|
33250
|
-
|
|
33251
|
-
|
|
33252
|
-
|
|
33457
|
+
var MIN_FUZZY_LENGTH = 4;
|
|
33458
|
+
var MIN_SHARED_FRACTION = 0.7;
|
|
33459
|
+
var UNSPACED_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
|
|
33460
|
+
var WORD_SPLIT = /[\s,.:;!?()[\]{}'"/\\-]+/;
|
|
33461
|
+
var MAX_DERIVED_PREFIX = 4;
|
|
33462
|
+
var MIN_DERIVED_STEM = 5;
|
|
33463
|
+
function termAppearsInText(text, term, opts = {}) {
|
|
33464
|
+
const haystack = opts.caseSensitive ? text : text.toLowerCase();
|
|
33465
|
+
const needle = (opts.caseSensitive ? term : term.toLowerCase()).trim();
|
|
33466
|
+
if (needle.length === 0) return false;
|
|
33467
|
+
if (UNSPACED_SCRIPT.test(needle)) return haystack.includes(needle);
|
|
33468
|
+
if (/\s/.test(needle) && haystack.includes(needle)) return true;
|
|
33469
|
+
const termWords = needle.split(WORD_SPLIT).filter(Boolean);
|
|
33470
|
+
const words2 = haystack.split(WORD_SPLIT).filter(Boolean);
|
|
33471
|
+
for (let start = 0; start + termWords.length <= words2.length; start++) {
|
|
33472
|
+
if (termWords.every(
|
|
33473
|
+
(termWord, offset) => wordMatches(
|
|
33474
|
+
words2[start + offset] ?? "",
|
|
33475
|
+
termWord,
|
|
33476
|
+
opts.derivedForms ?? false
|
|
33477
|
+
)
|
|
33478
|
+
)) {
|
|
33479
|
+
return true;
|
|
33480
|
+
}
|
|
33253
33481
|
}
|
|
33254
|
-
return
|
|
33482
|
+
return false;
|
|
33255
33483
|
}
|
|
33256
|
-
|
|
33257
|
-
|
|
33258
|
-
|
|
33259
|
-
|
|
33260
|
-
if (opts.dryRun) {
|
|
33261
|
-
await runDryRun(ctx, srcLocale, dstLocales, opts);
|
|
33262
|
-
return;
|
|
33484
|
+
function wordMatches(word, termWord, derivedForms) {
|
|
33485
|
+
if (word === termWord) return true;
|
|
33486
|
+
if (termWord.length < MIN_FUZZY_LENGTH || word.length < MIN_FUZZY_LENGTH) {
|
|
33487
|
+
return false;
|
|
33263
33488
|
}
|
|
33264
|
-
|
|
33265
|
-
|
|
33266
|
-
|
|
33267
|
-
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
/* @__PURE__ */ jsx19(Working, { text: `Planning ${dstLocales.join(", ")}\u2026` })
|
|
33271
|
-
);
|
|
33272
|
-
try {
|
|
33273
|
-
for (const dstLocale of dstLocales) {
|
|
33274
|
-
const { plan } = await generateTasks(ctx, srcLocale, dstLocale, {
|
|
33275
|
-
retranslateUntracked: opts.retranslateUntracked
|
|
33276
|
-
});
|
|
33277
|
-
languages.push({
|
|
33278
|
-
dstLocale,
|
|
33279
|
-
keys: plan.tasks.length,
|
|
33280
|
-
characters: plan.tasks.reduce(
|
|
33281
|
-
(n, task) => n + characters(task.src),
|
|
33282
|
-
0
|
|
33283
|
-
),
|
|
33284
|
-
tasks: plan.tasks.map((task) => ({
|
|
33285
|
-
key: fullKey5(task.namespace, task.key),
|
|
33286
|
-
path: task.path,
|
|
33287
|
-
reason: task.reason,
|
|
33288
|
-
...opts.verbose ? { src: task.src } : {}
|
|
33289
|
-
})),
|
|
33290
|
-
plan
|
|
33291
|
-
});
|
|
33489
|
+
const minShared = Math.ceil(termWord.length * MIN_SHARED_FRACTION);
|
|
33490
|
+
if (sharedPrefixLength(word, termWord) >= minShared) return true;
|
|
33491
|
+
if (!derivedForms || minShared < MIN_DERIVED_STEM) return false;
|
|
33492
|
+
for (let offset = 1; offset <= MAX_DERIVED_PREFIX; offset++) {
|
|
33493
|
+
if (sharedPrefixLength(word.slice(offset), termWord) >= minShared) {
|
|
33494
|
+
return true;
|
|
33292
33495
|
}
|
|
33293
|
-
} finally {
|
|
33294
|
-
progress.unmount();
|
|
33295
|
-
}
|
|
33296
|
-
const total = {
|
|
33297
|
-
keys: languages.reduce((n, l) => n + l.keys, 0),
|
|
33298
|
-
characters: languages.reduce((n, l) => n + l.characters, 0)
|
|
33299
|
-
};
|
|
33300
|
-
const balance = await readCreditBalance(ctx);
|
|
33301
|
-
track("cli_translate_completed", {
|
|
33302
|
-
mode: "plan",
|
|
33303
|
-
key_count: total.keys,
|
|
33304
|
-
locale_count: languages.length,
|
|
33305
|
-
job_count: 0,
|
|
33306
|
-
json: opts.json ?? false
|
|
33307
|
-
});
|
|
33308
|
-
if (opts.json) {
|
|
33309
|
-
jsonSuccess({
|
|
33310
|
-
dryRun: true,
|
|
33311
|
-
srcLocale,
|
|
33312
|
-
languages: languages.map(({ plan, ...language }) => ({
|
|
33313
|
-
...language,
|
|
33314
|
-
plan: planOutput(plan)
|
|
33315
|
-
})),
|
|
33316
|
-
total,
|
|
33317
|
-
estimate: null,
|
|
33318
|
-
estimateNote: NO_ESTIMATE,
|
|
33319
|
-
credits: { balance }
|
|
33320
|
-
});
|
|
33321
33496
|
}
|
|
33322
|
-
|
|
33323
|
-
|
|
33324
|
-
|
|
33325
|
-
|
|
33497
|
+
return false;
|
|
33498
|
+
}
|
|
33499
|
+
|
|
33500
|
+
// ../ai/src/translations/glossary.ts
|
|
33501
|
+
function extractGlossaryData(glossaries, dstLocale) {
|
|
33502
|
+
const terms = [];
|
|
33503
|
+
const doNotTranslate = [];
|
|
33504
|
+
const contextTerms = [];
|
|
33505
|
+
for (const g of glossaries) {
|
|
33506
|
+
const id = g.id ? { id: g.id } : {};
|
|
33507
|
+
if (g.doNotTranslate) {
|
|
33508
|
+
doNotTranslate.push({ ...id, term: g.src });
|
|
33509
|
+
if (g.aiContext) {
|
|
33510
|
+
contextTerms.push({ ...id, term: g.src, context: g.aiContext });
|
|
33511
|
+
}
|
|
33326
33512
|
} else {
|
|
33327
|
-
|
|
33328
|
-
|
|
33513
|
+
const translation = pickGlossaryTranslation(
|
|
33514
|
+
g.translations,
|
|
33515
|
+
dstLocale
|
|
33329
33516
|
);
|
|
33330
|
-
|
|
33331
|
-
|
|
33332
|
-
|
|
33333
|
-
|
|
33334
|
-
|
|
33335
|
-
|
|
33336
|
-
|
|
33517
|
+
if (translation) {
|
|
33518
|
+
terms.push({
|
|
33519
|
+
...id,
|
|
33520
|
+
source: g.src,
|
|
33521
|
+
target: translation.text,
|
|
33522
|
+
...g.aiContext ? { notes: g.aiContext } : {}
|
|
33523
|
+
});
|
|
33524
|
+
} else if (g.aiContext) {
|
|
33525
|
+
contextTerms.push({ ...id, term: g.src, context: g.aiContext });
|
|
33337
33526
|
}
|
|
33338
33527
|
}
|
|
33339
33528
|
}
|
|
33340
|
-
|
|
33341
|
-
`
|
|
33342
|
-
In all: ${total.keys} key(s), ${total.characters} characters.` + (balance === null ? "" : ` Credit balance: ${balance}.`)
|
|
33343
|
-
);
|
|
33344
|
-
console.log(NO_ESTIMATE);
|
|
33345
|
-
console.log("Nothing was started. Run without --dry-run to translate.");
|
|
33346
|
-
}
|
|
33347
|
-
function Working({ text }) {
|
|
33348
|
-
return /* @__PURE__ */ jsxs19(Box19, { children: [
|
|
33349
|
-
/* @__PURE__ */ jsx19(Spinner16, {}),
|
|
33350
|
-
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
33351
|
-
" ",
|
|
33352
|
-
text
|
|
33353
|
-
] })
|
|
33354
|
-
] });
|
|
33355
|
-
}
|
|
33356
|
-
function progressText(dstLocale, summary) {
|
|
33357
|
-
if (!summary) return `${dstLocale}: starting\u2026`;
|
|
33358
|
-
const done = summary.completed + summary.failed;
|
|
33359
|
-
return `${dstLocale}: translating ${done}/${summary.total}\u2026`;
|
|
33529
|
+
return { terms, doNotTranslate, contextTerms };
|
|
33360
33530
|
}
|
|
33361
|
-
function
|
|
33362
|
-
|
|
33363
|
-
|
|
33364
|
-
|
|
33365
|
-
|
|
33366
|
-
|
|
33367
|
-
|
|
33368
|
-
|
|
33369
|
-
|
|
33370
|
-
|
|
33371
|
-
|
|
33372
|
-
|
|
33373
|
-
|
|
33374
|
-
|
|
33375
|
-
|
|
33376
|
-
|
|
33377
|
-
|
|
33378
|
-
|
|
33379
|
-
|
|
33380
|
-
|
|
33381
|
-
|
|
33382
|
-
|
|
33383
|
-
|
|
33384
|
-
|
|
33531
|
+
function filterGlossaryByRelevance(glossary, src) {
|
|
33532
|
+
return {
|
|
33533
|
+
terms: glossary.terms.filter(
|
|
33534
|
+
(t) => termAppearsInText(src, t.source, {
|
|
33535
|
+
caseSensitive: t.caseSensitive
|
|
33536
|
+
})
|
|
33537
|
+
),
|
|
33538
|
+
doNotTranslate: glossary.doNotTranslate.filter(
|
|
33539
|
+
(t) => termAppearsInText(src, t.term)
|
|
33540
|
+
),
|
|
33541
|
+
contextTerms: glossary.contextTerms.filter(
|
|
33542
|
+
(t) => termAppearsInText(src, t.term)
|
|
33543
|
+
)
|
|
33544
|
+
};
|
|
33545
|
+
}
|
|
33546
|
+
function checkGlossaryCompliance(terms, translation) {
|
|
33547
|
+
const missing = [];
|
|
33548
|
+
for (const term of terms) {
|
|
33549
|
+
const target = term.target.trim();
|
|
33550
|
+
if (!target) continue;
|
|
33551
|
+
if (!termAppearsInText(translation, target, {
|
|
33552
|
+
caseSensitive: term.caseSensitive,
|
|
33553
|
+
derivedForms: true
|
|
33554
|
+
})) {
|
|
33555
|
+
missing.push({ source: term.source, target: term.target });
|
|
33556
|
+
}
|
|
33385
33557
|
}
|
|
33558
|
+
return missing;
|
|
33386
33559
|
}
|
|
33387
|
-
|
|
33388
|
-
|
|
33389
|
-
|
|
33390
|
-
|
|
33391
|
-
|
|
33392
|
-
|
|
33393
|
-
|
|
33394
|
-
|
|
33395
|
-
|
|
33396
|
-
|
|
33397
|
-
|
|
33398
|
-
|
|
33560
|
+
|
|
33561
|
+
// ../ai/src/translations/languages.ts
|
|
33562
|
+
var VERIFIER_LANGUAGES = {
|
|
33563
|
+
en: "English",
|
|
33564
|
+
nb: "Norwegian Bokm\xE5l",
|
|
33565
|
+
nn: "Norwegian Nynorsk",
|
|
33566
|
+
sv: "Swedish",
|
|
33567
|
+
da: "Danish",
|
|
33568
|
+
fi: "Finnish",
|
|
33569
|
+
is: "Icelandic",
|
|
33570
|
+
de: "German",
|
|
33571
|
+
nl: "Dutch",
|
|
33572
|
+
fr: "French",
|
|
33573
|
+
es: "Spanish",
|
|
33574
|
+
pt: "Portuguese",
|
|
33575
|
+
it: "Italian",
|
|
33576
|
+
ca: "Catalan",
|
|
33577
|
+
ro: "Romanian",
|
|
33578
|
+
pl: "Polish",
|
|
33579
|
+
cs: "Czech",
|
|
33580
|
+
sk: "Slovak",
|
|
33581
|
+
hu: "Hungarian",
|
|
33582
|
+
el: "Greek",
|
|
33583
|
+
ru: "Russian",
|
|
33584
|
+
uk: "Ukrainian",
|
|
33585
|
+
bg: "Bulgarian",
|
|
33586
|
+
sr: "Serbian",
|
|
33587
|
+
hr: "Croatian",
|
|
33588
|
+
tr: "Turkish",
|
|
33589
|
+
ar: "Arabic",
|
|
33590
|
+
he: "Hebrew",
|
|
33591
|
+
fa: "Persian",
|
|
33592
|
+
hi: "Hindi",
|
|
33593
|
+
bn: "Bengali",
|
|
33594
|
+
th: "Thai",
|
|
33595
|
+
vi: "Vietnamese",
|
|
33596
|
+
id: "Indonesian",
|
|
33597
|
+
ms: "Malay",
|
|
33598
|
+
ja: "Japanese",
|
|
33599
|
+
ko: "Korean",
|
|
33600
|
+
zh: "Chinese",
|
|
33601
|
+
other: "Any other language, or no natural language at all"
|
|
33602
|
+
};
|
|
33603
|
+
var LANGUAGE_ALIASES = { no: "nb" };
|
|
33604
|
+
function verifierLanguage(locale) {
|
|
33605
|
+
const head = locale.trim().split(/[-_]/)[0] ?? "";
|
|
33606
|
+
const base = (/^[a-z]{2,3}(?=[A-Z])/.exec(head) ?? /^[A-Za-z]{2,3}$/.exec(head))?.[0].toLowerCase();
|
|
33607
|
+
if (!base) return void 0;
|
|
33608
|
+
const language = LANGUAGE_ALIASES[base] ?? base;
|
|
33609
|
+
return language in VERIFIER_LANGUAGES && language !== "other" ? language : void 0;
|
|
33610
|
+
}
|
|
33611
|
+
|
|
33612
|
+
// ../ai/src/translations/deterministic-health.ts
|
|
33613
|
+
var REQUIRED_SCRIPT = {
|
|
33614
|
+
ko: /\p{Script=Hangul}/u,
|
|
33615
|
+
ja: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u,
|
|
33616
|
+
zh: /\p{Script=Han}/u,
|
|
33617
|
+
ar: /\p{Script=Arabic}/u,
|
|
33618
|
+
fa: /\p{Script=Arabic}/u,
|
|
33619
|
+
he: /\p{Script=Hebrew}/u,
|
|
33620
|
+
ru: /\p{Script=Cyrillic}/u,
|
|
33621
|
+
uk: /\p{Script=Cyrillic}/u,
|
|
33622
|
+
bg: /\p{Script=Cyrillic}/u,
|
|
33623
|
+
el: /\p{Script=Greek}/u,
|
|
33624
|
+
th: /\p{Script=Thai}/u,
|
|
33625
|
+
hi: /\p{Script=Devanagari}/u,
|
|
33626
|
+
bn: /\p{Script=Bengali}/u
|
|
33627
|
+
};
|
|
33628
|
+
var MIN_LETTERS = 4;
|
|
33629
|
+
var MIN_SENTENCE_WORDS = 3;
|
|
33630
|
+
var MIN_SENTENCE_LETTERS = 8;
|
|
33631
|
+
function proseOf(text) {
|
|
33632
|
+
return text.replace(/\{\{[^{}]*\}\}/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/<[^<>]*>/g, " ").replace(/`[^`]*`/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/%[sdif@]|%\d+\$[sdif@]/g, " ");
|
|
33633
|
+
}
|
|
33634
|
+
function letters(text) {
|
|
33635
|
+
return text.match(/\p{L}/gu)?.length ?? 0;
|
|
33636
|
+
}
|
|
33637
|
+
function words(text) {
|
|
33638
|
+
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
33639
|
+
}
|
|
33640
|
+
function deterministicHealth(input2) {
|
|
33641
|
+
const findings = [];
|
|
33642
|
+
const translation = input2.translation.trim();
|
|
33643
|
+
const source = input2.source.trim();
|
|
33644
|
+
if (!translation) return { findings, needsModel: false };
|
|
33645
|
+
if (!comparePlaceholders(source, translation, {
|
|
33646
|
+
dstLocale: input2.dstLocale
|
|
33647
|
+
}).ok) {
|
|
33648
|
+
findings.push("placeholder_mismatch");
|
|
33649
|
+
}
|
|
33650
|
+
const prose = proseOf(translation);
|
|
33651
|
+
const letterCount = letters(prose);
|
|
33652
|
+
const sameLanguage = verifierLanguage(input2.srcLocale) !== void 0 && verifierLanguage(input2.srcLocale) === verifierLanguage(input2.dstLocale);
|
|
33653
|
+
if (translation === source) {
|
|
33654
|
+
if (!sameLanguage && words(prose) >= MIN_SENTENCE_WORDS && letterCount >= MIN_SENTENCE_LETTERS) {
|
|
33655
|
+
findings.push("same_as_source");
|
|
33399
33656
|
}
|
|
33657
|
+
return { findings, needsModel: false };
|
|
33658
|
+
}
|
|
33659
|
+
if (letterCount < MIN_LETTERS) return { findings, needsModel: false };
|
|
33660
|
+
const language = verifierLanguage(input2.dstLocale);
|
|
33661
|
+
const script = language ? REQUIRED_SCRIPT[language] : void 0;
|
|
33662
|
+
if (script && !script.test(prose)) {
|
|
33663
|
+
findings.push("wrong_script");
|
|
33664
|
+
return { findings, needsModel: false };
|
|
33665
|
+
}
|
|
33666
|
+
return { findings, needsModel: true };
|
|
33667
|
+
}
|
|
33668
|
+
|
|
33669
|
+
// ../ai/src/translations/consistency.ts
|
|
33670
|
+
function findSourceInconsistencies(pairs, referencePairs = []) {
|
|
33671
|
+
const groups = /* @__PURE__ */ new Map();
|
|
33672
|
+
const add = (p, reviewable) => {
|
|
33673
|
+
const src = p.src.trim();
|
|
33674
|
+
if (!src) return;
|
|
33675
|
+
const list = groups.get(src) ?? [];
|
|
33676
|
+
list.push({
|
|
33677
|
+
key: p.key,
|
|
33678
|
+
dst: p.dst,
|
|
33679
|
+
reviewable,
|
|
33680
|
+
...p.context ? { context: p.context } : {}
|
|
33681
|
+
});
|
|
33682
|
+
groups.set(src, list);
|
|
33400
33683
|
};
|
|
33401
|
-
for (const
|
|
33402
|
-
|
|
33403
|
-
|
|
33684
|
+
for (const p of pairs) add(p, true);
|
|
33685
|
+
for (const p of referencePairs) add(p, false);
|
|
33686
|
+
const out = [];
|
|
33687
|
+
for (const [src, variants] of groups) {
|
|
33688
|
+
if (variants.length < 2) continue;
|
|
33689
|
+
if (new Set(variants.map((v) => v.dst.trim())).size < 2) continue;
|
|
33690
|
+
if (!variants.some((v) => v.reviewable)) continue;
|
|
33691
|
+
const contexts = new Set(variants.map((v) => v.context?.trim() ?? ""));
|
|
33692
|
+
out.push({ src, variants, unexplained: contexts.size === 1 });
|
|
33693
|
+
}
|
|
33694
|
+
return out;
|
|
33695
|
+
}
|
|
33696
|
+
|
|
33697
|
+
// src/commands/set/set-core.ts
|
|
33698
|
+
async function locateKey(ctx, key, locale) {
|
|
33699
|
+
const { root, project } = ctx;
|
|
33700
|
+
const scope = scopeOf(project);
|
|
33701
|
+
const format = formatOf(scope);
|
|
33702
|
+
const files = await listLocaleFiles(root, scope);
|
|
33703
|
+
const sourceFiles = filesForLocale(files, project.baseLanguage);
|
|
33704
|
+
if (sourceFiles.length === 0) {
|
|
33705
|
+
throw new CliError(
|
|
33706
|
+
`No ${project.baseLanguage} translation file matches "${fullPattern(scope)}" under ${root}.`,
|
|
33707
|
+
"The pattern comes from the project settings; check it there, or that you are in the right repository.",
|
|
33708
|
+
"no_translation_files"
|
|
33404
33709
|
);
|
|
33405
|
-
|
|
33406
|
-
|
|
33407
|
-
|
|
33408
|
-
|
|
33409
|
-
|
|
33410
|
-
|
|
33710
|
+
}
|
|
33711
|
+
const found = [];
|
|
33712
|
+
for (const file2 of sourceFiles) {
|
|
33713
|
+
const prefix = file2.namespace ? `${file2.namespace}.` : "";
|
|
33714
|
+
if (!key.startsWith(prefix)) continue;
|
|
33715
|
+
const fileKey = key.slice(prefix.length);
|
|
33716
|
+
const read = await readTranslationFile(file2.absolutePath, format);
|
|
33717
|
+
if (read.error !== void 0) {
|
|
33718
|
+
throw new CliError(
|
|
33719
|
+
`Cannot read the ${project.baseLanguage} file ${file2.relativePath}: ${read.error}`
|
|
33411
33720
|
);
|
|
33412
|
-
const base = {
|
|
33413
|
-
dstLocale,
|
|
33414
|
-
status: "up-to-date",
|
|
33415
|
-
jobId: null,
|
|
33416
|
-
keys: tasks.length,
|
|
33417
|
-
completed: null,
|
|
33418
|
-
failed: null,
|
|
33419
|
-
written: null,
|
|
33420
|
-
notApplied: [],
|
|
33421
|
-
error: null,
|
|
33422
|
-
plan: planOutput(plan)
|
|
33423
|
-
};
|
|
33424
|
-
const leftOut = describeLeftOut(plan);
|
|
33425
|
-
if (stopped !== null) {
|
|
33426
|
-
progress.unmount();
|
|
33427
|
-
report({ ...base, status: "not-started", error: stopped });
|
|
33428
|
-
continue;
|
|
33429
|
-
}
|
|
33430
|
-
if (tasks.length === 0) {
|
|
33431
|
-
progress.unmount();
|
|
33432
|
-
report(base);
|
|
33433
|
-
printNotes(json2, leftOut);
|
|
33434
|
-
continue;
|
|
33435
|
-
}
|
|
33436
|
-
progress.rerender(/* @__PURE__ */ jsx19(Working, { text: progressText(dstLocale, null) }));
|
|
33437
|
-
let jobId;
|
|
33438
|
-
try {
|
|
33439
|
-
jobId = await startTranslationJob({
|
|
33440
|
-
projectId: project._id,
|
|
33441
|
-
organizationId: project.organizationId,
|
|
33442
|
-
framework,
|
|
33443
|
-
srcLocale,
|
|
33444
|
-
dstLocale,
|
|
33445
|
-
tasks
|
|
33446
|
-
});
|
|
33447
|
-
} catch (error62) {
|
|
33448
|
-
const refused = toCliError(error62);
|
|
33449
|
-
failure2 ??= refused;
|
|
33450
|
-
progress.unmount();
|
|
33451
|
-
report({
|
|
33452
|
-
...base,
|
|
33453
|
-
status: "not-started",
|
|
33454
|
-
error: refused.baseMessage
|
|
33455
|
-
});
|
|
33456
|
-
if (exitCodeFor(refused.code) === EXIT_CODES.billing) {
|
|
33457
|
-
stopped = `TolkBot refused the ${dstLocale} job: ${refused.baseMessage}`;
|
|
33458
|
-
}
|
|
33459
|
-
continue;
|
|
33460
|
-
}
|
|
33461
|
-
if (!opts.wait) {
|
|
33462
|
-
progress.unmount();
|
|
33463
|
-
report({ ...base, status: "started", jobId });
|
|
33464
|
-
printNotes(json2, leftOut);
|
|
33465
|
-
continue;
|
|
33466
|
-
}
|
|
33467
|
-
let summary;
|
|
33468
|
-
try {
|
|
33469
|
-
summary = await waitForTranslationJob(
|
|
33470
|
-
project.organizationId,
|
|
33471
|
-
jobId,
|
|
33472
|
-
(latest) => progress.rerender(
|
|
33473
|
-
/* @__PURE__ */ jsx19(Working, { text: progressText(dstLocale, latest) })
|
|
33474
|
-
)
|
|
33475
|
-
);
|
|
33476
|
-
} catch (error62) {
|
|
33477
|
-
if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
|
|
33478
|
-
failure2 ??= error62;
|
|
33479
|
-
progress.unmount();
|
|
33480
|
-
report({
|
|
33481
|
-
...base,
|
|
33482
|
-
status: "timed-out",
|
|
33483
|
-
jobId,
|
|
33484
|
-
error: error62.baseMessage
|
|
33485
|
-
});
|
|
33486
|
-
stopped = `The CLI stopped waiting for the ${dstLocale} job.`;
|
|
33487
|
-
continue;
|
|
33488
|
-
}
|
|
33489
|
-
const finished = {
|
|
33490
|
-
...base,
|
|
33491
|
-
status: summary.status,
|
|
33492
|
-
jobId,
|
|
33493
|
-
completed: summary.completed,
|
|
33494
|
-
failed: summary.failed
|
|
33495
|
-
};
|
|
33496
|
-
if (summary.status !== "cancelled") {
|
|
33497
|
-
progress.rerender(
|
|
33498
|
-
/* @__PURE__ */ jsx19(Working, { text: `${dstLocale}: writing the files\u2026` })
|
|
33499
|
-
);
|
|
33500
|
-
const applied = await applyJob(ctx, jobId);
|
|
33501
|
-
finished.written = { files: applied.files, keys: applied.keys };
|
|
33502
|
-
finished.notApplied = applied.notApplied;
|
|
33503
|
-
}
|
|
33504
|
-
const unsuccessful = unsuccessfulJobError(jobId, summary.status);
|
|
33505
|
-
if (unsuccessful) {
|
|
33506
|
-
failure2 ??= unsuccessful;
|
|
33507
|
-
finished.error = unsuccessful.baseMessage;
|
|
33508
|
-
}
|
|
33509
|
-
progress.unmount();
|
|
33510
|
-
report(finished);
|
|
33511
|
-
printNotes(json2, leftOut);
|
|
33512
|
-
if (summary.status === "cancelled") {
|
|
33513
|
-
stopped = `The ${dstLocale} job was cancelled.`;
|
|
33514
|
-
}
|
|
33515
|
-
} finally {
|
|
33516
|
-
progress.unmount();
|
|
33517
33721
|
}
|
|
33722
|
+
const expected = expectedTargetKeys(getKeys(read.data), locale).get(
|
|
33723
|
+
fileKey
|
|
33724
|
+
);
|
|
33725
|
+
if (!expected) continue;
|
|
33726
|
+
const source = getByPath(read.data, expected.sourceKey);
|
|
33727
|
+
if (typeof source !== "string") continue;
|
|
33728
|
+
found.push({ file: file2, data: read.data, fileKey, source });
|
|
33518
33729
|
}
|
|
33519
|
-
|
|
33520
|
-
|
|
33521
|
-
|
|
33522
|
-
|
|
33523
|
-
|
|
33524
|
-
|
|
33525
|
-
|
|
33526
|
-
const payload = { srcLocale, languages: outcomes };
|
|
33527
|
-
if (json2) {
|
|
33528
|
-
if (!failure2) jsonSuccess(payload);
|
|
33529
|
-
jsonResult(payload, 1);
|
|
33530
|
-
fail(failure2);
|
|
33730
|
+
const [only, ...rest] = found;
|
|
33731
|
+
if (!only) {
|
|
33732
|
+
throw new CliError(
|
|
33733
|
+
`"${key}" is not a key of the ${project.baseLanguage} files.`,
|
|
33734
|
+
sourceFiles.some((file2) => file2.namespace) ? "Keys are named with their namespace first, as the dashboard names them: namespace.key." : "Run 'tolkbot status --keys' to list the keys a language is missing.",
|
|
33735
|
+
"key_not_found"
|
|
33736
|
+
);
|
|
33531
33737
|
}
|
|
33532
|
-
if (
|
|
33533
|
-
|
|
33534
|
-
"
|
|
33738
|
+
if (rest.length > 0) {
|
|
33739
|
+
throw new CliError(
|
|
33740
|
+
`"${key}" names a key in more than one file: ${found.map((f) => f.file.relativePath).join(", ")}.`
|
|
33535
33741
|
);
|
|
33536
33742
|
}
|
|
33537
|
-
|
|
33538
|
-
}
|
|
33539
|
-
function printNotes(json2, notes) {
|
|
33540
|
-
if (json2) return;
|
|
33541
|
-
for (const note of notes) console.log(` ${note}`);
|
|
33743
|
+
return only;
|
|
33542
33744
|
}
|
|
33543
|
-
function
|
|
33544
|
-
|
|
33745
|
+
async function setValue(ctx, input2) {
|
|
33746
|
+
const { root, project } = ctx;
|
|
33747
|
+
if (sameLocale(input2.locale, project.baseLanguage)) {
|
|
33545
33748
|
throw new UsageError(
|
|
33546
|
-
|
|
33547
|
-
"
|
|
33749
|
+
`${input2.locale} is the project's base language.`,
|
|
33750
|
+
"Edit the source file itself; 'tolkbot set' writes translations."
|
|
33548
33751
|
);
|
|
33549
33752
|
}
|
|
33550
|
-
if (
|
|
33551
|
-
throw new UsageError("
|
|
33753
|
+
if (input2.value.trim().length === 0) {
|
|
33754
|
+
throw new UsageError("--value is empty.");
|
|
33552
33755
|
}
|
|
33553
|
-
|
|
33554
|
-
|
|
33555
|
-
|
|
33756
|
+
const locale = governanceLocale(project, input2.locale);
|
|
33757
|
+
const located = await locateKey(ctx, input2.key, locale);
|
|
33758
|
+
const comparison = comparePlaceholders(located.source, input2.value, {
|
|
33759
|
+
dstLocale: locale
|
|
33760
|
+
});
|
|
33761
|
+
if (!comparison.ok && !input2.force) {
|
|
33762
|
+
const repaired = settlePlaceholders(located.source, input2.value, {
|
|
33763
|
+
dstLocale: locale
|
|
33764
|
+
});
|
|
33765
|
+
throw new CliError(
|
|
33766
|
+
`The value's placeholders do not match the source "${located.source}": ${comparison.problems.join("; ")}.`,
|
|
33767
|
+
repaired.repaired && repaired.value !== null ? `Did you mean "${repaired.value}"? Pass --force to write it as it is.` : "Fix the value, or pass --force to write it as it is.",
|
|
33768
|
+
"placeholder_mismatch"
|
|
33556
33769
|
);
|
|
33557
33770
|
}
|
|
33558
|
-
|
|
33559
|
-
|
|
33560
|
-
|
|
33561
|
-
|
|
33562
|
-
|
|
33563
|
-
|
|
33564
|
-
)
|
|
33565
|
-
|
|
33566
|
-
|
|
33567
|
-
|
|
33568
|
-
|
|
33569
|
-
|
|
33570
|
-
|
|
33571
|
-
|
|
33572
|
-
|
|
33573
|
-
|
|
33574
|
-
|
|
33575
|
-
|
|
33771
|
+
const scope = scopeOf(project);
|
|
33772
|
+
const files = await listLocaleFiles(root, scope);
|
|
33773
|
+
const target = targetFileFor(located.file, locale, files, scope);
|
|
33774
|
+
const before = target.existing ? await readTranslationFile(
|
|
33775
|
+
target.existing.absolutePath,
|
|
33776
|
+
formatOf(scope)
|
|
33777
|
+
) : void 0;
|
|
33778
|
+
const previousValue = before?.data ? getByPath(before.data, located.fileKey) : void 0;
|
|
33779
|
+
const applied = await applyTasks({
|
|
33780
|
+
root,
|
|
33781
|
+
scope,
|
|
33782
|
+
srcLocale: project.baseLanguage,
|
|
33783
|
+
dstLocale: locale,
|
|
33784
|
+
tasks: [
|
|
33785
|
+
{
|
|
33786
|
+
key: located.fileKey,
|
|
33787
|
+
namespace: located.file.namespace,
|
|
33788
|
+
path: located.file.relativePath,
|
|
33789
|
+
src: located.source,
|
|
33790
|
+
dst: input2.value,
|
|
33791
|
+
status: "succeeded"
|
|
33792
|
+
}
|
|
33793
|
+
]
|
|
33794
|
+
});
|
|
33795
|
+
const [written] = applied.written;
|
|
33796
|
+
if (!written) {
|
|
33797
|
+
const reason = applied.refused[0]?.reason;
|
|
33798
|
+
throw new CliError(
|
|
33799
|
+
reason === "target_unreadable" ? `Cannot read the ${locale} file ${target.relativePath}.` : `"${input2.key}" was not written (${reason ?? "unknown reason"}).`,
|
|
33800
|
+
reason === "target_unreadable" ? "Fix the file so it parses, then run this again." : void 0
|
|
33576
33801
|
);
|
|
33577
33802
|
}
|
|
33578
|
-
const
|
|
33579
|
-
|
|
33580
|
-
|
|
33581
|
-
|
|
33582
|
-
dryRun: z10.boolean().optional(),
|
|
33583
|
-
wait: z10.boolean(),
|
|
33584
|
-
retranslateUntracked: z10.boolean().optional(),
|
|
33585
|
-
verbose: z10.boolean().optional(),
|
|
33586
|
-
json: z10.boolean().optional()
|
|
33587
|
-
});
|
|
33588
|
-
const parsed = validateOrExit(schema, opts);
|
|
33589
|
-
checkTranslateOptions(parsed);
|
|
33590
|
-
await runTranslate(parsed);
|
|
33591
|
-
});
|
|
33592
|
-
translateCommand2.command("adopt").description(
|
|
33593
|
-
"Record existing translations TolkBot has no baseline for as reviewed, so they are kept until their source changes"
|
|
33594
|
-
).option(
|
|
33595
|
-
"--src <locale>",
|
|
33596
|
-
"Source locale (defaults to the project's base language)"
|
|
33597
|
-
).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
33598
|
-
const schema = z10.object({
|
|
33599
|
-
src: z10.string().min(1).optional(),
|
|
33600
|
-
dst: z10.string().min(1).optional(),
|
|
33601
|
-
all: z10.boolean().optional(),
|
|
33602
|
-
json: z10.boolean().optional()
|
|
33603
|
-
});
|
|
33604
|
-
const parsed = validateOrExit(schema, opts);
|
|
33605
|
-
if (parsed.dst && parsed.all) {
|
|
33606
|
-
throw new UsageError("Cannot use both --dst and --all.");
|
|
33607
|
-
}
|
|
33608
|
-
await runAdopt(parsed);
|
|
33609
|
-
});
|
|
33610
|
-
translateCommand2.command("edit").description("Edit a translation value in a locale file").requiredOption("--key <key>", "Translation key (e.g. common.hello)").requiredOption("--locale <locale>", "Target locale (e.g. fr, de)").requiredOption("--value <text>", "New translation value").option("--namespace <ns>", "Namespace to resolve file (if ambiguous)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
33611
|
-
const schema = z10.object({
|
|
33612
|
-
key: z10.string().min(1),
|
|
33613
|
-
locale: z10.string().min(1),
|
|
33614
|
-
value: z10.string().min(1),
|
|
33615
|
-
namespace: z10.string().optional(),
|
|
33616
|
-
json: z10.boolean().optional()
|
|
33617
|
-
});
|
|
33618
|
-
const parsed = validateOrExit(schema, opts);
|
|
33619
|
-
await runEdit(parsed);
|
|
33620
|
-
});
|
|
33621
|
-
|
|
33622
|
-
// src/commands/validate.tsx
|
|
33623
|
-
import { Box as Box20, render as render17, Text as Text20 } from "ink";
|
|
33624
|
-
|
|
33625
|
-
// src/utils/pattern-discovery.ts
|
|
33626
|
-
import fs9 from "fs/promises";
|
|
33627
|
-
var DEFAULT_NAMESPACE = "translation";
|
|
33628
|
-
async function parseFile(absolutePath, format) {
|
|
33629
|
-
let content;
|
|
33803
|
+
const ids = {
|
|
33804
|
+
organizationId: project.organizationId,
|
|
33805
|
+
projectId: project._id
|
|
33806
|
+
};
|
|
33630
33807
|
try {
|
|
33631
|
-
|
|
33632
|
-
|
|
33633
|
-
|
|
33808
|
+
await ctx.client.mutation(api.localeKeyStates.stampManualForCli, {
|
|
33809
|
+
...ids,
|
|
33810
|
+
locale,
|
|
33811
|
+
pathTemplate: written.pathTemplate,
|
|
33812
|
+
entries: [
|
|
33813
|
+
{
|
|
33814
|
+
key: located.fileKey,
|
|
33815
|
+
namespace: located.file.namespace,
|
|
33816
|
+
baseValue: located.source,
|
|
33817
|
+
translatedValue: input2.value
|
|
33818
|
+
}
|
|
33819
|
+
]
|
|
33820
|
+
});
|
|
33821
|
+
} catch (error62) {
|
|
33822
|
+
throw new CliError(
|
|
33823
|
+
`${written.relativePath} holds the new value, but TolkBot could not record it as yours: ${error62 instanceof Error ? error62.message : String(error62)}`,
|
|
33824
|
+
"Run the same command again to record it; until then a translation run may replace it."
|
|
33825
|
+
);
|
|
33634
33826
|
}
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
|
|
33638
|
-
|
|
33639
|
-
|
|
33640
|
-
|
|
33641
|
-
|
|
33642
|
-
|
|
33643
|
-
|
|
33644
|
-
|
|
33645
|
-
|
|
33646
|
-
for (const file2 of files) {
|
|
33647
|
-
try {
|
|
33648
|
-
const data = await parseFile(file2.absolutePath, format);
|
|
33649
|
-
const group = filesByLocale.get(file2.locale) ?? [];
|
|
33650
|
-
group.push({
|
|
33651
|
-
data,
|
|
33652
|
-
path: file2.absolutePath,
|
|
33653
|
-
relativePath: file2.relativePath,
|
|
33654
|
-
locale: file2.locale,
|
|
33655
|
-
namespace: file2.namespace ?? DEFAULT_NAMESPACE
|
|
33656
|
-
});
|
|
33657
|
-
filesByLocale.set(file2.locale, group);
|
|
33658
|
-
} catch (error62) {
|
|
33659
|
-
parseErrors.push({
|
|
33660
|
-
path: file2.absolutePath,
|
|
33661
|
-
relativePath: file2.relativePath,
|
|
33662
|
-
locale: file2.locale,
|
|
33663
|
-
message: error62 instanceof Error ? error62.message : String(error62)
|
|
33664
|
-
});
|
|
33665
|
-
}
|
|
33827
|
+
let learningId = null;
|
|
33828
|
+
if (input2.learn !== void 0) {
|
|
33829
|
+
learningId = await ctx.client.mutation(api.learnings.createForProject, {
|
|
33830
|
+
...ids,
|
|
33831
|
+
srcLocale: project.baseLanguage,
|
|
33832
|
+
dstLocale: locale,
|
|
33833
|
+
sourceText: located.source,
|
|
33834
|
+
suggestedTranslation: input2.value,
|
|
33835
|
+
reasoning: input2.learn,
|
|
33836
|
+
feedbackType: "incorrect-term"
|
|
33837
|
+
});
|
|
33666
33838
|
}
|
|
33667
|
-
const locales = [...new Set(files.map((file2) => file2.locale))];
|
|
33668
33839
|
return {
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
33672
|
-
|
|
33673
|
-
|
|
33674
|
-
|
|
33675
|
-
|
|
33840
|
+
key: input2.key,
|
|
33841
|
+
locale,
|
|
33842
|
+
file: written.relativePath,
|
|
33843
|
+
value: input2.value,
|
|
33844
|
+
previous: typeof previousValue === "string" ? previousValue : null,
|
|
33845
|
+
source: located.source,
|
|
33846
|
+
placeholderProblems: comparison.ok ? [] : comparison.problems,
|
|
33847
|
+
incompletePlurals: comparison.incompletePlurals,
|
|
33848
|
+
learningId
|
|
33676
33849
|
};
|
|
33677
33850
|
}
|
|
33678
33851
|
|
|
33679
|
-
//
|
|
33680
|
-
var
|
|
33681
|
-
|
|
33682
|
-
|
|
33683
|
-
|
|
33852
|
+
// src/commands/set/index.ts
|
|
33853
|
+
var setCommand = new Command6("set").description(
|
|
33854
|
+
"Write a translation yourself; TolkBot keeps it until its source text changes"
|
|
33855
|
+
).argument("<key>", "The key, as the dashboard names it (e.g. common.save)").requiredOption("--locale <locale>", "Target locale (e.g. nb)").requiredOption("--value <text>", "The translation").option(
|
|
33856
|
+
"--force",
|
|
33857
|
+
"Write the value even when its placeholders do not match the source"
|
|
33858
|
+
).option(
|
|
33859
|
+
"--learn <note>",
|
|
33860
|
+
`Also store a learning for the project, e.g. "use Oversikt, not Dashbord: it is the app's word"`
|
|
33861
|
+
).option("--json", "Output as JSON (machine-readable)").action(async (key, opts) => {
|
|
33862
|
+
const schema = z8.object({
|
|
33863
|
+
locale: z8.string().min(1),
|
|
33864
|
+
value: z8.string(),
|
|
33865
|
+
force: z8.boolean().optional(),
|
|
33866
|
+
learn: z8.string().trim().min(1).optional(),
|
|
33867
|
+
json: z8.boolean().optional()
|
|
33868
|
+
});
|
|
33869
|
+
const parsed = validateOrExit(schema, opts);
|
|
33870
|
+
await runSet({ key, ...parsed });
|
|
33871
|
+
});
|
|
33872
|
+
async function runSet(opts) {
|
|
33873
|
+
const ctx = await loadPlanContext(process.cwd());
|
|
33874
|
+
const result = await setValue(ctx, opts);
|
|
33875
|
+
if (opts.json) jsonSuccess(result);
|
|
33876
|
+
printResult(result);
|
|
33684
33877
|
}
|
|
33685
|
-
function
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
|
|
33689
|
-
|
|
33690
|
-
|
|
33691
|
-
if (depth === 0) return i;
|
|
33692
|
-
}
|
|
33878
|
+
function printResult(result) {
|
|
33879
|
+
console.log(
|
|
33880
|
+
`\u2713 ${result.locale}: set ${result.key} in ${result.file}, recorded as yours`
|
|
33881
|
+
);
|
|
33882
|
+
if (result.previous !== null && result.previous !== result.value) {
|
|
33883
|
+
console.log(` was: ${result.previous}`);
|
|
33693
33884
|
}
|
|
33694
|
-
|
|
33695
|
-
}
|
|
33696
|
-
|
|
33697
|
-
const
|
|
33698
|
-
|
|
33699
|
-
|
|
33700
|
-
for (
|
|
33701
|
-
|
|
33702
|
-
if (c === "{") depth++;
|
|
33703
|
-
else if (c === "}") depth--;
|
|
33704
|
-
else if (c === "," && depth === 0 && parts.length < limit - 1) {
|
|
33705
|
-
parts.push(s.slice(start, i));
|
|
33706
|
-
start = i + 1;
|
|
33707
|
-
}
|
|
33885
|
+
if (result.learningId) {
|
|
33886
|
+
console.log(` learning saved: ${result.learningId}`);
|
|
33887
|
+
}
|
|
33888
|
+
for (const problem of result.placeholderProblems) {
|
|
33889
|
+
console.error(`! written with --force: ${problem}`);
|
|
33890
|
+
}
|
|
33891
|
+
for (const plural2 of result.incompletePlurals) {
|
|
33892
|
+
console.error(`! ${plural2}`);
|
|
33708
33893
|
}
|
|
33709
|
-
parts.push(s.slice(start));
|
|
33710
|
-
return parts;
|
|
33711
33894
|
}
|
|
33712
|
-
|
|
33713
|
-
|
|
33714
|
-
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33720
|
-
|
|
33721
|
-
|
|
33722
|
-
|
|
33723
|
-
|
|
33724
|
-
continue;
|
|
33725
|
-
}
|
|
33726
|
-
while (i < s.length && /\s/.test(s[i] ?? "")) i++;
|
|
33727
|
-
if (s[i] !== "{") break;
|
|
33728
|
-
const close = matchBrace(s, i);
|
|
33729
|
-
if (close === -1) break;
|
|
33730
|
-
branches.push({ keyword, message: s.slice(i + 1, close) });
|
|
33731
|
-
i = close + 1;
|
|
33732
|
-
}
|
|
33733
|
-
return branches;
|
|
33734
|
-
}
|
|
33735
|
-
function parseIcuComplex(token) {
|
|
33736
|
-
return token.complex ?? null;
|
|
33895
|
+
|
|
33896
|
+
// src/commands/status/index.ts
|
|
33897
|
+
import { Command as Command7 } from "commander";
|
|
33898
|
+
import { z as z9 } from "zod/v4";
|
|
33899
|
+
|
|
33900
|
+
// src/commands/status/status.tsx
|
|
33901
|
+
import { Spinner as Spinner14 } from "@inkjs/ui";
|
|
33902
|
+
import { Box as Box17, Text as Text17 } from "ink";
|
|
33903
|
+
|
|
33904
|
+
// src/commands/status/status-core.ts
|
|
33905
|
+
function fullKey4(namespace, key) {
|
|
33906
|
+
return namespace ? `${namespace}.${key}` : key;
|
|
33737
33907
|
}
|
|
33738
|
-
function
|
|
33739
|
-
|
|
33740
|
-
const close2 = s.indexOf("}}", i + 2);
|
|
33741
|
-
if (close2 !== -1) {
|
|
33742
|
-
const inner2 = s.slice(i + 2, close2);
|
|
33743
|
-
if (!inner2.includes("{") && !inner2.includes("}")) {
|
|
33744
|
-
const name2 = (inner2.split(",")[0] ?? "").trim();
|
|
33745
|
-
if (isIdentifier(name2)) {
|
|
33746
|
-
const rest = inner2.slice(inner2.indexOf(name2) + name2.length);
|
|
33747
|
-
const suffix = rest.trim();
|
|
33748
|
-
return {
|
|
33749
|
-
text: s.slice(i, close2 + 2),
|
|
33750
|
-
kind: "mustache",
|
|
33751
|
-
name: name2,
|
|
33752
|
-
signature: `mustache:${name2}${suffix ? `:${suffix.replace(/\s+/g, " ")}` : ""}`,
|
|
33753
|
-
start: i,
|
|
33754
|
-
end: close2 + 2
|
|
33755
|
-
};
|
|
33756
|
-
}
|
|
33757
|
-
}
|
|
33758
|
-
}
|
|
33759
|
-
}
|
|
33760
|
-
const close = matchBrace(s, i);
|
|
33761
|
-
if (close === -1) return null;
|
|
33762
|
-
const inner = s.slice(i + 1, close);
|
|
33763
|
-
const parts = splitTopLevel(inner, 3);
|
|
33764
|
-
const name = (parts[0] ?? "").trim();
|
|
33765
|
-
if (!isIdentifier(name)) return null;
|
|
33766
|
-
const text = s.slice(i, close + 1);
|
|
33767
|
-
const type = (parts[1] ?? "").trim();
|
|
33768
|
-
if (parts.length === 3 && (type === "plural" || type === "select" || type === "selectordinal")) {
|
|
33769
|
-
const branches = parseBranches(parts[2] ?? "");
|
|
33770
|
-
return {
|
|
33771
|
-
text,
|
|
33772
|
-
kind: "icu-complex",
|
|
33773
|
-
name,
|
|
33774
|
-
complex: { name, type, branches },
|
|
33775
|
-
// Branch MESSAGES are translatable, so they stay out of the
|
|
33776
|
-
// signature; their keywords and nested tokens are compared separately.
|
|
33777
|
-
signature: `icu-complex:${name}:${type}:${branches.map((b) => b.keyword).sort().join("|")}`,
|
|
33778
|
-
start: i,
|
|
33779
|
-
end: close + 1
|
|
33780
|
-
};
|
|
33781
|
-
}
|
|
33782
|
-
const normalized = parts.map((p) => p.trim().replace(/\s+/g, " ")).join(", ");
|
|
33783
|
-
return {
|
|
33784
|
-
text,
|
|
33785
|
-
kind: "icu-simple",
|
|
33786
|
-
name,
|
|
33787
|
-
signature: `icu-simple:${normalized}`,
|
|
33788
|
-
start: i,
|
|
33789
|
-
end: close + 1
|
|
33790
|
-
};
|
|
33908
|
+
function isPersonHold(reason) {
|
|
33909
|
+
return reason === "locked" || reason === "pending-draft";
|
|
33791
33910
|
}
|
|
33792
|
-
function
|
|
33793
|
-
|
|
33794
|
-
|
|
33795
|
-
|
|
33796
|
-
|
|
33797
|
-
if (isIdentifier(name)) {
|
|
33798
|
-
return {
|
|
33799
|
-
text: s.slice(i, close + 1),
|
|
33800
|
-
kind: "ruby",
|
|
33801
|
-
name,
|
|
33802
|
-
signature: `ruby:${name}`,
|
|
33803
|
-
start: i,
|
|
33804
|
-
end: close + 1
|
|
33805
|
-
};
|
|
33806
|
-
}
|
|
33807
|
-
}
|
|
33808
|
-
}
|
|
33809
|
-
let j = i + 1;
|
|
33810
|
-
if (j >= s.length || s[j] === " ") return null;
|
|
33811
|
-
const rest = s.slice(j);
|
|
33812
|
-
const argnum = /^(\d+)\$/.exec(rest);
|
|
33813
|
-
if (argnum) j += argnum[0].length;
|
|
33814
|
-
while (j < s.length && "-+0#".includes(s[j] ?? "")) j++;
|
|
33815
|
-
while (j < s.length && /\d/.test(s[j] ?? "")) j++;
|
|
33816
|
-
if (s[j] === ".") {
|
|
33817
|
-
j++;
|
|
33818
|
-
while (j < s.length && /\d/.test(s[j] ?? "")) j++;
|
|
33819
|
-
}
|
|
33820
|
-
const conv = s[j];
|
|
33821
|
-
if (!conv || !PRINTF_CONV.includes(conv)) return null;
|
|
33822
|
-
const text = s.slice(i, j + 1);
|
|
33823
|
-
return {
|
|
33824
|
-
text,
|
|
33825
|
-
kind: "printf",
|
|
33826
|
-
name: argnum?.[1] ?? "",
|
|
33827
|
-
signature: `printf:${text}`,
|
|
33828
|
-
start: i,
|
|
33829
|
-
end: j + 1
|
|
33830
|
-
};
|
|
33911
|
+
function count(reasons2) {
|
|
33912
|
+
const byReason = {};
|
|
33913
|
+
for (const reason of reasons2)
|
|
33914
|
+
byReason[reason] = (byReason[reason] ?? 0) + 1;
|
|
33915
|
+
return { total: reasons2.length, byReason };
|
|
33831
33916
|
}
|
|
33832
|
-
|
|
33833
|
-
|
|
33834
|
-
|
|
33835
|
-
|
|
33836
|
-
|
|
33837
|
-
return {
|
|
33838
|
-
text,
|
|
33839
|
-
kind: "tag",
|
|
33840
|
-
name: text.replace(/[<>/\s]/g, ""),
|
|
33841
|
-
signature: `tag:${text.replace(/\s+/g, "")}`,
|
|
33842
|
-
start: i,
|
|
33843
|
-
end: i + text.length
|
|
33844
|
-
};
|
|
33917
|
+
function openPullRequest(rows, locale) {
|
|
33918
|
+
const row = rows.find(
|
|
33919
|
+
(r) => r.prState === "open" && r.prUrl !== void 0 && sameLocale(r.targetLanguage, locale)
|
|
33920
|
+
);
|
|
33921
|
+
if (!row?.prUrl) return null;
|
|
33922
|
+
return { number: row.prNumber ?? null, url: row.prUrl };
|
|
33845
33923
|
}
|
|
33846
|
-
function
|
|
33847
|
-
|
|
33848
|
-
const
|
|
33849
|
-
|
|
33850
|
-
|
|
33924
|
+
function languageStatus(locale, plan, pullRequests, opts = {}) {
|
|
33925
|
+
const { summary } = plan;
|
|
33926
|
+
const held = [
|
|
33927
|
+
...plan.heldKeys,
|
|
33928
|
+
...plan.protectedKeys.flatMap(
|
|
33929
|
+
({ key, reason }) => isPersonHold(reason) ? [{ key, reason }] : []
|
|
33930
|
+
)
|
|
33931
|
+
];
|
|
33932
|
+
const kept = plan.protectedKeys.flatMap(
|
|
33933
|
+
({ key, reason }) => isPersonHold(reason) ? [] : [{ key, reason }]
|
|
33934
|
+
);
|
|
33935
|
+
const tasks = (reason) => plan.tasks.filter((t) => t.reason === reason).map((t) => fullKey4(t.namespace, t.key));
|
|
33851
33936
|
return {
|
|
33852
|
-
|
|
33853
|
-
|
|
33854
|
-
|
|
33855
|
-
|
|
33856
|
-
|
|
33857
|
-
|
|
33937
|
+
locale,
|
|
33938
|
+
expected: summary.totalKeys,
|
|
33939
|
+
upToDate: summary.upToDate,
|
|
33940
|
+
missing: summary.missing,
|
|
33941
|
+
changed: summary.changed,
|
|
33942
|
+
heldByPerson: count(held.map((h) => h.reason)),
|
|
33943
|
+
keptForPerson: count(kept.map((k) => k.reason)),
|
|
33944
|
+
untracked: summary.untracked,
|
|
33945
|
+
pullRequest: openPullRequest(pullRequests, locale),
|
|
33946
|
+
...opts.keys ? {
|
|
33947
|
+
keys: {
|
|
33948
|
+
missing: tasks("missing"),
|
|
33949
|
+
changed: tasks("changed"),
|
|
33950
|
+
heldByPerson: held,
|
|
33951
|
+
keptForPerson: kept,
|
|
33952
|
+
untracked: plan.untrackedKeys.map((k) => k.key)
|
|
33953
|
+
}
|
|
33954
|
+
} : {}
|
|
33858
33955
|
};
|
|
33859
33956
|
}
|
|
33860
|
-
function
|
|
33861
|
-
|
|
33862
|
-
let i = 0;
|
|
33863
|
-
while (i < s.length) {
|
|
33864
|
-
const c = s[i];
|
|
33865
|
-
let token = null;
|
|
33866
|
-
if (c === "{") token = readBraceToken(s, i);
|
|
33867
|
-
else if (c === "%") token = readPercentToken(s, i);
|
|
33868
|
-
else if (c === "<") token = readTagToken(s, i);
|
|
33869
|
-
else if (c === "$") token = readNestingToken(s, i);
|
|
33870
|
-
if (token) {
|
|
33871
|
-
tokens.push(token);
|
|
33872
|
-
i = token.end;
|
|
33873
|
-
continue;
|
|
33874
|
-
}
|
|
33875
|
-
i++;
|
|
33876
|
-
}
|
|
33877
|
-
return tokens;
|
|
33957
|
+
function needsTranslation(language) {
|
|
33958
|
+
return language.missing > 0 || language.changed > 0;
|
|
33878
33959
|
}
|
|
33879
|
-
|
|
33880
|
-
|
|
33960
|
+
var REASON_WORDS = {
|
|
33961
|
+
locked: "locked",
|
|
33962
|
+
"pending-draft": "pending draft",
|
|
33963
|
+
manual: "written by a person",
|
|
33964
|
+
"target-edited": "edited since TolkBot wrote it",
|
|
33965
|
+
unknown: "no record of who wrote it"
|
|
33966
|
+
};
|
|
33967
|
+
function reasons(counts) {
|
|
33968
|
+
return Object.entries(counts.byReason).map(
|
|
33969
|
+
([reason, n]) => `${n} ${REASON_WORDS[reason]}`
|
|
33970
|
+
).join(", ");
|
|
33881
33971
|
}
|
|
33882
|
-
function
|
|
33883
|
-
const
|
|
33884
|
-
|
|
33885
|
-
|
|
33886
|
-
|
|
33887
|
-
|
|
33888
|
-
|
|
33889
|
-
|
|
33972
|
+
function describeLanguage(language) {
|
|
33973
|
+
const counts = [
|
|
33974
|
+
`${language.expected} expected`,
|
|
33975
|
+
`${language.upToDate} up to date`,
|
|
33976
|
+
`${language.missing} missing`,
|
|
33977
|
+
`${language.changed} changed since translated`
|
|
33978
|
+
].join(", ");
|
|
33979
|
+
const mark = needsTranslation(language) ? "\u2022" : "\u2713";
|
|
33980
|
+
const lines = [`${mark} ${language.locale}: ${counts}`];
|
|
33981
|
+
if (language.heldByPerson.total > 0) {
|
|
33982
|
+
lines.push(
|
|
33983
|
+
` ${language.heldByPerson.total} held by a person (${reasons(language.heldByPerson)})`
|
|
33984
|
+
);
|
|
33890
33985
|
}
|
|
33891
|
-
|
|
33986
|
+
if (language.keptForPerson.total > 0) {
|
|
33987
|
+
lines.push(
|
|
33988
|
+
` ${language.keptForPerson.total} changed but kept for a person (${reasons(language.keptForPerson)})`
|
|
33989
|
+
);
|
|
33990
|
+
}
|
|
33991
|
+
if (language.untracked > 0) {
|
|
33992
|
+
lines.push(
|
|
33993
|
+
` ${language.untracked} untracked: TolkBot has no record of them`
|
|
33994
|
+
);
|
|
33995
|
+
}
|
|
33996
|
+
if (language.pullRequest) {
|
|
33997
|
+
lines.push(` Pull request: ${language.pullRequest.url}`);
|
|
33998
|
+
}
|
|
33999
|
+
const { keys } = language;
|
|
34000
|
+
if (keys) {
|
|
34001
|
+
const list = (label, names) => {
|
|
34002
|
+
if (names.length === 0) return;
|
|
34003
|
+
lines.push(` ${label}:`);
|
|
34004
|
+
for (const name of names) lines.push(` ${name}`);
|
|
34005
|
+
};
|
|
34006
|
+
list("Missing", keys.missing);
|
|
34007
|
+
list("Changed", keys.changed);
|
|
34008
|
+
list(
|
|
34009
|
+
"Held by a person",
|
|
34010
|
+
keys.heldByPerson.map(
|
|
34011
|
+
(k) => `${k.key} (${REASON_WORDS[k.reason]})`
|
|
34012
|
+
)
|
|
34013
|
+
);
|
|
34014
|
+
list(
|
|
34015
|
+
"Kept for a person",
|
|
34016
|
+
keys.keptForPerson.map(
|
|
34017
|
+
(k) => `${k.key} (${REASON_WORDS[k.reason]})`
|
|
34018
|
+
)
|
|
34019
|
+
);
|
|
34020
|
+
list("Untracked", keys.untracked);
|
|
34021
|
+
}
|
|
34022
|
+
return lines;
|
|
33892
34023
|
}
|
|
33893
|
-
|
|
33894
|
-
|
|
33895
|
-
|
|
33896
|
-
|
|
33897
|
-
|
|
33898
|
-
|
|
33899
|
-
|
|
33900
|
-
|
|
33901
|
-
|
|
33902
|
-
|
|
33903
|
-
|
|
33904
|
-
|
|
33905
|
-
|
|
34024
|
+
|
|
34025
|
+
// src/commands/status/status.tsx
|
|
34026
|
+
import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
34027
|
+
var OFFLINE_HINT = "Without a login, 'tolkbot validate' checks the files on this machine.";
|
|
34028
|
+
async function runStatus(opts) {
|
|
34029
|
+
let ctx;
|
|
34030
|
+
try {
|
|
34031
|
+
ctx = await loadPlanContext(process.cwd());
|
|
34032
|
+
} catch (error62) {
|
|
34033
|
+
if (error62 instanceof AuthError) {
|
|
34034
|
+
throw new AuthError(
|
|
34035
|
+
error62.baseMessage,
|
|
34036
|
+
[error62.hint, OFFLINE_HINT].filter(Boolean).join(" "),
|
|
34037
|
+
error62.code
|
|
33906
34038
|
);
|
|
33907
|
-
if (srcHashes > outHashes) missing.push("#");
|
|
33908
|
-
else extra.push("#");
|
|
33909
34039
|
}
|
|
33910
|
-
|
|
33911
|
-
|
|
33912
|
-
|
|
33913
|
-
|
|
33914
|
-
|
|
33915
|
-
|
|
33916
|
-
|
|
33917
|
-
|
|
34040
|
+
throw error62;
|
|
34041
|
+
}
|
|
34042
|
+
const { project } = ctx;
|
|
34043
|
+
const srcLocale = project.baseLanguage;
|
|
34044
|
+
const locales = project.targetLanguages;
|
|
34045
|
+
const progress = renderTransient(
|
|
34046
|
+
/* @__PURE__ */ jsxs17(Box17, { children: [
|
|
34047
|
+
/* @__PURE__ */ jsx17(Spinner14, {}),
|
|
34048
|
+
/* @__PURE__ */ jsxs17(Text17, { children: [
|
|
34049
|
+
" Reading the status of ",
|
|
34050
|
+
project.name,
|
|
34051
|
+
"\u2026"
|
|
34052
|
+
] })
|
|
34053
|
+
] })
|
|
34054
|
+
);
|
|
34055
|
+
const languages = [];
|
|
34056
|
+
let credits;
|
|
34057
|
+
try {
|
|
34058
|
+
const pullRequests = locales.length === 0 ? [] : await ctx.client.query(api.prJobs.listByProject, {
|
|
34059
|
+
organizationId: project.organizationId,
|
|
34060
|
+
projectId: project._id
|
|
34061
|
+
});
|
|
34062
|
+
for (const locale of locales) {
|
|
34063
|
+
const plan = await planForLocale(ctx, srcLocale, locale);
|
|
34064
|
+
languages.push(
|
|
34065
|
+
languageStatus(locale, plan, pullRequests, {
|
|
34066
|
+
keys: opts.keys
|
|
34067
|
+
})
|
|
33918
34068
|
);
|
|
33919
34069
|
}
|
|
34070
|
+
credits = await readCreditBalance(ctx);
|
|
34071
|
+
} finally {
|
|
34072
|
+
progress.unmount();
|
|
33920
34073
|
}
|
|
33921
|
-
|
|
33922
|
-
|
|
33923
|
-
|
|
33924
|
-
|
|
33925
|
-
|
|
33926
|
-
|
|
33927
|
-
|
|
33928
|
-
|
|
33929
|
-
|
|
33930
|
-
|
|
33931
|
-
|
|
34074
|
+
const payload = {
|
|
34075
|
+
project: {
|
|
34076
|
+
name: project.name,
|
|
34077
|
+
slug: project.slug,
|
|
34078
|
+
url: `${env.TOLKBOT_WEB_URL}/projects/${project.slug}`,
|
|
34079
|
+
baseLanguage: srcLocale,
|
|
34080
|
+
pattern: fullPattern(scopeOf(project)),
|
|
34081
|
+
autoCreatePR: project.workflow?.autoCreatePR ?? DEFAULT_BEHAVIORAL_SETTINGS.workflow.autoCreatePR
|
|
34082
|
+
},
|
|
34083
|
+
credits: { balance: credits },
|
|
34084
|
+
languages
|
|
34085
|
+
};
|
|
34086
|
+
const outOfDate = languages.some(needsTranslation);
|
|
34087
|
+
const exitCode = opts.exitCode && outOfDate ? 1 : 0;
|
|
34088
|
+
if (opts.json) {
|
|
34089
|
+
if (exitCode === 0) jsonSuccess(payload);
|
|
34090
|
+
jsonResult(payload, exitCode);
|
|
34091
|
+
return;
|
|
33932
34092
|
}
|
|
33933
|
-
|
|
33934
|
-
|
|
33935
|
-
|
|
34093
|
+
const { project: p } = payload;
|
|
34094
|
+
console.log(`${p.name} (${p.slug}) ${p.url}`);
|
|
34095
|
+
console.log(
|
|
34096
|
+
`Base language ${p.baseLanguage}, files ${p.pattern}, auto-create pull requests ${p.autoCreatePR ? "on" : "off"}`
|
|
34097
|
+
);
|
|
34098
|
+
console.log(
|
|
34099
|
+
credits === null ? "Credits left: could not be read" : `Credits left: ${credits}`
|
|
34100
|
+
);
|
|
34101
|
+
console.log("");
|
|
34102
|
+
if (languages.length === 0) {
|
|
34103
|
+
console.log(
|
|
34104
|
+
`No target languages configured. Add them in the project settings at ${p.url}/settings.`
|
|
33936
34105
|
);
|
|
33937
34106
|
}
|
|
33938
|
-
const
|
|
33939
|
-
|
|
33940
|
-
if (t.kind !== "icu-complex") continue;
|
|
33941
|
-
const bucket = matchedOut.get(t.signature) ?? [];
|
|
33942
|
-
bucket.push(t);
|
|
33943
|
-
matchedOut.set(t.signature, bucket);
|
|
34107
|
+
for (const language of languages) {
|
|
34108
|
+
for (const line of describeLanguage(language)) console.log(line);
|
|
33944
34109
|
}
|
|
33945
|
-
|
|
33946
|
-
|
|
33947
|
-
|
|
33948
|
-
|
|
33949
|
-
compareIcuBranches(t, counterpart, problems, missing, extra);
|
|
33950
|
-
}
|
|
34110
|
+
if (outOfDate) {
|
|
34111
|
+
console.log(
|
|
34112
|
+
"\nRun 'tolkbot translate --all' to translate what is missing or changed."
|
|
34113
|
+
);
|
|
33951
34114
|
}
|
|
33952
|
-
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
|
|
33957
|
-
|
|
34115
|
+
process.exitCode = exitCode;
|
|
34116
|
+
}
|
|
34117
|
+
|
|
34118
|
+
// src/commands/status/index.ts
|
|
34119
|
+
var statusCommand = new Command7("status").description(
|
|
34120
|
+
"Show what each language is missing, what waits for a person, the open pull requests and the credits left"
|
|
34121
|
+
).option("--keys", "List the keys by name under each language").option(
|
|
34122
|
+
"--exit-code",
|
|
34123
|
+
"Exit 1 when any language has missing or changed keys"
|
|
34124
|
+
).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
34125
|
+
const schema = z9.object({
|
|
34126
|
+
keys: z9.boolean().optional(),
|
|
34127
|
+
exitCode: z9.boolean().optional(),
|
|
34128
|
+
json: z9.boolean().optional()
|
|
34129
|
+
});
|
|
34130
|
+
await runStatus(validateOrExit(schema, opts));
|
|
34131
|
+
});
|
|
34132
|
+
|
|
34133
|
+
// src/commands/telemetry.ts
|
|
34134
|
+
import { Command as Command8 } from "commander";
|
|
34135
|
+
import { z as z10 } from "zod/v4";
|
|
34136
|
+
var REASONS = {
|
|
34137
|
+
env: `disabled by $${TELEMETRY_ENV_VAR}`,
|
|
34138
|
+
"do-not-track": `disabled by $${DO_NOT_TRACK_ENV_VAR}`,
|
|
34139
|
+
preference: "disabled with `tolkbot telemetry off`",
|
|
34140
|
+
"no-key": "disabled: this build has no telemetry key",
|
|
34141
|
+
default: "enabled"
|
|
34142
|
+
};
|
|
34143
|
+
async function printStatus(json2) {
|
|
34144
|
+
const state = await telemetryState();
|
|
34145
|
+
if (json2) {
|
|
34146
|
+
jsonResult({ ...state, preferenceFile: getPreferencePath() }, 0);
|
|
34147
|
+
return;
|
|
34148
|
+
}
|
|
34149
|
+
console.log(
|
|
34150
|
+
`Telemetry is ${state.enabled ? "on" : "off"} (${REASONS[state.reason]}).`
|
|
34151
|
+
);
|
|
34152
|
+
console.log(
|
|
34153
|
+
`Turn it off with \`tolkbot telemetry off\`, $${TELEMETRY_ENV_VAR}=0 or $${DO_NOT_TRACK_ENV_VAR}=1.`
|
|
34154
|
+
);
|
|
34155
|
+
}
|
|
34156
|
+
var telemetryCommand = new Command8("telemetry").description(
|
|
34157
|
+
"Control anonymous usage reporting"
|
|
34158
|
+
);
|
|
34159
|
+
telemetryCommand.command("status", { isDefault: true }).description("Show whether telemetry is on and why").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
34160
|
+
const parsed = validateOrExit(
|
|
34161
|
+
z10.object({ json: z10.boolean().optional() }),
|
|
34162
|
+
opts
|
|
34163
|
+
);
|
|
34164
|
+
await printStatus(parsed.json);
|
|
34165
|
+
});
|
|
34166
|
+
telemetryCommand.command("off").description("Stop reporting usage from this machine").action(async () => {
|
|
34167
|
+
await setTelemetryPreference(false);
|
|
34168
|
+
console.log(
|
|
34169
|
+
`Telemetry is off. Preference saved to ${getPreferencePath()}.`
|
|
34170
|
+
);
|
|
34171
|
+
});
|
|
34172
|
+
telemetryCommand.command("on").description("Resume reporting usage from this machine").action(async () => {
|
|
34173
|
+
await setTelemetryPreference(true);
|
|
34174
|
+
await printStatus(false);
|
|
34175
|
+
});
|
|
34176
|
+
|
|
34177
|
+
// src/commands/translate/index.ts
|
|
34178
|
+
import { Command as Command9 } from "commander";
|
|
34179
|
+
import { z as z11 } from "zod/v4";
|
|
34180
|
+
|
|
34181
|
+
// src/commands/translate/adopt.tsx
|
|
34182
|
+
var STAMP_CHUNK2 = 200;
|
|
34183
|
+
async function adoptLocale(ctx, srcLocale, dstLocale) {
|
|
34184
|
+
const plan = await planForLocale(ctx, srcLocale, dstLocale);
|
|
34185
|
+
const locale = governanceLocale(ctx.project, dstLocale);
|
|
34186
|
+
const byTemplate = /* @__PURE__ */ new Map();
|
|
34187
|
+
for (const key of plan.untrackedKeys) {
|
|
34188
|
+
const group = byTemplate.get(key.pathTemplate) ?? [];
|
|
34189
|
+
group.push(key);
|
|
34190
|
+
byTemplate.set(key.pathTemplate, group);
|
|
34191
|
+
}
|
|
34192
|
+
const outcome = {
|
|
34193
|
+
dstLocale,
|
|
34194
|
+
untracked: plan.untrackedKeys.length,
|
|
34195
|
+
adopted: 0,
|
|
34196
|
+
skipped: 0
|
|
33958
34197
|
};
|
|
34198
|
+
for (const [pathTemplate, keys] of byTemplate) {
|
|
34199
|
+
for (let i = 0; i < keys.length; i += STAMP_CHUNK2) {
|
|
34200
|
+
const result = await ctx.client.mutation(
|
|
34201
|
+
api.localeKeyStates.adoptUntrackedForCli,
|
|
34202
|
+
{
|
|
34203
|
+
organizationId: ctx.project.organizationId,
|
|
34204
|
+
projectId: ctx.project._id,
|
|
34205
|
+
locale,
|
|
34206
|
+
pathTemplate,
|
|
34207
|
+
entries: keys.slice(i, i + STAMP_CHUNK2).map((key) => ({
|
|
34208
|
+
key: key.fileKey,
|
|
34209
|
+
namespace: key.namespace,
|
|
34210
|
+
baseValue: key.src,
|
|
34211
|
+
translatedValue: key.dst
|
|
34212
|
+
}))
|
|
34213
|
+
}
|
|
34214
|
+
);
|
|
34215
|
+
outcome.adopted += result.adopted;
|
|
34216
|
+
outcome.skipped += result.skipped;
|
|
34217
|
+
}
|
|
34218
|
+
}
|
|
34219
|
+
return outcome;
|
|
33959
34220
|
}
|
|
33960
|
-
function
|
|
33961
|
-
const
|
|
33962
|
-
|
|
33963
|
-
|
|
33964
|
-
|
|
34221
|
+
async function runAdopt(opts) {
|
|
34222
|
+
const run = async () => {
|
|
34223
|
+
const ctx = await loadPlanContext(process.cwd());
|
|
34224
|
+
const src = opts.src ?? ctx.project.baseLanguage;
|
|
34225
|
+
const locales = opts.all ? ctx.project.targetLanguages : opts.dst ? [opts.dst] : [];
|
|
34226
|
+
if (locales.length === 0) {
|
|
34227
|
+
throw opts.all ? new CliError(
|
|
34228
|
+
"No target languages configured for this project.",
|
|
34229
|
+
"Add one with 'tolkbot languages add <code>'.",
|
|
34230
|
+
"no_target_languages"
|
|
34231
|
+
) : new UsageError(
|
|
34232
|
+
"Either --dst <locale> or --all must be provided."
|
|
34233
|
+
);
|
|
34234
|
+
}
|
|
34235
|
+
const outcomes2 = [];
|
|
34236
|
+
for (const dst of locales)
|
|
34237
|
+
outcomes2.push(await adoptLocale(ctx, src, dst));
|
|
34238
|
+
track("cli_translate_completed", {
|
|
34239
|
+
mode: "adopt",
|
|
34240
|
+
key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
|
|
34241
|
+
locale_count: outcomes2.length,
|
|
34242
|
+
job_count: 0,
|
|
34243
|
+
json: opts.json === true
|
|
34244
|
+
});
|
|
34245
|
+
return outcomes2;
|
|
34246
|
+
};
|
|
34247
|
+
if (opts.json) {
|
|
34248
|
+
jsonSuccess({ adopted: await run() });
|
|
33965
34249
|
}
|
|
34250
|
+
const outcomes = await run();
|
|
34251
|
+
for (const o of outcomes) {
|
|
34252
|
+
console.log(
|
|
34253
|
+
`\u2713 ${o.dstLocale}: adopted ${o.adopted} untracked translation(s)` + (o.skipped > 0 ? `, ${o.skipped} already tracked` : "")
|
|
34254
|
+
);
|
|
34255
|
+
}
|
|
34256
|
+
}
|
|
34257
|
+
|
|
34258
|
+
// src/commands/translate/translate.tsx
|
|
34259
|
+
import { Spinner as Spinner15 } from "@inkjs/ui";
|
|
34260
|
+
import { Box as Box18, Text as Text18 } from "ink";
|
|
34261
|
+
import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
34262
|
+
function planOutput(plan) {
|
|
33966
34263
|
return {
|
|
33967
|
-
|
|
33968
|
-
|
|
33969
|
-
|
|
33970
|
-
|
|
34264
|
+
...plan.summary,
|
|
34265
|
+
protectedKeys: plan.protectedKeys,
|
|
34266
|
+
heldKeys: plan.heldKeys,
|
|
34267
|
+
untrackedKeys: plan.untrackedKeys.map((k) => k.key)
|
|
33971
34268
|
};
|
|
33972
34269
|
}
|
|
33973
|
-
function
|
|
33974
|
-
|
|
33975
|
-
const byPositionDesc = [...plurals].sort(
|
|
33976
|
-
(a, b) => b.token.start - a.token.start
|
|
33977
|
-
);
|
|
33978
|
-
for (const p of byPositionDesc) {
|
|
33979
|
-
out = out.slice(0, p.token.start) + " " + out.slice(p.token.end);
|
|
33980
|
-
}
|
|
33981
|
-
return out;
|
|
34270
|
+
function fullKey5(namespace, key) {
|
|
34271
|
+
return namespace ? `${namespace}.${key}` : key;
|
|
33982
34272
|
}
|
|
33983
|
-
function
|
|
33984
|
-
return
|
|
34273
|
+
function characters(text) {
|
|
34274
|
+
return Array.from(text).length;
|
|
33985
34275
|
}
|
|
33986
|
-
function
|
|
33987
|
-
|
|
33988
|
-
const
|
|
33989
|
-
if (
|
|
33990
|
-
|
|
33991
|
-
|
|
34276
|
+
function targetLocales(ctx, opts) {
|
|
34277
|
+
if (opts.dst) return [opts.dst];
|
|
34278
|
+
const locales = ctx.project.targetLanguages;
|
|
34279
|
+
if (locales.length === 0) {
|
|
34280
|
+
throw new CliError(
|
|
34281
|
+
"No target languages configured.",
|
|
34282
|
+
"Add one with 'tolkbot languages add <code>'.",
|
|
34283
|
+
"no_target_languages"
|
|
33992
34284
|
);
|
|
33993
34285
|
}
|
|
33994
|
-
|
|
33995
|
-
|
|
33996
|
-
|
|
33997
|
-
|
|
34286
|
+
return locales;
|
|
34287
|
+
}
|
|
34288
|
+
async function runTranslate(opts) {
|
|
34289
|
+
const ctx = await loadPlanContext(process.cwd());
|
|
34290
|
+
const srcLocale = opts.src ?? ctx.project.baseLanguage;
|
|
34291
|
+
const dstLocales = targetLocales(ctx, opts);
|
|
34292
|
+
if (opts.dryRun) {
|
|
34293
|
+
await runDryRun(ctx, srcLocale, dstLocales, opts);
|
|
34294
|
+
return;
|
|
34295
|
+
}
|
|
34296
|
+
await runLanguages(ctx, srcLocale, dstLocales, opts);
|
|
34297
|
+
}
|
|
34298
|
+
var NO_ESTIMATE = "No credit estimate exists for this project yet. Keys and characters are what a run would send.";
|
|
34299
|
+
async function runDryRun(ctx, srcLocale, dstLocales, opts) {
|
|
34300
|
+
const languages = [];
|
|
34301
|
+
const progress = renderTransient(
|
|
34302
|
+
/* @__PURE__ */ jsx18(Working, { text: `Planning ${dstLocales.join(", ")}\u2026` })
|
|
33998
34303
|
);
|
|
33999
|
-
|
|
34000
|
-
|
|
34001
|
-
|
|
34002
|
-
|
|
34304
|
+
try {
|
|
34305
|
+
for (const dstLocale of dstLocales) {
|
|
34306
|
+
const { plan } = await generateTasks(ctx, srcLocale, dstLocale, {
|
|
34307
|
+
retranslateUntracked: opts.retranslateUntracked
|
|
34308
|
+
});
|
|
34309
|
+
languages.push({
|
|
34310
|
+
dstLocale,
|
|
34311
|
+
keys: plan.tasks.length,
|
|
34312
|
+
characters: plan.tasks.reduce(
|
|
34313
|
+
(n, task) => n + characters(task.src),
|
|
34314
|
+
0
|
|
34315
|
+
),
|
|
34316
|
+
tasks: plan.tasks.map((task) => ({
|
|
34317
|
+
key: fullKey5(task.namespace, task.key),
|
|
34318
|
+
path: task.path,
|
|
34319
|
+
reason: task.reason,
|
|
34320
|
+
...opts.verbose ? { src: task.src } : {}
|
|
34321
|
+
})),
|
|
34322
|
+
plan
|
|
34323
|
+
});
|
|
34324
|
+
}
|
|
34325
|
+
} finally {
|
|
34326
|
+
progress.unmount();
|
|
34003
34327
|
}
|
|
34004
|
-
const
|
|
34005
|
-
|
|
34006
|
-
|
|
34007
|
-
|
|
34008
|
-
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
|
|
34012
|
-
|
|
34013
|
-
|
|
34014
|
-
|
|
34015
|
-
|
|
34328
|
+
const total = {
|
|
34329
|
+
keys: languages.reduce((n, l) => n + l.keys, 0),
|
|
34330
|
+
characters: languages.reduce((n, l) => n + l.characters, 0)
|
|
34331
|
+
};
|
|
34332
|
+
const balance = await readCreditBalance(ctx);
|
|
34333
|
+
track("cli_translate_completed", {
|
|
34334
|
+
mode: "plan",
|
|
34335
|
+
key_count: total.keys,
|
|
34336
|
+
locale_count: languages.length,
|
|
34337
|
+
job_count: 0,
|
|
34338
|
+
json: opts.json ?? false
|
|
34339
|
+
});
|
|
34340
|
+
if (opts.json) {
|
|
34341
|
+
jsonSuccess({
|
|
34342
|
+
dryRun: true,
|
|
34343
|
+
srcLocale,
|
|
34344
|
+
languages: languages.map(({ plan, ...language }) => ({
|
|
34345
|
+
...language,
|
|
34346
|
+
plan: planOutput(plan)
|
|
34347
|
+
})),
|
|
34348
|
+
total,
|
|
34349
|
+
estimate: null,
|
|
34350
|
+
estimateNote: NO_ESTIMATE,
|
|
34351
|
+
credits: { balance }
|
|
34352
|
+
});
|
|
34353
|
+
}
|
|
34354
|
+
for (const language of languages) {
|
|
34355
|
+
const head = `${srcLocale} \u2192 ${language.dstLocale}`;
|
|
34356
|
+
if (language.keys === 0) {
|
|
34357
|
+
console.log(`\u2713 ${head}: up to date`);
|
|
34358
|
+
} else {
|
|
34359
|
+
console.log(
|
|
34360
|
+
`\u2022 ${head}: ${language.keys} key(s) to translate, ${language.characters} characters`
|
|
34016
34361
|
);
|
|
34017
|
-
if (srcHashes > outHashes) result.missing.push("#");
|
|
34018
|
-
else result.extra.push("#");
|
|
34019
34362
|
}
|
|
34020
|
-
const
|
|
34021
|
-
|
|
34022
|
-
|
|
34023
|
-
|
|
34024
|
-
|
|
34025
|
-
|
|
34026
|
-
|
|
34027
|
-
|
|
34028
|
-
|
|
34029
|
-
|
|
34030
|
-
|
|
34031
|
-
|
|
34032
|
-
|
|
34363
|
+
for (const line of describePlan(language.plan)) {
|
|
34364
|
+
console.log(` ${line}`);
|
|
34365
|
+
}
|
|
34366
|
+
if (opts.verbose) {
|
|
34367
|
+
for (const task of language.tasks) {
|
|
34368
|
+
console.log(` ${task.key}: ${JSON.stringify(task.src)}`);
|
|
34369
|
+
}
|
|
34370
|
+
}
|
|
34371
|
+
}
|
|
34372
|
+
console.log(
|
|
34373
|
+
`
|
|
34374
|
+
In all: ${total.keys} key(s), ${total.characters} characters.` + (balance === null ? "" : ` Credit balance: ${balance}.`)
|
|
34375
|
+
);
|
|
34376
|
+
console.log(NO_ESTIMATE);
|
|
34377
|
+
console.log("Nothing was started. Run without --dry-run to translate.");
|
|
34378
|
+
}
|
|
34379
|
+
function Working({ text }) {
|
|
34380
|
+
return /* @__PURE__ */ jsxs18(Box18, { children: [
|
|
34381
|
+
/* @__PURE__ */ jsx18(Spinner15, {}),
|
|
34382
|
+
/* @__PURE__ */ jsxs18(Text18, { children: [
|
|
34383
|
+
" ",
|
|
34384
|
+
text
|
|
34385
|
+
] })
|
|
34386
|
+
] });
|
|
34387
|
+
}
|
|
34388
|
+
function progressText(dstLocale, summary) {
|
|
34389
|
+
if (!summary) return `${dstLocale}: starting\u2026`;
|
|
34390
|
+
const done = summary.completed + summary.failed;
|
|
34391
|
+
return `${dstLocale}: translating ${done}/${summary.total}\u2026`;
|
|
34392
|
+
}
|
|
34393
|
+
function describeOutcome2(outcome) {
|
|
34394
|
+
const { dstLocale, status, keys, written } = outcome;
|
|
34395
|
+
const wrote = written ? `, ${written.keys} written to ${written.files} file(s)` : "";
|
|
34396
|
+
const notApplied = outcome.notApplied.length > 0 ? `, ${outcome.notApplied.length} not applied` : "";
|
|
34397
|
+
switch (status) {
|
|
34398
|
+
case "up-to-date":
|
|
34399
|
+
return `\u2713 ${dstLocale}: up to date`;
|
|
34400
|
+
case "started":
|
|
34401
|
+
return `\u2192 ${dstLocale}: started job ${outcome.jobId} with ${keys} key(s)`;
|
|
34402
|
+
case "succeeded":
|
|
34403
|
+
return `\u2713 ${dstLocale}: ${outcome.completed} translated${wrote}${notApplied}`;
|
|
34404
|
+
case "partial":
|
|
34405
|
+
return `\u26A0 ${dstLocale}: ${outcome.completed} of ${keys} translated, ${outcome.failed} failed${wrote}${notApplied}`;
|
|
34406
|
+
case "failed":
|
|
34407
|
+
return `\u2717 ${dstLocale}: the job failed (${outcome.jobId})${wrote}${notApplied}`;
|
|
34408
|
+
case "cancelled":
|
|
34409
|
+
return `\u2717 ${dstLocale}: the job was cancelled (${outcome.jobId})`;
|
|
34410
|
+
case "timed-out":
|
|
34411
|
+
return `\u2717 ${dstLocale}: ${outcome.error}`;
|
|
34412
|
+
case "not-started":
|
|
34413
|
+
return `\u2717 ${dstLocale}: not started. ${outcome.error}`;
|
|
34414
|
+
case "pending":
|
|
34415
|
+
case "running":
|
|
34416
|
+
return `\u2026 ${dstLocale}: ${status}`;
|
|
34033
34417
|
}
|
|
34034
34418
|
}
|
|
34035
|
-
function
|
|
34036
|
-
const
|
|
34037
|
-
const
|
|
34038
|
-
const
|
|
34039
|
-
|
|
34040
|
-
|
|
34041
|
-
)
|
|
34042
|
-
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34419
|
+
async function runLanguages(ctx, srcLocale, dstLocales, opts) {
|
|
34420
|
+
const { project, framework } = ctx;
|
|
34421
|
+
const json2 = opts.json ?? false;
|
|
34422
|
+
const outcomes = [];
|
|
34423
|
+
let failure2 = null;
|
|
34424
|
+
let stopped = null;
|
|
34425
|
+
const report = (outcome) => {
|
|
34426
|
+
outcomes.push(outcome);
|
|
34427
|
+
if (json2) return;
|
|
34428
|
+
console.log(describeOutcome2(outcome));
|
|
34429
|
+
for (const line of describeNotApplied(outcome.notApplied)) {
|
|
34430
|
+
console.log(` ${line}`);
|
|
34431
|
+
}
|
|
34432
|
+
};
|
|
34433
|
+
for (const dstLocale of dstLocales) {
|
|
34434
|
+
const progress = renderTransient(
|
|
34435
|
+
/* @__PURE__ */ jsx18(Working, { text: `${dstLocale}: planning\u2026` })
|
|
34046
34436
|
);
|
|
34047
|
-
|
|
34048
|
-
|
|
34049
|
-
|
|
34050
|
-
|
|
34051
|
-
|
|
34437
|
+
try {
|
|
34438
|
+
const { tasks, plan } = await generateTasks(
|
|
34439
|
+
ctx,
|
|
34440
|
+
srcLocale,
|
|
34441
|
+
dstLocale,
|
|
34442
|
+
{ retranslateUntracked: opts.retranslateUntracked }
|
|
34052
34443
|
);
|
|
34053
|
-
|
|
34444
|
+
const base = {
|
|
34445
|
+
dstLocale,
|
|
34446
|
+
status: "up-to-date",
|
|
34447
|
+
jobId: null,
|
|
34448
|
+
keys: tasks.length,
|
|
34449
|
+
completed: null,
|
|
34450
|
+
failed: null,
|
|
34451
|
+
written: null,
|
|
34452
|
+
notApplied: [],
|
|
34453
|
+
error: null,
|
|
34454
|
+
plan: planOutput(plan)
|
|
34455
|
+
};
|
|
34456
|
+
const leftOut = describeLeftOut(plan);
|
|
34457
|
+
if (stopped !== null) {
|
|
34458
|
+
progress.unmount();
|
|
34459
|
+
report({ ...base, status: "not-started", error: stopped });
|
|
34460
|
+
continue;
|
|
34461
|
+
}
|
|
34462
|
+
if (tasks.length === 0) {
|
|
34463
|
+
progress.unmount();
|
|
34464
|
+
report(base);
|
|
34465
|
+
printNotes(json2, leftOut);
|
|
34466
|
+
continue;
|
|
34467
|
+
}
|
|
34468
|
+
progress.rerender(/* @__PURE__ */ jsx18(Working, { text: progressText(dstLocale, null) }));
|
|
34469
|
+
let jobId;
|
|
34470
|
+
try {
|
|
34471
|
+
jobId = await startTranslationJob({
|
|
34472
|
+
projectId: project._id,
|
|
34473
|
+
organizationId: project.organizationId,
|
|
34474
|
+
framework,
|
|
34475
|
+
srcLocale,
|
|
34476
|
+
dstLocale,
|
|
34477
|
+
tasks
|
|
34478
|
+
});
|
|
34479
|
+
} catch (error62) {
|
|
34480
|
+
const refused = toCliError(error62);
|
|
34481
|
+
failure2 ??= refused;
|
|
34482
|
+
progress.unmount();
|
|
34483
|
+
report({
|
|
34484
|
+
...base,
|
|
34485
|
+
status: "not-started",
|
|
34486
|
+
error: refused.baseMessage
|
|
34487
|
+
});
|
|
34488
|
+
if (exitCodeFor(refused.code) === EXIT_CODES.billing) {
|
|
34489
|
+
stopped = `TolkBot refused the ${dstLocale} job: ${refused.baseMessage}`;
|
|
34490
|
+
}
|
|
34491
|
+
continue;
|
|
34492
|
+
}
|
|
34493
|
+
if (!opts.wait) {
|
|
34494
|
+
progress.unmount();
|
|
34495
|
+
report({ ...base, status: "started", jobId });
|
|
34496
|
+
printNotes(json2, leftOut);
|
|
34497
|
+
continue;
|
|
34498
|
+
}
|
|
34499
|
+
let summary;
|
|
34500
|
+
try {
|
|
34501
|
+
summary = await waitForTranslationJob(
|
|
34502
|
+
project.organizationId,
|
|
34503
|
+
jobId,
|
|
34504
|
+
(latest) => progress.rerender(
|
|
34505
|
+
/* @__PURE__ */ jsx18(Working, { text: progressText(dstLocale, latest) })
|
|
34506
|
+
)
|
|
34507
|
+
);
|
|
34508
|
+
} catch (error62) {
|
|
34509
|
+
if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
|
|
34510
|
+
failure2 ??= error62;
|
|
34511
|
+
progress.unmount();
|
|
34512
|
+
report({
|
|
34513
|
+
...base,
|
|
34514
|
+
status: "timed-out",
|
|
34515
|
+
jobId,
|
|
34516
|
+
error: error62.baseMessage
|
|
34517
|
+
});
|
|
34518
|
+
stopped = `The CLI stopped waiting for the ${dstLocale} job.`;
|
|
34519
|
+
continue;
|
|
34520
|
+
}
|
|
34521
|
+
const finished = {
|
|
34522
|
+
...base,
|
|
34523
|
+
status: summary.status,
|
|
34524
|
+
jobId,
|
|
34525
|
+
completed: summary.completed,
|
|
34526
|
+
failed: summary.failed
|
|
34527
|
+
};
|
|
34528
|
+
if (summary.status !== "cancelled") {
|
|
34529
|
+
progress.rerender(
|
|
34530
|
+
/* @__PURE__ */ jsx18(Working, { text: `${dstLocale}: writing the files\u2026` })
|
|
34531
|
+
);
|
|
34532
|
+
const applied = await applyJob(ctx, jobId);
|
|
34533
|
+
finished.written = { files: applied.files, keys: applied.keys };
|
|
34534
|
+
finished.notApplied = applied.notApplied;
|
|
34535
|
+
}
|
|
34536
|
+
const unsuccessful = unsuccessfulJobError(jobId, summary.status);
|
|
34537
|
+
if (unsuccessful) {
|
|
34538
|
+
failure2 ??= unsuccessful;
|
|
34539
|
+
finished.error = unsuccessful.baseMessage;
|
|
34540
|
+
}
|
|
34541
|
+
progress.unmount();
|
|
34542
|
+
report(finished);
|
|
34543
|
+
printNotes(json2, leftOut);
|
|
34544
|
+
if (summary.status === "cancelled") {
|
|
34545
|
+
stopped = `The ${dstLocale} job was cancelled.`;
|
|
34546
|
+
}
|
|
34547
|
+
} finally {
|
|
34548
|
+
progress.unmount();
|
|
34054
34549
|
}
|
|
34055
|
-
comparePluralPair(p, counterpart, dstLocale, result);
|
|
34056
34550
|
}
|
|
34057
|
-
|
|
34058
|
-
|
|
34059
|
-
|
|
34060
|
-
|
|
34551
|
+
track("cli_translate_completed", {
|
|
34552
|
+
mode: "run",
|
|
34553
|
+
key_count: outcomes.reduce((n, o) => n + o.keys, 0),
|
|
34554
|
+
locale_count: outcomes.length,
|
|
34555
|
+
job_count: outcomes.filter((o) => o.jobId).length,
|
|
34556
|
+
json: json2
|
|
34557
|
+
});
|
|
34558
|
+
const payload = { srcLocale, languages: outcomes };
|
|
34559
|
+
if (json2) {
|
|
34560
|
+
if (!failure2) jsonSuccess(payload);
|
|
34561
|
+
jsonResult(payload, 1);
|
|
34562
|
+
fail(failure2);
|
|
34563
|
+
}
|
|
34564
|
+
if (!opts.wait && outcomes.some((o) => o.status === "started")) {
|
|
34565
|
+
console.log(
|
|
34566
|
+
"\nThe jobs keep running. Follow them with 'tolkbot jobs list' and write them into the files with 'tolkbot jobs apply'."
|
|
34061
34567
|
);
|
|
34062
34568
|
}
|
|
34063
|
-
|
|
34064
|
-
return result;
|
|
34065
|
-
}
|
|
34066
|
-
function comparePlaceholders(src, out, options = {}) {
|
|
34067
|
-
return options.dstLocale ? compareForLocale(src, out, options.dstLocale) : compareStrict(src, out);
|
|
34569
|
+
if (failure2) fail(failure2);
|
|
34068
34570
|
}
|
|
34069
|
-
|
|
34070
|
-
|
|
34071
|
-
|
|
34072
|
-
const minLen = Math.min(a.length, b.length);
|
|
34073
|
-
let shared = 0;
|
|
34074
|
-
while (shared < minLen && a[shared] === b[shared]) shared++;
|
|
34075
|
-
return shared;
|
|
34571
|
+
function printNotes(json2, notes) {
|
|
34572
|
+
if (json2) return;
|
|
34573
|
+
for (const note of notes) console.log(` ${note}`);
|
|
34076
34574
|
}
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
function termAppearsInText(text, term, opts = {}) {
|
|
34084
|
-
const haystack = opts.caseSensitive ? text : text.toLowerCase();
|
|
34085
|
-
const needle = (opts.caseSensitive ? term : term.toLowerCase()).trim();
|
|
34086
|
-
if (needle.length === 0) return false;
|
|
34087
|
-
if (UNSPACED_SCRIPT.test(needle)) return haystack.includes(needle);
|
|
34088
|
-
if (/\s/.test(needle) && haystack.includes(needle)) return true;
|
|
34089
|
-
const termWords = needle.split(WORD_SPLIT).filter(Boolean);
|
|
34090
|
-
const words2 = haystack.split(WORD_SPLIT).filter(Boolean);
|
|
34091
|
-
for (let start = 0; start + termWords.length <= words2.length; start++) {
|
|
34092
|
-
if (termWords.every(
|
|
34093
|
-
(termWord, offset) => wordMatches(
|
|
34094
|
-
words2[start + offset] ?? "",
|
|
34095
|
-
termWord,
|
|
34096
|
-
opts.derivedForms ?? false
|
|
34097
|
-
)
|
|
34098
|
-
)) {
|
|
34099
|
-
return true;
|
|
34100
|
-
}
|
|
34575
|
+
function checkTranslateOptions(opts) {
|
|
34576
|
+
if (!opts.dst && !opts.all) {
|
|
34577
|
+
throw new UsageError(
|
|
34578
|
+
"Either --dst <locale> or --all must be provided.",
|
|
34579
|
+
"Run 'tolkbot translate --help'."
|
|
34580
|
+
);
|
|
34101
34581
|
}
|
|
34102
|
-
|
|
34103
|
-
|
|
34104
|
-
function wordMatches(word, termWord, derivedForms) {
|
|
34105
|
-
if (word === termWord) return true;
|
|
34106
|
-
if (termWord.length < MIN_FUZZY_LENGTH || word.length < MIN_FUZZY_LENGTH) {
|
|
34107
|
-
return false;
|
|
34582
|
+
if (opts.dst && opts.all) {
|
|
34583
|
+
throw new UsageError("Cannot use both --dst and --all.");
|
|
34108
34584
|
}
|
|
34109
|
-
|
|
34110
|
-
|
|
34111
|
-
|
|
34112
|
-
|
|
34113
|
-
if (sharedPrefixLength(word.slice(offset), termWord) >= minShared) {
|
|
34114
|
-
return true;
|
|
34115
|
-
}
|
|
34585
|
+
if (opts.dryRun && !opts.wait) {
|
|
34586
|
+
throw new UsageError(
|
|
34587
|
+
"Cannot use --dry-run with --no-wait: a dry run starts nothing."
|
|
34588
|
+
);
|
|
34116
34589
|
}
|
|
34117
|
-
return false;
|
|
34118
34590
|
}
|
|
34119
34591
|
|
|
34120
|
-
//
|
|
34121
|
-
|
|
34122
|
-
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
|
|
34131
|
-
|
|
34132
|
-
|
|
34133
|
-
|
|
34134
|
-
|
|
34135
|
-
|
|
34136
|
-
|
|
34137
|
-
if (translation) {
|
|
34138
|
-
terms.push({
|
|
34139
|
-
...id,
|
|
34140
|
-
source: g.src,
|
|
34141
|
-
target: translation.text,
|
|
34142
|
-
...g.aiContext ? { notes: g.aiContext } : {}
|
|
34143
|
-
});
|
|
34144
|
-
} else if (g.aiContext) {
|
|
34145
|
-
contextTerms.push({ ...id, term: g.src, context: g.aiContext });
|
|
34146
|
-
}
|
|
34147
|
-
}
|
|
34592
|
+
// src/commands/translate/index.ts
|
|
34593
|
+
var RETRANSLATE_UNTRACKED_HELP = "Also retranslate translations TolkBot has no record of (kept by default)";
|
|
34594
|
+
var translateCommand2 = new Command9("translate").description(
|
|
34595
|
+
"Translate what is missing or changed, and write the results into your locale files"
|
|
34596
|
+
).option(
|
|
34597
|
+
"--src <locale>",
|
|
34598
|
+
"Source locale (defaults to the project's base language)"
|
|
34599
|
+
).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--dry-run", "Print what a run would translate, and start nothing").option(
|
|
34600
|
+
"--no-wait",
|
|
34601
|
+
"Start the jobs and print their IDs without waiting; apply them later with 'tolkbot jobs apply'"
|
|
34602
|
+
).option("--retranslate-untracked", RETRANSLATE_UNTRACKED_HELP).option("--verbose", "With --dry-run, include each key's source string").option("--json", "Output as JSON (machine-readable)").allowExcessArguments(true).enablePositionalOptions().action(async (opts, command) => {
|
|
34603
|
+
const [word] = command.args;
|
|
34604
|
+
if (word !== void 0) {
|
|
34605
|
+
throw new UsageError(
|
|
34606
|
+
`unknown command 'translate ${word}'`,
|
|
34607
|
+
"Run 'tolkbot translate --help'. Jobs are under 'tolkbot jobs'."
|
|
34608
|
+
);
|
|
34148
34609
|
}
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
)
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34171
|
-
|
|
34172
|
-
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34610
|
+
const schema = z11.object({
|
|
34611
|
+
src: z11.string().min(1).optional(),
|
|
34612
|
+
dst: z11.string().min(1).optional(),
|
|
34613
|
+
all: z11.boolean().optional(),
|
|
34614
|
+
dryRun: z11.boolean().optional(),
|
|
34615
|
+
wait: z11.boolean(),
|
|
34616
|
+
retranslateUntracked: z11.boolean().optional(),
|
|
34617
|
+
verbose: z11.boolean().optional(),
|
|
34618
|
+
json: z11.boolean().optional()
|
|
34619
|
+
});
|
|
34620
|
+
const parsed = validateOrExit(schema, opts);
|
|
34621
|
+
checkTranslateOptions(parsed);
|
|
34622
|
+
await runTranslate(parsed);
|
|
34623
|
+
});
|
|
34624
|
+
translateCommand2.command("adopt").description(
|
|
34625
|
+
"Record existing translations TolkBot has no baseline for as reviewed, so they are kept until their source changes"
|
|
34626
|
+
).option(
|
|
34627
|
+
"--src <locale>",
|
|
34628
|
+
"Source locale (defaults to the project's base language)"
|
|
34629
|
+
).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
34630
|
+
const schema = z11.object({
|
|
34631
|
+
src: z11.string().min(1).optional(),
|
|
34632
|
+
dst: z11.string().min(1).optional(),
|
|
34633
|
+
all: z11.boolean().optional(),
|
|
34634
|
+
json: z11.boolean().optional()
|
|
34635
|
+
});
|
|
34636
|
+
const parsed = validateOrExit(schema, opts);
|
|
34637
|
+
if (parsed.dst && parsed.all) {
|
|
34638
|
+
throw new UsageError("Cannot use both --dst and --all.");
|
|
34177
34639
|
}
|
|
34178
|
-
|
|
34179
|
-
}
|
|
34640
|
+
await runAdopt(parsed);
|
|
34641
|
+
});
|
|
34180
34642
|
|
|
34181
|
-
//
|
|
34182
|
-
|
|
34183
|
-
en: "English",
|
|
34184
|
-
nb: "Norwegian Bokm\xE5l",
|
|
34185
|
-
nn: "Norwegian Nynorsk",
|
|
34186
|
-
sv: "Swedish",
|
|
34187
|
-
da: "Danish",
|
|
34188
|
-
fi: "Finnish",
|
|
34189
|
-
is: "Icelandic",
|
|
34190
|
-
de: "German",
|
|
34191
|
-
nl: "Dutch",
|
|
34192
|
-
fr: "French",
|
|
34193
|
-
es: "Spanish",
|
|
34194
|
-
pt: "Portuguese",
|
|
34195
|
-
it: "Italian",
|
|
34196
|
-
ca: "Catalan",
|
|
34197
|
-
ro: "Romanian",
|
|
34198
|
-
pl: "Polish",
|
|
34199
|
-
cs: "Czech",
|
|
34200
|
-
sk: "Slovak",
|
|
34201
|
-
hu: "Hungarian",
|
|
34202
|
-
el: "Greek",
|
|
34203
|
-
ru: "Russian",
|
|
34204
|
-
uk: "Ukrainian",
|
|
34205
|
-
bg: "Bulgarian",
|
|
34206
|
-
sr: "Serbian",
|
|
34207
|
-
hr: "Croatian",
|
|
34208
|
-
tr: "Turkish",
|
|
34209
|
-
ar: "Arabic",
|
|
34210
|
-
he: "Hebrew",
|
|
34211
|
-
fa: "Persian",
|
|
34212
|
-
hi: "Hindi",
|
|
34213
|
-
bn: "Bengali",
|
|
34214
|
-
th: "Thai",
|
|
34215
|
-
vi: "Vietnamese",
|
|
34216
|
-
id: "Indonesian",
|
|
34217
|
-
ms: "Malay",
|
|
34218
|
-
ja: "Japanese",
|
|
34219
|
-
ko: "Korean",
|
|
34220
|
-
zh: "Chinese",
|
|
34221
|
-
other: "Any other language, or no natural language at all"
|
|
34222
|
-
};
|
|
34223
|
-
var LANGUAGE_ALIASES = { no: "nb" };
|
|
34224
|
-
function verifierLanguage(locale) {
|
|
34225
|
-
const head = locale.trim().split(/[-_]/)[0] ?? "";
|
|
34226
|
-
const base = (/^[a-z]{2,3}(?=[A-Z])/.exec(head) ?? /^[A-Za-z]{2,3}$/.exec(head))?.[0].toLowerCase();
|
|
34227
|
-
if (!base) return void 0;
|
|
34228
|
-
const language = LANGUAGE_ALIASES[base] ?? base;
|
|
34229
|
-
return language in VERIFIER_LANGUAGES && language !== "other" ? language : void 0;
|
|
34230
|
-
}
|
|
34643
|
+
// src/commands/validate.tsx
|
|
34644
|
+
import { Box as Box19, render as render16, Text as Text19 } from "ink";
|
|
34231
34645
|
|
|
34232
|
-
//
|
|
34233
|
-
|
|
34234
|
-
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34239
|
-
|
|
34240
|
-
|
|
34241
|
-
uk: /\p{Script=Cyrillic}/u,
|
|
34242
|
-
bg: /\p{Script=Cyrillic}/u,
|
|
34243
|
-
el: /\p{Script=Greek}/u,
|
|
34244
|
-
th: /\p{Script=Thai}/u,
|
|
34245
|
-
hi: /\p{Script=Devanagari}/u,
|
|
34246
|
-
bn: /\p{Script=Bengali}/u
|
|
34247
|
-
};
|
|
34248
|
-
var MIN_LETTERS = 4;
|
|
34249
|
-
var MIN_SENTENCE_WORDS = 3;
|
|
34250
|
-
var MIN_SENTENCE_LETTERS = 8;
|
|
34251
|
-
function proseOf(text) {
|
|
34252
|
-
return text.replace(/\{\{[^{}]*\}\}/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/<[^<>]*>/g, " ").replace(/`[^`]*`/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/%[sdif@]|%\d+\$[sdif@]/g, " ");
|
|
34253
|
-
}
|
|
34254
|
-
function letters(text) {
|
|
34255
|
-
return text.match(/\p{L}/gu)?.length ?? 0;
|
|
34256
|
-
}
|
|
34257
|
-
function words(text) {
|
|
34258
|
-
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
34259
|
-
}
|
|
34260
|
-
function deterministicHealth(input2) {
|
|
34261
|
-
const findings = [];
|
|
34262
|
-
const translation = input2.translation.trim();
|
|
34263
|
-
const source = input2.source.trim();
|
|
34264
|
-
if (!translation) return { findings, needsModel: false };
|
|
34265
|
-
if (!comparePlaceholders(source, translation, {
|
|
34266
|
-
dstLocale: input2.dstLocale
|
|
34267
|
-
}).ok) {
|
|
34268
|
-
findings.push("placeholder_mismatch");
|
|
34646
|
+
// src/utils/pattern-discovery.ts
|
|
34647
|
+
import fs9 from "fs/promises";
|
|
34648
|
+
var DEFAULT_NAMESPACE = "translation";
|
|
34649
|
+
async function parseFile(absolutePath, format) {
|
|
34650
|
+
let content;
|
|
34651
|
+
try {
|
|
34652
|
+
content = await fs9.readFile(absolutePath, "utf-8");
|
|
34653
|
+
} catch {
|
|
34654
|
+
throw new Error("Could not read file");
|
|
34269
34655
|
}
|
|
34270
|
-
const
|
|
34271
|
-
const
|
|
34272
|
-
|
|
34273
|
-
|
|
34274
|
-
|
|
34275
|
-
|
|
34656
|
+
const parsed = format === "ts" ? parseTsContent(content) : parseJsonContent(content);
|
|
34657
|
+
const problem = parsed.issues?.find((issue2) => issue2.type === "error");
|
|
34658
|
+
if (problem) throw new Error(problem.message);
|
|
34659
|
+
return parsed.data;
|
|
34660
|
+
}
|
|
34661
|
+
async function discoverByPattern(root, scope) {
|
|
34662
|
+
const format = formatOf(scope);
|
|
34663
|
+
const files = await listLocaleFiles(root, scope);
|
|
34664
|
+
if (files.length === 0) return null;
|
|
34665
|
+
const filesByLocale = /* @__PURE__ */ new Map();
|
|
34666
|
+
const parseErrors = [];
|
|
34667
|
+
for (const file2 of files) {
|
|
34668
|
+
try {
|
|
34669
|
+
const data = await parseFile(file2.absolutePath, format);
|
|
34670
|
+
const group = filesByLocale.get(file2.locale) ?? [];
|
|
34671
|
+
group.push({
|
|
34672
|
+
data,
|
|
34673
|
+
path: file2.absolutePath,
|
|
34674
|
+
relativePath: file2.relativePath,
|
|
34675
|
+
locale: file2.locale,
|
|
34676
|
+
namespace: file2.namespace ?? DEFAULT_NAMESPACE
|
|
34677
|
+
});
|
|
34678
|
+
filesByLocale.set(file2.locale, group);
|
|
34679
|
+
} catch (error62) {
|
|
34680
|
+
parseErrors.push({
|
|
34681
|
+
path: file2.absolutePath,
|
|
34682
|
+
relativePath: file2.relativePath,
|
|
34683
|
+
locale: file2.locale,
|
|
34684
|
+
message: error62 instanceof Error ? error62.message : String(error62)
|
|
34685
|
+
});
|
|
34276
34686
|
}
|
|
34277
|
-
return { findings, needsModel: false };
|
|
34278
|
-
}
|
|
34279
|
-
if (letterCount < MIN_LETTERS) return { findings, needsModel: false };
|
|
34280
|
-
const language = verifierLanguage(input2.dstLocale);
|
|
34281
|
-
const script = language ? REQUIRED_SCRIPT[language] : void 0;
|
|
34282
|
-
if (script && !script.test(prose)) {
|
|
34283
|
-
findings.push("wrong_script");
|
|
34284
|
-
return { findings, needsModel: false };
|
|
34285
34687
|
}
|
|
34286
|
-
|
|
34287
|
-
|
|
34288
|
-
|
|
34289
|
-
|
|
34290
|
-
|
|
34291
|
-
|
|
34292
|
-
|
|
34293
|
-
|
|
34294
|
-
|
|
34295
|
-
const list = groups.get(src) ?? [];
|
|
34296
|
-
list.push({
|
|
34297
|
-
key: p.key,
|
|
34298
|
-
dst: p.dst,
|
|
34299
|
-
reviewable,
|
|
34300
|
-
...p.context ? { context: p.context } : {}
|
|
34301
|
-
});
|
|
34302
|
-
groups.set(src, list);
|
|
34688
|
+
const locales = [...new Set(files.map((file2) => file2.locale))];
|
|
34689
|
+
return {
|
|
34690
|
+
format,
|
|
34691
|
+
pattern: fullPattern(scope),
|
|
34692
|
+
filesByLocale,
|
|
34693
|
+
locales,
|
|
34694
|
+
baseLocale: detectBaseLocale(locales),
|
|
34695
|
+
fileCount: files.length,
|
|
34696
|
+
parseErrors
|
|
34303
34697
|
};
|
|
34304
|
-
for (const p of pairs) add(p, true);
|
|
34305
|
-
for (const p of referencePairs) add(p, false);
|
|
34306
|
-
const out = [];
|
|
34307
|
-
for (const [src, variants] of groups) {
|
|
34308
|
-
if (variants.length < 2) continue;
|
|
34309
|
-
if (new Set(variants.map((v) => v.dst.trim())).size < 2) continue;
|
|
34310
|
-
if (!variants.some((v) => v.reviewable)) continue;
|
|
34311
|
-
const contexts = new Set(variants.map((v) => v.context?.trim() ?? ""));
|
|
34312
|
-
out.push({ src, variants, unexplained: contexts.size === 1 });
|
|
34313
|
-
}
|
|
34314
|
-
return out;
|
|
34315
34698
|
}
|
|
34316
34699
|
|
|
34317
34700
|
// ../quality/src/load.ts
|
|
@@ -34777,7 +35160,7 @@ function finalize2(issues, stats, strict) {
|
|
|
34777
35160
|
}
|
|
34778
35161
|
|
|
34779
35162
|
// src/commands/validate.tsx
|
|
34780
|
-
import { jsx as
|
|
35163
|
+
import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
34781
35164
|
var LEVEL_COLOR = {
|
|
34782
35165
|
error: "red",
|
|
34783
35166
|
warning: "yellow",
|
|
@@ -34786,12 +35169,12 @@ var LEVEL_COLOR = {
|
|
|
34786
35169
|
var GUTTER = 9;
|
|
34787
35170
|
function IssueLine({ issue: issue2 }) {
|
|
34788
35171
|
const location = [issue2.file, issue2.key].filter(Boolean).join(" \xB7 ");
|
|
34789
|
-
return /* @__PURE__ */
|
|
34790
|
-
/* @__PURE__ */
|
|
34791
|
-
/* @__PURE__ */
|
|
34792
|
-
/* @__PURE__ */
|
|
35172
|
+
return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
|
|
35173
|
+
/* @__PURE__ */ jsxs19(Box19, { children: [
|
|
35174
|
+
/* @__PURE__ */ jsx19(Box19, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx19(Text19, { color: LEVEL_COLOR[issue2.level], children: issue2.level }) }),
|
|
35175
|
+
/* @__PURE__ */ jsx19(Box19, { flexGrow: 1, children: /* @__PURE__ */ jsx19(Text19, { children: issue2.message }) })
|
|
34793
35176
|
] }),
|
|
34794
|
-
/* @__PURE__ */
|
|
35177
|
+
/* @__PURE__ */ jsx19(Box19, { marginLeft: GUTTER, children: /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
34795
35178
|
issue2.rule,
|
|
34796
35179
|
location ? ` \u2014 ${location}` : ""
|
|
34797
35180
|
] }) })
|
|
@@ -34800,20 +35183,20 @@ function IssueLine({ issue: issue2 }) {
|
|
|
34800
35183
|
function ReportUI({ report }) {
|
|
34801
35184
|
const { counts, stats } = report;
|
|
34802
35185
|
const summary = ["error", "warning", "info"].filter((level) => counts[level] > 0).map((level) => `${counts[level]} ${level}(s)`).join(", ");
|
|
34803
|
-
return /* @__PURE__ */
|
|
34804
|
-
/* @__PURE__ */
|
|
34805
|
-
/* @__PURE__ */
|
|
34806
|
-
/* @__PURE__ */
|
|
35186
|
+
return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
|
|
35187
|
+
/* @__PURE__ */ jsxs19(Box19, { children: [
|
|
35188
|
+
/* @__PURE__ */ jsx19(Text19, { color: report.valid ? "green" : "red", children: report.valid ? "\u2713" : "\u2717" }),
|
|
35189
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
34807
35190
|
" Validation ",
|
|
34808
35191
|
report.valid ? "passed" : "failed"
|
|
34809
35192
|
] }),
|
|
34810
|
-
summary ? /* @__PURE__ */
|
|
35193
|
+
summary ? /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
34811
35194
|
" \u2014 ",
|
|
34812
35195
|
summary
|
|
34813
35196
|
] }) : null
|
|
34814
35197
|
] }),
|
|
34815
|
-
stats.locales.length > 0 && /* @__PURE__ */
|
|
34816
|
-
/* @__PURE__ */
|
|
35198
|
+
stats.locales.length > 0 && /* @__PURE__ */ jsxs19(Box19, { marginTop: 1, flexDirection: "column", children: [
|
|
35199
|
+
/* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
34817
35200
|
stats.filesChecked,
|
|
34818
35201
|
" ",
|
|
34819
35202
|
stats.format,
|
|
@@ -34822,18 +35205,18 @@ function ReportUI({ report }) {
|
|
|
34822
35205
|
stats.totalKeys,
|
|
34823
35206
|
" keys"
|
|
34824
35207
|
] }),
|
|
34825
|
-
/* @__PURE__ */
|
|
35208
|
+
/* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
34826
35209
|
"Locales: ",
|
|
34827
35210
|
stats.locales.join(", "),
|
|
34828
35211
|
stats.baseLocale ? ` (base: ${stats.baseLocale})` : ""
|
|
34829
35212
|
] }),
|
|
34830
|
-
stats.pattern && /* @__PURE__ */
|
|
35213
|
+
stats.pattern && /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
34831
35214
|
"Pattern: ",
|
|
34832
35215
|
stats.pattern
|
|
34833
35216
|
] })
|
|
34834
35217
|
] }),
|
|
34835
|
-
report.issues.length > 0 && /* @__PURE__ */
|
|
34836
|
-
!report.strict && counts.warning > 0 && report.valid && /* @__PURE__ */
|
|
35218
|
+
report.issues.length > 0 && /* @__PURE__ */ jsx19(Box19, { marginTop: 1, flexDirection: "column", gap: 1, children: report.issues.map((issue2, idx) => /* @__PURE__ */ jsx19(IssueLine, { issue: issue2 }, idx)) }),
|
|
35219
|
+
!report.strict && counts.warning > 0 && report.valid && /* @__PURE__ */ jsx19(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Warnings did not fail this run. Use --strict to treat them as errors." }) })
|
|
34837
35220
|
] });
|
|
34838
35221
|
}
|
|
34839
35222
|
function emit(report, json2) {
|
|
@@ -34853,7 +35236,7 @@ function emit(report, json2) {
|
|
|
34853
35236
|
if (!report.valid) fail(validationFailure(report));
|
|
34854
35237
|
return;
|
|
34855
35238
|
}
|
|
34856
|
-
|
|
35239
|
+
render16(/* @__PURE__ */ jsx19(ReportUI, { report })).unmount();
|
|
34857
35240
|
process.exitCode = exitCode;
|
|
34858
35241
|
}
|
|
34859
35242
|
function validationFailure(report) {
|
|
@@ -34931,7 +35314,7 @@ function commandPath(command) {
|
|
|
34931
35314
|
return names.join(" ");
|
|
34932
35315
|
}
|
|
34933
35316
|
function buildProgram() {
|
|
34934
|
-
const program = new
|
|
35317
|
+
const program = new Command10().name("tolkbot").description("Localization workflow for your repo").version(version, "-v, --version", "Output the version number").helpOption("-h, --help", "Show help").helpCommand("help [command]", "Show help for a command").option("--token <token>", TOKEN_HELP).enablePositionalOptions();
|
|
34935
35318
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
34936
35319
|
const { token } = actionCommand.optsWithGlobals();
|
|
34937
35320
|
setTokenOverride(token);
|
|
@@ -34940,7 +35323,7 @@ function buildProgram() {
|
|
|
34940
35323
|
});
|
|
34941
35324
|
program.command("init").description("Initialize TolkBot in the current repo").option("--force", "Overwrite existing config").option(
|
|
34942
35325
|
"-y, --yes",
|
|
34943
|
-
"Skip all prompts, accepting detected settings
|
|
35326
|
+
"Skip all prompts, accepting detected settings (the default without a terminal)"
|
|
34944
35327
|
).option("--json", "Output the result as JSON (implies --yes)").option("--base <locale>", "Base locale (default: detected, else en)").option(
|
|
34945
35328
|
"--targets <locales>",
|
|
34946
35329
|
"Comma-separated target locales (default: detected)"
|
|
@@ -34953,6 +35336,9 @@ function buildProgram() {
|
|
|
34953
35336
|
).option(
|
|
34954
35337
|
"--project <slug>",
|
|
34955
35338
|
"Link this project. Default: the project whose directory you are in, else a new project here."
|
|
35339
|
+
).option(
|
|
35340
|
+
"--agents-file [path]",
|
|
35341
|
+
"Add TolkBot's instructions for coding agents to this file, by default the nearest AGENTS.md or CLAUDE.md. At a terminal init asks; otherwise it writes them only with this option"
|
|
34956
35342
|
).action((opts) => runInit(opts));
|
|
34957
35343
|
program.command("validate").description("Validate translation files and configuration").option("--json", "Output the report as JSON").option(
|
|
34958
35344
|
"--strict",
|
|
@@ -34965,6 +35351,7 @@ function buildProgram() {
|
|
|
34965
35351
|
program.addCommand(jobsCommand);
|
|
34966
35352
|
program.addCommand(languagesCommand);
|
|
34967
35353
|
program.addCommand(learningsCommand);
|
|
35354
|
+
program.addCommand(setCommand);
|
|
34968
35355
|
program.addCommand(statusCommand);
|
|
34969
35356
|
program.addCommand(telemetryCommand);
|
|
34970
35357
|
program.addCommand(translateCommand2);
|
|
@@ -35027,6 +35414,7 @@ Examples:
|
|
|
35027
35414
|
$ tolkbot languages add nb
|
|
35028
35415
|
$ tolkbot translate --all
|
|
35029
35416
|
$ tolkbot translate --all --dry-run
|
|
35417
|
+
$ tolkbot set common.save --locale nb --value "Lagre"
|
|
35030
35418
|
$ tolkbot jobs list
|
|
35031
35419
|
$ tolkbot validate
|
|
35032
35420
|
|