@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 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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAG5E,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAOjE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAGvD;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,CAcnG"}
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
- let lynguist = {
3
- locale: 'en',
4
- translations: {},
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
- lynguist.locale = options.locale;
8
- lynguist.translations = options.translations;
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 = lynguist.translations[key];
18
- if (!(key in lynguist.translations) || !translation)
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 lynguist.translations) || !lynguist.translations[key])
77
+ if (!(key in config.translations) || !config.translations[key])
27
78
  return key;
28
- const parts = lynguist.translations[key].split('|');
29
- let index = pluralIndex(count, lynguist.locale);
30
- let translation = parts[index];
31
- translation = translation.replaceAll(/:count/g, count.toString());
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
@@ -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,CAkBtF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CA4HzE"}
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 [placeholder, value] of Object.entries(replace)) {
4
- if (value !== null && value !== undefined) {
5
- let parameterValue = String(value);
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
- parameterValue = parameterValue.toUpperCase();
20
+ return stringValue.toUpperCase();
8
21
  }
9
22
  else if (placeholder[0] === placeholder[0].toUpperCase()) {
10
- parameterValue = parameterValue.charAt(0).toUpperCase() + parameterValue.slice(1);
23
+ return stringValue.charAt(0).toUpperCase() + stringValue.slice(1).toLowerCase();
11
24
  }
12
- translation = translation.replace(`:${placeholder}`, parameterValue);
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':
@@ -0,0 +1,4 @@
1
+ declare module 'virtual:lynguist-translations' {
2
+ const translations: Record<string, Record<string, string>>
3
+ export default translations
4
+ }
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.1",
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",