@lionrapid/cli 0.1.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 +159 -0
- package/dist/adapters/android-xml.d.ts +20 -0
- package/dist/adapters/android-xml.d.ts.map +1 -0
- package/dist/adapters/android-xml.js +101 -0
- package/dist/adapters/android-xml.js.map +1 -0
- package/dist/adapters/arb.d.ts +17 -0
- package/dist/adapters/arb.d.ts.map +1 -0
- package/dist/adapters/arb.js +45 -0
- package/dist/adapters/arb.js.map +1 -0
- package/dist/adapters/content.d.ts +13 -0
- package/dist/adapters/content.d.ts.map +1 -0
- package/dist/adapters/content.js +24 -0
- package/dist/adapters/content.js.map +1 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +34 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/ios-strings.d.ts +15 -0
- package/dist/adapters/ios-strings.d.ts.map +1 -0
- package/dist/adapters/ios-strings.js +54 -0
- package/dist/adapters/ios-strings.js.map +1 -0
- package/dist/adapters/json.d.ts +12 -0
- package/dist/adapters/json.d.ts.map +1 -0
- package/dist/adapters/json.js +31 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/nested.d.ts +9 -0
- package/dist/adapters/nested.d.ts.map +1 -0
- package/dist/adapters/nested.js +37 -0
- package/dist/adapters/nested.js.map +1 -0
- package/dist/adapters/po.d.ts +23 -0
- package/dist/adapters/po.d.ts.map +1 -0
- package/dist/adapters/po.js +197 -0
- package/dist/adapters/po.js.map +1 -0
- package/dist/adapters/types.d.ts +46 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +16 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/xcstrings.d.ts +20 -0
- package/dist/adapters/xcstrings.d.ts.map +1 -0
- package/dist/adapters/xcstrings.js +77 -0
- package/dist/adapters/xcstrings.js.map +1 -0
- package/dist/adapters/xliff.d.ts +22 -0
- package/dist/adapters/xliff.d.ts.map +1 -0
- package/dist/adapters/xliff.js +83 -0
- package/dist/adapters/xliff.js.map +1 -0
- package/dist/adapters/yaml.d.ts +16 -0
- package/dist/adapters/yaml.d.ts.map +1 -0
- package/dist/adapters/yaml.js +51 -0
- package/dist/adapters/yaml.js.map +1 -0
- package/dist/api-client.d.ts +123 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +236 -0
- package/dist/api-client.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +365 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/context.d.ts +38 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +70 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +27 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +87 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/pull.d.ts +35 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +123 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +37 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +63 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/scan.d.ts +46 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +68 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/snapshot.d.ts +48 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +52 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/commands/types.d.ts +38 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +82 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/validate.d.ts +28 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +48 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +80 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +279 -0
- package/dist/config.js.map +1 -0
- package/dist/exit-codes.d.ts +26 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +42 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +29 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +38 -0
- package/dist/output.js.map +1 -0
- package/dist/scan/canonical.d.ts +33 -0
- package/dist/scan/canonical.d.ts.map +1 -0
- package/dist/scan/canonical.js +57 -0
- package/dist/scan/canonical.js.map +1 -0
- package/dist/scan/discover.d.ts +13 -0
- package/dist/scan/discover.d.ts.map +1 -0
- package/dist/scan/discover.js +95 -0
- package/dist/scan/discover.js.map +1 -0
- package/dist/scan/scanner.d.ts +32 -0
- package/dist/scan/scanner.d.ts.map +1 -0
- package/dist/scan/scanner.js +137 -0
- package/dist/scan/scanner.js.map +1 -0
- package/dist/types-gen/extract.d.ts +22 -0
- package/dist/types-gen/extract.d.ts.map +1 -0
- package/dist/types-gen/extract.js +148 -0
- package/dist/types-gen/extract.js.map +1 -0
- package/dist/types-gen/generate.d.ts +10 -0
- package/dist/types-gen/generate.d.ts.map +1 -0
- package/dist/types-gen/generate.js +39 -0
- package/dist/types-gen/generate.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `snapshot` — produce offline-resilience snapshot files: the RAW
|
|
3
|
+
* `bundle.translations` (`Record<key, ContentUnit>`) the backend returns,
|
|
4
|
+
* written verbatim per (locale, namespace).
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL: this deliberately BYPASSES the format adapters. The pull/format
|
|
7
|
+
* adapters flatten ContentUnit to plain display strings (dropping the `styles`
|
|
8
|
+
* map) — fine for human-edited translation files, fatal for runtime seeding,
|
|
9
|
+
* because the runtime needs the full ContentUnit (text + styles) to render
|
|
10
|
+
* inline markup (`<1>…</1>`). The snapshot is byte-shaped exactly like
|
|
11
|
+
* `GET /api/integrations/translations/{locale}/{namespace}`, so it drops
|
|
12
|
+
* straight into `MemoryRepository.setNamespace` / `StaticRepository` on every
|
|
13
|
+
* surface with no re-derivation.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, resolve } from 'node:path';
|
|
17
|
+
export const DEFAULT_SNAPSHOT_DIR = 'snapshots';
|
|
18
|
+
export async function runSnapshot(config, client, opts) {
|
|
19
|
+
const out = opts.out ?? DEFAULT_SNAPSHOT_DIR;
|
|
20
|
+
const namespaces = new Set(config.files.map((f) => f.namespace ?? config.namespace));
|
|
21
|
+
if (namespaces.size === 0)
|
|
22
|
+
namespaces.add(config.namespace);
|
|
23
|
+
const locales = [config.sourceLocale, ...config.targetLocales];
|
|
24
|
+
const files = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
for (const locale of locales) {
|
|
27
|
+
for (const namespace of namespaces) {
|
|
28
|
+
const bundle = await client.getNamespace(locale, namespace);
|
|
29
|
+
const keys = Object.keys(bundle.translations).length;
|
|
30
|
+
if (keys === 0) {
|
|
31
|
+
skipped.push({ locale, namespace }); // nothing published yet
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const snapshot = {
|
|
35
|
+
meta: {
|
|
36
|
+
locale,
|
|
37
|
+
namespace,
|
|
38
|
+
...(bundle.version ? { version: bundle.version } : {}),
|
|
39
|
+
...(opts.generatedAt ? { generatedAt: opts.generatedAt } : {}),
|
|
40
|
+
},
|
|
41
|
+
translations: bundle.translations, // raw — styles preserved
|
|
42
|
+
};
|
|
43
|
+
const rel = `${out}/${locale}.${namespace}.json`;
|
|
44
|
+
const abs = resolve(opts.cwd, rel);
|
|
45
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
46
|
+
writeFileSync(abs, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
47
|
+
files.push({ path: rel, locale, namespace, keys });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { files, skipped };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/commands/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAK7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AA0BhD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAuB,EACvB,MAA0B,EAC1B,IAAqB;IAErB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,oBAAoB,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrF,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC;QAAE,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IAE/D,MAAM,KAAK,GAAyB,EAAE,CAAC;IACvC,MAAM,OAAO,GAA8B,EAAE,CAAC;IAE9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC;YACrD,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,wBAAwB;gBAC7D,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAiB;gBAC7B,IAAI,EAAE;oBACJ,MAAM;oBACN,SAAS;oBACT,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACtD,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC/D;gBACD,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,yBAAyB;aAC7D,CAAC;YACF,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,MAAM,IAAI,SAAS,OAAO,CAAC;YACjD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACnC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7C,aAAa,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `types generate` / `types check` — type-safe DX over your translations.
|
|
3
|
+
*
|
|
4
|
+
* By default the generator derives keys + ICU parameter types from your local
|
|
5
|
+
* source files (offline; the source of truth you edit and sync). `--server`
|
|
6
|
+
* instead fetches the source-locale bundle from the canonical
|
|
7
|
+
* `/api/integrations/translations/{locale}/{namespace}` endpoint and infers
|
|
8
|
+
* types from those ContentUnits — the "types from your live schema" story on a
|
|
9
|
+
* working endpoint (the old `@lionrapid/types` used a stale one). `pull --types`
|
|
10
|
+
* calls the offline `runTypesGenerate` so freshly-synced keys are typed.
|
|
11
|
+
*/
|
|
12
|
+
import type { LionRapidConfig } from '../config.js';
|
|
13
|
+
import type { LionRapidApiClient } from '../api-client.js';
|
|
14
|
+
/** Map every source key to its ICU message text (first definition wins). */
|
|
15
|
+
export declare function collectSourceKeyTexts(config: LionRapidConfig, cwd: string): Map<string, string>;
|
|
16
|
+
/** Fetch the source-locale ContentUnits per namespace from the live backend. */
|
|
17
|
+
export declare function collectServerKeyTexts(config: LionRapidConfig, client: LionRapidApiClient): Promise<Map<string, string>>;
|
|
18
|
+
export interface TypesGenerateOptions {
|
|
19
|
+
cwd: string;
|
|
20
|
+
output?: string;
|
|
21
|
+
strictIcu?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface TypesGenerateResult {
|
|
24
|
+
keys: number;
|
|
25
|
+
output: string;
|
|
26
|
+
}
|
|
27
|
+
/** Offline: generate types from local source files. */
|
|
28
|
+
export declare function runTypesGenerate(config: LionRapidConfig, opts: TypesGenerateOptions): TypesGenerateResult;
|
|
29
|
+
/** Networked: generate types from the live backend's source-locale bundle. */
|
|
30
|
+
export declare function runTypesGenerateServer(config: LionRapidConfig, client: LionRapidApiClient, opts: TypesGenerateOptions): Promise<TypesGenerateResult>;
|
|
31
|
+
export interface TypesCheckResult {
|
|
32
|
+
keys: number;
|
|
33
|
+
issues: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function runTypesCheck(config: LionRapidConfig, opts: {
|
|
36
|
+
cwd: string;
|
|
37
|
+
}): TypesCheckResult;
|
|
38
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/commands/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAO3D,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAa/F;AAED,gFAAgF;AAChF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAW9B;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAeD,uDAAuD;AACvD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,oBAAoB,GACzB,mBAAmB,CAErB;AAED,8EAA8E;AAC9E,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,mBAAmB,CAAC,CAE9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACpB,gBAAgB,CAYlB"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `types generate` / `types check` — type-safe DX over your translations.
|
|
3
|
+
*
|
|
4
|
+
* By default the generator derives keys + ICU parameter types from your local
|
|
5
|
+
* source files (offline; the source of truth you edit and sync). `--server`
|
|
6
|
+
* instead fetches the source-locale bundle from the canonical
|
|
7
|
+
* `/api/integrations/translations/{locale}/{namespace}` endpoint and infers
|
|
8
|
+
* types from those ContentUnits — the "types from your live schema" story on a
|
|
9
|
+
* working endpoint (the old `@lionrapid/types` used a stale one). `pull --types`
|
|
10
|
+
* calls the offline `runTypesGenerate` so freshly-synced keys are typed.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { getAdapter } from '../adapters/index.js';
|
|
15
|
+
import { generateDts } from '../types-gen/generate.js';
|
|
16
|
+
import { extractParams, IcuError } from '../types-gen/extract.js';
|
|
17
|
+
const DEFAULT_OUTPUT = 'types/translations.d.ts';
|
|
18
|
+
/** Map every source key to its ICU message text (first definition wins). */
|
|
19
|
+
export function collectSourceKeyTexts(config, cwd) {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const file of config.files) {
|
|
22
|
+
const abs = resolve(cwd, file.source);
|
|
23
|
+
if (!existsSync(abs))
|
|
24
|
+
continue;
|
|
25
|
+
const entries = getAdapter(file.format).parse(readFileSync(abs, 'utf8'), {
|
|
26
|
+
locale: config.sourceLocale,
|
|
27
|
+
});
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!map.has(entry.key))
|
|
30
|
+
map.set(entry.key, entry.value.text);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return map;
|
|
34
|
+
}
|
|
35
|
+
/** Fetch the source-locale ContentUnits per namespace from the live backend. */
|
|
36
|
+
export async function collectServerKeyTexts(config, client) {
|
|
37
|
+
const namespaces = new Set(config.files.map((f) => f.namespace ?? config.namespace));
|
|
38
|
+
if (namespaces.size === 0)
|
|
39
|
+
namespaces.add(config.namespace);
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const namespace of namespaces) {
|
|
42
|
+
const bundle = await client.getNamespace(config.sourceLocale, namespace);
|
|
43
|
+
for (const [key, unit] of Object.entries(bundle.translations)) {
|
|
44
|
+
if (!map.has(key))
|
|
45
|
+
map.set(key, unit.text);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
}
|
|
50
|
+
function writeDts(keyTexts, config, opts) {
|
|
51
|
+
const dts = generateDts(keyTexts, { strictIcu: opts.strictIcu });
|
|
52
|
+
const output = opts.output ?? config.typesOutput ?? DEFAULT_OUTPUT;
|
|
53
|
+
const abs = resolve(opts.cwd, output);
|
|
54
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
55
|
+
writeFileSync(abs, dts);
|
|
56
|
+
return { keys: keyTexts.size, output };
|
|
57
|
+
}
|
|
58
|
+
/** Offline: generate types from local source files. */
|
|
59
|
+
export function runTypesGenerate(config, opts) {
|
|
60
|
+
return writeDts(collectSourceKeyTexts(config, opts.cwd), config, opts);
|
|
61
|
+
}
|
|
62
|
+
/** Networked: generate types from the live backend's source-locale bundle. */
|
|
63
|
+
export async function runTypesGenerateServer(config, client, opts) {
|
|
64
|
+
return writeDts(await collectServerKeyTexts(config, client), config, opts);
|
|
65
|
+
}
|
|
66
|
+
export function runTypesCheck(config, opts) {
|
|
67
|
+
const keyTexts = collectSourceKeyTexts(config, opts.cwd);
|
|
68
|
+
const issues = [];
|
|
69
|
+
for (const [key, text] of keyTexts) {
|
|
70
|
+
try {
|
|
71
|
+
extractParams(text, true); // strict → throws on ICU without `other`
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof IcuError)
|
|
75
|
+
issues.push(`${key}: ${err.message}`);
|
|
76
|
+
else
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { keys: keyTexts.size, issues };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/commands/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,cAAc,GAAG,yBAAyB,CAAC;AAEjD,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,MAAuB,EAAE,GAAW;IACxE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE;YACvE,MAAM,EAAE,MAAM,CAAC,YAAY;SAC5B,CAAC,CAAC;QACH,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAuB,EACvB,MAA0B;IAE1B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrF,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC;QAAE,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QACzE,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAaD,SAAS,QAAQ,CACf,QAA6B,EAC7B,MAAuB,EACvB,IAA0B;IAE1B,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACjE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,IAAI,cAAc,CAAC;IACnE,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACxB,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;AACzC,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,gBAAgB,CAC9B,MAAuB,EACvB,IAA0B;IAE1B,OAAO,QAAQ,CAAC,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACzE,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAuB,EACvB,MAA0B,EAC1B,IAA0B;IAE1B,OAAO,QAAQ,CAAC,MAAM,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAC7E,CAAC;AAOD,MAAM,UAAU,aAAa,CAC3B,MAAuB,EACvB,IAAqB;IAErB,MAAM,QAAQ,GAAG,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACzD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,yCAAyC;QACtE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,QAAQ;gBAAE,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;;gBAC9D,MAAM,GAAG,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validate` — compare local source keys against the backend's published
|
|
3
|
+
* translations for every target locale, and report gaps:
|
|
4
|
+
* - missing: a source key with no published translation for the locale
|
|
5
|
+
* - empty: a published translation whose text is blank
|
|
6
|
+
* - orphan: a published key that no longer exists in the source
|
|
7
|
+
*
|
|
8
|
+
* Used in CI with a non-zero exit when any issue is found.
|
|
9
|
+
*/
|
|
10
|
+
import type { LionRapidConfig } from '../config.js';
|
|
11
|
+
import type { LionRapidApiClient } from '../api-client.js';
|
|
12
|
+
export type ValidateIssueType = 'missing' | 'empty' | 'orphan';
|
|
13
|
+
export interface ValidateIssue {
|
|
14
|
+
locale: string;
|
|
15
|
+
namespace: string;
|
|
16
|
+
type: ValidateIssueType;
|
|
17
|
+
key: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ValidateResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
sourceKeys: number;
|
|
22
|
+
issues: ValidateIssue[];
|
|
23
|
+
}
|
|
24
|
+
export interface ValidateOptions {
|
|
25
|
+
cwd: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function runValidate(config: LionRapidConfig, client: LionRapidApiClient, opts: ValidateOptions): Promise<ValidateResult>;
|
|
28
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3D,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,iBAAiB,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,cAAc,CAAC,CAkCzB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validate` — compare local source keys against the backend's published
|
|
3
|
+
* translations for every target locale, and report gaps:
|
|
4
|
+
* - missing: a source key with no published translation for the locale
|
|
5
|
+
* - empty: a published translation whose text is blank
|
|
6
|
+
* - orphan: a published key that no longer exists in the source
|
|
7
|
+
*
|
|
8
|
+
* Used in CI with a non-zero exit when any issue is found.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { getAdapter } from '../adapters/index.js';
|
|
13
|
+
export async function runValidate(config, client, opts) {
|
|
14
|
+
// Union of source keys per namespace.
|
|
15
|
+
const sourceByNs = new Map();
|
|
16
|
+
for (const file of config.files) {
|
|
17
|
+
const ns = file.namespace ?? config.namespace;
|
|
18
|
+
const abs = resolve(opts.cwd, file.source);
|
|
19
|
+
if (!existsSync(abs))
|
|
20
|
+
continue;
|
|
21
|
+
const set = sourceByNs.get(ns) ?? new Set();
|
|
22
|
+
for (const entry of getAdapter(file.format).parse(readFileSync(abs, 'utf8'))) {
|
|
23
|
+
set.add(entry.key);
|
|
24
|
+
}
|
|
25
|
+
sourceByNs.set(ns, set);
|
|
26
|
+
}
|
|
27
|
+
const totalSourceKeys = [...sourceByNs.values()].reduce((n, s) => n + s.size, 0);
|
|
28
|
+
const issues = [];
|
|
29
|
+
for (const locale of config.targetLocales) {
|
|
30
|
+
for (const [namespace, sourceKeys] of sourceByNs) {
|
|
31
|
+
const bundle = await client.getNamespace(locale, namespace);
|
|
32
|
+
const translations = bundle.translations;
|
|
33
|
+
const have = new Set(Object.keys(translations));
|
|
34
|
+
for (const key of sourceKeys) {
|
|
35
|
+
if (!have.has(key))
|
|
36
|
+
issues.push({ locale, namespace, type: 'missing', key });
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of Object.entries(translations)) {
|
|
39
|
+
if (!value.text.trim())
|
|
40
|
+
issues.push({ locale, namespace, type: 'empty', key });
|
|
41
|
+
if (!sourceKeys.has(key))
|
|
42
|
+
issues.push({ locale, namespace, type: 'orphan', key });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { ok: issues.length === 0, sourceKeys: totalSourceKeys, issues };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAqBlD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAuB,EACvB,MAA0B,EAC1B,IAAqB;IAErB,sCAAsC;IACtC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC;QAC9C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;QACpD,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;YAC7E,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,eAAe,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACjF,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1C,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,UAAU,EAAE,CAAC;YACjD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAC5D,MAAM,YAAY,GAAgC,MAAM,CAAC,YAAY,CAAC;YACtE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;YAEhD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;YAC/E,CAAC;YACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;gBACxD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;oBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC/E,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;YACpF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;AAC1E,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed loader + validator for `lionrapid.yml`.
|
|
3
|
+
*
|
|
4
|
+
* Design goals (developer-friendly, type-safe):
|
|
5
|
+
* - Parsing yields a fully-typed `LionRapidConfig` — no `any` leaks out.
|
|
6
|
+
* - Validation is hand-rolled (no schema-lib dependency) and reports *every*
|
|
7
|
+
* problem at once with a precise field path, e.g. `files[1].target: must
|
|
8
|
+
* contain the "{locale}" placeholder`.
|
|
9
|
+
* - `parseConfig(string)` is split from `loadConfig(path)` so the validator is
|
|
10
|
+
* unit-testable without touching the filesystem.
|
|
11
|
+
*/
|
|
12
|
+
/** Translation file formats the CLI can parse/serialize. */
|
|
13
|
+
export declare const FILE_FORMATS: readonly ["json", "yaml", "po", "ios-strings", "xcstrings", "android-xml", "arb", "xliff"];
|
|
14
|
+
export type FileFormat = (typeof FILE_FORMATS)[number];
|
|
15
|
+
/**
|
|
16
|
+
* Formats that store every locale in a single file (catalogs). For these the
|
|
17
|
+
* `target` path is one shared file and does not need a `{locale}` placeholder.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MULTI_LOCALE_FORMATS: ReadonlySet<FileFormat>;
|
|
20
|
+
/** One source→target file mapping. `target` carries a `{locale}` placeholder. */
|
|
21
|
+
export interface FileEntry {
|
|
22
|
+
format: FileFormat;
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
/** Optional per-entry namespace override (defaults to the top-level namespace). */
|
|
26
|
+
namespace?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LionRapidConfig {
|
|
29
|
+
project: string;
|
|
30
|
+
server: string;
|
|
31
|
+
sourceLocale: string;
|
|
32
|
+
targetLocales: string[];
|
|
33
|
+
namespace: string;
|
|
34
|
+
files: FileEntry[];
|
|
35
|
+
/** Regenerate TypeScript types after `pull`. */
|
|
36
|
+
generateTypes: boolean;
|
|
37
|
+
/** Output path for generated `.d.ts` (default `types/translations.d.ts`). */
|
|
38
|
+
typesOutput?: string;
|
|
39
|
+
/** Source-scan globs for `lionrapid scan` (defaults applied if omitted). */
|
|
40
|
+
scan?: {
|
|
41
|
+
include: string[];
|
|
42
|
+
exclude: string[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A single validation problem, with a dotted/bracketed field path. */
|
|
46
|
+
export interface ConfigIssue {
|
|
47
|
+
path: string;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
/** Thrown when `lionrapid.yml` is missing, unparseable, or invalid. */
|
|
51
|
+
export declare class ConfigError extends Error {
|
|
52
|
+
readonly issues: ConfigIssue[];
|
|
53
|
+
constructor(message: string, issues?: ConfigIssue[]);
|
|
54
|
+
}
|
|
55
|
+
/** Thrown when no API key can be resolved. */
|
|
56
|
+
export declare class ApiKeyError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/** Locate the config file in `cwd`, returning its path or null. */
|
|
60
|
+
export declare function findConfigPath(cwd?: string): string | null;
|
|
61
|
+
/** Parse + validate a YAML config string into a typed config. */
|
|
62
|
+
export declare function parseConfig(rawYaml: string): LionRapidConfig;
|
|
63
|
+
/** Read + parse + validate the config at `path`. */
|
|
64
|
+
export declare function loadConfig(path: string): LionRapidConfig;
|
|
65
|
+
export interface ResolveApiKeyOptions {
|
|
66
|
+
/** Environment to read `LIONRAPID_API_KEY` from (defaults to process.env). */
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
/** Credentials file path (defaults to ~/.lionrapid/credentials). */
|
|
69
|
+
credentialsPath?: string;
|
|
70
|
+
/** Server URL, used to look up a server-scoped key in the credentials file. */
|
|
71
|
+
server?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the API key. Precedence:
|
|
75
|
+
* 1. `LIONRAPID_API_KEY` environment variable
|
|
76
|
+
* 2. credentials file (JSON: `{ "default": "sk_...", "<server>": "sk_..." }`)
|
|
77
|
+
* The key is never read from `lionrapid.yml` (which is committed to git).
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveApiKey(opts?: ResolveApiKeyOptions): string;
|
|
80
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,4DAA4D;AAC5D,eAAO,MAAM,YAAY,4FASf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,UAAU,CAAsC,CAAC;AAEhG,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,gDAAgD;IAChD,aAAa,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CACjD;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uEAAuE;AACvE,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,WAAW,EAAO;CASxD;AAED,8CAA8C;AAC9C,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B;AAID,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAMzE;AA8KD,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAa5D;AAED,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAKxD;AAED,MAAM,WAAW,oBAAoB;IACnC,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,oBAAyB,GAAG,MAAM,CA6BrE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed loader + validator for `lionrapid.yml`.
|
|
3
|
+
*
|
|
4
|
+
* Design goals (developer-friendly, type-safe):
|
|
5
|
+
* - Parsing yields a fully-typed `LionRapidConfig` — no `any` leaks out.
|
|
6
|
+
* - Validation is hand-rolled (no schema-lib dependency) and reports *every*
|
|
7
|
+
* problem at once with a precise field path, e.g. `files[1].target: must
|
|
8
|
+
* contain the "{locale}" placeholder`.
|
|
9
|
+
* - `parseConfig(string)` is split from `loadConfig(path)` so the validator is
|
|
10
|
+
* unit-testable without touching the filesystem.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { parse as parseYaml } from 'yaml';
|
|
16
|
+
/** Translation file formats the CLI can parse/serialize. */
|
|
17
|
+
export const FILE_FORMATS = [
|
|
18
|
+
'json',
|
|
19
|
+
'yaml',
|
|
20
|
+
'po',
|
|
21
|
+
'ios-strings',
|
|
22
|
+
'xcstrings',
|
|
23
|
+
'android-xml',
|
|
24
|
+
'arb',
|
|
25
|
+
'xliff',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Formats that store every locale in a single file (catalogs). For these the
|
|
29
|
+
* `target` path is one shared file and does not need a `{locale}` placeholder.
|
|
30
|
+
*/
|
|
31
|
+
export const MULTI_LOCALE_FORMATS = new Set(['xcstrings']);
|
|
32
|
+
/** Thrown when `lionrapid.yml` is missing, unparseable, or invalid. */
|
|
33
|
+
export class ConfigError extends Error {
|
|
34
|
+
issues;
|
|
35
|
+
constructor(message, issues = []) {
|
|
36
|
+
const detail = issues.length > 0
|
|
37
|
+
? `${message}\n${issues.map((i) => ` - ${i.path}: ${i.message}`).join('\n')}`
|
|
38
|
+
: message;
|
|
39
|
+
super(detail);
|
|
40
|
+
this.name = 'ConfigError';
|
|
41
|
+
this.issues = issues;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Thrown when no API key can be resolved. */
|
|
45
|
+
export class ApiKeyError extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'ApiKeyError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const CONFIG_FILENAMES = ['lionrapid.yml', 'lionrapid.yaml'];
|
|
52
|
+
/** Locate the config file in `cwd`, returning its path or null. */
|
|
53
|
+
export function findConfigPath(cwd = process.cwd()) {
|
|
54
|
+
for (const name of CONFIG_FILENAMES) {
|
|
55
|
+
const candidate = join(cwd, name);
|
|
56
|
+
if (existsSync(candidate))
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function isRecord(value) {
|
|
62
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function isNonEmptyString(value) {
|
|
65
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
66
|
+
}
|
|
67
|
+
/** Validate a raw parsed object into a typed config, collecting all issues. */
|
|
68
|
+
function validateConfig(raw) {
|
|
69
|
+
const issues = [];
|
|
70
|
+
if (!isRecord(raw)) {
|
|
71
|
+
throw new ConfigError('Config must be a YAML mapping (key: value pairs).');
|
|
72
|
+
}
|
|
73
|
+
// Scalars
|
|
74
|
+
if (!isNonEmptyString(raw['project'])) {
|
|
75
|
+
issues.push({ path: 'project', message: 'required non-empty string' });
|
|
76
|
+
}
|
|
77
|
+
if (!isNonEmptyString(raw['server'])) {
|
|
78
|
+
issues.push({ path: 'server', message: 'required non-empty string' });
|
|
79
|
+
}
|
|
80
|
+
else if (!/^https?:\/\//.test(raw['server'])) {
|
|
81
|
+
issues.push({
|
|
82
|
+
path: 'server',
|
|
83
|
+
message: 'must be an http(s) URL (e.g. https://api.lionrapid.com)',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (!isNonEmptyString(raw['sourceLocale'])) {
|
|
87
|
+
issues.push({ path: 'sourceLocale', message: 'required non-empty string' });
|
|
88
|
+
}
|
|
89
|
+
// targetLocales
|
|
90
|
+
const targetLocales = [];
|
|
91
|
+
if (!Array.isArray(raw['targetLocales']) || raw['targetLocales'].length === 0) {
|
|
92
|
+
issues.push({
|
|
93
|
+
path: 'targetLocales',
|
|
94
|
+
message: 'required non-empty array of locale codes',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
raw['targetLocales'].forEach((loc, i) => {
|
|
99
|
+
if (!isNonEmptyString(loc)) {
|
|
100
|
+
issues.push({ path: `targetLocales[${i}]`, message: 'must be a non-empty string' });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
targetLocales.push(loc);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (isNonEmptyString(raw['sourceLocale']) && targetLocales.includes(raw['sourceLocale'])) {
|
|
107
|
+
issues.push({
|
|
108
|
+
path: 'targetLocales',
|
|
109
|
+
message: `must not include the sourceLocale "${raw['sourceLocale']}"`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// namespace (optional, default 'app')
|
|
114
|
+
let namespace = 'app';
|
|
115
|
+
if (raw['namespace'] !== undefined) {
|
|
116
|
+
if (!isNonEmptyString(raw['namespace'])) {
|
|
117
|
+
issues.push({ path: 'namespace', message: 'must be a non-empty string when set' });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
namespace = raw['namespace'];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// generateTypes (optional, default false)
|
|
124
|
+
let generateTypes = false;
|
|
125
|
+
if (raw['generateTypes'] !== undefined) {
|
|
126
|
+
if (typeof raw['generateTypes'] !== 'boolean') {
|
|
127
|
+
issues.push({ path: 'generateTypes', message: 'must be a boolean when set' });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
generateTypes = raw['generateTypes'];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// typesOutput (optional)
|
|
134
|
+
let typesOutput;
|
|
135
|
+
if (raw['typesOutput'] !== undefined) {
|
|
136
|
+
if (!isNonEmptyString(raw['typesOutput'])) {
|
|
137
|
+
issues.push({ path: 'typesOutput', message: 'must be a non-empty string when set' });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
typesOutput = raw['typesOutput'];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// scan (optional) — source-scan globs
|
|
144
|
+
let scan;
|
|
145
|
+
if (raw['scan'] !== undefined) {
|
|
146
|
+
if (!isRecord(raw['scan'])) {
|
|
147
|
+
issues.push({ path: 'scan', message: 'must be a mapping with include/exclude arrays' });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const s = raw['scan'];
|
|
151
|
+
const include = Array.isArray(s['include']) ? s['include'].filter(isNonEmptyString) : [];
|
|
152
|
+
const exclude = Array.isArray(s['exclude']) ? s['exclude'].filter(isNonEmptyString) : [];
|
|
153
|
+
scan = { include, exclude };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// files
|
|
157
|
+
const files = [];
|
|
158
|
+
if (!Array.isArray(raw['files']) || raw['files'].length === 0) {
|
|
159
|
+
issues.push({ path: 'files', message: 'required non-empty array of file mappings' });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
raw['files'].forEach((entry, i) => {
|
|
163
|
+
const base = `files[${i}]`;
|
|
164
|
+
if (!isRecord(entry)) {
|
|
165
|
+
issues.push({ path: base, message: 'must be a mapping with format/source/target' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const format = entry['format'];
|
|
169
|
+
const validFormat = isNonEmptyString(format) && FILE_FORMATS.includes(format);
|
|
170
|
+
const isMultiLocale = validFormat && MULTI_LOCALE_FORMATS.has(format);
|
|
171
|
+
if (!validFormat) {
|
|
172
|
+
issues.push({
|
|
173
|
+
path: `${base}.format`,
|
|
174
|
+
message: `must be one of: ${FILE_FORMATS.join(', ')}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!isNonEmptyString(entry['source'])) {
|
|
178
|
+
issues.push({ path: `${base}.source`, message: 'required non-empty string' });
|
|
179
|
+
}
|
|
180
|
+
if (!isNonEmptyString(entry['target'])) {
|
|
181
|
+
issues.push({ path: `${base}.target`, message: 'required non-empty string' });
|
|
182
|
+
}
|
|
183
|
+
else if (!isMultiLocale && !entry['target'].includes('{locale}')) {
|
|
184
|
+
issues.push({
|
|
185
|
+
path: `${base}.target`,
|
|
186
|
+
message: 'must contain the "{locale}" placeholder',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
let entryNs;
|
|
190
|
+
if (entry['namespace'] !== undefined) {
|
|
191
|
+
if (!isNonEmptyString(entry['namespace'])) {
|
|
192
|
+
issues.push({ path: `${base}.namespace`, message: 'must be a non-empty string when set' });
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
entryNs = entry['namespace'];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Only push a typed entry if its required fields are sound.
|
|
199
|
+
if (validFormat &&
|
|
200
|
+
isNonEmptyString(entry['source']) &&
|
|
201
|
+
isNonEmptyString(entry['target']) &&
|
|
202
|
+
(isMultiLocale || entry['target'].includes('{locale}'))) {
|
|
203
|
+
files.push({
|
|
204
|
+
format: format,
|
|
205
|
+
source: entry['source'],
|
|
206
|
+
target: entry['target'],
|
|
207
|
+
...(entryNs ? { namespace: entryNs } : {}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (issues.length > 0) {
|
|
213
|
+
throw new ConfigError('Invalid lionrapid config:', issues);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
project: raw['project'],
|
|
217
|
+
server: raw['server'].replace(/\/$/, ''),
|
|
218
|
+
sourceLocale: raw['sourceLocale'],
|
|
219
|
+
targetLocales,
|
|
220
|
+
namespace,
|
|
221
|
+
files,
|
|
222
|
+
generateTypes,
|
|
223
|
+
...(typesOutput ? { typesOutput } : {}),
|
|
224
|
+
...(scan ? { scan } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Parse + validate a YAML config string into a typed config. */
|
|
228
|
+
export function parseConfig(rawYaml) {
|
|
229
|
+
let parsed;
|
|
230
|
+
try {
|
|
231
|
+
parsed = parseYaml(rawYaml);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
throw new ConfigError(`Could not parse YAML: ${err instanceof Error ? err.message : String(err)}`);
|
|
235
|
+
}
|
|
236
|
+
if (parsed === null || parsed === undefined) {
|
|
237
|
+
throw new ConfigError('Config file is empty.');
|
|
238
|
+
}
|
|
239
|
+
return validateConfig(parsed);
|
|
240
|
+
}
|
|
241
|
+
/** Read + parse + validate the config at `path`. */
|
|
242
|
+
export function loadConfig(path) {
|
|
243
|
+
if (!existsSync(path)) {
|
|
244
|
+
throw new ConfigError(`Config file not found: ${path}`);
|
|
245
|
+
}
|
|
246
|
+
return parseConfig(readFileSync(path, 'utf8'));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the API key. Precedence:
|
|
250
|
+
* 1. `LIONRAPID_API_KEY` environment variable
|
|
251
|
+
* 2. credentials file (JSON: `{ "default": "sk_...", "<server>": "sk_..." }`)
|
|
252
|
+
* The key is never read from `lionrapid.yml` (which is committed to git).
|
|
253
|
+
*/
|
|
254
|
+
export function resolveApiKey(opts = {}) {
|
|
255
|
+
const env = opts.env ?? process.env;
|
|
256
|
+
const fromEnv = env['LIONRAPID_API_KEY'];
|
|
257
|
+
if (isNonEmptyString(fromEnv))
|
|
258
|
+
return fromEnv.trim();
|
|
259
|
+
const credentialsPath = opts.credentialsPath ?? join(homedir(), '.lionrapid', 'credentials');
|
|
260
|
+
if (existsSync(credentialsPath)) {
|
|
261
|
+
let creds;
|
|
262
|
+
try {
|
|
263
|
+
creds = JSON.parse(readFileSync(credentialsPath, 'utf8'));
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
throw new ApiKeyError(`Could not parse credentials file ${credentialsPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
|
+
}
|
|
268
|
+
if (isRecord(creds)) {
|
|
269
|
+
const scoped = opts.server ? creds[opts.server] : undefined;
|
|
270
|
+
const fallback = creds['default'];
|
|
271
|
+
if (isNonEmptyString(scoped))
|
|
272
|
+
return scoped.trim();
|
|
273
|
+
if (isNonEmptyString(fallback))
|
|
274
|
+
return fallback.trim();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
throw new ApiKeyError('No API key found. Set LIONRAPID_API_KEY or add it to ~/.lionrapid/credentials.');
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=config.js.map
|