@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 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};
@@ -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
+ }