lino-i18n 0.1.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # lino-i18n Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f73aa48: Add a tree-shakeable browser entry point with URL-based Links Notation catalog loading, navigator language detection, and the shared translation runtime.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 8e331fd: Add reactive runtime subscriptions and first-class React bindings with provider,
14
+ translation hooks, rich interpolation, locale selection, and Intl formatters.
15
+
3
16
  ## 0.1.1
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -33,6 +33,109 @@ i18n.t('role', { context: 'female' }); // → "She is a developer"
33
33
  i18n.t('telegram.help.solve.alias.detail'); // → "Tool aliases imply `--tool <tool>`"
34
34
  ```
35
35
 
36
+ ## Browser
37
+
38
+ Import `lino-i18n/browser` for native ES modules or browser bundles. This
39
+ entry point has no Node built-ins and uses the same translation engine and
40
+ `t(key, params, options)` API as the main entry point:
41
+
42
+ ```js
43
+ import { createI18n, detectLanguage, loadCatalogs } from 'lino-i18n/browser';
44
+
45
+ const locales = await loadCatalogs(['/locales/en.lino', '/locales/ru.lino']);
46
+ const i18n = createI18n({
47
+ locales,
48
+ defaultLocale: detectLanguage('auto', {
49
+ supportedLanguages: Object.keys(locales),
50
+ defaultLocale: 'en',
51
+ }),
52
+ fallback: ['en'],
53
+ });
54
+
55
+ i18n.t('greeting', { name: 'Ada' });
56
+ i18n.setLocale('ru');
57
+ i18n.t('greeting', { name: 'Ada' });
58
+ ```
59
+
60
+ `loadCatalogs(urls, options)` fetches URLs concurrently and merges every locale
61
+ root in URL order. Later catalogs override earlier values for matching keys.
62
+ It returns locale tables ready for `createI18n`; errors reject the promise and
63
+ identify the failing URL. `options.requestInit` forwards fetch options such as
64
+ `cache`, `credentials`, and `signal`. `options.fetch` overrides the platform
65
+ fetch, and `options.compatibilityAliases` enables migration aliases after merging.
66
+
67
+ `detectLanguage(preference, options)` checks an explicit preference first, then
68
+ `navigator.languages` in preference order. It uses `navigator.language` when
69
+ the language list is empty. `resolveLanguage(preference, candidates, options)`
70
+ performs the same matching with a supplied list, without reading the navigator.
71
+ Both accept `supportedLanguages` (default `['en']`) and `defaultLocale` (default
72
+ `'en'`). Tags are compared without case sensitivity, underscores become hyphens,
73
+ and exact tags are tried before parent tags (`pt-BR` before `pt`). An unsupported
74
+ preference or `'auto'` defers to candidates, then the default locale, then the
75
+ first supported locale. With no supported locales, the default is returned.
76
+ Detection also works in environments without a navigator.
77
+
78
+ The browser instance includes `subscribe`, `addLocale`, and `loadLocale` and
79
+ works with `lino-i18n/react`. File methods `loadLocaleFile` and `loadDirectory`
80
+ are available through the main entry point. URL loading stays explicit: fetch
81
+ more catalogs with `loadCatalogs`, then register their tables with `addLocale`.
82
+ ES module exports and `sideEffects: false` let bundlers remove unused helpers.
83
+ For TypeScript browser hooks, pass the exported `I18nCoreInstance` type to
84
+ `useI18n<I18nCoreInstance>()` or `useTranslation<I18nCoreInstance>()`. Their
85
+ default type preserves the main entry point's existing file-loading API.
86
+
87
+ Run the static browser example from `js/`:
88
+
89
+ ```bash
90
+ node examples/browser-usage/server.mjs
91
+ # Open http://127.0.0.1:4173/examples/browser-usage/
92
+ ```
93
+
94
+ ## React
95
+
96
+ React is an optional peer dependency. The `lino-i18n/react` adapter uses the
97
+ same `.lino` catalogues as the framework-independent runtime:
98
+
99
+ ```jsx
100
+ import { createI18n } from 'lino-i18n';
101
+ import {
102
+ CurrencyFormat,
103
+ I18nProvider,
104
+ LocaleSelector,
105
+ Trans,
106
+ useTranslation,
107
+ } from 'lino-i18n/react';
108
+
109
+ const i18n = createI18n({ locales: catalogues, defaultLocale: 'en' });
110
+
111
+ function Checkout() {
112
+ const { t } = useTranslation('checkout');
113
+ return (
114
+ <>
115
+ <h1>{t('title')}</h1>
116
+ <Trans
117
+ id="checkout.total"
118
+ values={{ amount: <CurrencyFormat value={19.99} currency="USD" /> }}
119
+ />
120
+ <LocaleSelector labels={{ en: 'English', fr: 'Français' }} />
121
+ </>
122
+ );
123
+ }
124
+
125
+ export default function App() {
126
+ return (
127
+ <I18nProvider i18n={i18n}>
128
+ <Checkout />
129
+ </I18nProvider>
130
+ );
131
+ }
132
+ ```
133
+
134
+ `useI18n`, `useLocale`, and `useTranslation` update when `setLocale`,
135
+ `addLocale`, or an asynchronous catalogue loader changes the instance.
136
+ `NumberFormat`, `DateTimeFormat`, `CurrencyFormat`, and `RelativeTimeFormat`
137
+ format values with the active locale through the platform `Intl` APIs.
138
+
36
139
  A sample `.lino` catalogue looks like this:
37
140
 
38
141
  ```lino
@@ -146,6 +249,9 @@ Run `npx lino-i18n --help` for every option.
146
249
 
147
250
  ```bash
148
251
  npm test # node --test --test-timeout=30000 tests/*.test.js
252
+ npm run test:types # browser and React TypeScript API checks
253
+ npx playwright install chromium
254
+ npm run test:browser # real browser catalog loading and runtime language switching
149
255
  ```
150
256
 
151
257
  ## License
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "lino-i18n",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Universal i18n library that stores translations in Links Notation (.lino) instead of JSON.",
5
5
  "type": "module",
6
+ "sideEffects": false,
6
7
  "main": "./src/index.js",
7
8
  "types": "./src/index.d.ts",
8
9
  "bin": {
@@ -21,10 +22,20 @@
21
22
  "import": "./src/index.js"
22
23
  },
23
24
  "./converters": "./src/converters/index.js",
24
- "./loaders": "./src/loaders.js"
25
+ "./loaders": "./src/loaders.js",
26
+ "./browser": {
27
+ "types": "./src/browser.d.ts",
28
+ "import": "./src/browser.js"
29
+ },
30
+ "./react": {
31
+ "types": "./src/react.d.ts",
32
+ "import": "./src/react.js"
33
+ }
25
34
  },
26
35
  "scripts": {
27
36
  "test": "node --test --test-timeout=30000 tests/*.test.js",
37
+ "test:browser": "playwright test",
38
+ "test:types": "tsc --project tests/types/tsconfig.json",
28
39
  "lint": "eslint .",
29
40
  "lint:fix": "eslint . --fix",
30
41
  "format": "prettier --write .",
@@ -69,11 +80,26 @@
69
80
  "devDependencies": {
70
81
  "@changesets/cli": "^2.31.0",
71
82
  "@eslint/js": "^10.0.1",
83
+ "@playwright/test": "^1.63.0",
84
+ "@types/node": "^26.1.1",
85
+ "@types/react": "^19.2.17",
86
+ "esbuild": "^0.28.2",
72
87
  "eslint": "^10.4.0",
73
88
  "eslint-config-prettier": "^10.1.8",
74
89
  "eslint-plugin-prettier": "^5.5.5",
75
90
  "jscpd": "^4.2.2",
76
91
  "prettier": "^3.8.3",
77
- "test-anywhere": "^0.9.1"
92
+ "react": "^19.1.1",
93
+ "react-test-renderer": "^19.1.1",
94
+ "test-anywhere": "^0.9.1",
95
+ "typescript": "^7.0.2"
96
+ },
97
+ "peerDependencies": {
98
+ "react": ">=18.0.0"
99
+ },
100
+ "peerDependenciesMeta": {
101
+ "react": {
102
+ "optional": true
103
+ }
78
104
  }
79
105
  }
@@ -0,0 +1,57 @@
1
+ import type {
2
+ CompatibilityAliasOptions,
3
+ I18nCoreInstance,
4
+ I18nOptions,
5
+ } from './index.js';
6
+
7
+ export type {
8
+ CompatibilityAlias,
9
+ CompatibilityAliasOptions,
10
+ I18nCoreInstance,
11
+ I18nOptions,
12
+ TOptions,
13
+ TParams,
14
+ } from './index.js';
15
+ export {
16
+ expandCompatibilityAliases,
17
+ parseLinoCatalog,
18
+ parseLinoCatalogs,
19
+ formatLinoCatalog,
20
+ formatLinoCatalogs,
21
+ loadLocaleFromString,
22
+ } from './index.js';
23
+
24
+ export declare function createI18n(options?: I18nOptions): I18nCoreInstance;
25
+
26
+ export interface LoadCatalogsOptions extends CompatibilityAliasOptions {
27
+ /** Override the platform fetch, for example in tests. */
28
+ fetch?: typeof globalThis.fetch;
29
+ /** Options forwarded to every request, including AbortSignal and cache. */
30
+ requestInit?: RequestInit;
31
+ }
32
+
33
+ /** Merge every locale root, with later URLs overriding earlier keys. */
34
+ export declare function loadCatalogs(
35
+ urls: Array<string | URL>,
36
+ options?: LoadCatalogsOptions
37
+ ): Promise<Record<string, Record<string, string>>>;
38
+
39
+ export interface LanguageOptions {
40
+ /** Available catalogue locales; defaults to ['en']. */
41
+ supportedLanguages?: string[];
42
+ /** Locale used when no preference matches; defaults to 'en'. */
43
+ defaultLocale?: string;
44
+ }
45
+
46
+ /** Match an explicit preference, candidates, then the default or first locale. */
47
+ export declare function resolveLanguage(
48
+ preference?: string | null,
49
+ candidates?: string | string[],
50
+ options?: LanguageOptions
51
+ ): string;
52
+
53
+ /** Read navigator.languages, falling back to navigator.language when empty. */
54
+ export declare function detectLanguage(
55
+ preference?: string | null,
56
+ options?: LanguageOptions
57
+ ): string;
package/src/browser.js ADDED
@@ -0,0 +1,57 @@
1
+ // Browser entry point: its entire import graph uses only platform APIs.
2
+ import { parseLinoCatalogs } from './catalogs.js';
3
+ import { expandCompatibilityAliases } from './compatibility.js';
4
+
5
+ export { createI18n } from './i18n.js';
6
+ export {
7
+ parseLinoCatalog,
8
+ parseLinoCatalogs,
9
+ formatLinoCatalog,
10
+ formatLinoCatalogs,
11
+ loadLocaleFromString,
12
+ } from './catalogs.js';
13
+ export { expandCompatibilityAliases } from './compatibility.js';
14
+ export { detectLanguage, resolveLanguage } from './language.js';
15
+
16
+ // Fetch concurrently, then merge in input order. Generate aliases only after
17
+ // merging so a later explicit key always wins over an earlier generated alias.
18
+ export async function loadCatalogs(urls, options = {}) {
19
+ const { fetch: fetchCatalog = globalThis.fetch, requestInit } = options;
20
+ if (typeof fetchCatalog !== 'function') {
21
+ throw new Error('loadCatalogs requires fetch');
22
+ }
23
+ const loaded = await Promise.all(
24
+ urls.map(async (url) => {
25
+ try {
26
+ const response = await fetchCatalog(url, requestInit);
27
+ if (!response.ok) {
28
+ throw new Error(`HTTP ${response.status}`);
29
+ }
30
+ return parseLinoCatalogs(await response.text());
31
+ } catch (cause) {
32
+ throw new Error(
33
+ `Failed to load catalog ${url}: ${cause?.message ?? String(cause)}`,
34
+ {
35
+ cause,
36
+ }
37
+ );
38
+ }
39
+ })
40
+ );
41
+
42
+ const catalogues = new Map();
43
+ for (const entries of loaded) {
44
+ for (const { locale, translations } of entries) {
45
+ catalogues.set(locale, {
46
+ ...catalogues.get(locale),
47
+ ...translations,
48
+ });
49
+ }
50
+ }
51
+ return Object.fromEntries(
52
+ Array.from(catalogues, ([locale, translations]) => [
53
+ locale,
54
+ expandCompatibilityAliases(translations, options),
55
+ ])
56
+ );
57
+ }