@radicalstorage-lingo/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.cjs +155 -19
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -52,6 +52,12 @@ npx lingo pull
|
|
|
52
52
|
|
|
53
53
|
Untranslated (missing, empty) values are never written — a key is either
|
|
54
54
|
pulled with real text or omitted entirely.
|
|
55
|
+
- `split` — `none` (default): `{outDir}/{lang}.json`. `namespace`:
|
|
56
|
+
`{outDir}/{lang}/{namespace}.json` per top-level namespace, keys relative to
|
|
57
|
+
it, `_root.json` for keys without one (next-i18next's `public/locales`
|
|
58
|
+
layout).
|
|
59
|
+
- `languageMap` — file/folder names instead of Lingo's codes, e.g.
|
|
60
|
+
`{ "zh-CN": "zh_cn", "pt-PT": "pt" }`.
|
|
55
61
|
|
|
56
62
|
## Commands
|
|
57
63
|
|
package/dist/cli.cjs
CHANGED
|
@@ -21067,6 +21067,31 @@ function getCodec(id) {
|
|
|
21067
21067
|
}
|
|
21068
21068
|
var FORMAT_LABELS = Object.fromEntries(codecs.map((c2) => [c2.id, c2.label]));
|
|
21069
21069
|
|
|
21070
|
+
// ../formats/src/util/bundle.ts
|
|
21071
|
+
var ROOT_NAMESPACE = "_root";
|
|
21072
|
+
function bundleMessages(rows, namespace) {
|
|
21073
|
+
const out = {};
|
|
21074
|
+
for (const row of rows) {
|
|
21075
|
+
if (row.value.trim().length === 0) continue;
|
|
21076
|
+
const path = row.namespacePath ?? null;
|
|
21077
|
+
if (namespace === void 0) {
|
|
21078
|
+
out[path ? `${path}.${row.key}` : row.key] = row.value;
|
|
21079
|
+
} else if (namespace === ROOT_NAMESPACE) {
|
|
21080
|
+
if (!path) out[row.key] = row.value;
|
|
21081
|
+
} else if (path === namespace) {
|
|
21082
|
+
out[row.key] = row.value;
|
|
21083
|
+
} else if (path?.startsWith(`${namespace}.`)) {
|
|
21084
|
+
out[`${path.slice(namespace.length + 1)}.${row.key}`] = row.value;
|
|
21085
|
+
}
|
|
21086
|
+
}
|
|
21087
|
+
return out;
|
|
21088
|
+
}
|
|
21089
|
+
function topLevelNamespaces(rows) {
|
|
21090
|
+
const names = /* @__PURE__ */ new Set();
|
|
21091
|
+
for (const row of rows) names.add(row.namespacePath ? row.namespacePath.split(".")[0] : ROOT_NAMESPACE);
|
|
21092
|
+
return [...names].sort();
|
|
21093
|
+
}
|
|
21094
|
+
|
|
21070
21095
|
// src/api.ts
|
|
21071
21096
|
var import_node_fs = require("fs");
|
|
21072
21097
|
var import_node_os = require("os");
|
|
@@ -25268,8 +25293,19 @@ var configSchema = external_exports.object({
|
|
|
25268
25293
|
languages: external_exports.array(external_exports.string().min(2)).default([]),
|
|
25269
25294
|
/** Format the pulled files are written in. */
|
|
25270
25295
|
format: external_exports.enum(["json-nested", "json-flat"]).default("json-nested"),
|
|
25271
|
-
/** Directory for pulled bundles
|
|
25296
|
+
/** Directory for pulled bundles. */
|
|
25272
25297
|
outDir: external_exports.string().default("messages"),
|
|
25298
|
+
/**
|
|
25299
|
+
* File layout of `lingo pull`:
|
|
25300
|
+
* - `none` (default): `{outDir}/{lang}.json`, every key under its full path;
|
|
25301
|
+
* - `namespace`: `{outDir}/{lang}/{namespace}.json` per top-level namespace,
|
|
25302
|
+
* keys relative to it (`_root.json` for keys without one), the layout of
|
|
25303
|
+
* next-i18next's `public/locales` and i18next namespace loading. Same content
|
|
25304
|
+
* as the API's `/bundles/{lang}/{namespace}`.
|
|
25305
|
+
*/
|
|
25306
|
+
split: external_exports.enum(["none", "namespace"]).default("none"),
|
|
25307
|
+
/** Names to use for language files/folders instead of Lingo's codes, e.g. `{ "zh-CN": "zh_cn", "pt-PT": "pt" }`. */
|
|
25308
|
+
languageMap: external_exports.record(external_exports.string()).default({}),
|
|
25273
25309
|
/** Where the generated types module is written. */
|
|
25274
25310
|
typesOut: external_exports.string().default(".lingo/types.ts"),
|
|
25275
25311
|
/**
|
|
@@ -25289,7 +25325,7 @@ function parseConfig(raw) {
|
|
|
25289
25325
|
}
|
|
25290
25326
|
function bundlePath(config, language) {
|
|
25291
25327
|
const ext = config.format === "json-flat" || config.format === "json-nested" ? ".json" : ".json";
|
|
25292
|
-
return `${config.outDir}/${language}${ext}`;
|
|
25328
|
+
return `${config.outDir}/${config.languageMap[language] ?? language}${ext}`;
|
|
25293
25329
|
}
|
|
25294
25330
|
function resolveOutDir(flagOut, config) {
|
|
25295
25331
|
return flagOut ?? config.outDir;
|
|
@@ -26581,11 +26617,12 @@ async function runInitWizard(opts) {
|
|
|
26581
26617
|
return;
|
|
26582
26618
|
}
|
|
26583
26619
|
}
|
|
26620
|
+
const apiUrlDefault = opts.apiUrl ?? "http://localhost:3000";
|
|
26584
26621
|
const apiUrl = orExit(
|
|
26585
26622
|
await text({
|
|
26586
26623
|
message: "API URL",
|
|
26587
|
-
|
|
26588
|
-
|
|
26624
|
+
placeholder: apiUrlDefault,
|
|
26625
|
+
defaultValue: apiUrlDefault
|
|
26589
26626
|
})
|
|
26590
26627
|
).trim();
|
|
26591
26628
|
const existingKey = resolveApiKey();
|
|
@@ -26633,9 +26670,10 @@ async function runInitWizard(opts) {
|
|
|
26633
26670
|
})
|
|
26634
26671
|
).trim();
|
|
26635
26672
|
}
|
|
26673
|
+
const outDefault = opts.out ?? "messages";
|
|
26636
26674
|
const outDir = orExit(
|
|
26637
|
-
await text({ message: "Output directory for bundles",
|
|
26638
|
-
).trim()
|
|
26675
|
+
await text({ message: "Output directory for bundles", placeholder: outDefault, defaultValue: outDefault })
|
|
26676
|
+
).trim();
|
|
26639
26677
|
const format = orExit(
|
|
26640
26678
|
await select({
|
|
26641
26679
|
message: "Bundle format",
|
|
@@ -26681,6 +26719,31 @@ async function runInitWizard(opts) {
|
|
|
26681
26719
|
);
|
|
26682
26720
|
}
|
|
26683
26721
|
|
|
26722
|
+
// src/layout.ts
|
|
26723
|
+
function localLanguage(config, language) {
|
|
26724
|
+
return config.languageMap[language] ?? language;
|
|
26725
|
+
}
|
|
26726
|
+
function bundleFiles(config, language, rows, namespaces = topLevelNamespaces(rows)) {
|
|
26727
|
+
const lang = localLanguage(config, language);
|
|
26728
|
+
if (config.split === "namespace") {
|
|
26729
|
+
return namespaces.map((namespace) => ({
|
|
26730
|
+
file: `${config.outDir}/${lang}/${namespace}.json`,
|
|
26731
|
+
namespace,
|
|
26732
|
+
messages: bundleMessages(rows, namespace)
|
|
26733
|
+
}));
|
|
26734
|
+
}
|
|
26735
|
+
return [{ file: bundlePath(config, language), namespace: null, messages: bundleMessages(rows) }];
|
|
26736
|
+
}
|
|
26737
|
+
function joinNamespaceFiles(files) {
|
|
26738
|
+
const out = {};
|
|
26739
|
+
for (const { namespace, messages } of files) {
|
|
26740
|
+
for (const [key, value] of Object.entries(messages)) {
|
|
26741
|
+
out[namespace && namespace !== ROOT_NAMESPACE ? `${namespace}.${key}` : key] = value;
|
|
26742
|
+
}
|
|
26743
|
+
}
|
|
26744
|
+
return out;
|
|
26745
|
+
}
|
|
26746
|
+
|
|
26684
26747
|
// src/messages.ts
|
|
26685
26748
|
function fullKey(row) {
|
|
26686
26749
|
return row.namespacePath ? `${row.namespacePath}.${row.key}` : row.key;
|
|
@@ -26716,12 +26779,21 @@ async function resolveProjectLanguages(api, config) {
|
|
|
26716
26779
|
const project = await api.getProject(config.projectId);
|
|
26717
26780
|
return resolveLanguages(config.languages, project);
|
|
26718
26781
|
}
|
|
26719
|
-
function
|
|
26720
|
-
const file = bundlePath(config, language);
|
|
26721
|
-
if (!(0, import_node_fs4.existsSync)(file)) return {};
|
|
26782
|
+
function readBundleFile(config, file) {
|
|
26722
26783
|
const resource = getCodec(config.format).parse((0, import_node_fs4.readFileSync)(file, "utf-8"));
|
|
26723
26784
|
return Object.fromEntries(resource.messages.map((m) => [m.key, m.value]));
|
|
26724
26785
|
}
|
|
26786
|
+
function readLocalMap(config, language) {
|
|
26787
|
+
if (config.split === "namespace") {
|
|
26788
|
+
const dir = `${config.outDir}/${localLanguage(config, language)}`;
|
|
26789
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return {};
|
|
26790
|
+
return joinNamespaceFiles(
|
|
26791
|
+
(0, import_node_fs4.readdirSync)(dir).filter((f) => f.endsWith(".json")).map((f) => ({ namespace: (0, import_node_path3.basename)(f, ".json"), messages: readBundleFile(config, `${dir}/${f}`) }))
|
|
26792
|
+
);
|
|
26793
|
+
}
|
|
26794
|
+
const file = bundlePath(config, language);
|
|
26795
|
+
return (0, import_node_fs4.existsSync)(file) ? readBundleFile(config, file) : {};
|
|
26796
|
+
}
|
|
26725
26797
|
async function readRemoteMap(api, config, language, status) {
|
|
26726
26798
|
const rows = await api.listTranslations(config.projectId, language, status);
|
|
26727
26799
|
return Object.fromEntries(rowsToMessages(rows).map((m) => [m.key, m.value]));
|
|
@@ -26791,19 +26863,25 @@ async function pullCommand(opts = {}) {
|
|
|
26791
26863
|
if (cfg.languages.length === 0) {
|
|
26792
26864
|
console.log(import_picocolors.default.dim(`\u2192 pulling all project languages: ${languages.join(", ")}`));
|
|
26793
26865
|
}
|
|
26866
|
+
const fetched = [];
|
|
26794
26867
|
for (const language of languages) {
|
|
26795
|
-
const
|
|
26796
|
-
|
|
26797
|
-
|
|
26798
|
-
|
|
26799
|
-
|
|
26868
|
+
const status = cfg.pull === "approved" ? "approved" : void 0;
|
|
26869
|
+
fetched.push({ language, rows: await api.listTranslations(cfg.projectId, language, status) });
|
|
26870
|
+
}
|
|
26871
|
+
const namespaces = [...new Set(fetched.flatMap((f) => topLevelNamespaces(f.rows)))].sort();
|
|
26872
|
+
for (const { language, rows } of fetched) {
|
|
26800
26873
|
const messages = rowsToMessages(rows);
|
|
26801
26874
|
const skipped = rows.length - messages.length;
|
|
26802
|
-
const
|
|
26803
|
-
|
|
26804
|
-
|
|
26875
|
+
const files = bundleFiles(cfg, language, rows, namespaces);
|
|
26876
|
+
for (const { file, messages: map } of files) {
|
|
26877
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(file), { recursive: true });
|
|
26878
|
+
const list = Object.entries(map).map(([key, value]) => ({ key, value }));
|
|
26879
|
+
(0, import_node_fs4.writeFileSync)(file, codec.serialize({ messages: list, language }));
|
|
26880
|
+
}
|
|
26805
26881
|
const skippedNote = skipped > 0 ? `, ${skipped} untranslated skipped` : "";
|
|
26806
|
-
|
|
26882
|
+
const summary = `${messages.length} keys${skippedNote}`;
|
|
26883
|
+
const line = cfg.split === "namespace" ? `${cfg.outDir}/${localLanguage(cfg, language)}/ \u2014 ${files.length} namespace file${files.length === 1 ? "" : "s"}, ${summary}` : `${files[0].file} (${summary})`;
|
|
26884
|
+
console.log(import_picocolors.default.green(`\u2713 ${line}`));
|
|
26807
26885
|
if (typeEntries.length === 0) {
|
|
26808
26886
|
typeEntries = messages.map((m) => ({ key: m.key, sample: m.value }));
|
|
26809
26887
|
}
|
|
@@ -26886,6 +26964,64 @@ async function pushCommand(opts) {
|
|
|
26886
26964
|
}
|
|
26887
26965
|
}
|
|
26888
26966
|
|
|
26967
|
+
// package.json
|
|
26968
|
+
var package_default = {
|
|
26969
|
+
name: "@radicalstorage-lingo/cli",
|
|
26970
|
+
version: "0.4.0",
|
|
26971
|
+
type: "module",
|
|
26972
|
+
description: "Lingo CLI \u2014 pull/push translations as JSON at build time.",
|
|
26973
|
+
license: "MIT",
|
|
26974
|
+
repository: { type: "git", url: "git+https://github.com/CashcallPro/lingo.git", directory: "packages/cli" },
|
|
26975
|
+
homepage: "https://github.com/CashcallPro/lingo#readme",
|
|
26976
|
+
bugs: { url: "https://github.com/CashcallPro/lingo/issues" },
|
|
26977
|
+
author: "Amin Ahrabian",
|
|
26978
|
+
keywords: [
|
|
26979
|
+
"i18n",
|
|
26980
|
+
"internationalization",
|
|
26981
|
+
"localization",
|
|
26982
|
+
"translations",
|
|
26983
|
+
"cli",
|
|
26984
|
+
"deepl",
|
|
26985
|
+
"icu"
|
|
26986
|
+
],
|
|
26987
|
+
engines: {
|
|
26988
|
+
node: ">=20.12"
|
|
26989
|
+
},
|
|
26990
|
+
bin: {
|
|
26991
|
+
lingo: "./dist/cli.cjs"
|
|
26992
|
+
},
|
|
26993
|
+
files: [
|
|
26994
|
+
"dist"
|
|
26995
|
+
],
|
|
26996
|
+
publishConfig: {
|
|
26997
|
+
access: "public"
|
|
26998
|
+
},
|
|
26999
|
+
scripts: {
|
|
27000
|
+
build: "tsup",
|
|
27001
|
+
lint: "eslint .",
|
|
27002
|
+
typecheck: "tsc --noEmit",
|
|
27003
|
+
test: "vitest run",
|
|
27004
|
+
prepublishOnly: "pnpm build"
|
|
27005
|
+
},
|
|
27006
|
+
dependencies: {
|
|
27007
|
+
"@clack/prompts": "^1.7.0",
|
|
27008
|
+
commander: "^12.1.0",
|
|
27009
|
+
picocolors: "^1.1.1",
|
|
27010
|
+
zod: "^3.24.1"
|
|
27011
|
+
},
|
|
27012
|
+
devDependencies: {
|
|
27013
|
+
"@eslint/js": "^9.18.0",
|
|
27014
|
+
"@lingo/formats": "workspace:*",
|
|
27015
|
+
"@types/node": "^22.10.5",
|
|
27016
|
+
eslint: "^9.18.0",
|
|
27017
|
+
globals: "^15.14.0",
|
|
27018
|
+
tsup: "^8.3.5",
|
|
27019
|
+
typescript: "^5.6.3",
|
|
27020
|
+
"typescript-eslint": "^8.20.0",
|
|
27021
|
+
vitest: "^2.1.8"
|
|
27022
|
+
}
|
|
27023
|
+
};
|
|
27024
|
+
|
|
26889
27025
|
// src/cli.ts
|
|
26890
27026
|
async function run(fn) {
|
|
26891
27027
|
try {
|
|
@@ -26896,7 +27032,7 @@ async function run(fn) {
|
|
|
26896
27032
|
}
|
|
26897
27033
|
}
|
|
26898
27034
|
var program2 = new Command();
|
|
26899
|
-
program2.name("lingo").description("Pull/push Lingo translations as JSON at build time.").version(
|
|
27035
|
+
program2.name("lingo").description("Pull/push Lingo translations as JSON at build time.").version(package_default.version);
|
|
26900
27036
|
program2.command("init").description("Create a lingo.config.json (interactive wizard, or --yes for defaults)").option("--project <id>", "project id or slug").option("--api-url <url>", "API base URL").option("--out <dir>", "output directory for pulled bundles").option("-y, --yes", "skip the wizard; write config from flags + defaults").action(
|
|
26901
27037
|
(opts) => run(() => initCommand({ project: opts.project, apiUrl: opts.apiUrl, out: opts.out, yes: opts.yes }))
|
|
26902
27038
|
);
|
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radicalstorage-lingo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lingo CLI — pull/push translations as JSON at build time.",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/CashcallPro/lingo.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/CashcallPro/lingo#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/CashcallPro/lingo/issues"
|
|
15
|
+
},
|
|
7
16
|
"author": "Amin Ahrabian",
|
|
8
|
-
"homepage": "https://lingo-app-amin.fly.dev",
|
|
9
17
|
"keywords": [
|
|
10
18
|
"i18n",
|
|
11
19
|
"internationalization",
|