locon 1.5.1 → 1.6.1
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/CHANGELOG.md +50 -0
- package/README.md +116 -13
- package/dist/core.d.mts +46 -0
- package/dist/core.d.ts +46 -0
- package/dist/core.js +352 -0
- package/dist/core.mjs +322 -0
- package/dist/react.d.mts +27 -0
- package/dist/react.d.ts +27 -0
- package/dist/react.js +334 -0
- package/dist/react.mjs +305 -0
- package/dist/resources-BwFIzg3m.d.mts +52 -0
- package/dist/resources-BwFIzg3m.d.ts +52 -0
- package/package.json +24 -7
- package/skills/locon-sync/SKILL.md +14 -1
- package/skills/locon-sync/scripts/check-locon-assets.mjs +16 -7
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-call options for `l()`, `lIn()` and `<LText>`.
|
|
3
|
+
*
|
|
4
|
+
* All four are optional and independent: `params` interpolates, `count`
|
|
5
|
+
* selects a plural form, and `locale` renders one call in a language other
|
|
6
|
+
* than the current one (used when an app exports a document in a language
|
|
7
|
+
* the interface is not currently showing). `fallback` is returned only when
|
|
8
|
+
* no locale resolves the input.
|
|
9
|
+
*/
|
|
10
|
+
interface TranslateOptions {
|
|
11
|
+
/** Values for `{token}` placeholders inside the resolved string. */
|
|
12
|
+
params?: Record<string, string | number>;
|
|
13
|
+
/** Selects `key_one` / `key_few` / `key_other` … via `Intl.PluralRules`. */
|
|
14
|
+
count?: number;
|
|
15
|
+
/** Resolve against this locale instead of the current one. */
|
|
16
|
+
locale?: string;
|
|
17
|
+
/** Human-readable fallback when no locale resolves the key/value. */
|
|
18
|
+
fallback?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Resource = Record<string, string>;
|
|
22
|
+
type ResourceLoader = () => Promise<Resource | {
|
|
23
|
+
default: Resource;
|
|
24
|
+
}>;
|
|
25
|
+
interface ResourceOptions {
|
|
26
|
+
loaders: Record<string, ResourceLoader>;
|
|
27
|
+
defaultLocale: string;
|
|
28
|
+
projectLocale?: string;
|
|
29
|
+
locale?: string | null;
|
|
30
|
+
/** Ordered browser/device preferences, or an explicit list in Node. */
|
|
31
|
+
languages?: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
interface LocaleSnapshot {
|
|
34
|
+
currentLocale: string;
|
|
35
|
+
loading: boolean;
|
|
36
|
+
ready: boolean;
|
|
37
|
+
error: Error | null;
|
|
38
|
+
}
|
|
39
|
+
/** First matching preference wins; regional variants can use a base resource. */
|
|
40
|
+
declare function matchLocale(languages: readonly string[], supported: readonly string[], fallback: string): string;
|
|
41
|
+
/** A per-app/per-request store. Importing it touches neither React nor a platform API. */
|
|
42
|
+
declare function createLocon(options: ResourceOptions): {
|
|
43
|
+
locales: string[];
|
|
44
|
+
l: (key: string, translateOptions?: TranslateOptions) => string;
|
|
45
|
+
loadLocale: (locale: string) => Promise<Resource>;
|
|
46
|
+
setLocale: (locale: string | null) => Promise<boolean>;
|
|
47
|
+
getSnapshot: () => LocaleSnapshot;
|
|
48
|
+
subscribe(listener: () => void): () => void;
|
|
49
|
+
};
|
|
50
|
+
type LoconInstance = ReturnType<typeof createLocon>;
|
|
51
|
+
|
|
52
|
+
export { type LocaleSnapshot as L, type ResourceLoader as R, type TranslateOptions as T, type LoconInstance as a, type ResourceOptions as b, createLocon as c, matchLocale as m };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locon",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.6.1",
|
|
4
|
+
"description": "Lightweight localization for React Native, React and Node with lazy locale resources.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -10,17 +10,28 @@
|
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"./skills/*": "./skills/*",
|
|
13
|
-
"./package.json": "./package.json"
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./dist/core.d.ts",
|
|
16
|
+
"import": "./dist/core.mjs",
|
|
17
|
+
"require": "./dist/core.js"
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"types": "./dist/react.d.ts",
|
|
21
|
+
"import": "./dist/react.mjs",
|
|
22
|
+
"require": "./dist/react.js"
|
|
23
|
+
}
|
|
14
24
|
},
|
|
15
25
|
"files": [
|
|
16
26
|
"dist",
|
|
17
|
-
"skills"
|
|
27
|
+
"skills",
|
|
28
|
+
"CHANGELOG.md"
|
|
18
29
|
],
|
|
19
30
|
"scripts": {
|
|
20
|
-
"build": "tsup",
|
|
31
|
+
"build": "tsup && tsup --config tsup.web.config.ts",
|
|
21
32
|
"lint": "eslint src",
|
|
22
|
-
"prepare": "
|
|
23
|
-
"pretest": "
|
|
33
|
+
"prepare": "npm run build",
|
|
34
|
+
"pretest": "npm run build",
|
|
24
35
|
"test": "jest"
|
|
25
36
|
},
|
|
26
37
|
"keywords": [
|
|
@@ -59,6 +70,12 @@
|
|
|
59
70
|
},
|
|
60
71
|
"react-native-localize": {
|
|
61
72
|
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"react-native": {
|
|
78
|
+
"optional": true
|
|
62
79
|
}
|
|
63
80
|
},
|
|
64
81
|
"devDependencies": {
|
|
@@ -36,7 +36,7 @@ node "$LOCON_SYNC_SKILL_DIR/scripts/check-locon-assets.mjs" \
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
This scans static strings in JavaScript/TypeScript (single, double, or template
|
|
39
|
-
quotes), `lIn()` calls and `LText` children
|
|
39
|
+
quotes), `lIn()` calls and `LText` / `L` children and `assetKey` props. It ignores calls
|
|
40
40
|
inside comments and string examples. Orphaned keys and plural categories that
|
|
41
41
|
currently fall back to `_other` are advisory because static analysis cannot
|
|
42
42
|
prove either is wrong. Enforce them in CI with `--strict-orphans` and
|
|
@@ -129,6 +129,19 @@ When extracting phrases from source, remember **JSX collapses whitespace**: a
|
|
|
129
129
|
multi-line `<LText>` child becomes one space-joined string at runtime, so the
|
|
130
130
|
asset value must match the collapsed form.
|
|
131
131
|
|
|
132
|
+
For React web (locon 1.6.1+), use `L` from `locon/react` for text children:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<button><L>Überraschung öffnen</L></button>
|
|
136
|
+
<h2><L params={{ name }}>{'Buh, {name}!'}</L></h2>
|
|
137
|
+
<input placeholder={l('Name eingeben')} />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`L` adds no DOM element and works inside SVG `<text>`. Pass one source string,
|
|
141
|
+
not nested markup or interpolated JSX children. Use `l()` for string props,
|
|
142
|
+
metadata, and other non-rendered values. Use source phrases as resource keys
|
|
143
|
+
when lazy loading must work without loading the source locale first.
|
|
144
|
+
|
|
132
145
|
## 5. Beyond the strings
|
|
133
146
|
|
|
134
147
|
Shipping a language is more than its JSON. Check whether the project needs:
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* language to someone who cannot read it. Nothing else in a typical toolchain
|
|
8
8
|
* catches that, or any of the other failure modes below.
|
|
9
9
|
*
|
|
10
|
-
* 1. every phrase passed to `l()` / `<LText>` resolves
|
|
10
|
+
* 1. every phrase passed to `l()` / `<LText>` / `<L>` resolves
|
|
11
11
|
* 2. every locale has exactly the keys the source locale has
|
|
12
12
|
* 3. placeholders match (with a documented exception for plurals)
|
|
13
13
|
* 4. no unreachable duplicate values
|
|
@@ -376,23 +376,32 @@ for (const file of files) {
|
|
|
376
376
|
if (literal) used.push({ phrase: literal.value, file })
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
for (const match of executable.matchAll(/<LText\b([^>]*)>([\s\S]*?)
|
|
379
|
+
for (const match of executable.matchAll(/<(LText|L)\b([^>]*)>([\s\S]*?)<\/\1>/g)) {
|
|
380
|
+
const tag = match[1]
|
|
380
381
|
const openingTagEnd = match[0].indexOf('>')
|
|
381
|
-
const attributes = code.slice(match.index +
|
|
382
|
+
const attributes = code.slice(match.index + tag.length + 1, match.index + openingTagEnd)
|
|
382
383
|
const childStart = match.index + openingTagEnd + 1
|
|
383
|
-
const childEnd = match.index + match[0].length -
|
|
384
|
+
const childEnd = match.index + match[0].length - tag.length - 3
|
|
384
385
|
const child = code.slice(childStart, childEnd)
|
|
385
386
|
const assetKey = attributeLiteral(attributes, 'assetKey')
|
|
386
387
|
|
|
387
388
|
if (assetKey) {
|
|
388
389
|
used.push({ phrase: assetKey, file, directKey: true })
|
|
389
|
-
} else if (!/[<>{}]/.test(
|
|
390
|
+
} else if (!/[<>{}]/.test(child)) {
|
|
390
391
|
used.push({ phrase: collapse(child), file })
|
|
392
|
+
} else {
|
|
393
|
+
// A single literal expression preserves placeholders: <L>{'Hi {name}'}</L>.
|
|
394
|
+
const expression = child.trim()
|
|
395
|
+
if (expression.startsWith('{') && expression.endsWith('}')) {
|
|
396
|
+
const contents = expression.slice(1, -1).trim()
|
|
397
|
+
const literal = readLiteralAt(contents, 0)
|
|
398
|
+
if (literal && !contents.slice(literal.end).trim()) used.push({ phrase: literal.value, file })
|
|
399
|
+
}
|
|
391
400
|
}
|
|
392
401
|
}
|
|
393
402
|
|
|
394
|
-
for (const match of executable.matchAll(/<LText\b([^>]*)\/>/g)) {
|
|
395
|
-
const attributes = code.slice(match.index +
|
|
403
|
+
for (const match of executable.matchAll(/<(LText|L)\b([^>]*)\/>/g)) {
|
|
404
|
+
const attributes = code.slice(match.index + match[1].length + 1, match.index + match[0].length - 2)
|
|
396
405
|
const assetKey = attributeLiteral(attributes, 'assetKey')
|
|
397
406
|
if (assetKey) used.push({ phrase: assetKey, file, directKey: true })
|
|
398
407
|
}
|