@forgedevstack/lingo 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/CHANGELOG.md +17 -0
- package/README.md +257 -0
- package/dist/constants/index.d.ts +12 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +553 -0
- package/dist/lingo-react/index.d.ts +53 -0
- package/dist/lingo-react/index.d.ts.map +1 -0
- package/dist/load-local-bundles.d.ts +3 -0
- package/dist/load-local-bundles.d.ts.map +1 -0
- package/dist/locale-globs.d.ts +13 -0
- package/dist/locale-globs.d.ts.map +1 -0
- package/dist/logo.d.ts +7 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/normalize-config.d.ts +3 -0
- package/dist/normalize-config.d.ts.map +1 -0
- package/dist/react/index.cjs +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +89 -0
- package/dist/types/index.d.ts +176 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +30 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
- **Core engine**: `createLingo()` with key resolution, interpolation, and plural support.
|
|
9
|
+
- **Local source**: Bundle translations inline.
|
|
10
|
+
- **Remote source**: Fetch translations from Lingo Portal API by project ID.
|
|
11
|
+
- **React adapter**: `LingoProvider`, `useLingo`, `<T>`, `<LocaleSwitcher>`.
|
|
12
|
+
- **RTL support**: Auto `dir` and `lang` on locale change.
|
|
13
|
+
- **Locale catalog**: 30+ locales with name, native name, flag, and direction.
|
|
14
|
+
- **Plural rules**: CLDR categories for 15+ language families.
|
|
15
|
+
- **Caching**: Persist selected locale in localStorage.
|
|
16
|
+
- **Utilities**: `flattenTranslations`, `unflattenTranslations`, `mergeTranslations`, `getDirection`.
|
|
17
|
+
- **Logo and icon**: Speech-bubble with "A文" symbolizing translation.
|
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Lingo
|
|
2
|
+
|
|
3
|
+
Translation and localization library for **ForgeStack**. Manage languages, resolve keys, handle plurals, and switch locales — all with a clean API.
|
|
4
|
+
|
|
5
|
+
Use **Lingo Portal** to manage translations visually: create a project, add keys, and AI-translate to all languages in one click.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ForgeStack
|
|
10
|
+
|
|
11
|
+
Lingo is part of the **ForgeStack** ecosystem. The **Lingo Portal** (the web app where you manage projects and keys) is built with:
|
|
12
|
+
|
|
13
|
+
| Package | Purpose |
|
|
14
|
+
|--------|--------|
|
|
15
|
+
| **@forgedevstack/bear** | Full UI kit — buttons, inputs, cards, modals, typography, icons, alerts, tabs, etc. |
|
|
16
|
+
| **@forgedevstack/forge-compass** | Client-side routing — `CompassProvider`, `Routes`, `useNavigate`, `useRoute`. |
|
|
17
|
+
| **@forgedevstack/grid-table** | Data tables for listing and editing (e.g. keys, projects). |
|
|
18
|
+
|
|
19
|
+
You can use Lingo in any stack. If you build with React, Bear and Compass pair well for UI and navigation; grid-table is optional for table-heavy admin screens.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Key resolution** — Dot-separated nested keys (`auth.login.title`).
|
|
26
|
+
- **Interpolation** — `Hello, {{name}}!` → `Hello, John!`.
|
|
27
|
+
- **Plurals** — CLDR plural categories (`zero`, `one`, `two`, `few`, `many`, `other`) for 30+ languages.
|
|
28
|
+
- **RTL support** — Automatic `dir` and `lang` on the document and provider wrapper.
|
|
29
|
+
- **Local or remote** — Bundle translations or fetch from Lingo Portal API.
|
|
30
|
+
- **Caching** — Persist selected locale in `localStorage`.
|
|
31
|
+
- **Framework-agnostic core** — React adapter included; core works anywhere.
|
|
32
|
+
- **Tiny** — Zero dependencies (React optional).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @forgedevstack/lingo
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
React is optional (peer dependency when using the React build):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @forgedevstack/lingo react
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
### Local translations
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { createLingo, LingoProvider, useLingo } from '@forgedevstack/lingo';
|
|
56
|
+
|
|
57
|
+
const lingo = createLingo({
|
|
58
|
+
defaultLocale: 'en',
|
|
59
|
+
fallbackLocale: 'en',
|
|
60
|
+
locales: ['en', 'es', 'fr', 'ar'],
|
|
61
|
+
source: {
|
|
62
|
+
type: 'local',
|
|
63
|
+
translations: {
|
|
64
|
+
en: {
|
|
65
|
+
greeting: 'Hello, {{name}}!',
|
|
66
|
+
auth: { login: 'Sign In', register: 'Sign Up' },
|
|
67
|
+
items: { one: '{{count}} item', other: '{{count}} items' },
|
|
68
|
+
},
|
|
69
|
+
es: {
|
|
70
|
+
greeting: '¡Hola, {{name}}!',
|
|
71
|
+
auth: { login: 'Iniciar sesión', register: 'Registrarse' },
|
|
72
|
+
items: { one: '{{count}} elemento', other: '{{count}} elementos' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function App() {
|
|
79
|
+
return (
|
|
80
|
+
<LingoProvider instance={lingo}>
|
|
81
|
+
<MyPage />
|
|
82
|
+
</LingoProvider>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function MyPage() {
|
|
87
|
+
const { t, locale, setLocale, direction } = useLingo();
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<h1>{t('greeting', { name: 'John' })}</h1>
|
|
92
|
+
<p>{t('auth.login')}</p>
|
|
93
|
+
<p>{t('items', { count: 5 })}</p>
|
|
94
|
+
<p>Direction: {direction}</p>
|
|
95
|
+
<button onClick={() => setLocale('es')}>Español</button>
|
|
96
|
+
<button onClick={() => setLocale('ar')}>العربية</button>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Remote translations (Lingo Portal)
|
|
103
|
+
|
|
104
|
+
Configure Lingo to load translations from the Portal API (e.g. after creating a project and copying the API URL from the Export page):
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { createLingo } from '@forgedevstack/lingo';
|
|
108
|
+
|
|
109
|
+
const lingo = createLingo({
|
|
110
|
+
defaultLocale: 'en',
|
|
111
|
+
fallbackLocale: 'en',
|
|
112
|
+
locales: ['en', 'es', 'fr'],
|
|
113
|
+
source: {
|
|
114
|
+
type: 'remote',
|
|
115
|
+
projectId: 'proj_abc123',
|
|
116
|
+
endpoint: 'https://your-lingo-portal.com/api',
|
|
117
|
+
apiKey: 'your_optional_api_key', // if your API requires it
|
|
118
|
+
},
|
|
119
|
+
cache: true,
|
|
120
|
+
storageKey: 'lingo_locale',
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Translations are fetched when the user switches locale. The API returns a flat key → value map for the requested locale.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API reference
|
|
129
|
+
|
|
130
|
+
### `createLingo(config)`
|
|
131
|
+
|
|
132
|
+
Creates a Lingo instance.
|
|
133
|
+
|
|
134
|
+
| Config | Type | Description |
|
|
135
|
+
|--------|------|-------------|
|
|
136
|
+
| `defaultLocale` | `string` | Locale to use on first load |
|
|
137
|
+
| `fallbackLocale` | `string` | Locale to fall back to when a key is missing |
|
|
138
|
+
| `locales` | `string[]` | Supported locale codes |
|
|
139
|
+
| `source` | `TranslationSource` | `{ type: 'local', translations }` or `{ type: 'remote', projectId, endpoint?, apiKey? }` |
|
|
140
|
+
| `cache` | `boolean` | Persist locale in localStorage (default: `true`) |
|
|
141
|
+
| `storageKey` | `string` | localStorage key (default: `'lingo_locale'`) |
|
|
142
|
+
| `debug` | `boolean` | Log missing keys (default: `true`) |
|
|
143
|
+
| `onLocaleChange` | `(locale: string) => void` | Callback on locale change |
|
|
144
|
+
| `onMissingKey` | `(key: string, locale: string) => void` | Callback when a translation is missing |
|
|
145
|
+
|
|
146
|
+
### Instance methods
|
|
147
|
+
|
|
148
|
+
| Method | Description |
|
|
149
|
+
|--------|-------------|
|
|
150
|
+
| `t(key, vars?)` | Translate a key with optional interpolation vars |
|
|
151
|
+
| `locale` | Current active locale |
|
|
152
|
+
| `setLocale(locale)` | Change locale (async; may fetch remote) |
|
|
153
|
+
| `locales` | Array of supported locales |
|
|
154
|
+
| `direction` | `'ltr'` or `'rtl'` for the current locale |
|
|
155
|
+
| `getLocaleInfo(locale)` | Get locale metadata (name, nativeName, flag, direction) |
|
|
156
|
+
| `isLoading` | `true` while fetching remote translations |
|
|
157
|
+
| `subscribe(listener)` | Listen for state changes; returns unsubscribe fn |
|
|
158
|
+
| `addTranslations(locale, map)` | Add or merge translations at runtime |
|
|
159
|
+
|
|
160
|
+
### React exports
|
|
161
|
+
|
|
162
|
+
| Export | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| `<LingoProvider instance={lingo}>` | Wraps app with Lingo context; sets `dir` and `lang` |
|
|
165
|
+
| `useLingo()` | Returns `{ t, locale, setLocale, locales, direction, isLoading, getLocaleInfo }` |
|
|
166
|
+
| `<T k="key" vars={{}} />` | Inline translation component |
|
|
167
|
+
| `<Lingo k="key" vars={{}} />` | Same as `<T>`, with optional `className` and `style` |
|
|
168
|
+
| `<LocaleSwitcher />` | Simple `<select>` dropdown for switching locales |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Interpolation
|
|
173
|
+
|
|
174
|
+
Use `{{variableName}}` in translation strings and pass vars as the second argument to `t()` or via the `vars` prop on `<T>` / `<Lingo>`:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// String: "Hello, {{name}}!"
|
|
178
|
+
t('greeting', { name: 'Jane' }); // → "Hello, Jane!"
|
|
179
|
+
|
|
180
|
+
// With <T>
|
|
181
|
+
<T k="greeting" vars={{ name: 'Jane' }} />
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Plurals
|
|
187
|
+
|
|
188
|
+
Define plural forms using CLDR categories in your translation map:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"items": {
|
|
193
|
+
"zero": "No items",
|
|
194
|
+
"one": "{{count}} item",
|
|
195
|
+
"other": "{{count}} items"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Pass `count` in vars: `t('items', { count: 3 })` → `"3 items"`.
|
|
201
|
+
|
|
202
|
+
Supported plural rules: English, Spanish, French, Portuguese, German, Russian, Ukrainian, Polish, Czech, Arabic, Hebrew, Japanese, Korean, Chinese, and more.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## RTL
|
|
207
|
+
|
|
208
|
+
When you switch to an RTL locale (`ar`, `he`, `fa`, `ur`), Lingo automatically:
|
|
209
|
+
|
|
210
|
+
- Sets `document.documentElement.dir = 'rtl'`
|
|
211
|
+
- Sets `document.documentElement.lang` to the locale
|
|
212
|
+
- Wraps the provider children in a `<div dir="rtl">`
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Utilities
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import {
|
|
220
|
+
flattenTranslations,
|
|
221
|
+
unflattenTranslations,
|
|
222
|
+
mergeTranslations,
|
|
223
|
+
getDirection,
|
|
224
|
+
} from '@forgedevstack/lingo';
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Utility | Description |
|
|
228
|
+
|---------|-------------|
|
|
229
|
+
| `flattenTranslations(map)` | Nested object → flat dot-separated keys |
|
|
230
|
+
| `unflattenTranslations(flat)` | Flat key-value → nested map |
|
|
231
|
+
| `mergeTranslations(a, b)` | Deep merge (b overrides a) |
|
|
232
|
+
| `getDirection(locale)` | Returns `'ltr'` or `'rtl'` |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Locale catalog
|
|
237
|
+
|
|
238
|
+
`LOCALE_CATALOG` provides metadata for 30+ locales (name, nativeName, direction, flag):
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { LOCALE_CATALOG } from '@forgedevstack/lingo';
|
|
242
|
+
|
|
243
|
+
// e.g. { code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', flag: '🇸🇦' }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Package entry points
|
|
249
|
+
|
|
250
|
+
- **`@forgedevstack/lingo`** — Core + React (createLingo, LingoProvider, useLingo, <T>, <Lingo>, LocaleSwitcher, utilities, types).
|
|
251
|
+
- **`@forgedevstack/lingo/react`** — Same React components and hooks; use this subpath if you want to tree-shake the core.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Version
|
|
256
|
+
|
|
257
|
+
1.0.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LocaleInfo, LingoConfig } from '../types';
|
|
2
|
+
export declare const VERSION = "1.0.0";
|
|
3
|
+
export declare const SOURCE_LOCAL: "local";
|
|
4
|
+
export declare const SOURCE_REMOTE: "remote";
|
|
5
|
+
export declare const SOURCE_HYBRID: "hybrid";
|
|
6
|
+
export declare const LOG_PREFIX = "[Lingo]";
|
|
7
|
+
export declare const RTL_LOCALES: Set<string>;
|
|
8
|
+
export declare const LOCALE_CATALOG: LocaleInfo[];
|
|
9
|
+
export declare const DEFAULT_STORAGE_KEY = "lingo_locale";
|
|
10
|
+
export declare const DEFAULT_REMOTE_ENDPOINT = "https://lingo.forgedevstack.com/api";
|
|
11
|
+
export declare const DEFAULT_CONFIG: Partial<LingoConfig>;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAExD,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,eAAO,MAAM,YAAY,EAAG,OAAgB,CAAC;AAC7C,eAAO,MAAM,aAAa,EAAG,QAAiB,CAAC;AAC/C,eAAO,MAAM,aAAa,EAAG,QAAiB,CAAC;AAC/C,eAAO,MAAM,UAAU,YAAY,CAAC;AAEpC,eAAO,MAAM,WAAW,aAEtB,CAAC;AAEH,eAAO,MAAM,cAAc,EAAE,UAAU,EAiCtC,CAAC;AAEF,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD,eAAO,MAAM,uBAAuB,wCAAwC,CAAC;AAE7E,eAAO,MAAM,cAAc,EAAE,OAAO,CAAC,WAAW,CAI/C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EAWd,MAAM,UAAU,CAAC;AA8DlB,wBAAgB,WAAW,CAAC,UAAU,EAAE,WAAW,GAAG,aAAa,CAqUlE"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("./react/index.cjs"),pe="1.0.0",he="local",U="remote",N="hybrid",b="[Lingo]",X=new Set(["ar","he","fa","ur","ps","sd","yi","ku"]),q=[{code:"en",name:"English",nativeName:"English",direction:"ltr",flag:"🇺🇸"},{code:"es",name:"Spanish",nativeName:"Español",direction:"ltr",flag:"🇪🇸"},{code:"fr",name:"French",nativeName:"Français",direction:"ltr",flag:"🇫🇷"},{code:"de",name:"German",nativeName:"Deutsch",direction:"ltr",flag:"🇩🇪"},{code:"it",name:"Italian",nativeName:"Italiano",direction:"ltr",flag:"🇮🇹"},{code:"pt",name:"Portuguese",nativeName:"Português",direction:"ltr",flag:"🇧🇷"},{code:"nl",name:"Dutch",nativeName:"Nederlands",direction:"ltr",flag:"🇳🇱"},{code:"pl",name:"Polish",nativeName:"Polski",direction:"ltr",flag:"🇵🇱"},{code:"ru",name:"Russian",nativeName:"Русский",direction:"ltr",flag:"🇷🇺"},{code:"uk",name:"Ukrainian",nativeName:"Українська",direction:"ltr",flag:"🇺🇦"},{code:"ja",name:"Japanese",nativeName:"日本語",direction:"ltr",flag:"🇯🇵"},{code:"ko",name:"Korean",nativeName:"한국어",direction:"ltr",flag:"🇰🇷"},{code:"zh",name:"Chinese",nativeName:"中文",direction:"ltr",flag:"🇨🇳"},{code:"ar",name:"Arabic",nativeName:"العربية",direction:"rtl",flag:"🇸🇦"},{code:"he",name:"Hebrew",nativeName:"עברית",direction:"rtl",flag:"🇮🇱"},{code:"fa",name:"Persian",nativeName:"فارسی",direction:"rtl",flag:"🇮🇷"},{code:"hi",name:"Hindi",nativeName:"हिन्दी",direction:"ltr",flag:"🇮🇳"},{code:"tr",name:"Turkish",nativeName:"Türkçe",direction:"ltr",flag:"🇹🇷"},{code:"th",name:"Thai",nativeName:"ไทย",direction:"ltr",flag:"🇹🇭"},{code:"vi",name:"Vietnamese",nativeName:"Tiếng Việt",direction:"ltr",flag:"🇻🇳"},{code:"sv",name:"Swedish",nativeName:"Svenska",direction:"ltr",flag:"🇸🇪"},{code:"da",name:"Danish",nativeName:"Dansk",direction:"ltr",flag:"🇩🇰"},{code:"fi",name:"Finnish",nativeName:"Suomi",direction:"ltr",flag:"🇫🇮"},{code:"no",name:"Norwegian",nativeName:"Norsk",direction:"ltr",flag:"🇳🇴"},{code:"cs",name:"Czech",nativeName:"Čeština",direction:"ltr",flag:"🇨🇿"},{code:"ro",name:"Romanian",nativeName:"Română",direction:"ltr",flag:"🇷🇴"},{code:"hu",name:"Hungarian",nativeName:"Magyar",direction:"ltr",flag:"🇭🇺"},{code:"el",name:"Greek",nativeName:"Ελληνικά",direction:"ltr",flag:"🇬🇷"},{code:"id",name:"Indonesian",nativeName:"Bahasa Indonesia",direction:"ltr",flag:"🇮🇩"},{code:"ms",name:"Malay",nativeName:"Bahasa Melayu",direction:"ltr",flag:"🇲🇾"},{code:"bn",name:"Bengali",nativeName:"বাংলা",direction:"ltr",flag:"🇧🇩"},{code:"ur",name:"Urdu",nativeName:"اردو",direction:"rtl",flag:"🇵🇰"}],F="lingo_locale",Q="https://lingo.forgedevstack.com/api",ve={cache:!0,storageKey:F,debug:!0};function I(e,t){const n=t.split(".");let o=e;for(const r of n){if(o==null||typeof o!="object")return;o=o[r]}if(typeof o=="string"||o&&typeof o=="object"&&"other"in o)return o}function K(e,t){return t?e.replace(/\{\{(\s*\w+\s*)\}\}/g,(n,o)=>{const r=o.trim();return t[r]!=null?String(t[r]):`{{${r}}}`}):e}function Z(e,t,n){const o=Math.abs(t),r=be(o,n);return r==="zero"&&e.zero?e.zero:r==="one"&&e.one?e.one:r==="two"&&e.two?e.two:r==="few"&&e.few?e.few:r==="many"&&e.many?e.many:e.other}function be(e,t){const n=t.split("-")[0];if(e===0)return"zero";switch(n){case"ar":return e===1?"one":e===2?"two":e%100>=3&&e%100<=10?"few":e%100>=11&&e%100<=99?"many":"other";case"he":return e===1?"one":e===2?"two":"other";case"ru":case"uk":case"pl":case"cs":{const o=e%10,r=e%100;return o===1&&r!==11?"one":o>=2&&o<=4&&!(r>=12&&r<=14)?"few":"many"}case"fr":case"pt":return e<=1?"one":"other";case"ja":case"ko":case"zh":case"th":case"vi":case"id":case"ms":return"other";default:return e===1?"one":"other"}}function k(e){return X.has(e.split("-")[0])?"rtl":"ltr"}function E(e,t=""){const n={};for(const[o,r]of Object.entries(e)){const c=t?`${t}.${o}`:o;typeof r=="string"?n[c]=r:Object.assign(n,E(r,c))}return n}function ee(e){const t={...e};for(const[n,o]of Object.entries(e)){const r=n.split(".");if(r.length>=3&&r[0]===r[1]){const c=r.slice(1).join(".");t[c]===void 0&&(t[c]=o)}}return t}function $(e){const t={};for(const[n,o]of Object.entries(e)){const r=n.split(".");let c=t;for(let i=0;i<r.length-1;i++){const d=r[i];(!c[d]||typeof c[d]=="string")&&(c[d]={}),c=c[d]}c[r[r.length-1]]=o}return t}function T(e,t){const n={...e};for(const[o,r]of Object.entries(t))typeof r=="string"?n[o]=r:typeof n[o]=="object"&&typeof r=="object"?n[o]=T(n[o],r):n[o]=r;return n}function P(e,t){const n=new Set([...Object.keys(e),...Object.keys(t)]),o={};for(const r of n)o[r]=T(e[r]??{},t[r]??{});return o}function Le(e,t,n){if(!n)return;const o=E(e),r=E(t);for(const c of Object.keys(r))Object.prototype.hasOwnProperty.call(o,c)&&console.warn(`${b} Local file overlay overrides remote for "${c}" (local wins).`)}function te(e,t,n){if(n==null)return;if(typeof n=="function")return n(e,t);if(typeof n=="string")return n;const o=Array.isArray(n)?n:[n];for(const r of o){if(r[e]!==void 0)return r[e];const c=e.indexOf(".");if(c!==-1){const i=e.slice(0,c);if(r[i]!==void 0)return r[i]}}}function we(e){const t=e.trim();if(t){if(t.startsWith("[")||t.startsWith("{"))try{const n=JSON.parse(t);if(Array.isArray(n)&&n.every(o=>o!=null&&typeof o=="object"&&!Array.isArray(o))||n!=null&&typeof n=="object"&&!Array.isArray(n))return n}catch{return t}return t}}const ne=/\.(json|mjs|cjs|ts|js)$/i;function C(e){var n;const t=((n=e.split("/").pop())==null?void 0:n.split("\\").pop())??"";return/^hybrid-overlay-/i.test(t)||/^overlay[-.]/i.test(t)}function oe(e){var o;const n=(((o=e.split("/").pop())==null?void 0:o.split("\\").pop())??"").replace(ne,"");return!n||C(e)?null:n}function Oe(e){var c;const n=(((c=e.split("/").pop())==null?void 0:c.split("\\").pop())??"").replace(ne,""),o=n.match(/^hybrid-overlay-(.+)$/i);if(o)return o[1]??null;const r=n.match(/^overlay[-.](.+)$/i);return r?r[1]??null:oe(e)}function x(e){const t=e&&typeof e=="object"&&"default"in e&&e.default!==void 0?e.default:e;if(!t||typeof t!="object")return{};const n=t,o=Object.values(n);return o.length>0&&o.every(r=>typeof r=="string")?n:E(n)}function H(e){return Object.fromEntries(e.map(t=>[t,{}]))}function Ee(e,t,n,o){if(o)for(const r of Object.keys(t))Object.prototype.hasOwnProperty.call(e,r)&&console.warn(`${b} "${r}" exists in multiple ${n} files; later value wins (use local overlay to override remote).`)}function J(e,t,n,o,r){const c=E(e[t]??{});Ee(c,n,r,o);const i={...c,...n};e[t]=$(i)}function re(e){const t=H(e.locales),n=H(e.locales),o=e.debug===!0;for(const[c,i]of Object.entries(e.baseGlob)){if(C(c))continue;const d=oe(c);if(!d||!e.locales.includes(d))continue;const y=x(i);J(t,d,y,o,"base locale file")}const r=e.overlayGlob??{};for(const[c,i]of Object.entries(r)){const d=Oe(c);if(!d||!e.locales.includes(d))continue;const y=x(i);J(n,d,y,o,"overlay locale file")}return{translations:t,overlay:n}}function Se(e){if(!e.localeLayersFromGlobs)return e;const{translations:t,overlay:n}=re({...e.localeLayersFromGlobs,debug:e.debug}),o=e.source;let r=o;if(o.type==="local")r={type:"local",translations:t};else if(o.type==="hybrid"){let d=P(t,n);const y=o.local;y&&Object.keys(y).length>0&&(d=P(d,y)),r={type:"hybrid",remote:o.remote,local:d}}const{localeLayersFromGlobs:c,...i}=e;return{...i,source:r}}function M(e){return JSON.parse(JSON.stringify(e))}function Ne(e){if(e.type===U){if("remotes"in e&&Array.isArray(e.remotes))return e.remotes;const t=e;return[{projectId:t.projectId,endpoint:t.endpoint,apiKey:t.apiKey}]}if(e.type===N){const t=e.remote;return Array.isArray(t)?t:[t]}return null}function V(e){return e.type===U||e.type===N}function je(e){const t=e.source;return t.type==="local"?M(t.translations):t.type===N?M(t.local??{}):{}}function Y(e,t){var n;return e.source.type!==N?{}:M({[t]:((n=e.source.local)==null?void 0:n[t])??{}})[t]??{}}function ke(e){const t=Se({...ve,...e}),n=new Set,o=new Set,r=new Map,c=new Map;let i={locale:ie(t)??t.defaultLocale,direction:k(t.defaultLocale),isLoading:!1,translations:je(t),stateRevision:0,remoteFetchError:null};i.direction=k(i.locale),V(t.source)&&R(i.locale);let d=null;function y(){var l;const a=(l=t.remoteBundleWebSocketUrl)==null?void 0:l.trim();if(typeof WebSocket>"u"||!a)return;const s=()=>{d=new WebSocket(a),d.onmessage=f=>{let g;try{const u=JSON.parse(String(f.data));((u==null?void 0:u.type)==="lingo:bundle-updated"||(u==null?void 0:u.type)==="bundle-updated")&&(g=u.locales)}catch{return}if(g!=null&&g.length)for(const u of g)o.delete(u);else o.clear();B({force:!0})},d.onclose=()=>{t.remoteBundleWebSocketUrl&&typeof window<"u"&&window.setTimeout(s,4e3)}};s()}typeof window<"u"&&t.remoteBundleWebSocketUrl&&y();function ae(){for(const a of n)a(i)}function w(a){const s=typeof a=="function"?a(i):a;i={...i,...s},ae()}function ie(a){if(!a.cache||typeof window>"u")return null;try{const s=localStorage.getItem(a.storageKey??F);if(s&&a.locales.includes(s))return s}catch{}return null}function se(a){if(!(!t.cache||typeof window>"u"))try{localStorage.setItem(t.storageKey??F,a)}catch{}}function ce(a,s){if(s||!o.has(a))return!1;const l=t.remoteRevalidateIntervalMs;return l==null?!0:Date.now()-(c.get(a)??0)<l}function D(a,s){if(!a)return;let l=I(a,s);if(l!==void 0)return l;const f=t.namespaces;if(f!=null&&f.length&&!s.includes(".")){for(const u of f)if(l=I(a,`${u}.${s}`),l!==void 0)return l;const g=E(a);for(const u of f){const O=`${u}.${s}`;let m,p=1/0;for(const[h,S]of Object.entries(g))if(typeof S=="string"){if(h===O)return S;h.endsWith(`.${O}`)&&h.length<p&&(m=S,p=h.length)}if(m!==void 0)return m}}}async function R(a,s){const l=Ne(t.source);if(!(l!=null&&l.length))return;const f=(s==null?void 0:s.force)===!0;if(ce(a,f))return;const g=r.get(a);if(g)return g;const u=(async()=>{var O;w({isLoading:!0});try{let m={};for(let v=0;v<l.length;v++){const j=l[v],G=`${j.endpoint??Q}/translations/${j.projectId}/${a}`,A=(O=j.apiKey)==null?void 0:O.trim(),ye=A!=null&&A!==""?`${G}?${new URLSearchParams({apiKey:A}).toString()}`:G,_=await fetch(ye);if(!_.ok)throw new Error(`${b} failed to fetch ${a} (${_.status})`);const W=await _.json();if(t.debug)for(const z of Object.keys(W))Object.prototype.hasOwnProperty.call(m,z)&&console.warn(`${b} Remote key "${z}" duplicated across remote sources; later source wins (index ${v}).`);m={...m,...W}}m=ee(m);const p=t.remoteKeysAllowlist;if(p!=null&&p.length){const v=new Set(p);m=Object.fromEntries(Object.entries(m).filter(([j])=>v.has(j)))}let h=$(m);const S=Y(t,a);Le(h,S,t.debug===!0),h=T(h,S),w(v=>({isLoading:!1,remoteFetchError:null,translations:{...v.translations,[a]:h}})),o.add(a),c.set(a,Date.now())}catch(m){t.debug&&console.warn(b,m);const p=m instanceof Error?m.message:String(m);w({isLoading:!1,remoteFetchError:p})}})();r.set(a,u);try{await u}finally{r.delete(a)}}async function B(a){const s=(a==null?void 0:a.locale)??i.locale;(a==null?void 0:a.force)===!0&&(o.delete(s),t.source.type===N?w(f=>({translations:{...f.translations,[s]:Y(t,s)}})):w(f=>{const g={...f.translations};return delete g[s],{translations:g}})),await R(s,{force:!0})}function le(a,s){var g;const l=i.translations[i.locale];let f=D(l,a);if(f===void 0&&t.fallbackLocale&&t.fallbackLocale!==i.locale){const u=i.translations[t.fallbackLocale];f=D(u,a)}if(f===void 0){t.debug&&console.warn(`${b} Missing key: "${a}" (locale: ${i.locale})`),(g=t.onMissingKey)==null||g.call(t,a,i.locale);const u=te(a,i.locale,t.missingKeyFallback);return u!==void 0?u:a}if(typeof f=="object"){const u=(s==null?void 0:s.count)!=null?Number(s.count):0,O=Z(f,u,i.locale);return K(O,s)}return K(f,s)}async function ue(a){var s;if(!t.locales.includes(a)){t.debug&&console.warn(`${b} Locale "${a}" not in supported list`);return}V(t.source)&&await R(a),se(a),w({locale:a,direction:k(a)}),typeof document<"u"&&(document.documentElement.dir=k(a),document.documentElement.lang=a),(s=t.onLocaleChange)==null||s.call(t,a)}function fe(a){return q.find(s=>s.code===a)}function de(a){return n.add(a),a(i),()=>n.delete(a)}function me(){return i.translations[i.locale]??{}}function ge(a,s){w(l=>{const f=l.translations[a]??{};return{translations:{...l.translations,[a]:T(f,s)}}})}return{t:le,get locale(){return i.locale},setLocale:ue,locales:t.locales,get direction(){return i.direction},getLocaleInfo:fe,get isLoading(){return i.isLoading},get remoteFetchError(){return i.remoteFetchError},subscribe:de,getTranslations:me,addTranslations:ge,refreshRemote:B}}async function Te(e){const t={},n=Object.entries(e);return await Promise.all(n.map(async([o,r])=>{const c=await fetch(r);if(!c.ok)throw new Error(`[Lingo] Failed to load ${r} (${c.status})`);const i=await c.json();t[o]=$(i)})),t}exports.Lingo=L.Lingo;exports.LingoProvider=L.LingoProvider;exports.LocaleSwitcher=L.LocaleSwitcher;exports.T=L.T;exports.useDirection=L.useDirection;exports.useLingo=L.useLingo;exports.useLocale=L.useLocale;exports.useTranslate=L.useTranslate;exports.DEFAULT_REMOTE_ENDPOINT=Q;exports.DEFAULT_STORAGE_KEY=F;exports.LOCALE_CATALOG=q;exports.LOG_PREFIX=b;exports.RTL_LOCALES=X;exports.SOURCE_HYBRID=N;exports.SOURCE_LOCAL=he;exports.SOURCE_REMOTE=U;exports.VERSION=pe;exports.buildLocaleLayersFromGlobs=re;exports.createLingo=ke;exports.dedupeStutterNamespaceInFlatKeys=ee;exports.flattenTranslations=E;exports.getDirection=k;exports.interpolate=K;exports.isLikelyOverlayLocalePath=C;exports.loadLocalBundlesFromUrls=Te;exports.mergeTranslationBundles=P;exports.mergeTranslations=T;exports.parseMissingKeyFallbackEnv=we;exports.resolveKey=I;exports.resolveMissingKeyFallback=te;exports.selectPlural=Z;exports.unflattenTranslations=$;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createLingo } from './core';
|
|
2
|
+
export { loadLocalBundlesFromUrls } from './load-local-bundles';
|
|
3
|
+
export { buildLocaleLayersFromGlobs, isLikelyOverlayLocalePath } from './locale-globs';
|
|
4
|
+
export type { Locale, Direction, TranslationMap, TranslationBundle, TranslateVars, PluralEntry, RemoteSourceConfig, TranslationSource, LocaleInfo, LingoConfig, LingoInstance, LingoState, LingoListener, LingoNamespace, LingoKey, LingoProject, TranslateFn, } from './types';
|
|
5
|
+
export { VERSION, RTL_LOCALES, LOCALE_CATALOG, DEFAULT_STORAGE_KEY, DEFAULT_REMOTE_ENDPOINT, SOURCE_LOCAL, SOURCE_REMOTE, SOURCE_HYBRID, LOG_PREFIX, } from './constants';
|
|
6
|
+
export { resolveKey, interpolate, selectPlural, getDirection, flattenTranslations, unflattenTranslations, mergeTranslations, mergeTranslationBundles, parseMissingKeyFallbackEnv, resolveMissingKeyFallback, dedupeStutterNamespaceInFlatKeys, } from './utils';
|
|
7
|
+
export { LingoProvider, useLingo, useTranslate, useLocale, useDirection, T, Lingo, LocaleSwitcher, } from './lingo-react';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEvF,YAAY,EACV,MAAM,EACN,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,aAAa,EACb,UAAU,EACV,aAAa,EACb,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,OAAO,EACP,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,EACV,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,GACjC,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,CAAC,EACD,KAAK,EACL,cAAc,GACf,MAAM,eAAe,CAAC"}
|