locon 1.6.0 → 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 +17 -0
- package/README.md +41 -3
- package/dist/react.d.mts +11 -3
- package/dist/react.d.ts +11 -3
- package/dist/react.js +6 -0
- package/dist/react.mjs +6 -1
- package/package.json +1 -1
- package/skills/locon-sync/SKILL.md +14 -1
- package/skills/locon-sync/scripts/check-locon-assets.mjs +16 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `L` and `LProps` from `locon/react`: localized source-language text without
|
|
8
|
+
extra DOM elements, including use inside SVG `<text>`.
|
|
9
|
+
- `L` supports interpolation, CLDR plurals, per-call locale overrides, fallback,
|
|
10
|
+
and optional `assetKey` disambiguation. React escapes the translated text.
|
|
11
|
+
- The asset checker recognizes `<L>` alongside `<LText>`, including static
|
|
12
|
+
string-expression children used with placeholders.
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
- Source-phrase examples for React web and Node; phrase-indexed JSON examples
|
|
17
|
+
preserve lazy loading without preloading the source locale.
|
|
18
|
+
- Updated website examples and release instructions for 1.6.1.
|
|
19
|
+
|
|
3
20
|
## 1.6.0
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ load browser languages only when they are selected.
|
|
|
16
16
|
| React web (React 19+) | `locon/react` | Async loaders, provider and reactive hook |
|
|
17
17
|
| Node / plain TypeScript | `locon/core` | Standalone translator and lazy resource store |
|
|
18
18
|
|
|
19
|
-
**New in 1.6:** platform-specific entries and lazy resources. See
|
|
19
|
+
**New in 1.6.1:** the React web `<L>` component. Version 1.6 introduced platform-specific entries and lazy resources. See
|
|
20
20
|
[React (web), Node and lazy resources](#react-web-node-and-lazy-resources).
|
|
21
21
|
The existing React Native entry remains compatible.
|
|
22
22
|
|
|
@@ -833,8 +833,46 @@ selection won. Failed loads reject, preserve the displayed language and can be
|
|
|
833
833
|
retried. Parallel loads are deduplicated. Catch rejections in event handlers and
|
|
834
834
|
offer a retry. Local storage and document `lang` remain application decisions.
|
|
835
835
|
Only already loaded resources participate in per-key fallback: the store never
|
|
836
|
-
silently downloads every fallback language.
|
|
837
|
-
lazy apps
|
|
836
|
+
silently downloads every fallback language. Write source-language text in code.
|
|
837
|
+
For lazy apps, use the source phrases as JSON keys so translating them does not
|
|
838
|
+
require downloading the source locale first:
|
|
839
|
+
|
|
840
|
+
```json
|
|
841
|
+
// en.json
|
|
842
|
+
{ "Überraschung öffnen": "Open your surprise" }
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
```json
|
|
846
|
+
// de.json
|
|
847
|
+
{ "Überraschung öffnen": "Überraschung öffnen" }
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
Set `projectLocale: 'de'` for German source strings. Reverse lookup against
|
|
851
|
+
separate technical keys still needs the source resource loaded.
|
|
852
|
+
|
|
853
|
+
#### Localized React text (`L`, 1.6.1+)
|
|
854
|
+
|
|
855
|
+
```tsx
|
|
856
|
+
import { L, useLocon } from 'locon/react'
|
|
857
|
+
|
|
858
|
+
function Card({ name }: { name: string }) {
|
|
859
|
+
const { l } = useLocon()
|
|
860
|
+
return <>
|
|
861
|
+
<button><L>Überraschung öffnen</L></button>
|
|
862
|
+
<h2><L params={{ name }}>{'Buh, {name}!'}</L></h2>
|
|
863
|
+
<input placeholder={l('Name eingeben')} />
|
|
864
|
+
</>
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
`L` renders a text fragment without a wrapping span. Use it inside HTML or SVG
|
|
869
|
+
text elements. It updates when the provider's locale changes. Pass one source
|
|
870
|
+
string as children; put dynamic values in `params` instead of JSX concatenation
|
|
871
|
+
or nested markup. `count`, `locale`, and `fallback` have the same meaning as in
|
|
872
|
+
`l()`. An optional `assetKey` can disambiguate identical source phrases; missing
|
|
873
|
+
translations fall back to the children unless `fallback` is explicitly set.
|
|
874
|
+
Keep `l()` for string props, document metadata, and other non-rendered values.
|
|
875
|
+
The existing React Native `LText` API is unchanged.
|
|
838
876
|
|
|
839
877
|
In Node, create an instance per request, pass the request's language preferences
|
|
840
878
|
explicitly, and await `setLocale` before rendering. For resources already loaded
|
package/dist/react.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as LoconInstance, T as TranslateOptions } from './resources-BwFIzg3m.mjs';
|
|
2
|
-
export { R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.mjs';
|
|
3
1
|
import * as react from 'react';
|
|
4
2
|
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { T as TranslateOptions, a as LoconInstance } from './resources-BwFIzg3m.mjs';
|
|
4
|
+
export { R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.mjs';
|
|
5
5
|
|
|
6
6
|
declare function LoconProvider({ instance, children }: PropsWithChildren<{
|
|
7
7
|
instance: LoconInstance;
|
|
@@ -15,5 +15,13 @@ declare function useLocon(): {
|
|
|
15
15
|
ready: boolean;
|
|
16
16
|
error: Error | null;
|
|
17
17
|
};
|
|
18
|
+
interface LProps extends TranslateOptions {
|
|
19
|
+
/** Plain text in the project/default language; use params for dynamic values. */
|
|
20
|
+
children: string;
|
|
21
|
+
/** Only needed to disambiguate identical source phrases. */
|
|
22
|
+
assetKey?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Localized text without an extra DOM element, also usable inside SVG text. */
|
|
25
|
+
declare function L({ children, assetKey, ...options }: LProps): react.JSX.Element;
|
|
18
26
|
|
|
19
|
-
export { LoconInstance, LoconProvider, useLocon };
|
|
27
|
+
export { L, type LProps, LoconInstance, LoconProvider, useLocon };
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as LoconInstance, T as TranslateOptions } from './resources-BwFIzg3m.js';
|
|
2
|
-
export { R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.js';
|
|
3
1
|
import * as react from 'react';
|
|
4
2
|
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { T as TranslateOptions, a as LoconInstance } from './resources-BwFIzg3m.js';
|
|
4
|
+
export { R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.js';
|
|
5
5
|
|
|
6
6
|
declare function LoconProvider({ instance, children }: PropsWithChildren<{
|
|
7
7
|
instance: LoconInstance;
|
|
@@ -15,5 +15,13 @@ declare function useLocon(): {
|
|
|
15
15
|
ready: boolean;
|
|
16
16
|
error: Error | null;
|
|
17
17
|
};
|
|
18
|
+
interface LProps extends TranslateOptions {
|
|
19
|
+
/** Plain text in the project/default language; use params for dynamic values. */
|
|
20
|
+
children: string;
|
|
21
|
+
/** Only needed to disambiguate identical source phrases. */
|
|
22
|
+
assetKey?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Localized text without an extra DOM element, also usable inside SVG text. */
|
|
25
|
+
declare function L({ children, assetKey, ...options }: LProps): react.JSX.Element;
|
|
18
26
|
|
|
19
|
-
export { LoconInstance, LoconProvider, useLocon };
|
|
27
|
+
export { L, type LProps, LoconInstance, LoconProvider, useLocon };
|
package/dist/react.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/react/index.tsx
|
|
21
21
|
var react_exports = {};
|
|
22
22
|
__export(react_exports, {
|
|
23
|
+
L: () => L,
|
|
23
24
|
LoconProvider: () => LoconProvider,
|
|
24
25
|
createLocon: () => createLocon,
|
|
25
26
|
matchLocale: () => matchLocale,
|
|
@@ -319,8 +320,13 @@ function useLocon() {
|
|
|
319
320
|
const snapshot = (0, import_react.useSyncExternalStore)(instance.subscribe, instance.getSnapshot, instance.getSnapshot);
|
|
320
321
|
return { ...snapshot, locales: instance.locales, l: instance.l, setLocale: instance.setLocale };
|
|
321
322
|
}
|
|
323
|
+
function L({ children, assetKey, ...options }) {
|
|
324
|
+
const { l } = useLocon();
|
|
325
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: l(assetKey ?? children, { ...options, fallback: options.fallback ?? children }) });
|
|
326
|
+
}
|
|
322
327
|
// Annotate the CommonJS export names for ESM import in node:
|
|
323
328
|
0 && (module.exports = {
|
|
329
|
+
L,
|
|
324
330
|
LoconProvider,
|
|
325
331
|
createLocon,
|
|
326
332
|
matchLocale,
|
package/dist/react.mjs
CHANGED
|
@@ -281,7 +281,7 @@ function createLocon(options) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// src/react/index.tsx
|
|
284
|
-
import { jsx } from "react/jsx-runtime";
|
|
284
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
285
285
|
var Context = createContext(null);
|
|
286
286
|
function LoconProvider({ instance, children }) {
|
|
287
287
|
return /* @__PURE__ */ jsx(Context, { value: instance, children });
|
|
@@ -292,7 +292,12 @@ function useLocon() {
|
|
|
292
292
|
const snapshot = useSyncExternalStore(instance.subscribe, instance.getSnapshot, instance.getSnapshot);
|
|
293
293
|
return { ...snapshot, locales: instance.locales, l: instance.l, setLocale: instance.setLocale };
|
|
294
294
|
}
|
|
295
|
+
function L({ children, assetKey, ...options }) {
|
|
296
|
+
const { l } = useLocon();
|
|
297
|
+
return /* @__PURE__ */ jsx(Fragment, { children: l(assetKey ?? children, { ...options, fallback: options.fallback ?? children }) });
|
|
298
|
+
}
|
|
295
299
|
export {
|
|
300
|
+
L,
|
|
296
301
|
LoconProvider,
|
|
297
302
|
createLocon,
|
|
298
303
|
matchLocale,
|
package/package.json
CHANGED
|
@@ -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
|
}
|