@sehv-oss/i18n 1.0.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/LICENSE +7 -0
- package/README.md +115 -0
- package/dist/formatters/formatters.d.mts +42 -0
- package/dist/formatters/formatters.mjs +1 -0
- package/dist/formatters-Bvs6JDy4.mjs +1 -0
- package/dist/i18n.d.mts +48 -0
- package/dist/i18n.mjs +2 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 sehv-oss
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @sehv-oss/i18n
|
|
2
|
+
|
|
3
|
+
Core i18n library for JavaScript.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm install @sehv-oss/i18n
|
|
10
|
+
|
|
11
|
+
# yarn
|
|
12
|
+
yarn add @sehv-oss/i18n
|
|
13
|
+
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add @sehv-oss/i18n
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createI18n } from '@sehv-oss/i18n';
|
|
24
|
+
|
|
25
|
+
const i18n = createI18n({
|
|
26
|
+
locale: 'en',
|
|
27
|
+
fallbackLocale: 'en',
|
|
28
|
+
messages: {
|
|
29
|
+
en: {
|
|
30
|
+
greeting: 'Hello, {$name}!',
|
|
31
|
+
items: `.match {$count :number}
|
|
32
|
+
one {{You have {$count} item}}
|
|
33
|
+
* {{You have {$count} items}}`,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
i18n.translate('greeting', { name: 'World' });
|
|
39
|
+
// → "Hello, World!"
|
|
40
|
+
|
|
41
|
+
i18n.translate('items', { count: 5 });
|
|
42
|
+
// → "You have 5 items"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Formatters
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
i18n.formatNumber(1234.56);
|
|
49
|
+
// → "1,234.56"
|
|
50
|
+
|
|
51
|
+
i18n.formatCurrency(99.9, 'USD');
|
|
52
|
+
// → "$99.90"
|
|
53
|
+
|
|
54
|
+
i18n.formatDate(new Date(), { dateStyle: 'long' });
|
|
55
|
+
// → "January 27, 2026"
|
|
56
|
+
|
|
57
|
+
i18n.formatList(['apple', 'banana', 'orange']);
|
|
58
|
+
// → "apple, banana, and orange"
|
|
59
|
+
|
|
60
|
+
i18n.formatRelativeTime(-2, 'days');
|
|
61
|
+
// → "2 days ago"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Lazy Loading
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
await i18n.loadMessagesAsync('/locales/en.json');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Custom Loader
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { createI18n, type ILoader } from '@sehv-oss/i18n';
|
|
74
|
+
import YAML from 'yaml';
|
|
75
|
+
|
|
76
|
+
const yamlLoader: ILoader = {
|
|
77
|
+
extensions: ['.yaml', '.yml'],
|
|
78
|
+
parse(content) {
|
|
79
|
+
return YAML.parse(content);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const i18n = createI18n({
|
|
84
|
+
locale: 'en',
|
|
85
|
+
loaders: [yamlLoader],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await i18n.loadMessagesAsync('/locales/en.yaml');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
### `createI18n(config)`
|
|
94
|
+
|
|
95
|
+
Creates an i18n instance.
|
|
96
|
+
|
|
97
|
+
### Methods
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
| ------------------------------------------- | --------------------------- |
|
|
101
|
+
| `translate(key, values?)` | Translate a message key |
|
|
102
|
+
| `formatNumber(value, options?)` | Format a number |
|
|
103
|
+
| `formatCurrency(value, currency, options?)` | Format currency |
|
|
104
|
+
| `formatDate(value, options?)` | Format a date |
|
|
105
|
+
| `formatList(values, options?)` | Format a list |
|
|
106
|
+
| `formatRelativeTime(value, unit, options?)` | Format relative time |
|
|
107
|
+
| `loadMessages(locale, messages)` | Load messages synchronously |
|
|
108
|
+
| `loadMessagesAsync(url)` | Load messages via fetch |
|
|
109
|
+
| `setLocale(locale)` | Change current locale |
|
|
110
|
+
| `getLocales()` | Get available locales |
|
|
111
|
+
| `onLocaleChange(listener)` | Subscribe to locale changes |
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
ISC
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/formatters/currency.d.ts
|
|
2
|
+
type FormatCurrencyOptions = Omit<Intl.NumberFormatOptions, 'style'> & {
|
|
3
|
+
locale?: string;
|
|
4
|
+
};
|
|
5
|
+
declare class FormatCurrency {
|
|
6
|
+
static format(value: number, currency: string, locale: string, options?: FormatCurrencyOptions): string;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/formatters/date.d.ts
|
|
10
|
+
type FormatDateOptions = Intl.DateTimeFormatOptions & {
|
|
11
|
+
locale?: string;
|
|
12
|
+
};
|
|
13
|
+
declare class FormatDate {
|
|
14
|
+
static format(value: Date | number, locale: string, options?: FormatDateOptions): string;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/formatters/list.d.ts
|
|
18
|
+
type FormatListOptions = Intl.ListFormatOptions & {
|
|
19
|
+
locale?: string;
|
|
20
|
+
};
|
|
21
|
+
declare class FormatList {
|
|
22
|
+
static format(values: string[], locale: string, options?: FormatListOptions): string;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/formatters/number.d.ts
|
|
26
|
+
type FormatNumberOptions = Intl.NumberFormatOptions & {
|
|
27
|
+
locale?: string;
|
|
28
|
+
};
|
|
29
|
+
declare class FormatNumber {
|
|
30
|
+
static format(value: number, locale: string, options?: FormatNumberOptions): string;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/formatters/relative-time.d.ts
|
|
34
|
+
type FormatRelativeTimeUnit = 'year' | 'years' | 'quarter' | 'quarters' | 'month' | 'months' | 'week' | 'weeks' | 'day' | 'days' | 'hour' | 'hours' | 'minute' | 'minutes' | 'second' | 'seconds';
|
|
35
|
+
type FormatRelativeTimeOptions = Intl.RelativeTimeFormatOptions & {
|
|
36
|
+
locale?: string;
|
|
37
|
+
};
|
|
38
|
+
declare class FormatRelativeTime {
|
|
39
|
+
static format(value: number, unit: FormatRelativeTimeUnit, locale: string, options?: FormatRelativeTimeOptions): string;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { FormatCurrency, FormatCurrencyOptions, FormatDate, FormatDateOptions, FormatList, FormatListOptions, FormatNumber, FormatNumberOptions, FormatRelativeTime, FormatRelativeTimeOptions, FormatRelativeTimeUnit };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,i as t,n,r,t as i}from"../formatters-Bvs6JDy4.mjs";export{e as FormatCurrency,t as FormatDate,r as FormatList,n as FormatNumber,i as FormatRelativeTime};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class{cache=new Map;constructor(e){this.Formatter=e}get(e,t){let n=this.buildKey(e,t),r=this.cache.get(n);return r||(r=new this.Formatter(e,t),this.cache.set(n,r)),r}clear(){this.cache.clear()}buildKey(e,t){return t?`${e}:${JSON.stringify(t)}`:e}};const t=new e(Intl.NumberFormat);var n=class{static format(e,n,r,i){let{locale:a,...o}=i??{};return t.get(r,{...o,style:`currency`,currency:n}).format(e)}};const r=new e(Intl.DateTimeFormat);var i=class{static format(e,t,n){let{locale:i,...a}=n??{};return r.get(t,a).format(e)}};const a=new e(Intl.ListFormat);var o=class{static format(e,t,n){let{locale:r,...i}=n??{};return a.get(t,i).format(e)}};const s=new e(Intl.NumberFormat);var c=class{static format(e,t,n){let{locale:r,...i}=n??{};return s.get(t,i).format(e)}};const l=new e(Intl.RelativeTimeFormat),u={year:`year`,years:`year`,quarter:`quarter`,quarters:`quarter`,month:`month`,months:`month`,week:`week`,weeks:`week`,day:`day`,days:`day`,hour:`hour`,hours:`hour`,minute:`minute`,minutes:`minute`,second:`second`,seconds:`second`};var d=class{static format(e,t,n,r){let{locale:i,...a}=r??{},o=l.get(n,a),s=u[t];return o.format(e,s)}};export{n as a,i,c as n,o as r,d as t};
|
package/dist/i18n.d.mts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { FormatCurrency, FormatCurrencyOptions, FormatDate, FormatDateOptions, FormatList, FormatListOptions, FormatNumber, FormatNumberOptions, FormatRelativeTime, FormatRelativeTimeOptions, FormatRelativeTimeUnit } from "./formatters/formatters.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/messages/messages.d.ts
|
|
4
|
+
type Messages = Record<string, string>;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/loaders/loader.interface.d.ts
|
|
7
|
+
interface ILoader {
|
|
8
|
+
extensions: string[];
|
|
9
|
+
parse(content: string): Messages;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/i18n.d.ts
|
|
13
|
+
type I18nConfig = {
|
|
14
|
+
locale: string;
|
|
15
|
+
fallbackLocale?: string;
|
|
16
|
+
messages?: Record<string, Messages>;
|
|
17
|
+
loaders?: ILoader[];
|
|
18
|
+
};
|
|
19
|
+
declare class I18nInstance {
|
|
20
|
+
private locale;
|
|
21
|
+
private fallbackLocale;
|
|
22
|
+
private messagesManager;
|
|
23
|
+
private messageCache;
|
|
24
|
+
private loaders;
|
|
25
|
+
private mf2ParserByLocale;
|
|
26
|
+
private localeChangeListeners;
|
|
27
|
+
constructor(config: I18nConfig);
|
|
28
|
+
getLocale(): string;
|
|
29
|
+
setLocale(locale: string): void;
|
|
30
|
+
onLocaleChange(listener: (locale: string) => void): () => void;
|
|
31
|
+
getFallbackLocale(): string | undefined;
|
|
32
|
+
getLocales(): string[];
|
|
33
|
+
translate(key: string, values?: Record<string, unknown>): string;
|
|
34
|
+
formatNumber(value: number, options?: FormatNumberOptions): string;
|
|
35
|
+
formatCurrency(value: number, currency: string, options?: FormatCurrencyOptions): string;
|
|
36
|
+
formatDate(value: Date | number, options?: FormatDateOptions): string;
|
|
37
|
+
formatList(values: string[], options?: FormatListOptions): string;
|
|
38
|
+
formatRelativeTime(value: number, unit: FormatRelativeTimeUnit, options?: FormatRelativeTimeOptions): string;
|
|
39
|
+
loadMessages(locale: string, messages: Messages): void;
|
|
40
|
+
loadMessagesAsync(url: string): Promise<void>;
|
|
41
|
+
private getParser;
|
|
42
|
+
private findLoader;
|
|
43
|
+
private getExtension;
|
|
44
|
+
private extractLocaleFromUrl;
|
|
45
|
+
}
|
|
46
|
+
declare function createI18n(config: I18nConfig): I18nInstance;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { FormatCurrency, FormatCurrencyOptions, FormatDate, FormatDateOptions, FormatList, FormatListOptions, FormatNumber, FormatNumberOptions, FormatRelativeTime, FormatRelativeTimeOptions, FormatRelativeTimeUnit, I18nConfig, I18nInstance, ILoader, createI18n };
|
package/dist/i18n.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as e,i as t,n,r,t as i}from"./formatters-Bvs6JDy4.mjs";var a=class{cache=new Map;maxSize;constructor(e=1e3){this.maxSize=e}get(e){return this.cache.get(e)}set(e,t){if(this.cache.size>=this.maxSize){let e=this.cache.keys().next().value;e&&this.cache.delete(e)}this.cache.set(e,t)}has(e){return this.cache.has(e)}clear(){this.cache.clear()}},o=class{extensions=[`.json`];parse(e){try{let t=JSON.parse(e);if(!this.isValidMessages(t))throw Error(`Invalid messages format!`);return t}catch(e){throw e instanceof SyntaxError?Error(`Failed to parse JSON: ${e.message}.`):e}}isValidMessages(e){return typeof e!=`object`||!e||Array.isArray(e)?!1:Object.values(e).every(e=>typeof e==`string`)}},s=class{messages=new Map;get(e){return this.messages.get(e)}set(e,t){this.messages.set(e,t)}has(e){return this.messages.has(e)}getLocales(){return Array.from(this.messages.keys())}getMessage(e,t){let n=this.get(e);if(n)return n[t]}},c=class{pluralRules;constructor(e){this.pluralRules=new Intl.PluralRules(e)}parse(e,t={}){let n=e.trim();return n.startsWith(`.match`)?this.parseMatch(n,t):this.interpolate(n,t)}parseMatch(e,t){let n=this.parseMatchSyntax(e);if(!n.selectors?.length||!n.variants?.length)return e;let r=this.selectVariant(n.selectors,n.variants,t);return this.interpolate(r,t)}parseMatchSyntax(e){let t=e.split(`
|
|
2
|
+
`).map(e=>e.trim()).filter(Boolean);if(t.length===0||!t[0]?.startsWith(`.match`))return{type:`simple`,pattern:e};let n=t[0];return{type:`match`,selectors:this.parseSelectors(n),variants:this.parseVariants(t.slice(1))}}parseSelectors(e){let t=[],n=/\{\s*\$(\w+)(?:\s+:(\w+))?\s*\}/g,r;for(;(r=n.exec(e))!==null;){let[,e,n]=r;e&&t.push({variable:e,function:n})}return t}parseVariants(e){let t=[],n=/^([\w\s*]+)\s*\{\{(.+?)\}\}$/;for(let r of e){let e=n.exec(r);if(!e)continue;let[,i,a]=e;if(!i||!a)continue;let o=i.trim().split(/\s+/),s=a.trim();t.push({keys:o,pattern:s})}return t}selectVariant(e,t,n){let r=e.map(e=>{let t=n[e.variable];return e.function===`number`&&typeof t==`number`?this.pluralRules.select(t):String(t??`*`)});for(let e of t)if(this.matchesVariant(e.keys,r))return e.pattern;return t.find(e=>e.keys.every(e=>e===`*`))?.pattern??``}matchesVariant(e,t){return e.length===t.length?e.every((e,n)=>e===`*`||e===t[n]):!1}interpolate(e,t){return e.replace(/\{\s*\$(\w+)\s*\}/g,(e,n)=>{let r=t[n];return r===void 0?`{$${n}}`:String(r)})}},l=class{locale;fallbackLocale;messagesManager;messageCache;loaders;mf2ParserByLocale;localeChangeListeners=new Set;constructor(e){let{locale:t,fallbackLocale:n,loaders:r,messages:i}=e;this.locale=t,this.fallbackLocale=n,this.messagesManager=new s,this.messageCache=new a,this.mf2ParserByLocale=new Map,this.loaders=[new o,...r??[]],i&&Object.entries(i).forEach(([e,t])=>{this.messagesManager.set(e,t)})}getLocale(){return this.locale}setLocale(e){this.locale=e,this.messageCache.clear(),this.localeChangeListeners.forEach(t=>t(e))}onLocaleChange(e){return this.localeChangeListeners.add(e),()=>{this.localeChangeListeners.delete(e)}}getFallbackLocale(){return this.fallbackLocale}getLocales(){return this.messagesManager.getLocales()}translate(e,t){let n=`${this.locale}:${e}:${JSON.stringify(t??{})}`;if(this.messageCache.has(n))return this.messageCache.get(n);let r=this.messagesManager.getMessage(this.locale,e);if(!r&&this.fallbackLocale&&(r=this.messagesManager.getMessage(this.fallbackLocale,e)),!r)return e;let i=this.getParser(this.locale).parse(r,t);return this.messageCache.set(n,i),i}formatNumber(e,t){let r=t?.locale??this.locale;return n.format(e,r,t)}formatCurrency(t,n,r){let i=r?.locale??this.locale;return e.format(t,n,i,r)}formatDate(e,n){let r=n?.locale??this.locale;return t.format(e,r,n)}formatList(e,t){let n=t?.locale??this.locale;return r.format(e,n,t)}formatRelativeTime(e,t,n){let r=n?.locale??this.locale;return i.format(e,t,r,n)}loadMessages(e,t){this.messagesManager.set(e,t),this.messageCache.clear()}async loadMessagesAsync(e){let t=await fetch(e);if(!t.ok)throw Error(`Failed to load messages from ${e}: ${t.statusText}.`);let n=await t.text(),r=this.findLoader(e);if(!r)throw Error(`No loader found for ${e}.`);let i=r.parse(n),a=this.extractLocaleFromUrl(e);this.loadMessages(a,i)}getParser(e){let t=this.mf2ParserByLocale.get(e);return t||(t=new c(e),this.mf2ParserByLocale.set(e,t)),t}findLoader(e){let t=this.getExtension(e);return this.loaders.find(e=>e.extensions.includes(t))}getExtension(e){return e.match(/\.[^.]+$/)?.[0]??``}extractLocaleFromUrl(e){let t=(e.split(`/`).pop()??``).replace(/\.[^.]+$/,``);try{let[e]=Intl.getCanonicalLocales(t);return e??this.locale}catch{return this.locale}}};function u(e){return new l(e)}export{e as FormatCurrency,t as FormatDate,r as FormatList,n as FormatNumber,i as FormatRelativeTime,l as I18nInstance,u as createI18n};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sehv-oss/i18n",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Modern i18n library for JavaScript. Web standards based, runs everywhere.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"./dist"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/i18n.d.ts",
|
|
13
|
+
"import": "./dist/i18n.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./formatters": {
|
|
16
|
+
"types": "./dist/formatters/formatters.d.ts",
|
|
17
|
+
"import": "./dist/formatters/formatters.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"i18n",
|
|
22
|
+
"l10n",
|
|
23
|
+
"internationalization",
|
|
24
|
+
"localization",
|
|
25
|
+
"intl",
|
|
26
|
+
"message format",
|
|
27
|
+
"translation"
|
|
28
|
+
],
|
|
29
|
+
"license": "ISC",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/sehv-oss/i18n.git",
|
|
33
|
+
"directory": "packages/core"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22 <25"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown"
|
|
40
|
+
}
|
|
41
|
+
}
|