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 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. Prefer stable translation keys in
837
- lazy apps; source-phrase reverse lookup needs the source resources loaded.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locon",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
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",
@@ -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/`assetKey` props. It ignores calls
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]*?)<\/LText>/g)) {
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 + '<LText'.length, match.index + openingTagEnd)
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 - '</LText>'.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(match[2])) {
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 + '<LText'.length, match.index + match[0].length - 2)
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
  }