@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
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @lionrapid/cli
|
|
2
|
+
|
|
3
|
+
The LionRapid command-line tool. Syncs translation files in many formats
|
|
4
|
+
(iOS `.strings`/`.xcstrings`, Android `strings.xml`, Flutter `.arb`, generic
|
|
5
|
+
JSON, YAML, PO) with the LionRapid backend, and generates type-safe TypeScript
|
|
6
|
+
keys from your translations.
|
|
7
|
+
|
|
8
|
+
This is the **non-web integration wedge**: it reaches platforms the web
|
|
9
|
+
surfaces (WordPress, edge proxy, JS snippet, React SDK) can't — mobile apps,
|
|
10
|
+
backend services, static-site generators, and CI/CD — by speaking the existing
|
|
11
|
+
`/api/integrations/*` REST surface instead of the DOM.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Phase 1 (file-sync dev-loop) is implemented. Command surface:
|
|
16
|
+
|
|
17
|
+
| Command | Status | What it does |
|
|
18
|
+
| ----------------- | ------ | ------------------------------------------------------------------- |
|
|
19
|
+
| `init` | ✅ | Scaffold a `lionrapid.yml` (detects Flutter/iOS/Android/Rails/JS) |
|
|
20
|
+
| `push` | ✅ | Upload source strings to the backend (server handles MT) |
|
|
21
|
+
| `pull [--types]` | ✅ | Download translations into target files; optionally regen types |
|
|
22
|
+
| `validate [--ci]` | ✅ | Check local files vs the backend for missing/orphan/empty keys |
|
|
23
|
+
| `types generate` | ✅ | Generate a typed `.d.ts` from your source files (offline) |
|
|
24
|
+
| `types check` | ✅ | Report ICU issues in your source translations |
|
|
25
|
+
| `scan [--json]` | ✅ | Find user-facing strings in source → inventory keyed by auto_key |
|
|
26
|
+
| `scan --push` | ✅ | Seed the safe `wrap` candidates to the backend under their auto_key |
|
|
27
|
+
| `snapshot` | ✅ | Write offline-resilience bundles (raw ContentUnit) per locale/ns |
|
|
28
|
+
| `doctor` | ✅ | Preflight: config, credentials, server connectivity, source files |
|
|
29
|
+
|
|
30
|
+
Formats: JSON, YAML, PO (incl. plurals + `msgctxt`), Flutter `.arb`, iOS
|
|
31
|
+
`.strings` + `.xcstrings`, Android `strings.xml`, XLIFF (1.2/2.0, CAT-tool
|
|
32
|
+
interchange).
|
|
33
|
+
|
|
34
|
+
**Multiple namespaces:** give any `files` entry its own `namespace` to map
|
|
35
|
+
different file trees to different backend namespaces (defaults to the top-level
|
|
36
|
+
`namespace`). Each is pushed/pulled/validated independently, so the same key name
|
|
37
|
+
in two namespaces never collides:
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
namespace: app
|
|
41
|
+
files:
|
|
42
|
+
- { format: json, source: common/en.json, target: common/{locale}.json, namespace: common }
|
|
43
|
+
- { format: json, source: app/en.json, target: app/{locale}.json } # → namespace: app
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Reliability & automation
|
|
47
|
+
|
|
48
|
+
Built for CI and AI agents — every networked command is resilient and scriptable.
|
|
49
|
+
|
|
50
|
+
- **`--json`** (all commands): emit one result envelope `{ ok, command, … }` on
|
|
51
|
+
**stdout**; human prose goes to **stderr**. On failure, a structured error
|
|
52
|
+
envelope `{ ok: false, error: { type, message, status? }, exitCode }` is written
|
|
53
|
+
to stdout. Parse stdout, read stderr.
|
|
54
|
+
- **`--dry-run`** (`push`, `pull`): report exactly what _would_ happen — counts,
|
|
55
|
+
keys, target files — without any network mutation (`push`) or file write
|
|
56
|
+
(`pull`). `pull --dry-run` still does the read-only fetch so the counts are real.
|
|
57
|
+
- **Exit codes** (stable, gate CI on them): `0` ok · `1` unexpected · `2`
|
|
58
|
+
validation/config · `3` auth · `4` network/5xx · `5` partial.
|
|
59
|
+
- **Automatic retries**: transient failures (network error, `5xx`, `429`) are
|
|
60
|
+
retried with exponential backoff + jitter; a `429` honors `Retry-After`. Tune
|
|
61
|
+
with `LIONRAPID_RETRIES` (default `3`; `0` disables). Writes are safe to retry —
|
|
62
|
+
the integration endpoints upsert by composite key.
|
|
63
|
+
- **Concurrent `pull`**: namespace fetches fan out with bounded concurrency
|
|
64
|
+
(`LIONRAPID_CONCURRENCY`, default `6`) and show interactive progress on a TTY.
|
|
65
|
+
Output stays deterministic — only the network fan-out is parallelized.
|
|
66
|
+
- **`doctor`**: one command to verify config validity, credential resolution,
|
|
67
|
+
backend reachability/auth, and that declared source files exist — each checked
|
|
68
|
+
independently so you get an actionable list, not a stack trace.
|
|
69
|
+
|
|
70
|
+
## Offline resilience (`snapshot`)
|
|
71
|
+
|
|
72
|
+
`lionrapid snapshot` writes the **raw `ContentUnit` bundle** (`{ meta, translations }`)
|
|
73
|
+
per locale/namespace, byte-shaped like the API response — **bypassing the format
|
|
74
|
+
adapters** (which flatten to display strings and drop the `styles` map). Bundle
|
|
75
|
+
these with your app and load them via the runtime so translations keep working
|
|
76
|
+
**with the server down**:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import bundle from './snapshots/es.app.json' assert { type: 'json' };
|
|
80
|
+
import {
|
|
81
|
+
LionRapidBuilder,
|
|
82
|
+
MemoryRepository,
|
|
83
|
+
StaticRepository,
|
|
84
|
+
} from '@lionrapid/core';
|
|
85
|
+
|
|
86
|
+
const core = await LionRapidBuilder.init({
|
|
87
|
+
defaultLocale: 'es',
|
|
88
|
+
namespace: 'app',
|
|
89
|
+
})
|
|
90
|
+
.use(new MemoryRepository({ enabled: true }))
|
|
91
|
+
.useStaticBundle({ es: { app: bundle.translations } }) // offline layer (memory → static → network)
|
|
92
|
+
.build();
|
|
93
|
+
core.t('welcome'); // works offline, styles intact
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Node apps can read a snapshot directory at runtime via
|
|
97
|
+
`new FileRepository('./snapshots')` from `@lionrapid/storage/node`.
|
|
98
|
+
|
|
99
|
+
## No-library localization (`scan`)
|
|
100
|
+
|
|
101
|
+
For a codebase with **no i18n library** (hardcoded strings, no `t()`), `scan`
|
|
102
|
+
finds user-facing strings via the TypeScript AST over a finite allowlist of UI
|
|
103
|
+
sinks (JSX text + `placeholder`/`alt`/`aria-label`/…), classifies them, and keys
|
|
104
|
+
each by **content-hash `auto_key`** — the same key the web edge proxy / DOM
|
|
105
|
+
auto-detect produces, so a string already translated on your website is reused
|
|
106
|
+
in your app.
|
|
107
|
+
|
|
108
|
+
- `action: wrap` — static text/attributes (deterministic, safe).
|
|
109
|
+
- `action: review` — interpolation / inline markup / dynamic values (left for a
|
|
110
|
+
human or AI agent to author correctly).
|
|
111
|
+
|
|
112
|
+
`scan --json` emits a machine-readable inventory on stdout (human prose on
|
|
113
|
+
stderr) so an AI coding agent can drive the judgment-heavy wrapping while the CLI
|
|
114
|
+
owns the correctness-critical parts (the `auto_key` hash, file writes, sync).
|
|
115
|
+
`scan --push` seeds the safe candidates into the translation memory.
|
|
116
|
+
|
|
117
|
+
### Wrapping (done by you / your agent)
|
|
118
|
+
|
|
119
|
+
The CLI intentionally does **not** rewrite your source. It hands you the
|
|
120
|
+
inventory — each candidate carries its `autoKey`, and the result carries the
|
|
121
|
+
`namespace` — and you (or your AI coding agent, e.g. Claude Code / Cursor) wrap
|
|
122
|
+
using the SDK, which also lets the agent handle the `review` cases (interpolation,
|
|
123
|
+
plurals) a deterministic codemod can't:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// inventory: { namespace: "app", autoKey: "auto_2d0f6b8300be", text: "Add to cart", sink: "jsx-text" }
|
|
127
|
+
<Text>Add to cart</Text>
|
|
128
|
+
// →
|
|
129
|
+
<Text><Trans namespace="app" i18nKey="auto_2d0f6b8300be">Add to cart</Trans></Text>
|
|
130
|
+
|
|
131
|
+
// inventory: { autoKey: "auto_13348442cc6a", text: "Search", sink: "jsx-attr:placeholder" }
|
|
132
|
+
placeholder="Search" → placeholder={t('app:auto_13348442cc6a', 'Search')}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Because the key is the same content-hash the web produces, any of these strings
|
|
136
|
+
already translated on your website are translated in the app for free.
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
lionrapid init --project <id> --targets es,fr
|
|
142
|
+
lionrapid push
|
|
143
|
+
lionrapid pull --types
|
|
144
|
+
lionrapid validate --ci
|
|
145
|
+
|
|
146
|
+
# no-library localization
|
|
147
|
+
lionrapid scan --json # inventory of discovered strings (auto_key-keyed)
|
|
148
|
+
lionrapid scan --push # seed the safe candidates into the TM
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The API key is read from `LIONRAPID_API_KEY` (or `~/.lionrapid/credentials`),
|
|
152
|
+
never from the committed `lionrapid.yml`.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm run build -w packages/cli
|
|
158
|
+
npm test -w packages/cli
|
|
159
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Android `res/values-<locale>/strings.xml` adapter (one locale per file).
|
|
3
|
+
*
|
|
4
|
+
* Handles `<string>` and `<plurals>` (quantity items flatten to `name[quantity]`
|
|
5
|
+
* keys, e.g. `num_items[other]`). `translatable="false"` strings are skipped.
|
|
6
|
+
* Inner text is XML-entity-decoded and Android-unescaped (`\'`, `\"`, `\n`,
|
|
7
|
+
* `\t`); inline markup (`<b>`, `<i>`, …) is left intact so it flows through
|
|
8
|
+
* core's ContentUnit pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Phase-1 limitations (documented intentionally): styled strings whose markup
|
|
11
|
+
* carries quoted attributes (e.g. `<a href="…">`) are uncommon in Android and
|
|
12
|
+
* not round-trip-guaranteed; CDATA is unwrapped to its literal contents.
|
|
13
|
+
*/
|
|
14
|
+
import type { FormatAdapter, SerializeOptions, TranslationEntry } from './types.js';
|
|
15
|
+
export declare class AndroidXmlAdapter implements FormatAdapter {
|
|
16
|
+
readonly format: "android-xml";
|
|
17
|
+
parse(content: string): TranslationEntry[];
|
|
18
|
+
serialize(entries: TranslationEntry[], _options?: SerializeOptions): string;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=android-xml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-xml.d.ts","sourceRoot":"","sources":["../../src/adapters/android-xml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAmCpF,qBAAa,iBAAkB,YAAW,aAAa;IACrD,QAAQ,CAAC,MAAM,gBAA0B;IAEzC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE;IA8B1C,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM;CAwC5E"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Android `res/values-<locale>/strings.xml` adapter (one locale per file).
|
|
3
|
+
*
|
|
4
|
+
* Handles `<string>` and `<plurals>` (quantity items flatten to `name[quantity]`
|
|
5
|
+
* keys, e.g. `num_items[other]`). `translatable="false"` strings are skipped.
|
|
6
|
+
* Inner text is XML-entity-decoded and Android-unescaped (`\'`, `\"`, `\n`,
|
|
7
|
+
* `\t`); inline markup (`<b>`, `<i>`, …) is left intact so it flows through
|
|
8
|
+
* core's ContentUnit pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Phase-1 limitations (documented intentionally): styled strings whose markup
|
|
11
|
+
* carries quoted attributes (e.g. `<a href="…">`) are uncommon in Android and
|
|
12
|
+
* not round-trip-guaranteed; CDATA is unwrapped to its literal contents.
|
|
13
|
+
*/
|
|
14
|
+
import { toContentUnit, fromContentUnit } from './content.js';
|
|
15
|
+
const STRING_RE = /<string\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/string>/g;
|
|
16
|
+
const PLURALS_RE = /<plurals\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/plurals>/g;
|
|
17
|
+
const ITEM_RE = /<item\s+quantity="([^"]+)"[^>]*>([\s\S]*?)<\/item>/g;
|
|
18
|
+
const PLURAL_KEY_RE = /^(.+)\[(\w+)\]$/;
|
|
19
|
+
function decodeXmlEntities(s) {
|
|
20
|
+
return s
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'")
|
|
25
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
|
26
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
27
|
+
.replace(/&/g, '&');
|
|
28
|
+
}
|
|
29
|
+
function decodeInner(raw) {
|
|
30
|
+
const noCdata = raw.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
|
|
31
|
+
return decodeXmlEntities(noCdata).replace(/\\(.)/g, (_, c) => {
|
|
32
|
+
if (c === 'n')
|
|
33
|
+
return '\n';
|
|
34
|
+
if (c === 't')
|
|
35
|
+
return '\t';
|
|
36
|
+
return c; // \' → ', \" → ", \\ → \
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function encodeInner(text, hasStyles) {
|
|
40
|
+
let s = text.replace(/&(?!(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g, '&');
|
|
41
|
+
if (!hasStyles)
|
|
42
|
+
s = s.replace(/</g, '<').replace(/>/g, '>');
|
|
43
|
+
s = s.replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\t/g, '\\t');
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
export class AndroidXmlAdapter {
|
|
47
|
+
format = 'android-xml';
|
|
48
|
+
parse(content) {
|
|
49
|
+
const hits = [];
|
|
50
|
+
for (const m of content.matchAll(STRING_RE)) {
|
|
51
|
+
const [, name, attrs, inner] = m;
|
|
52
|
+
if (/translatable\s*=\s*"false"/.test(attrs))
|
|
53
|
+
continue;
|
|
54
|
+
hits.push({
|
|
55
|
+
index: m.index,
|
|
56
|
+
entries: [{ key: name, value: toContentUnit(decodeInner(inner)) }],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const m of content.matchAll(PLURALS_RE)) {
|
|
60
|
+
const [, name, body] = m;
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const im of body.matchAll(ITEM_RE)) {
|
|
63
|
+
const [, quantity, inner] = im;
|
|
64
|
+
entries.push({ key: `${name}[${quantity}]`, value: toContentUnit(decodeInner(inner)) });
|
|
65
|
+
}
|
|
66
|
+
hits.push({ index: m.index, entries });
|
|
67
|
+
}
|
|
68
|
+
hits.sort((a, b) => a.index - b.index);
|
|
69
|
+
return hits.flatMap((h) => h.entries);
|
|
70
|
+
}
|
|
71
|
+
serialize(entries, _options) {
|
|
72
|
+
const lines = ['<?xml version="1.0" encoding="utf-8"?>', '<resources>'];
|
|
73
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
74
|
+
const entry = entries[i];
|
|
75
|
+
const pluralMatch = entry.key.match(PLURAL_KEY_RE);
|
|
76
|
+
if (pluralMatch) {
|
|
77
|
+
const base = pluralMatch[1];
|
|
78
|
+
lines.push(` <plurals name="${base}">`);
|
|
79
|
+
// Consume consecutive items of the same plural base.
|
|
80
|
+
while (i < entries.length) {
|
|
81
|
+
const current = entries[i];
|
|
82
|
+
const cm = current.key.match(PLURAL_KEY_RE);
|
|
83
|
+
if (!cm || cm[1] !== base)
|
|
84
|
+
break;
|
|
85
|
+
const hasStyles = Object.keys(current.value.styles).length > 0;
|
|
86
|
+
lines.push(` <item quantity="${cm[2]}">${encodeInner(fromContentUnit(current.value), hasStyles)}</item>`);
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
i -= 1; // outer loop will increment
|
|
90
|
+
lines.push(' </plurals>');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const hasStyles = Object.keys(entry.value.styles).length > 0;
|
|
94
|
+
lines.push(` <string name="${entry.key}">${encodeInner(fromContentUnit(entry.value), hasStyles)}</string>`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lines.push('</resources>', '');
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=android-xml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-xml.js","sourceRoot":"","sources":["../../src/adapters/android-xml.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flutter ARB adapter (Application Resource Bundle — JSON).
|
|
3
|
+
*
|
|
4
|
+
* ARB files are JSON where translation keys map to strings and metadata lives
|
|
5
|
+
* under `@`-prefixed keys (`@@locale`, `@key: { description, placeholders }`).
|
|
6
|
+
* Per Flutter's `gen-l10n`, placeholder/description metadata lives in the
|
|
7
|
+
* *template* ARB (the source); target ARBs carry only `@@locale` + translated
|
|
8
|
+
* strings. So we parse real (non-`@`) entries and, on serialize, write
|
|
9
|
+
* `@@locale` followed by key→value pairs.
|
|
10
|
+
*/
|
|
11
|
+
import type { FormatAdapter, SerializeOptions, TranslationEntry } from './types.js';
|
|
12
|
+
export declare class ArbAdapter implements FormatAdapter {
|
|
13
|
+
readonly format: "arb";
|
|
14
|
+
parse(content: string): TranslationEntry[];
|
|
15
|
+
serialize(entries: TranslationEntry[], options?: SerializeOptions): string;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=arb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arb.d.ts","sourceRoot":"","sources":["../../src/adapters/arb.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGpF,qBAAa,UAAW,YAAW,aAAa;IAC9C,QAAQ,CAAC,MAAM,QAAkB;IAEjC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAoB1C,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM;CAM3E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flutter ARB adapter (Application Resource Bundle — JSON).
|
|
3
|
+
*
|
|
4
|
+
* ARB files are JSON where translation keys map to strings and metadata lives
|
|
5
|
+
* under `@`-prefixed keys (`@@locale`, `@key: { description, placeholders }`).
|
|
6
|
+
* Per Flutter's `gen-l10n`, placeholder/description metadata lives in the
|
|
7
|
+
* *template* ARB (the source); target ARBs carry only `@@locale` + translated
|
|
8
|
+
* strings. So we parse real (non-`@`) entries and, on serialize, write
|
|
9
|
+
* `@@locale` followed by key→value pairs.
|
|
10
|
+
*/
|
|
11
|
+
import { toContentUnit, fromContentUnit } from './content.js';
|
|
12
|
+
export class ArbAdapter {
|
|
13
|
+
format = 'arb';
|
|
14
|
+
parse(content) {
|
|
15
|
+
const trimmed = content.trim();
|
|
16
|
+
if (trimmed === '')
|
|
17
|
+
return [];
|
|
18
|
+
let data;
|
|
19
|
+
try {
|
|
20
|
+
data = JSON.parse(trimmed);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
throw new Error(`Invalid ARB (JSON): ${err instanceof Error ? err.message : String(err)}`);
|
|
24
|
+
}
|
|
25
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
26
|
+
throw new Error('Expected an ARB object.');
|
|
27
|
+
}
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const [key, value] of Object.entries(data)) {
|
|
30
|
+
if (key.startsWith('@'))
|
|
31
|
+
continue; // @@locale and @key metadata
|
|
32
|
+
out.push({ key, value: toContentUnit(String(value)) });
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
serialize(entries, options) {
|
|
37
|
+
const obj = {};
|
|
38
|
+
if (options?.locale)
|
|
39
|
+
obj['@@locale'] = options.locale;
|
|
40
|
+
for (const entry of entries)
|
|
41
|
+
obj[entry.key] = fromContentUnit(entry.value);
|
|
42
|
+
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=arb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arb.js","sourceRoot":"","sources":["../../src/adapters/arb.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,OAAO,UAAU;IACZ,MAAM,GAAG,KAAc,CAAC;IAEjC,KAAK,CAAC,OAAe;QACnB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,MAAM,GAAG,GAAuB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAA+B,CAAC,EAAE,CAAC;YAC3E,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS,CAAC,6BAA6B;YAChE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,SAAS,CAAC,OAA2B,EAAE,OAA0B;QAC/D,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,IAAI,OAAO,EAAE,MAAM;YAAE,GAAG,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QACtD,KAAK,MAAM,KAAK,IAAI,OAAO;YAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3E,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion between a format's display string and the universal `ContentUnit`.
|
|
3
|
+
*
|
|
4
|
+
* Plain strings (the overwhelming majority of mobile/file translations) are
|
|
5
|
+
* stored verbatim as `{ text, styles: {} }` — no entity decoding, so they
|
|
6
|
+
* round-trip exactly. Only when a value actually contains inline markup do we
|
|
7
|
+
* run core's `MarkupParser`, so inline `<b>`/`<a>` survive the round-trip and
|
|
8
|
+
* hash to the same `auto_key` as the web stack.
|
|
9
|
+
*/
|
|
10
|
+
import { type ContentUnit } from '@lionrapid/core';
|
|
11
|
+
export declare function toContentUnit(text: string): ContentUnit;
|
|
12
|
+
export declare function fromContentUnit(unit: ContentUnit): string;
|
|
13
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/adapters/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAMjF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGvD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAGzD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion between a format's display string and the universal `ContentUnit`.
|
|
3
|
+
*
|
|
4
|
+
* Plain strings (the overwhelming majority of mobile/file translations) are
|
|
5
|
+
* stored verbatim as `{ text, styles: {} }` — no entity decoding, so they
|
|
6
|
+
* round-trip exactly. Only when a value actually contains inline markup do we
|
|
7
|
+
* run core's `MarkupParser`, so inline `<b>`/`<a>` survive the round-trip and
|
|
8
|
+
* hash to the same `auto_key` as the web stack.
|
|
9
|
+
*/
|
|
10
|
+
import { MarkupParser, HTMLSerializer } from '@lionrapid/core';
|
|
11
|
+
const parser = new MarkupParser();
|
|
12
|
+
const serializer = new HTMLSerializer();
|
|
13
|
+
const HAS_TAG = /<[^>]+>/;
|
|
14
|
+
export function toContentUnit(text) {
|
|
15
|
+
if (!HAS_TAG.test(text))
|
|
16
|
+
return { text, styles: {} };
|
|
17
|
+
return parser.parse(text);
|
|
18
|
+
}
|
|
19
|
+
export function fromContentUnit(unit) {
|
|
20
|
+
if (!unit.styles || Object.keys(unit.styles).length === 0)
|
|
21
|
+
return unit.text;
|
|
22
|
+
return serializer.serialize(unit);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/adapters/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAoB,MAAM,iBAAiB,CAAC;AAEjF,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;AAClC,MAAM,UAAU,GAAG,IAAI,cAAc,EAAE,CAAC;AACxC,MAAM,OAAO,GAAG,SAAS,CAAC;AAE1B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACrD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAiB;IAC/C,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC;IAC5E,OAAO,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format adapter registry. Adapters are added one per increment; asking for a
|
|
3
|
+
* format that isn't wired up yet throws a clear, actionable error rather than
|
|
4
|
+
* failing obscurely.
|
|
5
|
+
*/
|
|
6
|
+
import type { FileFormat } from '../config.js';
|
|
7
|
+
import type { FormatAdapter } from './types.js';
|
|
8
|
+
export declare function getAdapter(format: FileFormat): FormatAdapter;
|
|
9
|
+
export declare function supportedFormats(): FileFormat[];
|
|
10
|
+
export type { FormatAdapter, TranslationEntry, SerializeOptions } from './types.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqBhD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAQ5D;AAED,wBAAgB,gBAAgB,IAAI,UAAU,EAAE,CAE/C;AAED,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format adapter registry. Adapters are added one per increment; asking for a
|
|
3
|
+
* format that isn't wired up yet throws a clear, actionable error rather than
|
|
4
|
+
* failing obscurely.
|
|
5
|
+
*/
|
|
6
|
+
import { JsonAdapter } from './json.js';
|
|
7
|
+
import { ArbAdapter } from './arb.js';
|
|
8
|
+
import { IosStringsAdapter } from './ios-strings.js';
|
|
9
|
+
import { AndroidXmlAdapter } from './android-xml.js';
|
|
10
|
+
import { XcstringsAdapter } from './xcstrings.js';
|
|
11
|
+
import { YamlAdapter } from './yaml.js';
|
|
12
|
+
import { PoAdapter } from './po.js';
|
|
13
|
+
import { XliffAdapter } from './xliff.js';
|
|
14
|
+
const ADAPTERS = new Map([
|
|
15
|
+
['json', new JsonAdapter()],
|
|
16
|
+
['arb', new ArbAdapter()],
|
|
17
|
+
['ios-strings', new IosStringsAdapter()],
|
|
18
|
+
['android-xml', new AndroidXmlAdapter()],
|
|
19
|
+
['xcstrings', new XcstringsAdapter()],
|
|
20
|
+
['yaml', new YamlAdapter()],
|
|
21
|
+
['po', new PoAdapter()],
|
|
22
|
+
['xliff', new XliffAdapter()],
|
|
23
|
+
]);
|
|
24
|
+
export function getAdapter(format) {
|
|
25
|
+
const adapter = ADAPTERS.get(format);
|
|
26
|
+
if (!adapter) {
|
|
27
|
+
throw new Error(`No adapter for format "${format}" yet. Supported: ${[...ADAPTERS.keys()].join(', ')}.`);
|
|
28
|
+
}
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
export function supportedFormats() {
|
|
32
|
+
return [...ADAPTERS.keys()];
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAA4B;IAClD,CAAC,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;IAC3B,CAAC,KAAK,EAAE,IAAI,UAAU,EAAE,CAAC;IACzB,CAAC,aAAa,EAAE,IAAI,iBAAiB,EAAE,CAAC;IACxC,CAAC,aAAa,EAAE,IAAI,iBAAiB,EAAE,CAAC;IACxC,CAAC,WAAW,EAAE,IAAI,gBAAgB,EAAE,CAAC;IACrC,CAAC,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;IAC3B,CAAC,IAAI,EAAE,IAAI,SAAS,EAAE,CAAC;IACvB,CAAC,OAAO,EAAE,IAAI,YAAY,EAAE,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,0BAA0B,MAAM,qBAAqB,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple `.strings` adapter (legacy iOS/macOS localization).
|
|
3
|
+
*
|
|
4
|
+
* Format: `"key" = "value";` pairs, each optionally preceded by a C-style
|
|
5
|
+
* block comment. Keys and values are double-quoted with C-style escapes
|
|
6
|
+
* (`\"`, `\\`, `\n`, `\t`, `\r`). Each `.strings` file holds a single locale,
|
|
7
|
+
* which fits the source/target per-locale file model directly.
|
|
8
|
+
*/
|
|
9
|
+
import type { FormatAdapter, TranslationEntry } from './types.js';
|
|
10
|
+
export declare class IosStringsAdapter implements FormatAdapter {
|
|
11
|
+
readonly format: "ios-strings";
|
|
12
|
+
parse(content: string): TranslationEntry[];
|
|
13
|
+
serialize(entries: TranslationEntry[]): string;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ios-strings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ios-strings.d.ts","sourceRoot":"","sources":["../../src/adapters/ios-strings.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAwBlE,qBAAa,iBAAkB,YAAW,aAAa;IACrD,QAAQ,CAAC,MAAM,gBAA0B;IAEzC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAa1C,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM;CAQ/C"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple `.strings` adapter (legacy iOS/macOS localization).
|
|
3
|
+
*
|
|
4
|
+
* Format: `"key" = "value";` pairs, each optionally preceded by a C-style
|
|
5
|
+
* block comment. Keys and values are double-quoted with C-style escapes
|
|
6
|
+
* (`\"`, `\\`, `\n`, `\t`, `\r`). Each `.strings` file holds a single locale,
|
|
7
|
+
* which fits the source/target per-locale file model directly.
|
|
8
|
+
*/
|
|
9
|
+
import { toContentUnit, fromContentUnit } from './content.js';
|
|
10
|
+
const ENTRY_RE = /(?:\/\*\s*([\s\S]*?)\s*\*\/\s*)?"((?:\\.|[^"\\])*)"\s*=\s*"((?:\\.|[^"\\])*)"\s*;/g;
|
|
11
|
+
function unescape(s) {
|
|
12
|
+
return s.replace(/\\(.)/g, (_, c) => {
|
|
13
|
+
if (c === 'n')
|
|
14
|
+
return '\n';
|
|
15
|
+
if (c === 't')
|
|
16
|
+
return '\t';
|
|
17
|
+
if (c === 'r')
|
|
18
|
+
return '\r';
|
|
19
|
+
return c; // \" → " , \\ → \
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function escape(s) {
|
|
23
|
+
return s
|
|
24
|
+
.replace(/\\/g, '\\\\')
|
|
25
|
+
.replace(/"/g, '\\"')
|
|
26
|
+
.replace(/\n/g, '\\n')
|
|
27
|
+
.replace(/\t/g, '\\t')
|
|
28
|
+
.replace(/\r/g, '\\r');
|
|
29
|
+
}
|
|
30
|
+
export class IosStringsAdapter {
|
|
31
|
+
format = 'ios-strings';
|
|
32
|
+
parse(content) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const match of content.matchAll(ENTRY_RE)) {
|
|
35
|
+
const [, comment, rawKey, rawValue] = match;
|
|
36
|
+
out.push({
|
|
37
|
+
key: unescape(rawKey),
|
|
38
|
+
value: toContentUnit(unescape(rawValue)),
|
|
39
|
+
...(comment ? { comment: comment.trim() } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
serialize(entries) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.comment)
|
|
48
|
+
lines.push(`/* ${entry.comment} */`);
|
|
49
|
+
lines.push(`"${escape(entry.key)}" = "${escape(fromContentUnit(entry.value))}";`);
|
|
50
|
+
}
|
|
51
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ios-strings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ios-strings.js","sourceRoot":"","sources":["../../src/adapters/ios-strings.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,QAAQ,GACZ,oFAAoF,CAAC;AAEvF,SAAS,QAAQ,CAAC,CAAS;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAS,EAAE,EAAE;QAC1C,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAC3B,OAAO,CAAC,CAAC,CAAC,kBAAkB;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,CAAC;SACL,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,OAAO,iBAAiB;IACnB,MAAM,GAAG,aAAsB,CAAC;IAEzC,KAAK,CAAC,OAAe;QACnB,MAAM,GAAG,GAAuB,EAAE,CAAC;QACnC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC;YAC5C,GAAG,CAAC,IAAI,CAAC;gBACP,GAAG,EAAE,QAAQ,CAAC,MAAO,CAAC;gBACtB,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,QAAS,CAAC,CAAC;gBACzC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChD,CAAC,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,SAAS,CAAC,OAA2B;QACnC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC;YACxD,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON adapter — flat or nested i18n JSON (i18next, react-intl, Vue-i18n, RN,
|
|
3
|
+
* node backends). Nested objects flatten to dot-notation keys (`home.cta`) and
|
|
4
|
+
* unflatten back on serialize, preserving insertion order.
|
|
5
|
+
*/
|
|
6
|
+
import type { FormatAdapter, TranslationEntry } from './types.js';
|
|
7
|
+
export declare class JsonAdapter implements FormatAdapter {
|
|
8
|
+
readonly format: "json";
|
|
9
|
+
parse(content: string): TranslationEntry[];
|
|
10
|
+
serialize(entries: TranslationEntry[]): string;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/adapters/json.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGlE,qBAAa,WAAY,YAAW,aAAa;IAC/C,QAAQ,CAAC,MAAM,SAAmB;IAElC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAiB1C,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM;CAG/C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON adapter — flat or nested i18n JSON (i18next, react-intl, Vue-i18n, RN,
|
|
3
|
+
* node backends). Nested objects flatten to dot-notation keys (`home.cta`) and
|
|
4
|
+
* unflatten back on serialize, preserving insertion order.
|
|
5
|
+
*/
|
|
6
|
+
import { flattenObject, unflattenEntries } from './nested.js';
|
|
7
|
+
export class JsonAdapter {
|
|
8
|
+
format = 'json';
|
|
9
|
+
parse(content) {
|
|
10
|
+
const trimmed = content.trim();
|
|
11
|
+
if (trimmed === '')
|
|
12
|
+
return [];
|
|
13
|
+
let data;
|
|
14
|
+
try {
|
|
15
|
+
data = JSON.parse(trimmed);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
|
+
}
|
|
20
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
21
|
+
throw new Error('Expected a JSON object of translation keys.');
|
|
22
|
+
}
|
|
23
|
+
const out = [];
|
|
24
|
+
flattenObject(data, '', out);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
serialize(entries) {
|
|
28
|
+
return `${JSON.stringify(unflattenEntries(entries), null, 2)}\n`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/adapters/json.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9D,MAAM,OAAO,WAAW;IACb,MAAM,GAAG,MAAe,CAAC;IAElC,KAAK,CAAC,OAAe;QACnB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,GAAG,GAAuB,EAAE,CAAC;QACnC,aAAa,CAAC,IAA+B,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QACxD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,SAAS,CAAC,OAA2B;QACnC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACnE,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared flatten/unflatten for nested key/value formats (JSON, YAML). Nested
|
|
3
|
+
* objects become dot-notation keys (`home.cta`) and rebuild on serialize,
|
|
4
|
+
* preserving insertion order. Values convert through the ContentUnit helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { TranslationEntry } from './types.js';
|
|
7
|
+
export declare function flattenObject(obj: Record<string, unknown>, prefix: string, out: TranslationEntry[]): void;
|
|
8
|
+
export declare function unflattenEntries(entries: TranslationEntry[]): Record<string, unknown>;
|
|
9
|
+
//# sourceMappingURL=nested.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nested.d.ts","sourceRoot":"","sources":["../../src/adapters/nested.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,gBAAgB,EAAE,GACtB,IAAI,CAYN;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAerF"}
|