@vixen-tech/lynguist 0.0.1 → 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/README.md +272 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -13
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +58 -7
- package/dist/virtual-lynguist.d.ts +4 -0
- package/dist/vite.d.ts +48 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +113 -0
- package/package.json +23 -2
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Lynguist
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe internationalization (i18n) library for JavaScript/TypeScript applications with Laravel translation format support.
|
|
4
|
+
|
|
5
|
+
> For Laravel version, see [vixen-tech/laravel-lynguist](https://github.com/vixen-tech/laravel-lynguist).
|
|
6
|
+
|
|
7
|
+
Inspired by [laravel-translator-js](https://github.com/sergix44/laravel-translator-js) and [lingua](https://github.com/cyberwolf-studio/lingua).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Multi-language support** - 60+ languages with proper pluralization rules
|
|
12
|
+
- **Runtime locale switching** - Dynamically change languages with event notifications
|
|
13
|
+
- **Smart placeholders** - Laravel-style variable replacement with case transformation
|
|
14
|
+
- **Advanced pluralization** - Pipe-separated and interval notation support
|
|
15
|
+
- **Vite integration** - Virtual module plugin for loading JSON translation files (supports any framework)
|
|
16
|
+
- **Type-safe** - Full TypeScript support with typed translation keys
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @vixen-tech/lynguist
|
|
22
|
+
```
|
|
23
|
+
```bash
|
|
24
|
+
yarn install @vixen-tech/lynguist
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### With Vite
|
|
30
|
+
|
|
31
|
+
**1. Configure the Vite plugin:**
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// vite.config.ts
|
|
35
|
+
import { defineConfig } from 'vite'
|
|
36
|
+
import { lynguist } from '@vixen-tech/lynguist/vite'
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
plugins: [
|
|
40
|
+
lynguist({
|
|
41
|
+
langPath: 'lang', // default
|
|
42
|
+
additionalLangPaths: ['vendor/package/lang'] // optional
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**2. Create translation files:**
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
// lang/en.json
|
|
52
|
+
{
|
|
53
|
+
"greeting": "Hello :name",
|
|
54
|
+
"items": "One item|:count items",
|
|
55
|
+
"welcome": "Hello World"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> Works best with its Laravel counterpart: [vixen-tech/laravel-lynguist](https://github.com/vixen-tech/laravel-lynguist).
|
|
60
|
+
|
|
61
|
+
**3. Use in your application:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { __, setLocale } from '@vixen-tech/lynguist'
|
|
65
|
+
|
|
66
|
+
// Simple translation
|
|
67
|
+
__('welcome') // "Hello World"
|
|
68
|
+
|
|
69
|
+
// With placeholder
|
|
70
|
+
__('greeting', { name: 'John' }) // "Hello John"
|
|
71
|
+
|
|
72
|
+
// With pluralization
|
|
73
|
+
__('items', 5) // "5 items"
|
|
74
|
+
|
|
75
|
+
// Switch locale
|
|
76
|
+
setLocale('de')
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Manual Setup
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { Lynguist, __ } from '@vixen-tech/lynguist'
|
|
83
|
+
|
|
84
|
+
Lynguist({
|
|
85
|
+
locale: 'en',
|
|
86
|
+
translations: {
|
|
87
|
+
welcome: 'Hello World',
|
|
88
|
+
greeting: 'Hello :name'
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
__('welcome') // "Hello World"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Inertia.js
|
|
96
|
+
|
|
97
|
+
To reactively switch locale, you can create a wrapper component that assigns current locale to document's `lang` attribute:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const locale = usePage().props.locale
|
|
101
|
+
|
|
102
|
+
if (typeof window !== 'undefined') {
|
|
103
|
+
document.documentElement.lang = locale
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Here's what it looks like in React:
|
|
108
|
+
|
|
109
|
+
```typescript jsx
|
|
110
|
+
export function Wrapper({ children }: PropsWithChildren) {
|
|
111
|
+
const locale = usePage<SharedData>().props.locale
|
|
112
|
+
|
|
113
|
+
if (typeof window !== 'undefined') {
|
|
114
|
+
document.documentElement.lang = locale
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return <>{children}</>
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Placeholders
|
|
122
|
+
|
|
123
|
+
Lynguist supports Laravel-style placeholder replacement with automatic case transformation:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const translations = {
|
|
127
|
+
greeting: 'Hello :name',
|
|
128
|
+
welcome: 'Welcome :Name', // capitalized
|
|
129
|
+
shout: 'HEY :NAME' // uppercase
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
__('greeting', { name: 'John' }) // "Hello John"
|
|
133
|
+
__('welcome', { Name: 'john' }) // "Welcome John"
|
|
134
|
+
__('shout', { NAME: 'john' }) // "HEY JOHN"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Pluralization
|
|
138
|
+
|
|
139
|
+
### Basic Pluralization
|
|
140
|
+
|
|
141
|
+
Use pipe-separated values for simple plural forms:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const translations = {
|
|
145
|
+
items: 'One item|:count items'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
__('items', 1) // "One item"
|
|
149
|
+
__('items', 5) // "5 items"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Interval Notation
|
|
153
|
+
|
|
154
|
+
For more control, use interval notation:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const translations = {
|
|
158
|
+
apples: '{0} No apples|{1} One apple|[2,*] :count apples'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
__('apples', 0) // "No apples"
|
|
162
|
+
__('apples', 1) // "One apple"
|
|
163
|
+
__('apples', 5) // "5 apples"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Language-Specific Rules
|
|
167
|
+
|
|
168
|
+
Lynguist handles complex pluralization rules for languages like Russian, Arabic, and Polish:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Russian has 3 plural forms
|
|
172
|
+
const translations = {
|
|
173
|
+
apples: ':count яблоко|:count яблока|:count яблок'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
__('apples', 1) // "1 яблоко"
|
|
177
|
+
__('apples', 2) // "2 яблока"
|
|
178
|
+
__('apples', 5) // "5 яблок"
|
|
179
|
+
__('apples', 21) // "21 яблоко"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API Reference
|
|
183
|
+
|
|
184
|
+
### `__(key, countOrReplace?, replace?)`
|
|
185
|
+
|
|
186
|
+
Main translation function supporting both simple translations and pluralization.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
__('welcome') // Simple translation
|
|
190
|
+
__('greeting', { name: 'John' }) // With placeholders
|
|
191
|
+
__('items', 5) // With count
|
|
192
|
+
__('items', 5, { type: 'file' }) // With count and placeholders
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `trans(key, replace?)`
|
|
196
|
+
|
|
197
|
+
Simple translation without pluralization.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
trans('greeting', { name: 'John' }) // "Hello John"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `transChoice(key, count, replace?)`
|
|
204
|
+
|
|
205
|
+
Plural-aware translation.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
transChoice('items', 5) // "5 items"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `setLocale(locale)`
|
|
212
|
+
|
|
213
|
+
Change the current language.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
setLocale('de')
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `getLocale()`
|
|
220
|
+
|
|
221
|
+
Get the current language code.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
getLocale() // "en"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `getAvailableLocales()`
|
|
228
|
+
|
|
229
|
+
Get all available language codes.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
getAvailableLocales() // ["en", "de", "fr"]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `getTranslations(locale?)`
|
|
236
|
+
|
|
237
|
+
Get translations for a specific or current locale.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
getTranslations() // Current locale translations
|
|
241
|
+
getTranslations('de') // German translations
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `onLocaleChange(callback)`
|
|
245
|
+
|
|
246
|
+
Subscribe to locale change events. Returns an unsubscribe function.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const unsubscribe = onLocaleChange((newLocale, previousLocale) => {
|
|
250
|
+
console.log(`Changed from ${previousLocale} to ${newLocale}`)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Later: stop listening
|
|
254
|
+
unsubscribe()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Vite Plugin Options
|
|
258
|
+
|
|
259
|
+
| Option | Type | Default | Description |
|
|
260
|
+
|-----------------------|------------|-------------|----------------------------------------|
|
|
261
|
+
| `langPath` | `string` | `'lang'` | Path to the translation directory |
|
|
262
|
+
| `additionalLangPaths` | `string[]` | `undefined` | Additional paths for translation files |
|
|
263
|
+
|
|
264
|
+
## Supported Languages
|
|
265
|
+
|
|
266
|
+
Lynguist supports 60+ languages with proper pluralization rules including:
|
|
267
|
+
|
|
268
|
+
Afrikaans, Amharic, Arabic, Belarusian, Bengali, Bosnian, Bulgarian, Catalan, Czech, Welsh, Danish, German, Greek, English, Esperanto, Spanish, Estonian, Basque, Persian, Finnish, Filipino, French, Irish, Galician, Hebrew, Hindi, Croatian, Hungarian, Armenian, Icelandic, Italian, Japanese, Korean, Lithuanian, Latvian, Macedonian, Mongolian, Maltese, Dutch, Norwegian, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Serbian, Swedish, Tamil, Thai, Turkish, Ukrainian, Urdu, Vietnamese, Chinese, and more.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
ISC
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { LynguistOptions, LynguistTerm, ReplacePlaceholders } from './types';
|
|
2
|
-
export type { LynguistTranslations, LynguistTerm } from './types';
|
|
2
|
+
export type { LynguistTranslations, LynguistTerm, LynguistLocale, ReplacePlaceholders } from './types';
|
|
3
|
+
type LocaleChangeCallback = (locale: string, previousLocale: string) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Initialize Lynguist with custom options.
|
|
6
|
+
* When using the Vite plugin, this is called automatically.
|
|
7
|
+
*/
|
|
3
8
|
export declare function Lynguist(options: LynguistOptions): void;
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to locale changes.
|
|
11
|
+
* @returns Unsubscribe function
|
|
12
|
+
*/
|
|
13
|
+
export declare function onLocaleChange(callback: LocaleChangeCallback): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Set the current locale.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setLocale(locale: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Get the current locale.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getLocale(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Get all available locales.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getAvailableLocales(): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Get translations for a specific locale (or current locale if not specified).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getTranslations(locale?: string): Record<string, string>;
|
|
4
30
|
export declare function __(key: LynguistTerm, replace?: ReplacePlaceholders): string;
|
|
5
31
|
export declare function __(key: LynguistTerm, count: number, replace?: ReplacePlaceholders): string;
|
|
6
32
|
export declare function trans(key: LynguistTerm, replace?: ReplacePlaceholders): string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,eAAe,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAK5F,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAEtG,KAAK,oBAAoB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;AAqB5E;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAGvD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,IAAI,CAIzE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAW9C;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEvE;AAED,wBAAgB,EAAE,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAA;AAC5E,wBAAgB,EAAE,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAA;AAS3F,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAU9E;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CA6BnG"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,62 @@
|
|
|
1
|
-
import { pluralIndex, replacePlaceholders } from './utils';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { hasIntervalNotation, parseIntervalNotation, pluralIndex, replacePlaceholders } from './utils';
|
|
2
|
+
// @ts-ignore - Virtual module provided by Vite plugin
|
|
3
|
+
import translations from 'virtual:lynguist-translations';
|
|
4
|
+
const isServer = typeof window === 'undefined';
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
function detectLocale() {
|
|
7
|
+
return !isServer ? document.documentElement.lang?.replace('-', '_') || 'en' : 'en';
|
|
8
|
+
}
|
|
9
|
+
let config = {
|
|
10
|
+
locale: detectLocale(),
|
|
11
|
+
translations: translations[detectLocale()] ?? {},
|
|
12
|
+
allTranslations: translations,
|
|
5
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Initialize Lynguist with custom options.
|
|
16
|
+
* When using the Vite plugin, this is called automatically.
|
|
17
|
+
*/
|
|
6
18
|
export function Lynguist(options) {
|
|
7
|
-
|
|
8
|
-
|
|
19
|
+
config.locale = options.locale;
|
|
20
|
+
config.translations = options.translations;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Subscribe to locale changes.
|
|
24
|
+
* @returns Unsubscribe function
|
|
25
|
+
*/
|
|
26
|
+
export function onLocaleChange(callback) {
|
|
27
|
+
listeners.add(callback);
|
|
28
|
+
return () => listeners.delete(callback);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set the current locale.
|
|
32
|
+
*/
|
|
33
|
+
export function setLocale(locale) {
|
|
34
|
+
if (!config.allTranslations[locale]) {
|
|
35
|
+
console.warn(`[lynguist] Locale "${locale}" not found. Available: ${Object.keys(config.allTranslations).join(', ')}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const previousLocale = config.locale;
|
|
39
|
+
config.locale = locale;
|
|
40
|
+
config.translations = config.allTranslations[locale];
|
|
41
|
+
listeners.forEach(callback => callback(locale, previousLocale));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the current locale.
|
|
45
|
+
*/
|
|
46
|
+
export function getLocale() {
|
|
47
|
+
return config.locale;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get all available locales.
|
|
51
|
+
*/
|
|
52
|
+
export function getAvailableLocales() {
|
|
53
|
+
return Object.keys(config.allTranslations);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get translations for a specific locale (or current locale if not specified).
|
|
57
|
+
*/
|
|
58
|
+
export function getTranslations(locale) {
|
|
59
|
+
return config.allTranslations[locale ?? config.locale] ?? {};
|
|
9
60
|
}
|
|
10
61
|
export function __(key, countOrReplace, replace) {
|
|
11
62
|
if (typeof countOrReplace === 'number') {
|
|
@@ -14,8 +65,8 @@ export function __(key, countOrReplace, replace) {
|
|
|
14
65
|
return trans(key, countOrReplace);
|
|
15
66
|
}
|
|
16
67
|
export function trans(key, replace) {
|
|
17
|
-
let translation =
|
|
18
|
-
if (!(key in
|
|
68
|
+
let translation = config.translations[key];
|
|
69
|
+
if (!(key in config.translations) || !translation)
|
|
19
70
|
return key;
|
|
20
71
|
if (replace) {
|
|
21
72
|
translation = replacePlaceholders(translation, replace);
|
|
@@ -23,12 +74,26 @@ export function trans(key, replace) {
|
|
|
23
74
|
return translation;
|
|
24
75
|
}
|
|
25
76
|
export function transChoice(key, count, replace) {
|
|
26
|
-
if (!(key in
|
|
77
|
+
if (!(key in config.translations) || !config.translations[key])
|
|
27
78
|
return key;
|
|
28
|
-
const
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
79
|
+
const translationString = config.translations[key];
|
|
80
|
+
let translation;
|
|
81
|
+
// Check for interval notation first ({0}, {1}, [1,19], [20,*])
|
|
82
|
+
if (hasIntervalNotation(translationString)) {
|
|
83
|
+
const intervalResult = parseIntervalNotation(translationString, count);
|
|
84
|
+
if (intervalResult !== null) {
|
|
85
|
+
translation = intervalResult;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
translation = translationString.split('|')[0].replace(/^[{[].*?[}\]]\s*/, '');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const parts = translationString.split('|');
|
|
93
|
+
const index = pluralIndex(count, config.locale);
|
|
94
|
+
translation = parts[index] ?? parts[parts.length - 1];
|
|
95
|
+
}
|
|
96
|
+
translation = translation.replace(/:count/gi, count.toString());
|
|
32
97
|
if (replace) {
|
|
33
98
|
translation = replacePlaceholders(translation, replace);
|
|
34
99
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { LynguistLocale, ReplacePlaceholders } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Replace placeholders in translation string with values.
|
|
4
|
+
* Supports Laravel-style case transformation:
|
|
5
|
+
* - :name → lowercase value
|
|
6
|
+
* - :Name → capitalized value
|
|
7
|
+
* - :NAME → uppercase value
|
|
8
|
+
*/
|
|
2
9
|
export declare function replacePlaceholders(text: string, replace: ReplacePlaceholders): string;
|
|
10
|
+
/**
|
|
11
|
+
* Parse interval notation and return the matching translation part.
|
|
12
|
+
* Supports: {0}, {1}, [1,19], [20,*]
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseIntervalNotation(translation: string, count: number): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a translation string uses interval notation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasIntervalNotation(translation: string): boolean;
|
|
3
19
|
export declare function pluralIndex(count: number, locale: LynguistLocale): number;
|
|
4
20
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAE7D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAE7D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA0BtF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmCvF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CA4HzE"}
|
package/dist/utils.js
CHANGED
|
@@ -1,19 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace placeholders in translation string with values.
|
|
3
|
+
* Supports Laravel-style case transformation:
|
|
4
|
+
* - :name → lowercase value
|
|
5
|
+
* - :Name → capitalized value
|
|
6
|
+
* - :NAME → uppercase value
|
|
7
|
+
*/
|
|
1
8
|
export function replacePlaceholders(text, replace) {
|
|
2
9
|
let translation = text;
|
|
3
|
-
for (const [
|
|
4
|
-
if (value
|
|
5
|
-
|
|
10
|
+
for (const [key, value] of Object.entries(replace)) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
const stringValue = String(value);
|
|
14
|
+
const lowerKey = key.toLowerCase();
|
|
15
|
+
// Match all case variants of the placeholder in the template
|
|
16
|
+
const regex = new RegExp(`:${lowerKey}`, 'gi');
|
|
17
|
+
translation = translation.replace(regex, match => {
|
|
18
|
+
const placeholder = match.slice(1); // Remove the ':'
|
|
6
19
|
if (placeholder === placeholder.toUpperCase()) {
|
|
7
|
-
|
|
20
|
+
return stringValue.toUpperCase();
|
|
8
21
|
}
|
|
9
22
|
else if (placeholder[0] === placeholder[0].toUpperCase()) {
|
|
10
|
-
|
|
23
|
+
return stringValue.charAt(0).toUpperCase() + stringValue.slice(1).toLowerCase();
|
|
11
24
|
}
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
else {
|
|
26
|
+
return stringValue;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
14
29
|
}
|
|
15
30
|
return translation;
|
|
16
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse interval notation and return the matching translation part.
|
|
34
|
+
* Supports: {0}, {1}, [1,19], [20,*]
|
|
35
|
+
*/
|
|
36
|
+
export function parseIntervalNotation(translation, count) {
|
|
37
|
+
const parts = translation.split('|');
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
const trimmed = part.trim();
|
|
40
|
+
// Match {n} - exact value
|
|
41
|
+
const exactMatch = trimmed.match(/^\{(\d+)\}\s*(.*)$/);
|
|
42
|
+
if (exactMatch) {
|
|
43
|
+
const exactValue = parseInt(exactMatch[1], 10);
|
|
44
|
+
if (count === exactValue) {
|
|
45
|
+
return exactMatch[2];
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Match [n,m] or [n,*] - range
|
|
50
|
+
const rangeMatch = trimmed.match(/^\[(\d+),(\d+|\*)\]\s*(.*)$/);
|
|
51
|
+
if (rangeMatch) {
|
|
52
|
+
const min = parseInt(rangeMatch[1], 10);
|
|
53
|
+
const max = rangeMatch[2] === '*' ? Infinity : parseInt(rangeMatch[2], 10);
|
|
54
|
+
if (count >= min && count <= max) {
|
|
55
|
+
return rangeMatch[3];
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if a translation string uses interval notation.
|
|
64
|
+
*/
|
|
65
|
+
export function hasIntervalNotation(translation) {
|
|
66
|
+
return /(\{[\d]+\}|\[[\d]+,[\d*]+\])/.test(translation);
|
|
67
|
+
}
|
|
17
68
|
export function pluralIndex(count, locale) {
|
|
18
69
|
switch (locale) {
|
|
19
70
|
case 'af':
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Plugin } from './vite';
|
|
2
|
+
export interface LynguistPluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Path to the Laravel lang directory containing translation files.
|
|
5
|
+
* @default 'lang'
|
|
6
|
+
*/
|
|
7
|
+
langPath?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Additional paths to scan for translation files.
|
|
10
|
+
*/
|
|
11
|
+
additionalLangPaths?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Vite plugin that loads Laravel translation files and provides them
|
|
15
|
+
* via a virtual module for use with Lynguist.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // vite.config.ts
|
|
20
|
+
* import { lynguist } from '@vixen-tech/lynguist/vite'
|
|
21
|
+
*
|
|
22
|
+
* export default defineConfig({
|
|
23
|
+
* plugins: [
|
|
24
|
+
* lynguist({
|
|
25
|
+
* langPath: 'lang',
|
|
26
|
+
* additionalLangPaths: ['vendor/package/lang']
|
|
27
|
+
* })
|
|
28
|
+
* ]
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // In your app
|
|
35
|
+
* import { __, setLocale, getLocale, onLocaleChange } from '@vixen-tech/lynguist'
|
|
36
|
+
*
|
|
37
|
+
* // Use translations
|
|
38
|
+
* __('welcome')
|
|
39
|
+
* __('greeting', { name: 'John' })
|
|
40
|
+
* __('items', 5)
|
|
41
|
+
*
|
|
42
|
+
* // Switch locale at runtime
|
|
43
|
+
* setLocale('de')
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function lynguist(options?: LynguistPluginOptions): Plugin;
|
|
47
|
+
export default lynguist;
|
|
48
|
+
//# sourceMappingURL=vite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAIlC,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;CACjC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA2FpE;AAED,eAAe,QAAQ,CAAA"}
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that loads Laravel translation files and provides them
|
|
5
|
+
* via a virtual module for use with Lynguist.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // vite.config.ts
|
|
10
|
+
* import { lynguist } from '@vixen-tech/lynguist/vite'
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* plugins: [
|
|
14
|
+
* lynguist({
|
|
15
|
+
* langPath: 'lang',
|
|
16
|
+
* additionalLangPaths: ['vendor/package/lang']
|
|
17
|
+
* })
|
|
18
|
+
* ]
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // In your app
|
|
25
|
+
* import { __, setLocale, getLocale, onLocaleChange } from '@vixen-tech/lynguist'
|
|
26
|
+
*
|
|
27
|
+
* // Use translations
|
|
28
|
+
* __('welcome')
|
|
29
|
+
* __('greeting', { name: 'John' })
|
|
30
|
+
* __('items', 5)
|
|
31
|
+
*
|
|
32
|
+
* // Switch locale at runtime
|
|
33
|
+
* setLocale('de')
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function lynguist(options = {}) {
|
|
37
|
+
const virtualModuleId = 'virtual:lynguist-translations';
|
|
38
|
+
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|
39
|
+
const langPath = (options.langPath ?? 'lang').replace(/[\\/]$/, '') + path.sep;
|
|
40
|
+
const additionalLangPaths = options.additionalLangPaths ?? [];
|
|
41
|
+
const paths = [langPath, ...additionalLangPaths];
|
|
42
|
+
function loadTranslations(...langPaths) {
|
|
43
|
+
const translations = {};
|
|
44
|
+
for (const lp of langPaths) {
|
|
45
|
+
if (!fs.existsSync(lp)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const files = fs.readdirSync(lp);
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (!file.endsWith('.json'))
|
|
51
|
+
continue;
|
|
52
|
+
const locale = path.basename(file, '.json');
|
|
53
|
+
const filePath = path.join(lp, file);
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
56
|
+
const parsed = JSON.parse(content);
|
|
57
|
+
// Merge with existing translations for this locale
|
|
58
|
+
translations[locale] = {
|
|
59
|
+
...translations[locale],
|
|
60
|
+
...parsed,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(`[lynguist] Failed to load translation file: ${filePath}`, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return translations;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: 'vite-plugin-lynguist',
|
|
72
|
+
enforce: 'pre',
|
|
73
|
+
config() {
|
|
74
|
+
return {
|
|
75
|
+
optimizeDeps: {
|
|
76
|
+
exclude: [virtualModuleId],
|
|
77
|
+
},
|
|
78
|
+
ssr: {
|
|
79
|
+
noExternal: ['@vixen-tech/lynguist'],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
resolveId(id) {
|
|
84
|
+
if (id === virtualModuleId) {
|
|
85
|
+
return resolvedVirtualModuleId;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
load(id) {
|
|
89
|
+
if (id === resolvedVirtualModuleId) {
|
|
90
|
+
const translations = loadTranslations(...paths);
|
|
91
|
+
return `export default ${JSON.stringify(translations)}`;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
handleHotUpdate(ctx) {
|
|
95
|
+
for (const lp of paths) {
|
|
96
|
+
const relative = path.relative(lp, ctx.file);
|
|
97
|
+
const isSubpath = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
98
|
+
if (isSubpath && ctx.file.endsWith('.json')) {
|
|
99
|
+
const virtualModule = ctx.server.moduleGraph.getModuleById(resolvedVirtualModuleId);
|
|
100
|
+
if (virtualModule) {
|
|
101
|
+
ctx.server.moduleGraph.invalidateModule(virtualModule);
|
|
102
|
+
ctx.server.ws.send({
|
|
103
|
+
type: 'full-reload',
|
|
104
|
+
path: '*',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export default lynguist;
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vixen-tech/lynguist",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./vite": {
|
|
14
|
+
"types": "./dist/vite.d.ts",
|
|
15
|
+
"import": "./dist/vite.js"
|
|
16
|
+
},
|
|
17
|
+
"./virtual": {
|
|
18
|
+
"types": "./dist/virtual-lynguist.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
8
21
|
"files": [
|
|
9
22
|
"dist"
|
|
10
23
|
],
|
|
11
24
|
"scripts": {
|
|
12
|
-
"build": "tsc -p tsconfig.json && tsc-alias",
|
|
25
|
+
"build": "tsc -p tsconfig.json && tsc-alias && cp src/virtual-lynguist.d.ts dist/",
|
|
13
26
|
"clean": "rm -rf dist",
|
|
14
27
|
"prepublishOnly": "pnpm clean && pnpm build",
|
|
15
28
|
"test": "vitest run"
|
|
@@ -21,6 +34,14 @@
|
|
|
21
34
|
],
|
|
22
35
|
"author": "Alex Torscho <contact@alextorscho.com> (https://alextorscho.com)",
|
|
23
36
|
"license": "ISC",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"vite": ">=5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"vite": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
24
45
|
"devDependencies": {
|
|
25
46
|
"@types/node": "^25.0.3",
|
|
26
47
|
"prettier": "^3.7.4",
|