lino-i18n 0.2.0 → 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 +6 -0
- package/README.md +61 -0
- package/package.json +12 -2
- package/src/browser.d.ts +57 -0
- package/src/browser.js +57 -0
- package/src/catalogs.js +482 -0
- package/src/i18n.js +2 -28
- package/src/index.d.ts +6 -3
- package/src/index.js +1 -1
- package/src/language.js +46 -0
- package/src/loaders.js +9 -481
- package/src/node-i18n.js +25 -0
- package/src/react.d.ts +14 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -33,6 +33,64 @@ 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
|
+
|
|
36
94
|
## React
|
|
37
95
|
|
|
38
96
|
React is an optional peer dependency. The `lino-i18n/react` adapter uses the
|
|
@@ -191,6 +249,9 @@ Run `npx lino-i18n --help` for every option.
|
|
|
191
249
|
|
|
192
250
|
```bash
|
|
193
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
|
|
194
255
|
```
|
|
195
256
|
|
|
196
257
|
## License
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lino-i18n",
|
|
3
|
-
"version": "0.
|
|
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": {
|
|
@@ -22,6 +23,10 @@
|
|
|
22
23
|
},
|
|
23
24
|
"./converters": "./src/converters/index.js",
|
|
24
25
|
"./loaders": "./src/loaders.js",
|
|
26
|
+
"./browser": {
|
|
27
|
+
"types": "./src/browser.d.ts",
|
|
28
|
+
"import": "./src/browser.js"
|
|
29
|
+
},
|
|
25
30
|
"./react": {
|
|
26
31
|
"types": "./src/react.d.ts",
|
|
27
32
|
"import": "./src/react.js"
|
|
@@ -29,6 +34,8 @@
|
|
|
29
34
|
},
|
|
30
35
|
"scripts": {
|
|
31
36
|
"test": "node --test --test-timeout=30000 tests/*.test.js",
|
|
37
|
+
"test:browser": "playwright test",
|
|
38
|
+
"test:types": "tsc --project tests/types/tsconfig.json",
|
|
32
39
|
"lint": "eslint .",
|
|
33
40
|
"lint:fix": "eslint . --fix",
|
|
34
41
|
"format": "prettier --write .",
|
|
@@ -73,8 +80,10 @@
|
|
|
73
80
|
"devDependencies": {
|
|
74
81
|
"@changesets/cli": "^2.31.0",
|
|
75
82
|
"@eslint/js": "^10.0.1",
|
|
83
|
+
"@playwright/test": "^1.63.0",
|
|
76
84
|
"@types/node": "^26.1.1",
|
|
77
85
|
"@types/react": "^19.2.17",
|
|
86
|
+
"esbuild": "^0.28.2",
|
|
78
87
|
"eslint": "^10.4.0",
|
|
79
88
|
"eslint-config-prettier": "^10.1.8",
|
|
80
89
|
"eslint-plugin-prettier": "^5.5.5",
|
|
@@ -82,7 +91,8 @@
|
|
|
82
91
|
"prettier": "^3.8.3",
|
|
83
92
|
"react": "^19.1.1",
|
|
84
93
|
"react-test-renderer": "^19.1.1",
|
|
85
|
-
"test-anywhere": "^0.9.1"
|
|
94
|
+
"test-anywhere": "^0.9.1",
|
|
95
|
+
"typescript": "^7.0.2"
|
|
86
96
|
},
|
|
87
97
|
"peerDependencies": {
|
|
88
98
|
"react": ">=18.0.0"
|
package/src/browser.d.ts
ADDED
|
@@ -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
|
+
}
|