@cliff-format/i18n 0.0.0-stage → 0.2.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 +227 -2
- package/dist/chunk-47VERBYX.js +1789 -0
- package/dist/chunk-47VERBYX.js.map +1 -0
- package/dist/index.cjs +1935 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.cjs +1798 -0
- package/dist/lite.cjs.map +1 -0
- package/dist/lite.d.cts +8 -0
- package/dist/lite.d.ts +8 -0
- package/dist/lite.js +88 -0
- package/dist/lite.js.map +1 -0
- package/dist/react.cjs +59 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +18 -0
- package/dist/react.d.ts +18 -0
- package/dist/react.js +45 -0
- package/dist/react.js.map +1 -0
- package/dist/types-CtbwKSot.d.cts +462 -0
- package/dist/types-CtbwKSot.d.ts +462 -0
- package/dist/vocabulary-riKZ5HrX.d.ts +135 -0
- package/dist/vocabulary-u-i15yFj.d.cts +135 -0
- package/package.json +117 -4
package/README.md
CHANGED
|
@@ -1,3 +1,228 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cliff-format/i18n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLIFF-native runtime localization for the browser. It reads and writes CLIFF documents directly:
|
|
4
|
+
there is no JSON resource layer, and a type/emotion/context field is part of the message rather
|
|
5
|
+
than an external comment.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
~~~bash
|
|
10
|
+
pnpm add @cliff-format/core @cliff-format/i18n
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
The main entry bundles a full ICU runtime. Use `@cliff-format/i18n/lite` when you need only
|
|
14
|
+
simple placeholders, or `@cliff-format/i18n/react` for React 18+.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
~~~ts
|
|
19
|
+
import { createI18n, TYPE, EMOTION } from "@cliff-format/i18n";
|
|
20
|
+
|
|
21
|
+
const i18n = createI18n({
|
|
22
|
+
locale: "zh-CN",
|
|
23
|
+
fallbackLocale: "en-US",
|
|
24
|
+
resources: [cliffText],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { t } = i18n.scope({ namespace: "demo", clan: "settings", group: "video" });
|
|
28
|
+
|
|
29
|
+
const label = t("resolution").render();
|
|
30
|
+
const authored = t("contrast")
|
|
31
|
+
.source("Contrast")
|
|
32
|
+
.type(TYPE.noun)
|
|
33
|
+
.emotion(EMOTION.objective)
|
|
34
|
+
.render();
|
|
35
|
+
~~~
|
|
36
|
+
|
|
37
|
+
The scope is mandatory. CLIFF requires a namespace and a clan in every document, so the runtime
|
|
38
|
+
does not hide defaults:
|
|
39
|
+
|
|
40
|
+
~~~ts
|
|
41
|
+
// compile error: clan is required
|
|
42
|
+
i18n.scope({ namespace: "demo" });
|
|
43
|
+
~~~
|
|
44
|
+
|
|
45
|
+
## Entry IDs
|
|
46
|
+
|
|
47
|
+
An entry starts with `<id>` and contains fields such as `source`, `target`, `type` and
|
|
48
|
+
`status`. Its canonical ID is `namespace.clan.group-path.entry-id`.
|
|
49
|
+
|
|
50
|
+
Pass a canonical ID, a scoped short form, or an entry ID object to `t()` or `i18n.resolve()`.
|
|
51
|
+
|
|
52
|
+
## Entry ID syntax
|
|
53
|
+
|
|
54
|
+
The resolver accepts these forms:
|
|
55
|
+
|
|
56
|
+
| Form | Example |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| Colon namespace | `demo:settings.video.resolution` |
|
|
59
|
+
| CLIFF canonical | `demo.settings.video.resolution` |
|
|
60
|
+
| Friendly path | `demo/settings/video/resolution` |
|
|
61
|
+
| Scope clan | `settings.video.resolution` |
|
|
62
|
+
| Scope group | `video.resolution` |
|
|
63
|
+
| Unique entry | `resolution` |
|
|
64
|
+
|
|
65
|
+
If an entry ID is ambiguous, the default `strictEntryIds: true` throws `CliffAmbiguousEntryError`. Set it
|
|
66
|
+
to `false` only when a deterministic diagnostic-assisted choice is acceptable.
|
|
67
|
+
|
|
68
|
+
Define aliases explicitly:
|
|
69
|
+
|
|
70
|
+
~~~ts
|
|
71
|
+
createI18n({
|
|
72
|
+
locale: "zh-CN",
|
|
73
|
+
aliases: { "home-title": "demo:marketing.home.hero-title" },
|
|
74
|
+
resources: [cliffText],
|
|
75
|
+
});
|
|
76
|
+
~~~
|
|
77
|
+
|
|
78
|
+
## Authoring states
|
|
79
|
+
|
|
80
|
+
`t("home.title")` returns a builder. Calling `.source()` enters authoring mode; `render()` and
|
|
81
|
+
`commit()` do not exist until a type is supplied.
|
|
82
|
+
|
|
83
|
+
~~~ts
|
|
84
|
+
// A: explicit type, the default
|
|
85
|
+
t("contrast").source("Contrast").type(TYPE.noun).render();
|
|
86
|
+
|
|
87
|
+
// B: explicit group inheritance, only when the group already has a type
|
|
88
|
+
t("contrast").source("Contrast").typeFromGroup().render();
|
|
89
|
+
|
|
90
|
+
// @ts-expect-error render is unavailable before type()
|
|
91
|
+
t("contrast").source("Contrast").render();
|
|
92
|
+
~~~
|
|
93
|
+
|
|
94
|
+
A draft is created on `render()` or `commit()`, not when the builder is created. It is a CLIFF
|
|
95
|
+
entry with `status: initial`; `i18n.toCliff()` exports it. Lookup needs neither `source()` nor
|
|
96
|
+
`type()`:
|
|
97
|
+
|
|
98
|
+
~~~ts
|
|
99
|
+
t("resolution").render();
|
|
100
|
+
~~~
|
|
101
|
+
|
|
102
|
+
## CLIFF import and export
|
|
103
|
+
|
|
104
|
+
~~~ts
|
|
105
|
+
i18n.add("CLIFF 1.1\n...", { sourcePath: "zh-CN/settings.cliff" });
|
|
106
|
+
i18n.add(await file.text(), { tolerant: true });
|
|
107
|
+
|
|
108
|
+
i18n.files(); // [{ filePath, namespace, clan, locale, text, document }]
|
|
109
|
+
i18n.toCliff(); // one serialized CLIFF document
|
|
110
|
+
~~~
|
|
111
|
+
|
|
112
|
+
`filePath` and `sourcePath` are runtime/build metadata; the serializer does not write them.
|
|
113
|
+
|
|
114
|
+
Entries whose target is missing or `status: initial` fall back to the source text. A fallback
|
|
115
|
+
locale is used before the source-language record when it has a target.
|
|
116
|
+
|
|
117
|
+
## Status and queries
|
|
118
|
+
|
|
119
|
+
The builder reconciles status on writes without promoting through review:
|
|
120
|
+
|
|
121
|
+
- adding a target to an initial entry → `translated`;
|
|
122
|
+
- reworking the target of a `reviewed`/`final` entry → `translated`;
|
|
123
|
+
- changing the source through `commit({ overwrite: true })` → `translated`;
|
|
124
|
+
- an explicit `.status(...)` always wins;
|
|
125
|
+
- a missing target means `initial`.
|
|
126
|
+
|
|
127
|
+
Review and final are deliberate states: the library never promotes to `reviewed` or `final` by
|
|
128
|
+
itself. The core validator still enforces that `translated`, `reviewed` and `final` require a
|
|
129
|
+
target.
|
|
130
|
+
|
|
131
|
+
~~~ts
|
|
132
|
+
i18n.untranslated({ namespace: "demo", clan: "settings" });
|
|
133
|
+
i18n.byStatus("reviewed", { namespace: "demo", clan: "settings" });
|
|
134
|
+
i18n.query({ status: ["initial", "translated"], target: "present" });
|
|
135
|
+
i18n.statusCounts({ namespace: "demo", clan: "settings" });
|
|
136
|
+
~~~
|
|
137
|
+
|
|
138
|
+
`locale` defaults to `"all"`. Pass a BCP 47 tag to restrict a query to one target language.
|
|
139
|
+
|
|
140
|
+
Fixed vocabularies are exported as enum-like const objects and guards:
|
|
141
|
+
|
|
142
|
+
~~~ts
|
|
143
|
+
TYPE.label; // "label"
|
|
144
|
+
EMOTION.objective; // "objective"
|
|
145
|
+
STATUS.final; // "final"
|
|
146
|
+
VARIANT.glossary; // "glossary"
|
|
147
|
+
isStatusTag("final"); // true
|
|
148
|
+
isVariantTag("glossary"); // true
|
|
149
|
+
canTransitionStatus("reviewed", "final"); // true
|
|
150
|
+
~~~
|
|
151
|
+
|
|
152
|
+
## ICU
|
|
153
|
+
|
|
154
|
+
The full entry point uses:
|
|
155
|
+
|
|
156
|
+
- [messageformat](https://messageformat.github.io/) for MessageFormat 2 (LDML 48);
|
|
157
|
+
- [@messageformat/icu-messageformat-1](https://github.com/messageformat/messageformat/blob/main/mf2/icu-messageformat-1/README.md)
|
|
158
|
+
to compile MF1 into the same formatter.
|
|
159
|
+
|
|
160
|
+
Dialect detection follows CLIFF section 14, with an MF1 precedence check for plural/select
|
|
161
|
+
messages whose closing braces look like MF2 markers.
|
|
162
|
+
|
|
163
|
+
~~~ts
|
|
164
|
+
i18n.format("{count, plural, one {# message} other {# messages}}", { count: 2 });
|
|
165
|
+
i18n.formatToParts("{#b}Hello{/b} {$name}", { name: "Alice" });
|
|
166
|
+
~~~
|
|
167
|
+
|
|
168
|
+
`format()` returns text without invisible bidi-isolation characters. `formatToParts()` returns
|
|
169
|
+
`literal`, `expression` and `markup` parts for safe framework rendering. The lite entry reports
|
|
170
|
+
`icu-unsupported` instead of pretending to format a complex message.
|
|
171
|
+
|
|
172
|
+
## React
|
|
173
|
+
|
|
174
|
+
~~~tsx
|
|
175
|
+
import { I18nProvider, useTranslation } from "@cliff-format/i18n/react";
|
|
176
|
+
|
|
177
|
+
createRoot(root).render(
|
|
178
|
+
<I18nProvider i18n={i18n}>
|
|
179
|
+
<App />
|
|
180
|
+
</I18nProvider>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
function Home() {
|
|
184
|
+
const { t, i18n } = useTranslation({
|
|
185
|
+
namespace: "demo",
|
|
186
|
+
clan: "marketing",
|
|
187
|
+
group: "home",
|
|
188
|
+
});
|
|
189
|
+
return <h1>{t("hero-title").render()}</h1>;
|
|
190
|
+
}
|
|
191
|
+
~~~
|
|
192
|
+
|
|
193
|
+
`useTranslation` returns a scope-bound `t` and the controller. It re-renders on locale changes via
|
|
194
|
+
`useSyncExternalStore`. The core package does not depend on React.
|
|
195
|
+
|
|
196
|
+
Use **one hook per clan** for a page that spans several groups; put the group path in the key:
|
|
197
|
+
|
|
198
|
+
~~~tsx
|
|
199
|
+
const { t } = useTranslation({ namespace: "demo", clan: "marketing" });
|
|
200
|
+
t("home.title").render();
|
|
201
|
+
t("features.context-title").render();
|
|
202
|
+
t("format.line-1").render();
|
|
203
|
+
~~~
|
|
204
|
+
|
|
205
|
+
The `group` option is a convenience prefix for a component whose entry IDs all belong to one group. Do
|
|
206
|
+
not call `useTranslation` once per section in the same component: each call creates another scoped
|
|
207
|
+
translator and another locale subscription.
|
|
208
|
+
|
|
209
|
+
## Diagnostics
|
|
210
|
+
|
|
211
|
+
`onDiagnostic` and `i18n.diagnostics()` report `missing-key`, `ambiguous-key`,
|
|
212
|
+
`type-mismatch`, `emotion-mismatch`, `source-mismatch`, `width-exceeded`, `icu-error`,
|
|
213
|
+
`draft-created` and related codes. `missing` accepts `"key"`, `"empty"`, `"throw"` or
|
|
214
|
+
`"source"`.
|
|
215
|
+
|
|
216
|
+
## API
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
createI18n(options): I18n
|
|
220
|
+
i18n.scope({ namespace, clan, group? }): { t, scope }
|
|
221
|
+
i18n.add(resource, options?): AddResult
|
|
222
|
+
i18n.files(options?): CliffFile[]
|
|
223
|
+
i18n.toCliff(options?): string
|
|
224
|
+
i18n.setLocale(locale) / changeLanguage(locale)
|
|
225
|
+
i18n.subscribe(listener): () => void
|
|
226
|
+
i18n.format(message, params?, options?) / formatToParts(...)
|
|
227
|
+
i18n.validate() / diagnostics()
|
|
228
|
+
```
|