@zachhandley/ez-i18n 0.3.4 → 0.3.5
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 +69 -0
- package/package.json +2 -2
- package/src/index.ts +0 -130
- package/src/middleware.ts +0 -82
- package/src/runtime/index.ts +0 -19
- package/src/runtime/store.ts +0 -122
- package/src/types.ts +0 -125
- package/src/utils/index.ts +0 -16
- package/src/utils/locales.ts +0 -240
- package/src/utils/translations.ts +0 -418
- package/src/virtual.d.ts +0 -70
- package/src/vite-plugin.ts +0 -716
package/src/utils/locales.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comprehensive locale database for ez-i18n
|
|
3
|
-
* Contains display names (in native language) and BCP47 codes
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface LocaleInfo {
|
|
7
|
-
/** Display name in native language */
|
|
8
|
-
name: string;
|
|
9
|
-
/** Display name in English */
|
|
10
|
-
englishName: string;
|
|
11
|
-
/** BCP47 language tag */
|
|
12
|
-
bcp47: string;
|
|
13
|
-
/** Text direction */
|
|
14
|
-
dir: 'ltr' | 'rtl';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Comprehensive mapping of locale codes to their metadata
|
|
19
|
-
* Covers all major languages and regional variants
|
|
20
|
-
*/
|
|
21
|
-
export const LOCALE_DATABASE: Record<string, LocaleInfo> = {
|
|
22
|
-
// Major Western European Languages
|
|
23
|
-
en: { name: 'English', englishName: 'English', bcp47: 'en-US', dir: 'ltr' },
|
|
24
|
-
'en-US': { name: 'English (US)', englishName: 'English (US)', bcp47: 'en-US', dir: 'ltr' },
|
|
25
|
-
'en-GB': { name: 'English (UK)', englishName: 'English (UK)', bcp47: 'en-GB', dir: 'ltr' },
|
|
26
|
-
'en-AU': { name: 'English (Australia)', englishName: 'English (Australia)', bcp47: 'en-AU', dir: 'ltr' },
|
|
27
|
-
'en-CA': { name: 'English (Canada)', englishName: 'English (Canada)', bcp47: 'en-CA', dir: 'ltr' },
|
|
28
|
-
|
|
29
|
-
de: { name: 'Deutsch', englishName: 'German', bcp47: 'de-DE', dir: 'ltr' },
|
|
30
|
-
'de-DE': { name: 'Deutsch (Deutschland)', englishName: 'German (Germany)', bcp47: 'de-DE', dir: 'ltr' },
|
|
31
|
-
'de-AT': { name: 'Deutsch (Österreich)', englishName: 'German (Austria)', bcp47: 'de-AT', dir: 'ltr' },
|
|
32
|
-
'de-CH': { name: 'Deutsch (Schweiz)', englishName: 'German (Switzerland)', bcp47: 'de-CH', dir: 'ltr' },
|
|
33
|
-
|
|
34
|
-
fr: { name: 'Français', englishName: 'French', bcp47: 'fr-FR', dir: 'ltr' },
|
|
35
|
-
'fr-FR': { name: 'Français (France)', englishName: 'French (France)', bcp47: 'fr-FR', dir: 'ltr' },
|
|
36
|
-
'fr-CA': { name: 'Français (Canada)', englishName: 'French (Canada)', bcp47: 'fr-CA', dir: 'ltr' },
|
|
37
|
-
'fr-BE': { name: 'Français (Belgique)', englishName: 'French (Belgium)', bcp47: 'fr-BE', dir: 'ltr' },
|
|
38
|
-
'fr-CH': { name: 'Français (Suisse)', englishName: 'French (Switzerland)', bcp47: 'fr-CH', dir: 'ltr' },
|
|
39
|
-
|
|
40
|
-
es: { name: 'Español', englishName: 'Spanish', bcp47: 'es-ES', dir: 'ltr' },
|
|
41
|
-
'es-ES': { name: 'Español (España)', englishName: 'Spanish (Spain)', bcp47: 'es-ES', dir: 'ltr' },
|
|
42
|
-
'es-MX': { name: 'Español (México)', englishName: 'Spanish (Mexico)', bcp47: 'es-MX', dir: 'ltr' },
|
|
43
|
-
'es-AR': { name: 'Español (Argentina)', englishName: 'Spanish (Argentina)', bcp47: 'es-AR', dir: 'ltr' },
|
|
44
|
-
'es-CO': { name: 'Español (Colombia)', englishName: 'Spanish (Colombia)', bcp47: 'es-CO', dir: 'ltr' },
|
|
45
|
-
'es-419': { name: 'Español (Latinoamérica)', englishName: 'Spanish (Latin America)', bcp47: 'es-419', dir: 'ltr' },
|
|
46
|
-
|
|
47
|
-
it: { name: 'Italiano', englishName: 'Italian', bcp47: 'it-IT', dir: 'ltr' },
|
|
48
|
-
'it-IT': { name: 'Italiano (Italia)', englishName: 'Italian (Italy)', bcp47: 'it-IT', dir: 'ltr' },
|
|
49
|
-
'it-CH': { name: 'Italiano (Svizzera)', englishName: 'Italian (Switzerland)', bcp47: 'it-CH', dir: 'ltr' },
|
|
50
|
-
|
|
51
|
-
pt: { name: 'Português', englishName: 'Portuguese', bcp47: 'pt-PT', dir: 'ltr' },
|
|
52
|
-
'pt-PT': { name: 'Português (Portugal)', englishName: 'Portuguese (Portugal)', bcp47: 'pt-PT', dir: 'ltr' },
|
|
53
|
-
'pt-BR': { name: 'Português (Brasil)', englishName: 'Portuguese (Brazil)', bcp47: 'pt-BR', dir: 'ltr' },
|
|
54
|
-
|
|
55
|
-
nl: { name: 'Nederlands', englishName: 'Dutch', bcp47: 'nl-NL', dir: 'ltr' },
|
|
56
|
-
'nl-NL': { name: 'Nederlands (Nederland)', englishName: 'Dutch (Netherlands)', bcp47: 'nl-NL', dir: 'ltr' },
|
|
57
|
-
'nl-BE': { name: 'Nederlands (België)', englishName: 'Dutch (Belgium)', bcp47: 'nl-BE', dir: 'ltr' },
|
|
58
|
-
|
|
59
|
-
// Nordic Languages
|
|
60
|
-
sv: { name: 'Svenska', englishName: 'Swedish', bcp47: 'sv-SE', dir: 'ltr' },
|
|
61
|
-
'sv-SE': { name: 'Svenska (Sverige)', englishName: 'Swedish (Sweden)', bcp47: 'sv-SE', dir: 'ltr' },
|
|
62
|
-
|
|
63
|
-
da: { name: 'Dansk', englishName: 'Danish', bcp47: 'da-DK', dir: 'ltr' },
|
|
64
|
-
'da-DK': { name: 'Dansk (Danmark)', englishName: 'Danish (Denmark)', bcp47: 'da-DK', dir: 'ltr' },
|
|
65
|
-
|
|
66
|
-
no: { name: 'Norsk', englishName: 'Norwegian', bcp47: 'nb-NO', dir: 'ltr' },
|
|
67
|
-
nb: { name: 'Norsk bokmål', englishName: 'Norwegian Bokmål', bcp47: 'nb-NO', dir: 'ltr' },
|
|
68
|
-
nn: { name: 'Norsk nynorsk', englishName: 'Norwegian Nynorsk', bcp47: 'nn-NO', dir: 'ltr' },
|
|
69
|
-
|
|
70
|
-
fi: { name: 'Suomi', englishName: 'Finnish', bcp47: 'fi-FI', dir: 'ltr' },
|
|
71
|
-
'fi-FI': { name: 'Suomi (Suomi)', englishName: 'Finnish (Finland)', bcp47: 'fi-FI', dir: 'ltr' },
|
|
72
|
-
|
|
73
|
-
is: { name: 'Íslenska', englishName: 'Icelandic', bcp47: 'is-IS', dir: 'ltr' },
|
|
74
|
-
|
|
75
|
-
// Eastern European Languages
|
|
76
|
-
pl: { name: 'Polski', englishName: 'Polish', bcp47: 'pl-PL', dir: 'ltr' },
|
|
77
|
-
'pl-PL': { name: 'Polski (Polska)', englishName: 'Polish (Poland)', bcp47: 'pl-PL', dir: 'ltr' },
|
|
78
|
-
|
|
79
|
-
cs: { name: 'Čeština', englishName: 'Czech', bcp47: 'cs-CZ', dir: 'ltr' },
|
|
80
|
-
'cs-CZ': { name: 'Čeština (Česko)', englishName: 'Czech (Czech Republic)', bcp47: 'cs-CZ', dir: 'ltr' },
|
|
81
|
-
|
|
82
|
-
sk: { name: 'Slovenčina', englishName: 'Slovak', bcp47: 'sk-SK', dir: 'ltr' },
|
|
83
|
-
'sk-SK': { name: 'Slovenčina (Slovensko)', englishName: 'Slovak (Slovakia)', bcp47: 'sk-SK', dir: 'ltr' },
|
|
84
|
-
|
|
85
|
-
hu: { name: 'Magyar', englishName: 'Hungarian', bcp47: 'hu-HU', dir: 'ltr' },
|
|
86
|
-
'hu-HU': { name: 'Magyar (Magyarország)', englishName: 'Hungarian (Hungary)', bcp47: 'hu-HU', dir: 'ltr' },
|
|
87
|
-
|
|
88
|
-
ro: { name: 'Română', englishName: 'Romanian', bcp47: 'ro-RO', dir: 'ltr' },
|
|
89
|
-
'ro-RO': { name: 'Română (România)', englishName: 'Romanian (Romania)', bcp47: 'ro-RO', dir: 'ltr' },
|
|
90
|
-
|
|
91
|
-
bg: { name: 'Български', englishName: 'Bulgarian', bcp47: 'bg-BG', dir: 'ltr' },
|
|
92
|
-
'bg-BG': { name: 'Български (България)', englishName: 'Bulgarian (Bulgaria)', bcp47: 'bg-BG', dir: 'ltr' },
|
|
93
|
-
|
|
94
|
-
hr: { name: 'Hrvatski', englishName: 'Croatian', bcp47: 'hr-HR', dir: 'ltr' },
|
|
95
|
-
sr: { name: 'Српски', englishName: 'Serbian', bcp47: 'sr-RS', dir: 'ltr' },
|
|
96
|
-
sl: { name: 'Slovenščina', englishName: 'Slovenian', bcp47: 'sl-SI', dir: 'ltr' },
|
|
97
|
-
|
|
98
|
-
uk: { name: 'Українська', englishName: 'Ukrainian', bcp47: 'uk-UA', dir: 'ltr' },
|
|
99
|
-
'uk-UA': { name: 'Українська (Україна)', englishName: 'Ukrainian (Ukraine)', bcp47: 'uk-UA', dir: 'ltr' },
|
|
100
|
-
|
|
101
|
-
ru: { name: 'Русский', englishName: 'Russian', bcp47: 'ru-RU', dir: 'ltr' },
|
|
102
|
-
'ru-RU': { name: 'Русский (Россия)', englishName: 'Russian (Russia)', bcp47: 'ru-RU', dir: 'ltr' },
|
|
103
|
-
|
|
104
|
-
// Baltic Languages
|
|
105
|
-
lt: { name: 'Lietuvių', englishName: 'Lithuanian', bcp47: 'lt-LT', dir: 'ltr' },
|
|
106
|
-
lv: { name: 'Latviešu', englishName: 'Latvian', bcp47: 'lv-LV', dir: 'ltr' },
|
|
107
|
-
et: { name: 'Eesti', englishName: 'Estonian', bcp47: 'et-EE', dir: 'ltr' },
|
|
108
|
-
|
|
109
|
-
// Greek
|
|
110
|
-
el: { name: 'Ελληνικά', englishName: 'Greek', bcp47: 'el-GR', dir: 'ltr' },
|
|
111
|
-
'el-GR': { name: 'Ελληνικά (Ελλάδα)', englishName: 'Greek (Greece)', bcp47: 'el-GR', dir: 'ltr' },
|
|
112
|
-
|
|
113
|
-
// Asian Languages
|
|
114
|
-
zh: { name: '中文', englishName: 'Chinese', bcp47: 'zh-CN', dir: 'ltr' },
|
|
115
|
-
'zh-CN': { name: '中文 (简体)', englishName: 'Chinese (Simplified)', bcp47: 'zh-CN', dir: 'ltr' },
|
|
116
|
-
'zh-TW': { name: '中文 (繁體)', englishName: 'Chinese (Traditional)', bcp47: 'zh-TW', dir: 'ltr' },
|
|
117
|
-
'zh-HK': { name: '中文 (香港)', englishName: 'Chinese (Hong Kong)', bcp47: 'zh-HK', dir: 'ltr' },
|
|
118
|
-
|
|
119
|
-
ja: { name: '日本語', englishName: 'Japanese', bcp47: 'ja-JP', dir: 'ltr' },
|
|
120
|
-
'ja-JP': { name: '日本語 (日本)', englishName: 'Japanese (Japan)', bcp47: 'ja-JP', dir: 'ltr' },
|
|
121
|
-
|
|
122
|
-
ko: { name: '한국어', englishName: 'Korean', bcp47: 'ko-KR', dir: 'ltr' },
|
|
123
|
-
'ko-KR': { name: '한국어 (대한민국)', englishName: 'Korean (South Korea)', bcp47: 'ko-KR', dir: 'ltr' },
|
|
124
|
-
|
|
125
|
-
vi: { name: 'Tiếng Việt', englishName: 'Vietnamese', bcp47: 'vi-VN', dir: 'ltr' },
|
|
126
|
-
'vi-VN': { name: 'Tiếng Việt (Việt Nam)', englishName: 'Vietnamese (Vietnam)', bcp47: 'vi-VN', dir: 'ltr' },
|
|
127
|
-
|
|
128
|
-
th: { name: 'ไทย', englishName: 'Thai', bcp47: 'th-TH', dir: 'ltr' },
|
|
129
|
-
'th-TH': { name: 'ไทย (ประเทศไทย)', englishName: 'Thai (Thailand)', bcp47: 'th-TH', dir: 'ltr' },
|
|
130
|
-
|
|
131
|
-
id: { name: 'Bahasa Indonesia', englishName: 'Indonesian', bcp47: 'id-ID', dir: 'ltr' },
|
|
132
|
-
'id-ID': { name: 'Bahasa Indonesia (Indonesia)', englishName: 'Indonesian (Indonesia)', bcp47: 'id-ID', dir: 'ltr' },
|
|
133
|
-
|
|
134
|
-
ms: { name: 'Bahasa Melayu', englishName: 'Malay', bcp47: 'ms-MY', dir: 'ltr' },
|
|
135
|
-
'ms-MY': { name: 'Bahasa Melayu (Malaysia)', englishName: 'Malay (Malaysia)', bcp47: 'ms-MY', dir: 'ltr' },
|
|
136
|
-
|
|
137
|
-
tl: { name: 'Tagalog', englishName: 'Tagalog', bcp47: 'tl-PH', dir: 'ltr' },
|
|
138
|
-
fil: { name: 'Filipino', englishName: 'Filipino', bcp47: 'fil-PH', dir: 'ltr' },
|
|
139
|
-
|
|
140
|
-
// South Asian Languages
|
|
141
|
-
hi: { name: 'हिन्दी', englishName: 'Hindi', bcp47: 'hi-IN', dir: 'ltr' },
|
|
142
|
-
'hi-IN': { name: 'हिन्दी (भारत)', englishName: 'Hindi (India)', bcp47: 'hi-IN', dir: 'ltr' },
|
|
143
|
-
|
|
144
|
-
bn: { name: 'বাংলা', englishName: 'Bengali', bcp47: 'bn-BD', dir: 'ltr' },
|
|
145
|
-
'bn-BD': { name: 'বাংলা (বাংলাদেশ)', englishName: 'Bengali (Bangladesh)', bcp47: 'bn-BD', dir: 'ltr' },
|
|
146
|
-
'bn-IN': { name: 'বাংলা (ভারত)', englishName: 'Bengali (India)', bcp47: 'bn-IN', dir: 'ltr' },
|
|
147
|
-
|
|
148
|
-
ta: { name: 'தமிழ்', englishName: 'Tamil', bcp47: 'ta-IN', dir: 'ltr' },
|
|
149
|
-
te: { name: 'తెలుగు', englishName: 'Telugu', bcp47: 'te-IN', dir: 'ltr' },
|
|
150
|
-
mr: { name: 'मराठी', englishName: 'Marathi', bcp47: 'mr-IN', dir: 'ltr' },
|
|
151
|
-
gu: { name: 'ગુજરાતી', englishName: 'Gujarati', bcp47: 'gu-IN', dir: 'ltr' },
|
|
152
|
-
kn: { name: 'ಕನ್ನಡ', englishName: 'Kannada', bcp47: 'kn-IN', dir: 'ltr' },
|
|
153
|
-
ml: { name: 'മലയാളം', englishName: 'Malayalam', bcp47: 'ml-IN', dir: 'ltr' },
|
|
154
|
-
pa: { name: 'ਪੰਜਾਬੀ', englishName: 'Punjabi', bcp47: 'pa-IN', dir: 'ltr' },
|
|
155
|
-
ur: { name: 'اردو', englishName: 'Urdu', bcp47: 'ur-PK', dir: 'rtl' },
|
|
156
|
-
|
|
157
|
-
// Middle Eastern Languages (RTL)
|
|
158
|
-
ar: { name: 'العربية', englishName: 'Arabic', bcp47: 'ar-SA', dir: 'rtl' },
|
|
159
|
-
'ar-SA': { name: 'العربية (السعودية)', englishName: 'Arabic (Saudi Arabia)', bcp47: 'ar-SA', dir: 'rtl' },
|
|
160
|
-
'ar-EG': { name: 'العربية (مصر)', englishName: 'Arabic (Egypt)', bcp47: 'ar-EG', dir: 'rtl' },
|
|
161
|
-
'ar-AE': { name: 'العربية (الإمارات)', englishName: 'Arabic (UAE)', bcp47: 'ar-AE', dir: 'rtl' },
|
|
162
|
-
|
|
163
|
-
he: { name: 'עברית', englishName: 'Hebrew', bcp47: 'he-IL', dir: 'rtl' },
|
|
164
|
-
'he-IL': { name: 'עברית (ישראל)', englishName: 'Hebrew (Israel)', bcp47: 'he-IL', dir: 'rtl' },
|
|
165
|
-
|
|
166
|
-
fa: { name: 'فارسی', englishName: 'Persian', bcp47: 'fa-IR', dir: 'rtl' },
|
|
167
|
-
'fa-IR': { name: 'فارسی (ایران)', englishName: 'Persian (Iran)', bcp47: 'fa-IR', dir: 'rtl' },
|
|
168
|
-
|
|
169
|
-
tr: { name: 'Türkçe', englishName: 'Turkish', bcp47: 'tr-TR', dir: 'ltr' },
|
|
170
|
-
'tr-TR': { name: 'Türkçe (Türkiye)', englishName: 'Turkish (Turkey)', bcp47: 'tr-TR', dir: 'ltr' },
|
|
171
|
-
|
|
172
|
-
// African Languages
|
|
173
|
-
sw: { name: 'Kiswahili', englishName: 'Swahili', bcp47: 'sw-KE', dir: 'ltr' },
|
|
174
|
-
af: { name: 'Afrikaans', englishName: 'Afrikaans', bcp47: 'af-ZA', dir: 'ltr' },
|
|
175
|
-
zu: { name: 'isiZulu', englishName: 'Zulu', bcp47: 'zu-ZA', dir: 'ltr' },
|
|
176
|
-
|
|
177
|
-
// Celtic Languages
|
|
178
|
-
cy: { name: 'Cymraeg', englishName: 'Welsh', bcp47: 'cy-GB', dir: 'ltr' },
|
|
179
|
-
ga: { name: 'Gaeilge', englishName: 'Irish', bcp47: 'ga-IE', dir: 'ltr' },
|
|
180
|
-
gd: { name: 'Gàidhlig', englishName: 'Scottish Gaelic', bcp47: 'gd-GB', dir: 'ltr' },
|
|
181
|
-
|
|
182
|
-
// Other European Languages
|
|
183
|
-
ca: { name: 'Català', englishName: 'Catalan', bcp47: 'ca-ES', dir: 'ltr' },
|
|
184
|
-
eu: { name: 'Euskara', englishName: 'Basque', bcp47: 'eu-ES', dir: 'ltr' },
|
|
185
|
-
gl: { name: 'Galego', englishName: 'Galician', bcp47: 'gl-ES', dir: 'ltr' },
|
|
186
|
-
|
|
187
|
-
// Constructed Languages
|
|
188
|
-
eo: { name: 'Esperanto', englishName: 'Esperanto', bcp47: 'eo', dir: 'ltr' },
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get locale info for a given locale code
|
|
193
|
-
* Falls back to a generated entry if not found
|
|
194
|
-
*/
|
|
195
|
-
export function getLocaleInfo(locale: string): LocaleInfo {
|
|
196
|
-
if (LOCALE_DATABASE[locale]) {
|
|
197
|
-
return LOCALE_DATABASE[locale];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Generate fallback for unknown locales
|
|
201
|
-
return {
|
|
202
|
-
name: locale.toUpperCase(),
|
|
203
|
-
englishName: locale.toUpperCase(),
|
|
204
|
-
bcp47: locale,
|
|
205
|
-
dir: 'ltr',
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Build locale names mapping for discovered locales
|
|
211
|
-
*/
|
|
212
|
-
export function buildLocaleNames(locales: string[]): Record<string, string> {
|
|
213
|
-
const names: Record<string, string> = {};
|
|
214
|
-
for (const locale of locales) {
|
|
215
|
-
names[locale] = getLocaleInfo(locale).name;
|
|
216
|
-
}
|
|
217
|
-
return names;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Build locale to BCP47 mapping for discovered locales
|
|
222
|
-
*/
|
|
223
|
-
export function buildLocaleToBCP47(locales: string[]): Record<string, string> {
|
|
224
|
-
const bcp47: Record<string, string> = {};
|
|
225
|
-
for (const locale of locales) {
|
|
226
|
-
bcp47[locale] = getLocaleInfo(locale).bcp47;
|
|
227
|
-
}
|
|
228
|
-
return bcp47;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Build locale directions mapping for discovered locales
|
|
233
|
-
*/
|
|
234
|
-
export function buildLocaleDirections(locales: string[]): Record<string, 'ltr' | 'rtl'> {
|
|
235
|
-
const dirs: Record<string, 'ltr' | 'rtl'> = {};
|
|
236
|
-
for (const locale of locales) {
|
|
237
|
-
dirs[locale] = getLocaleInfo(locale).dir;
|
|
238
|
-
}
|
|
239
|
-
return dirs;
|
|
240
|
-
}
|
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import { glob } from 'tinyglobby';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
|
-
import type { LocaleTranslationPath, TranslationsConfig, TranslationCache } from '../types';
|
|
5
|
-
|
|
6
|
-
const CACHE_FILE = '.ez-i18n.json';
|
|
7
|
-
const CACHE_VERSION = 1;
|
|
8
|
-
const DEFAULT_I18N_DIR = './public/i18n';
|
|
9
|
-
|
|
10
|
-
export type PathType = 'file' | 'folder' | 'glob' | 'array';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Detect the type of translation path
|
|
14
|
-
*/
|
|
15
|
-
export function detectPathType(input: string | string[]): PathType {
|
|
16
|
-
if (Array.isArray(input)) return 'array';
|
|
17
|
-
if (input.includes('*')) return 'glob';
|
|
18
|
-
if (input.endsWith('/') || input.endsWith(path.sep)) return 'folder';
|
|
19
|
-
return 'file';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if a path is a directory (handles missing trailing slash)
|
|
24
|
-
*/
|
|
25
|
-
function isDirectory(filePath: string): boolean {
|
|
26
|
-
try {
|
|
27
|
-
return fs.statSync(filePath).isDirectory();
|
|
28
|
-
} catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Resolve a single translation path to an array of absolute file paths.
|
|
35
|
-
* Results are sorted alphabetically for predictable merge order.
|
|
36
|
-
*/
|
|
37
|
-
export async function resolveTranslationPaths(
|
|
38
|
-
input: LocaleTranslationPath,
|
|
39
|
-
projectRoot: string
|
|
40
|
-
): Promise<string[]> {
|
|
41
|
-
const type = detectPathType(input);
|
|
42
|
-
let files: string[] = [];
|
|
43
|
-
|
|
44
|
-
switch (type) {
|
|
45
|
-
case 'array':
|
|
46
|
-
// Each entry could itself be a glob, folder, or file
|
|
47
|
-
for (const entry of input as string[]) {
|
|
48
|
-
const resolved = await resolveTranslationPaths(entry, projectRoot);
|
|
49
|
-
files.push(...resolved);
|
|
50
|
-
}
|
|
51
|
-
break;
|
|
52
|
-
|
|
53
|
-
case 'glob':
|
|
54
|
-
files = await glob(input as string, {
|
|
55
|
-
cwd: projectRoot,
|
|
56
|
-
absolute: true,
|
|
57
|
-
});
|
|
58
|
-
break;
|
|
59
|
-
|
|
60
|
-
case 'folder': {
|
|
61
|
-
const folderPath = path.resolve(projectRoot, (input as string).replace(/\/$/, ''));
|
|
62
|
-
files = await glob('**/*.json', {
|
|
63
|
-
cwd: folderPath,
|
|
64
|
-
absolute: true,
|
|
65
|
-
});
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
case 'file':
|
|
70
|
-
default: {
|
|
71
|
-
const filePath = path.resolve(projectRoot, input as string);
|
|
72
|
-
// Check if it's actually a directory (user omitted trailing slash)
|
|
73
|
-
if (isDirectory(filePath)) {
|
|
74
|
-
files = await glob('**/*.json', {
|
|
75
|
-
cwd: filePath,
|
|
76
|
-
absolute: true,
|
|
77
|
-
});
|
|
78
|
-
} else {
|
|
79
|
-
files = [filePath];
|
|
80
|
-
}
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Sort alphabetically for predictable merge order
|
|
86
|
-
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Auto-discover translations from a base directory.
|
|
91
|
-
* Scans for locale folders (e.g., en/, es/, fr/) and their JSON files.
|
|
92
|
-
* Returns both discovered locales and their file mappings.
|
|
93
|
-
*/
|
|
94
|
-
export async function autoDiscoverTranslations(
|
|
95
|
-
baseDir: string,
|
|
96
|
-
projectRoot: string,
|
|
97
|
-
configuredLocales?: string[]
|
|
98
|
-
): Promise<{ locales: string[]; translations: Record<string, string[]> }> {
|
|
99
|
-
const absoluteBaseDir = path.resolve(projectRoot, baseDir.replace(/\/$/, ''));
|
|
100
|
-
|
|
101
|
-
if (!isDirectory(absoluteBaseDir)) {
|
|
102
|
-
console.warn(`[ez-i18n] Translation directory not found: ${absoluteBaseDir}`);
|
|
103
|
-
return { locales: configuredLocales || [], translations: {} };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const translations: Record<string, string[]> = {};
|
|
107
|
-
const discoveredLocales: string[] = [];
|
|
108
|
-
|
|
109
|
-
// Read directory entries
|
|
110
|
-
const entries = fs.readdirSync(absoluteBaseDir, { withFileTypes: true });
|
|
111
|
-
|
|
112
|
-
for (const entry of entries) {
|
|
113
|
-
if (entry.isDirectory()) {
|
|
114
|
-
// This is a locale folder (e.g., en/, es/)
|
|
115
|
-
const locale = entry.name;
|
|
116
|
-
|
|
117
|
-
// If locales were configured, only include matching ones
|
|
118
|
-
if (configuredLocales && configuredLocales.length > 0) {
|
|
119
|
-
if (!configuredLocales.includes(locale)) continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const localePath = path.join(absoluteBaseDir, locale);
|
|
123
|
-
const files = await glob('**/*.json', {
|
|
124
|
-
cwd: localePath,
|
|
125
|
-
absolute: true,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (files.length > 0) {
|
|
129
|
-
discoveredLocales.push(locale);
|
|
130
|
-
translations[locale] = files.sort((a, b) => a.localeCompare(b));
|
|
131
|
-
}
|
|
132
|
-
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
133
|
-
// Root-level JSON files (e.g., en.json, es.json)
|
|
134
|
-
// Extract locale from filename
|
|
135
|
-
const locale = path.basename(entry.name, '.json');
|
|
136
|
-
|
|
137
|
-
// If locales were configured, only include matching ones
|
|
138
|
-
if (configuredLocales && configuredLocales.length > 0) {
|
|
139
|
-
if (!configuredLocales.includes(locale)) continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const filePath = path.join(absoluteBaseDir, entry.name);
|
|
143
|
-
|
|
144
|
-
if (!translations[locale]) {
|
|
145
|
-
discoveredLocales.push(locale);
|
|
146
|
-
translations[locale] = [];
|
|
147
|
-
}
|
|
148
|
-
translations[locale].push(filePath);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Sort locales for consistency
|
|
153
|
-
const sortedLocales = [...new Set(discoveredLocales)].sort();
|
|
154
|
-
|
|
155
|
-
return { locales: sortedLocales, translations };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Resolve the full translations config to normalized form.
|
|
160
|
-
* Handles string (base dir), object (per-locale), or undefined (auto-discover).
|
|
161
|
-
*/
|
|
162
|
-
export async function resolveTranslationsConfig(
|
|
163
|
-
config: TranslationsConfig | undefined,
|
|
164
|
-
projectRoot: string,
|
|
165
|
-
configuredLocales?: string[]
|
|
166
|
-
): Promise<{ locales: string[]; translations: Record<string, string[]> }> {
|
|
167
|
-
// No config - auto-discover from default location
|
|
168
|
-
if (!config) {
|
|
169
|
-
return autoDiscoverTranslations(DEFAULT_I18N_DIR, projectRoot, configuredLocales);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// String - treat as base directory for auto-discovery
|
|
173
|
-
if (typeof config === 'string') {
|
|
174
|
-
return autoDiscoverTranslations(config, projectRoot, configuredLocales);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Object - per-locale mapping
|
|
178
|
-
const translations: Record<string, string[]> = {};
|
|
179
|
-
const locales = Object.keys(config);
|
|
180
|
-
|
|
181
|
-
for (const [locale, localePath] of Object.entries(config)) {
|
|
182
|
-
translations[locale] = await resolveTranslationPaths(localePath, projectRoot);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return { locales, translations };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Deep merge translation objects.
|
|
190
|
-
* - Objects are recursively merged
|
|
191
|
-
* - Arrays are REPLACED (not concatenated)
|
|
192
|
-
* - Primitives are overwritten by later values
|
|
193
|
-
* - Prototype pollution safe
|
|
194
|
-
*/
|
|
195
|
-
export function deepMerge<T extends Record<string, unknown>>(
|
|
196
|
-
target: T,
|
|
197
|
-
...sources: T[]
|
|
198
|
-
): T {
|
|
199
|
-
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
200
|
-
const result = { ...target };
|
|
201
|
-
|
|
202
|
-
for (const source of sources) {
|
|
203
|
-
if (!source || typeof source !== 'object') continue;
|
|
204
|
-
|
|
205
|
-
for (const key of Object.keys(source)) {
|
|
206
|
-
if (FORBIDDEN_KEYS.has(key)) continue;
|
|
207
|
-
|
|
208
|
-
const targetVal = result[key as keyof T];
|
|
209
|
-
const sourceVal = source[key as keyof T];
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
sourceVal !== null &&
|
|
213
|
-
typeof sourceVal === 'object' &&
|
|
214
|
-
!Array.isArray(sourceVal) &&
|
|
215
|
-
targetVal !== null &&
|
|
216
|
-
typeof targetVal === 'object' &&
|
|
217
|
-
!Array.isArray(targetVal)
|
|
218
|
-
) {
|
|
219
|
-
// Both are plain objects - recurse
|
|
220
|
-
(result as Record<string, unknown>)[key] = deepMerge(
|
|
221
|
-
targetVal as Record<string, unknown>,
|
|
222
|
-
sourceVal as Record<string, unknown>
|
|
223
|
-
);
|
|
224
|
-
} else {
|
|
225
|
-
// Arrays replace, primitives overwrite
|
|
226
|
-
(result as Record<string, unknown>)[key] = sourceVal;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Load cached translation discovery results
|
|
236
|
-
*/
|
|
237
|
-
export function loadCache(projectRoot: string): TranslationCache | null {
|
|
238
|
-
const cachePath = path.join(projectRoot, CACHE_FILE);
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
if (!fs.existsSync(cachePath)) return null;
|
|
242
|
-
|
|
243
|
-
const content = fs.readFileSync(cachePath, 'utf-8');
|
|
244
|
-
const cache = JSON.parse(content) as TranslationCache;
|
|
245
|
-
|
|
246
|
-
// Validate cache version
|
|
247
|
-
if (cache.version !== CACHE_VERSION) return null;
|
|
248
|
-
|
|
249
|
-
return cache;
|
|
250
|
-
} catch {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Save translation discovery results to cache
|
|
257
|
-
*/
|
|
258
|
-
export function saveCache(
|
|
259
|
-
projectRoot: string,
|
|
260
|
-
discovered: Record<string, string[]>
|
|
261
|
-
): void {
|
|
262
|
-
const cachePath = path.join(projectRoot, CACHE_FILE);
|
|
263
|
-
|
|
264
|
-
const cache: TranslationCache = {
|
|
265
|
-
version: CACHE_VERSION,
|
|
266
|
-
discovered,
|
|
267
|
-
lastScan: new Date().toISOString(),
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
272
|
-
} catch (error) {
|
|
273
|
-
console.warn('[ez-i18n] Failed to write cache file:', error);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Check if cache is still valid (files haven't changed)
|
|
279
|
-
*/
|
|
280
|
-
export function isCacheValid(
|
|
281
|
-
cache: TranslationCache,
|
|
282
|
-
projectRoot: string
|
|
283
|
-
): boolean {
|
|
284
|
-
// Check if all cached files still exist
|
|
285
|
-
for (const files of Object.values(cache.discovered)) {
|
|
286
|
-
for (const file of files) {
|
|
287
|
-
if (!fs.existsSync(file)) return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Convert an absolute path to a relative import path for Vite
|
|
296
|
-
*/
|
|
297
|
-
export function toRelativeImport(absolutePath: string, projectRoot: string): string {
|
|
298
|
-
const relativePath = path.relative(projectRoot, absolutePath);
|
|
299
|
-
// Ensure it starts with ./ and uses forward slashes
|
|
300
|
-
const normalized = relativePath.replace(/\\/g, '/');
|
|
301
|
-
return normalized.startsWith('.') ? normalized : './' + normalized;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Generate a glob pattern for import.meta.glob from a base directory.
|
|
306
|
-
* In virtual modules, globs must start with '/' (project root relative).
|
|
307
|
-
* Returns null if the path is in public/ (can't use import.meta.glob for public files).
|
|
308
|
-
*/
|
|
309
|
-
export function toGlobPattern(baseDir: string, projectRoot: string): string | null {
|
|
310
|
-
const relativePath = path.relative(projectRoot, baseDir).replace(/\\/g, '/');
|
|
311
|
-
// Can't use import.meta.glob for public directory files
|
|
312
|
-
if (relativePath.startsWith('public/') || relativePath === 'public') {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
// Virtual modules require globs to start with '/' (project root relative)
|
|
316
|
-
return `/${relativePath}/**/*.json`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Check if an absolute path is inside the public directory
|
|
321
|
-
*/
|
|
322
|
-
export function isInPublicDir(filePath: string, projectRoot: string): boolean {
|
|
323
|
-
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
324
|
-
return relativePath.startsWith('public/') || relativePath === 'public';
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Convert a public directory path to its served URL.
|
|
329
|
-
* public/i18n/en/common.json → /i18n/en/common.json
|
|
330
|
-
*/
|
|
331
|
-
export function toPublicUrl(filePath: string, projectRoot: string): string {
|
|
332
|
-
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
333
|
-
// Remove 'public/' prefix - files in public/ are served at root
|
|
334
|
-
if (relativePath.startsWith('public/')) {
|
|
335
|
-
return '/' + relativePath.slice('public/'.length);
|
|
336
|
-
}
|
|
337
|
-
return '/' + relativePath;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Get the locale base directory for namespace calculation.
|
|
342
|
-
* For public paths, strips the 'public/' prefix.
|
|
343
|
-
*/
|
|
344
|
-
export function getLocaleBaseDirForNamespace(localeBaseDir: string, projectRoot: string): string {
|
|
345
|
-
const relativePath = path.relative(projectRoot, localeBaseDir).replace(/\\/g, '/');
|
|
346
|
-
// For namespace calculation, we want the path without 'public/' prefix
|
|
347
|
-
if (relativePath.startsWith('public/')) {
|
|
348
|
-
return relativePath.slice('public/'.length);
|
|
349
|
-
}
|
|
350
|
-
return relativePath;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Get namespace from file path relative to locale base directory.
|
|
355
|
-
*
|
|
356
|
-
* Examples:
|
|
357
|
-
* - filePath: /project/public/i18n/en/auth/login.json, localeDir: /project/public/i18n/en
|
|
358
|
-
* → namespace: 'auth.login'
|
|
359
|
-
* - filePath: /project/public/i18n/en/common.json, localeDir: /project/public/i18n/en
|
|
360
|
-
* → namespace: 'common'
|
|
361
|
-
* - filePath: /project/public/i18n/en/settings/index.json, localeDir: /project/public/i18n/en
|
|
362
|
-
* → namespace: 'settings' (index is stripped)
|
|
363
|
-
*/
|
|
364
|
-
export function getNamespaceFromPath(filePath: string, localeDir: string): string {
|
|
365
|
-
// Get relative path from locale directory
|
|
366
|
-
const relative = path.relative(localeDir, filePath);
|
|
367
|
-
|
|
368
|
-
// Remove .json extension
|
|
369
|
-
const withoutExt = relative.replace(/\.json$/i, '');
|
|
370
|
-
|
|
371
|
-
// Convert path separators to dots
|
|
372
|
-
const namespace = withoutExt.replace(/[\\/]/g, '.');
|
|
373
|
-
|
|
374
|
-
// Remove trailing .index (index.json files represent the folder itself)
|
|
375
|
-
return namespace.replace(/\.index$/, '');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Wrap a translation object with its namespace.
|
|
380
|
-
*
|
|
381
|
-
* Example:
|
|
382
|
-
* - namespace: 'auth.login'
|
|
383
|
-
* - content: { title: 'Welcome', subtitle: 'Login here' }
|
|
384
|
-
* - result: { auth: { login: { title: 'Welcome', subtitle: 'Login here' } } }
|
|
385
|
-
*/
|
|
386
|
-
export function wrapWithNamespace(
|
|
387
|
-
namespace: string,
|
|
388
|
-
content: Record<string, unknown>
|
|
389
|
-
): Record<string, unknown> {
|
|
390
|
-
if (!namespace) return content;
|
|
391
|
-
|
|
392
|
-
const parts = namespace.split('.');
|
|
393
|
-
let result: Record<string, unknown> = content;
|
|
394
|
-
|
|
395
|
-
// Build from inside out
|
|
396
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
397
|
-
result = { [parts[i]]: result };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return result;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Generate code that wraps imported content with namespace at runtime.
|
|
405
|
-
* Used in virtual module generation.
|
|
406
|
-
*/
|
|
407
|
-
export function generateNamespaceWrapperCode(): string {
|
|
408
|
-
return `
|
|
409
|
-
function __wrapWithNamespace(namespace, content) {
|
|
410
|
-
if (!namespace) return content;
|
|
411
|
-
const parts = namespace.split('.');
|
|
412
|
-
let result = content;
|
|
413
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
414
|
-
result = { [parts[i]]: result };
|
|
415
|
-
}
|
|
416
|
-
return result;
|
|
417
|
-
}`;
|
|
418
|
-
}
|