@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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.cjs +155 -19
  3. 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 (one file per language). */
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
- initialValue: opts.apiUrl ?? "http://localhost:3000",
26588
- validate: (v) => v?.trim() ? void 0 : "Required"
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", initialValue: opts.out ?? "messages" })
26638
- ).trim() || "messages";
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 readLocalMap(config, language) {
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 rows = await api.listTranslations(
26796
- cfg.projectId,
26797
- language,
26798
- cfg.pull === "approved" ? "approved" : void 0
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 file = bundlePath(cfg, language);
26803
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(file), { recursive: true });
26804
- (0, import_node_fs4.writeFileSync)(file, codec.serialize({ messages, language }));
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
- console.log(import_picocolors.default.green(`\u2713 ${file} (${messages.length} keys${skippedNote})`));
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("0.0.0");
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.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",