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.
@@ -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.5.1",
4
- "description": "locon is a simple and lightweight library for React Native that provides a simple way to manage translations in your app.",
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": "tsup",
23
- "pretest": "tsup",
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/`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
  }