@genrwork/laravel-i18next 0.1.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.
Potentially problematic release.
This version of @genrwork/laravel-i18next might be problematic. Click here for more details.
- package/README.md +367 -0
- package/client.d.ts +12 -0
- package/dist/index.cjs +12 -0
- package/dist/index.mjs +2 -0
- package/dist/react.cjs +40 -0
- package/dist/react.mjs +38 -0
- package/dist/shared/create-i18n-BSEwKsCX.mjs +641 -0
- package/dist/shared/create-i18n-WsDK4Z8L.cjs +647 -0
- package/dist/svelte.cjs +77 -0
- package/dist/svelte.mjs +74 -0
- package/dist/types/backend.d.ts +29 -0
- package/dist/types/contrib/get-plural-index.d.ts +15 -0
- package/dist/types/format.d.ts +43 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/interfaces/locale-file.d.ts +8 -0
- package/dist/types/interfaces/options.d.ts +19 -0
- package/dist/types/interfaces/replacements.d.ts +6 -0
- package/dist/types/plugin/helper.d.ts +6 -0
- package/dist/types/plugin/locale.d.ts +11 -0
- package/dist/types/plugin/parser.d.ts +22 -0
- package/dist/types/plugin/sources.d.ts +53 -0
- package/dist/types/react/i18n-provider-props.d.ts +23 -0
- package/dist/types/react/index.d.ts +3 -0
- package/dist/types/react/provider.d.ts +10 -0
- package/dist/types/shared/create-i18n.d.ts +26 -0
- package/dist/types/svelte/index.d.ts +48 -0
- package/dist/types/utils/pluralization.d.ts +9 -0
- package/dist/types/utils/recognizer.d.ts +29 -0
- package/dist/types/utils/replacer.d.ts +9 -0
- package/dist/types/utils/resolver.d.ts +23 -0
- package/dist/types/utils/sources.d.ts +14 -0
- package/dist/types/vite.d.ts +25 -0
- package/dist/types/vue/index.d.ts +21 -0
- package/dist/vite.cjs +315 -0
- package/dist/vite.mjs +310 -0
- package/dist/vue.cjs +39 -0
- package/dist/vue.mjs +34 -0
- package/package.json +155 -0
package/dist/vite.cjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
var vite = require('vite');
|
|
8
|
+
var phpArrayReader = require('php-array-reader');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize the separators of a directory name and ensure a trailing separator.
|
|
12
|
+
*
|
|
13
|
+
* @param rawDirname
|
|
14
|
+
*/ function dirnameSanitize(rawDirname) {
|
|
15
|
+
return rawDirname.replace(/[\\/]+/g, path.sep).replace(/[\\/]+$/, '') + path.sep;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether `dirname` contains at least one `.php` file, at any depth.
|
|
20
|
+
*
|
|
21
|
+
* @param dirname
|
|
22
|
+
*/ function hasPhpFile(dirname) {
|
|
23
|
+
return fs.readdirSync(dirname, {
|
|
24
|
+
withFileTypes: true
|
|
25
|
+
}).some((entry)=>{
|
|
26
|
+
if (entry.isDirectory()) return hasPhpFile(path.join(dirname, entry.name));
|
|
27
|
+
return entry.isFile() && entry.name.endsWith('.php');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
var locale = {
|
|
31
|
+
/**
|
|
32
|
+
* Locales having a `lang/{locale}/` directory containing PHP files,
|
|
33
|
+
* however deep -- a locale whose PHP all lives in a nested namespace
|
|
34
|
+
* subdirectory still counts.
|
|
35
|
+
*
|
|
36
|
+
* @param dirname
|
|
37
|
+
*/ getPhpLocale: (dirname)=>{
|
|
38
|
+
const sanitizedDirname = dirnameSanitize(dirname);
|
|
39
|
+
if (!fs.existsSync(sanitizedDirname)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return fs.readdirSync(sanitizedDirname).filter((folder)=>{
|
|
43
|
+
const fullPath = path.join(sanitizedDirname, folder);
|
|
44
|
+
return fs.statSync(fullPath).isDirectory();
|
|
45
|
+
}).filter((folder)=>hasPhpFile(path.join(sanitizedDirname, folder))).sort();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert every `lang/{locale}/{namespace}.php` file into a sibling
|
|
51
|
+
* `lang/{locale}/{namespace}.json` file, one per PHP file, so each PHP file
|
|
52
|
+
* becomes its own i18next namespace instead of being merged with the others.
|
|
53
|
+
* A PHP file may sit at any depth under the locale directory --
|
|
54
|
+
* `lang/{locale}/{ns1}/{ns2}.php` and deeper -- and gets its sibling `.json`
|
|
55
|
+
* written at that same nested path; the resulting namespace name itself is
|
|
56
|
+
* inferred later, from the generated file's path, by `src/utils/recognizer.ts`.
|
|
57
|
+
*
|
|
58
|
+
* A source meant to hold hand-written JSON (e.g. a frontend-only catalog)
|
|
59
|
+
* should never also hold PHP files of the same name -- this directory is a
|
|
60
|
+
* PHP source, so its sibling `.json` is always regenerated, overwriting
|
|
61
|
+
* whatever was there before (including a leftover from a previous, uncleaned
|
|
62
|
+
* run). Keep hand-written translations in their own source directory.
|
|
63
|
+
*
|
|
64
|
+
* @param dirname
|
|
65
|
+
*/ function parser(dirname) {
|
|
66
|
+
const sanitizedDirname = dirnameSanitize(dirname);
|
|
67
|
+
if (!fs.existsSync(sanitizedDirname)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return fs.readdirSync(sanitizedDirname).filter((locale)=>fs.statSync(path.join(sanitizedDirname, locale)).isDirectory()).sort().flatMap((locale)=>generateNamespaceFiles(path.join(sanitizedDirname, locale)));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert every `.php` file found anywhere under a locale directory into a
|
|
74
|
+
* sibling `.json` file at the same relative path, depth-first and
|
|
75
|
+
* alphabetically within each directory level.
|
|
76
|
+
*
|
|
77
|
+
* @param localeDirname
|
|
78
|
+
*/ function generateNamespaceFiles(localeDirname) {
|
|
79
|
+
return walkPhpFiles(localeDirname).sort().map((phpPath)=>{
|
|
80
|
+
const content = fs.readFileSync(phpPath, 'utf-8');
|
|
81
|
+
const translations = convertToDottedKey(phpArrayReader.fromString(content));
|
|
82
|
+
const outputPath = phpPath.replace(/\.php$/, '.json');
|
|
83
|
+
fs.writeFileSync(outputPath, JSON.stringify(translations));
|
|
84
|
+
return {
|
|
85
|
+
basename: path.basename(outputPath),
|
|
86
|
+
path: outputPath
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Every `.php` file under `dirname`, at any depth, as absolute paths.
|
|
92
|
+
*
|
|
93
|
+
* @param dirname
|
|
94
|
+
*/ function walkPhpFiles(dirname) {
|
|
95
|
+
return fs.readdirSync(dirname, {
|
|
96
|
+
withFileTypes: true
|
|
97
|
+
}).flatMap((entry)=>{
|
|
98
|
+
const entryPath = path.join(dirname, entry.name);
|
|
99
|
+
if (entry.isDirectory()) return walkPhpFiles(entryPath);
|
|
100
|
+
return entry.isFile() && entry.name.endsWith('.php') ? [
|
|
101
|
+
entryPath
|
|
102
|
+
] : [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Flatten nested translations into dotted keys, e.g. `sub_level1.sub_level2.text`.
|
|
107
|
+
*
|
|
108
|
+
* @param source
|
|
109
|
+
* @param target
|
|
110
|
+
* @param keys
|
|
111
|
+
*/ function convertToDottedKey(source, target = {}, keys = []) {
|
|
112
|
+
Object.entries(source).forEach(([key, value])=>{
|
|
113
|
+
const newPrefix = [
|
|
114
|
+
...keys,
|
|
115
|
+
key
|
|
116
|
+
];
|
|
117
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
118
|
+
convertToDottedKey(value, target, newPrefix);
|
|
119
|
+
} else {
|
|
120
|
+
target[newPrefix.join('.')] = value;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isDirectory(dir) {
|
|
127
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
128
|
+
}
|
|
129
|
+
function countWildcardSegments(normalizedPattern) {
|
|
130
|
+
return normalizedPattern.split('/').filter((segment)=>segment === '*').length;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Every directory an ordered source `pattern` currently expands to, relative
|
|
134
|
+
* to `root`. A pattern with no `*` is a single literal directory (existing or
|
|
135
|
+
* not -- callers decide what an absent literal directory means). A pattern
|
|
136
|
+
* with exactly one `*` segment (e.g. `Modules/*\/lang`) is expanded against
|
|
137
|
+
* the filesystem: every existing subdirectory of the prefix before the `*`,
|
|
138
|
+
* with the suffix after it appended, kept only when the full path also
|
|
139
|
+
* exists, sorted. Two or more `*` segments throw -- unsupported.
|
|
140
|
+
*
|
|
141
|
+
* @param pattern
|
|
142
|
+
* @param root
|
|
143
|
+
*/ function expandDirs(pattern, root) {
|
|
144
|
+
const normalized = pattern.replace(/[\\/]+/g, '/');
|
|
145
|
+
if (countWildcardSegments(normalized) > 1) {
|
|
146
|
+
throw new Error(`laravel-i18next: a source pattern may contain at most one "*" segment, got "${pattern}".`);
|
|
147
|
+
}
|
|
148
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
149
|
+
const wildcardIndex = segments.indexOf('*');
|
|
150
|
+
if (wildcardIndex === -1) {
|
|
151
|
+
const dir = path.resolve(root, normalized);
|
|
152
|
+
return isDirectory(dir) ? [
|
|
153
|
+
dir
|
|
154
|
+
] : [];
|
|
155
|
+
}
|
|
156
|
+
const prefix = path.resolve(root, segments.slice(0, wildcardIndex).join('/'));
|
|
157
|
+
const suffix = segments.slice(wildcardIndex + 1);
|
|
158
|
+
if (!isDirectory(prefix)) return [];
|
|
159
|
+
return fs.readdirSync(prefix, {
|
|
160
|
+
withFileTypes: true
|
|
161
|
+
}).filter((entry)=>entry.isDirectory()).map((entry)=>path.join(prefix, entry.name, ...suffix)).filter(isDirectory).sort();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The root-relative glob `import.meta.glob()` needs for a source pattern.
|
|
165
|
+
* Globs inside a virtual module have no containing file to be relative to,
|
|
166
|
+
* so this is always root-relative (a leading `/`). A `*` segment in the
|
|
167
|
+
* pattern passes through `path.resolve`/`path.relative`/`normalizePath`
|
|
168
|
+
* completely untouched -- none of them treat it specially -- which is why
|
|
169
|
+
* the emitted glob needs no separate wildcard handling: `Modules/*\/lang`
|
|
170
|
+
* becomes `/Modules/*\/lang/**\/*.json` and `import.meta.glob()` expands the
|
|
171
|
+
* `*` itself, natively, at both dev and build time.
|
|
172
|
+
*
|
|
173
|
+
* @param pattern
|
|
174
|
+
* @param root
|
|
175
|
+
*/ function toGlob(pattern, root) {
|
|
176
|
+
const relative = vite.normalizePath(path.relative(root, path.resolve(root, pattern.replace(/[\\/]+/g, '/'))));
|
|
177
|
+
return `/${relative}/**/*.json`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether `file` sits under one of `dirnames` (any depth) -- used to decide
|
|
181
|
+
* whether a changed file belongs to any configured source at all.
|
|
182
|
+
*
|
|
183
|
+
* @param file
|
|
184
|
+
* @param dirnames
|
|
185
|
+
*/ function isUnderAnySource(file, dirnames) {
|
|
186
|
+
const normalizedFile = vite.normalizePath(file);
|
|
187
|
+
return dirnames.some((dir)=>normalizedFile.startsWith(`${vite.normalizePath(dir)}/`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Language files, one namespace per language file: loaded eagerly by server
|
|
192
|
+
* code (SSR), so it renders translated, and lazily by client code. The
|
|
193
|
+
* default export is an ARRAY, one entry per configured source in `sources`,
|
|
194
|
+
* highest priority first (see `LocaleFileSources` in `src/interfaces/options.ts`).
|
|
195
|
+
*/ const FILES_MODULE_ID = 'virtual:laravel-i18next/files';
|
|
196
|
+
const RESOLVED_FILES_MODULE_ID = `\0${FILES_MODULE_ID}`;
|
|
197
|
+
/**
|
|
198
|
+
* Makes Laravel PHP translations available to i18next by generating a sibling
|
|
199
|
+
* `.json` file next to each `.php` file, across one or more ordered sources.
|
|
200
|
+
*/ function i18n(config) {
|
|
201
|
+
var _a;
|
|
202
|
+
const logger = vite.createLogger('info', {
|
|
203
|
+
prefix: '[laravel-i18next]'
|
|
204
|
+
});
|
|
205
|
+
const sourcePatterns = (_a = config === null || config === void 0 ? void 0 : config.sources) !== null && _a !== void 0 ? _a : [
|
|
206
|
+
'lang'
|
|
207
|
+
];
|
|
208
|
+
let root = process.cwd();
|
|
209
|
+
let generated = [];
|
|
210
|
+
let exitHandlersBound = false;
|
|
211
|
+
function clean() {
|
|
212
|
+
generated.forEach((file)=>fs.existsSync(file.path) && fs.unlinkSync(file.path));
|
|
213
|
+
generated = [];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* (Re)generate every source's sibling `.json` files. Run once up front
|
|
217
|
+
* (`config()`) and again whenever a `.php` file changes (`hotUpdate` /
|
|
218
|
+
* `handleHotUpdate`).
|
|
219
|
+
*/ function parseAll() {
|
|
220
|
+
let anyPhp = false;
|
|
221
|
+
generated = sourcePatterns.flatMap((pattern)=>{
|
|
222
|
+
const dirnames = expandDirs(pattern, root);
|
|
223
|
+
// A literal (wildcard-free) source that does not exist is very likely
|
|
224
|
+
// a setup mistake -- a wrong path, or (Laravel 9+) `php artisan
|
|
225
|
+
// lang:publish` was never run.
|
|
226
|
+
// A wildcard source matching nothing (no modules yet) is normal, so it
|
|
227
|
+
// stays silent.
|
|
228
|
+
if (dirnames.length === 0 && !pattern.includes('*')) {
|
|
229
|
+
logger.error(`Source "${pattern}" does not exist. Check the "sources" option (Laravel 9+ keeps language files in "lang", older versions in "resources/lang"), or on Laravel 9+ publish them with \`php artisan lang:publish\`.`, {
|
|
230
|
+
timestamp: true
|
|
231
|
+
});
|
|
232
|
+
logger.error('For more information please visit: https://laravel.com/docs/10.x/localization#publishing-the-language-files', {
|
|
233
|
+
timestamp: true
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return dirnames.flatMap((dirname)=>{
|
|
237
|
+
if (locale.getPhpLocale(dirname).length > 0) anyPhp = true;
|
|
238
|
+
return parser(dirname);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
if (!anyPhp) {
|
|
242
|
+
logger.info('No source contains PHP translation files.', {
|
|
243
|
+
timestamp: true
|
|
244
|
+
});
|
|
245
|
+
logger.info('For more information please visit: https://laravel.com/docs/10.x/localization#introduction', {
|
|
246
|
+
timestamp: true
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return generated;
|
|
250
|
+
}
|
|
251
|
+
function isTranslationSource(file) {
|
|
252
|
+
return sourcePatterns.some((pattern)=>isUnderAnySource(file, expandDirs(pattern, root)));
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
name: 'i18n',
|
|
256
|
+
enforce: 'post',
|
|
257
|
+
configResolved (resolvedConfig) {
|
|
258
|
+
root = resolvedConfig.root;
|
|
259
|
+
},
|
|
260
|
+
resolveId: {
|
|
261
|
+
order: 'pre',
|
|
262
|
+
handler (id) {
|
|
263
|
+
return id === FILES_MODULE_ID ? RESOLVED_FILES_MODULE_ID : undefined;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
load (id, options) {
|
|
267
|
+
if (id !== RESOLVED_FILES_MODULE_ID) return undefined;
|
|
268
|
+
// Globs of virtual modules must be relative to the root. Recursive, so
|
|
269
|
+
// each also matches namespaces nested any number of levels deep.
|
|
270
|
+
const entries = sourcePatterns.map((pattern)=>JSON.stringify(toGlob(pattern, root))).map((pattern)=>(options === null || options === void 0 ? void 0 : options.ssr) ? `import.meta.glob(${pattern}, { eager: true })` : `import.meta.glob(${pattern})`).join(', ');
|
|
271
|
+
return `export default [${entries}];`;
|
|
272
|
+
},
|
|
273
|
+
config (userConfig) {
|
|
274
|
+
var _a;
|
|
275
|
+
// `config()` runs before `configResolved()`, so `root` is not yet the
|
|
276
|
+
// fully resolved one here -- close enough for source expansion, and
|
|
277
|
+
// `configResolved` overwrites it before `load()` ever runs.
|
|
278
|
+
root = path.resolve((_a = userConfig === null || userConfig === void 0 ? void 0 : userConfig.root) !== null && _a !== void 0 ? _a : process.cwd());
|
|
279
|
+
parseAll();
|
|
280
|
+
},
|
|
281
|
+
buildEnd: clean,
|
|
282
|
+
// `hotUpdate` (Vite 6+) is the modern hook; `handleHotUpdate` is kept for
|
|
283
|
+
// the `vite >= 5` peer range and is ignored by Vite whenever `hotUpdate`
|
|
284
|
+
// is also defined. Neither hook needs to invalidate the virtual module
|
|
285
|
+
// for an ADDED or DELETED json file, nor for a CHANGED one -- Vite's own
|
|
286
|
+
// `import.meta.glob` HMR already does that, virtual ids included. What
|
|
287
|
+
// is not free: a brand-new `.php` file has no sibling `.json` yet for
|
|
288
|
+
// that glob to notice, so it must be parsed here first.
|
|
289
|
+
hotUpdate (options) {
|
|
290
|
+
var _a;
|
|
291
|
+
if (!isTranslationSource(options.file)) return undefined;
|
|
292
|
+
if (options.file.endsWith('.php')) parseAll();
|
|
293
|
+
const filesModule = this.environment.moduleGraph.getModuleById(RESOLVED_FILES_MODULE_ID);
|
|
294
|
+
if (!filesModule) return undefined;
|
|
295
|
+
return [
|
|
296
|
+
...(_a = this.environment.moduleGraph.getModulesByFile(options.file)) !== null && _a !== void 0 ? _a : [],
|
|
297
|
+
filesModule
|
|
298
|
+
];
|
|
299
|
+
},
|
|
300
|
+
handleHotUpdate (ctx) {
|
|
301
|
+
if (isTranslationSource(ctx.file) && ctx.file.endsWith('.php')) parseAll();
|
|
302
|
+
},
|
|
303
|
+
configureServer () {
|
|
304
|
+
if (exitHandlersBound) return;
|
|
305
|
+
process.on('exit', clean);
|
|
306
|
+
process.on('SIGINT', process.exit);
|
|
307
|
+
process.on('SIGTERM', process.exit);
|
|
308
|
+
process.on('SIGHUP', process.exit);
|
|
309
|
+
exitHandlersBound = true;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
exports.FILES_MODULE_ID = FILES_MODULE_ID;
|
|
315
|
+
exports.default = i18n;
|
package/dist/vite.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { normalizePath, createLogger } from 'vite';
|
|
4
|
+
import { fromString } from 'php-array-reader';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize the separators of a directory name and ensure a trailing separator.
|
|
8
|
+
*
|
|
9
|
+
* @param rawDirname
|
|
10
|
+
*/ function dirnameSanitize(rawDirname) {
|
|
11
|
+
return rawDirname.replace(/[\\/]+/g, path.sep).replace(/[\\/]+$/, '') + path.sep;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether `dirname` contains at least one `.php` file, at any depth.
|
|
16
|
+
*
|
|
17
|
+
* @param dirname
|
|
18
|
+
*/ function hasPhpFile(dirname) {
|
|
19
|
+
return fs.readdirSync(dirname, {
|
|
20
|
+
withFileTypes: true
|
|
21
|
+
}).some((entry)=>{
|
|
22
|
+
if (entry.isDirectory()) return hasPhpFile(path.join(dirname, entry.name));
|
|
23
|
+
return entry.isFile() && entry.name.endsWith('.php');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
var locale = {
|
|
27
|
+
/**
|
|
28
|
+
* Locales having a `lang/{locale}/` directory containing PHP files,
|
|
29
|
+
* however deep -- a locale whose PHP all lives in a nested namespace
|
|
30
|
+
* subdirectory still counts.
|
|
31
|
+
*
|
|
32
|
+
* @param dirname
|
|
33
|
+
*/ getPhpLocale: (dirname)=>{
|
|
34
|
+
const sanitizedDirname = dirnameSanitize(dirname);
|
|
35
|
+
if (!fs.existsSync(sanitizedDirname)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return fs.readdirSync(sanitizedDirname).filter((folder)=>{
|
|
39
|
+
const fullPath = path.join(sanitizedDirname, folder);
|
|
40
|
+
return fs.statSync(fullPath).isDirectory();
|
|
41
|
+
}).filter((folder)=>hasPhpFile(path.join(sanitizedDirname, folder))).sort();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert every `lang/{locale}/{namespace}.php` file into a sibling
|
|
47
|
+
* `lang/{locale}/{namespace}.json` file, one per PHP file, so each PHP file
|
|
48
|
+
* becomes its own i18next namespace instead of being merged with the others.
|
|
49
|
+
* A PHP file may sit at any depth under the locale directory --
|
|
50
|
+
* `lang/{locale}/{ns1}/{ns2}.php` and deeper -- and gets its sibling `.json`
|
|
51
|
+
* written at that same nested path; the resulting namespace name itself is
|
|
52
|
+
* inferred later, from the generated file's path, by `src/utils/recognizer.ts`.
|
|
53
|
+
*
|
|
54
|
+
* A source meant to hold hand-written JSON (e.g. a frontend-only catalog)
|
|
55
|
+
* should never also hold PHP files of the same name -- this directory is a
|
|
56
|
+
* PHP source, so its sibling `.json` is always regenerated, overwriting
|
|
57
|
+
* whatever was there before (including a leftover from a previous, uncleaned
|
|
58
|
+
* run). Keep hand-written translations in their own source directory.
|
|
59
|
+
*
|
|
60
|
+
* @param dirname
|
|
61
|
+
*/ function parser(dirname) {
|
|
62
|
+
const sanitizedDirname = dirnameSanitize(dirname);
|
|
63
|
+
if (!fs.existsSync(sanitizedDirname)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return fs.readdirSync(sanitizedDirname).filter((locale)=>fs.statSync(path.join(sanitizedDirname, locale)).isDirectory()).sort().flatMap((locale)=>generateNamespaceFiles(path.join(sanitizedDirname, locale)));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convert every `.php` file found anywhere under a locale directory into a
|
|
70
|
+
* sibling `.json` file at the same relative path, depth-first and
|
|
71
|
+
* alphabetically within each directory level.
|
|
72
|
+
*
|
|
73
|
+
* @param localeDirname
|
|
74
|
+
*/ function generateNamespaceFiles(localeDirname) {
|
|
75
|
+
return walkPhpFiles(localeDirname).sort().map((phpPath)=>{
|
|
76
|
+
const content = fs.readFileSync(phpPath, 'utf-8');
|
|
77
|
+
const translations = convertToDottedKey(fromString(content));
|
|
78
|
+
const outputPath = phpPath.replace(/\.php$/, '.json');
|
|
79
|
+
fs.writeFileSync(outputPath, JSON.stringify(translations));
|
|
80
|
+
return {
|
|
81
|
+
basename: path.basename(outputPath),
|
|
82
|
+
path: outputPath
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every `.php` file under `dirname`, at any depth, as absolute paths.
|
|
88
|
+
*
|
|
89
|
+
* @param dirname
|
|
90
|
+
*/ function walkPhpFiles(dirname) {
|
|
91
|
+
return fs.readdirSync(dirname, {
|
|
92
|
+
withFileTypes: true
|
|
93
|
+
}).flatMap((entry)=>{
|
|
94
|
+
const entryPath = path.join(dirname, entry.name);
|
|
95
|
+
if (entry.isDirectory()) return walkPhpFiles(entryPath);
|
|
96
|
+
return entry.isFile() && entry.name.endsWith('.php') ? [
|
|
97
|
+
entryPath
|
|
98
|
+
] : [];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Flatten nested translations into dotted keys, e.g. `sub_level1.sub_level2.text`.
|
|
103
|
+
*
|
|
104
|
+
* @param source
|
|
105
|
+
* @param target
|
|
106
|
+
* @param keys
|
|
107
|
+
*/ function convertToDottedKey(source, target = {}, keys = []) {
|
|
108
|
+
Object.entries(source).forEach(([key, value])=>{
|
|
109
|
+
const newPrefix = [
|
|
110
|
+
...keys,
|
|
111
|
+
key
|
|
112
|
+
];
|
|
113
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
114
|
+
convertToDottedKey(value, target, newPrefix);
|
|
115
|
+
} else {
|
|
116
|
+
target[newPrefix.join('.')] = value;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return target;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isDirectory(dir) {
|
|
123
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
124
|
+
}
|
|
125
|
+
function countWildcardSegments(normalizedPattern) {
|
|
126
|
+
return normalizedPattern.split('/').filter((segment)=>segment === '*').length;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Every directory an ordered source `pattern` currently expands to, relative
|
|
130
|
+
* to `root`. A pattern with no `*` is a single literal directory (existing or
|
|
131
|
+
* not -- callers decide what an absent literal directory means). A pattern
|
|
132
|
+
* with exactly one `*` segment (e.g. `Modules/*\/lang`) is expanded against
|
|
133
|
+
* the filesystem: every existing subdirectory of the prefix before the `*`,
|
|
134
|
+
* with the suffix after it appended, kept only when the full path also
|
|
135
|
+
* exists, sorted. Two or more `*` segments throw -- unsupported.
|
|
136
|
+
*
|
|
137
|
+
* @param pattern
|
|
138
|
+
* @param root
|
|
139
|
+
*/ function expandDirs(pattern, root) {
|
|
140
|
+
const normalized = pattern.replace(/[\\/]+/g, '/');
|
|
141
|
+
if (countWildcardSegments(normalized) > 1) {
|
|
142
|
+
throw new Error(`laravel-i18next: a source pattern may contain at most one "*" segment, got "${pattern}".`);
|
|
143
|
+
}
|
|
144
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
145
|
+
const wildcardIndex = segments.indexOf('*');
|
|
146
|
+
if (wildcardIndex === -1) {
|
|
147
|
+
const dir = path.resolve(root, normalized);
|
|
148
|
+
return isDirectory(dir) ? [
|
|
149
|
+
dir
|
|
150
|
+
] : [];
|
|
151
|
+
}
|
|
152
|
+
const prefix = path.resolve(root, segments.slice(0, wildcardIndex).join('/'));
|
|
153
|
+
const suffix = segments.slice(wildcardIndex + 1);
|
|
154
|
+
if (!isDirectory(prefix)) return [];
|
|
155
|
+
return fs.readdirSync(prefix, {
|
|
156
|
+
withFileTypes: true
|
|
157
|
+
}).filter((entry)=>entry.isDirectory()).map((entry)=>path.join(prefix, entry.name, ...suffix)).filter(isDirectory).sort();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The root-relative glob `import.meta.glob()` needs for a source pattern.
|
|
161
|
+
* Globs inside a virtual module have no containing file to be relative to,
|
|
162
|
+
* so this is always root-relative (a leading `/`). A `*` segment in the
|
|
163
|
+
* pattern passes through `path.resolve`/`path.relative`/`normalizePath`
|
|
164
|
+
* completely untouched -- none of them treat it specially -- which is why
|
|
165
|
+
* the emitted glob needs no separate wildcard handling: `Modules/*\/lang`
|
|
166
|
+
* becomes `/Modules/*\/lang/**\/*.json` and `import.meta.glob()` expands the
|
|
167
|
+
* `*` itself, natively, at both dev and build time.
|
|
168
|
+
*
|
|
169
|
+
* @param pattern
|
|
170
|
+
* @param root
|
|
171
|
+
*/ function toGlob(pattern, root) {
|
|
172
|
+
const relative = normalizePath(path.relative(root, path.resolve(root, pattern.replace(/[\\/]+/g, '/'))));
|
|
173
|
+
return `/${relative}/**/*.json`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether `file` sits under one of `dirnames` (any depth) -- used to decide
|
|
177
|
+
* whether a changed file belongs to any configured source at all.
|
|
178
|
+
*
|
|
179
|
+
* @param file
|
|
180
|
+
* @param dirnames
|
|
181
|
+
*/ function isUnderAnySource(file, dirnames) {
|
|
182
|
+
const normalizedFile = normalizePath(file);
|
|
183
|
+
return dirnames.some((dir)=>normalizedFile.startsWith(`${normalizePath(dir)}/`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Language files, one namespace per language file: loaded eagerly by server
|
|
188
|
+
* code (SSR), so it renders translated, and lazily by client code. The
|
|
189
|
+
* default export is an ARRAY, one entry per configured source in `sources`,
|
|
190
|
+
* highest priority first (see `LocaleFileSources` in `src/interfaces/options.ts`).
|
|
191
|
+
*/ const FILES_MODULE_ID = 'virtual:laravel-i18next/files';
|
|
192
|
+
const RESOLVED_FILES_MODULE_ID = `\0${FILES_MODULE_ID}`;
|
|
193
|
+
/**
|
|
194
|
+
* Makes Laravel PHP translations available to i18next by generating a sibling
|
|
195
|
+
* `.json` file next to each `.php` file, across one or more ordered sources.
|
|
196
|
+
*/ function i18n(config) {
|
|
197
|
+
var _a;
|
|
198
|
+
const logger = createLogger('info', {
|
|
199
|
+
prefix: '[laravel-i18next]'
|
|
200
|
+
});
|
|
201
|
+
const sourcePatterns = (_a = config === null || config === void 0 ? void 0 : config.sources) !== null && _a !== void 0 ? _a : [
|
|
202
|
+
'lang'
|
|
203
|
+
];
|
|
204
|
+
let root = process.cwd();
|
|
205
|
+
let generated = [];
|
|
206
|
+
let exitHandlersBound = false;
|
|
207
|
+
function clean() {
|
|
208
|
+
generated.forEach((file)=>fs.existsSync(file.path) && fs.unlinkSync(file.path));
|
|
209
|
+
generated = [];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* (Re)generate every source's sibling `.json` files. Run once up front
|
|
213
|
+
* (`config()`) and again whenever a `.php` file changes (`hotUpdate` /
|
|
214
|
+
* `handleHotUpdate`).
|
|
215
|
+
*/ function parseAll() {
|
|
216
|
+
let anyPhp = false;
|
|
217
|
+
generated = sourcePatterns.flatMap((pattern)=>{
|
|
218
|
+
const dirnames = expandDirs(pattern, root);
|
|
219
|
+
// A literal (wildcard-free) source that does not exist is very likely
|
|
220
|
+
// a setup mistake -- a wrong path, or (Laravel 9+) `php artisan
|
|
221
|
+
// lang:publish` was never run.
|
|
222
|
+
// A wildcard source matching nothing (no modules yet) is normal, so it
|
|
223
|
+
// stays silent.
|
|
224
|
+
if (dirnames.length === 0 && !pattern.includes('*')) {
|
|
225
|
+
logger.error(`Source "${pattern}" does not exist. Check the "sources" option (Laravel 9+ keeps language files in "lang", older versions in "resources/lang"), or on Laravel 9+ publish them with \`php artisan lang:publish\`.`, {
|
|
226
|
+
timestamp: true
|
|
227
|
+
});
|
|
228
|
+
logger.error('For more information please visit: https://laravel.com/docs/10.x/localization#publishing-the-language-files', {
|
|
229
|
+
timestamp: true
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return dirnames.flatMap((dirname)=>{
|
|
233
|
+
if (locale.getPhpLocale(dirname).length > 0) anyPhp = true;
|
|
234
|
+
return parser(dirname);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
if (!anyPhp) {
|
|
238
|
+
logger.info('No source contains PHP translation files.', {
|
|
239
|
+
timestamp: true
|
|
240
|
+
});
|
|
241
|
+
logger.info('For more information please visit: https://laravel.com/docs/10.x/localization#introduction', {
|
|
242
|
+
timestamp: true
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return generated;
|
|
246
|
+
}
|
|
247
|
+
function isTranslationSource(file) {
|
|
248
|
+
return sourcePatterns.some((pattern)=>isUnderAnySource(file, expandDirs(pattern, root)));
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
name: 'i18n',
|
|
252
|
+
enforce: 'post',
|
|
253
|
+
configResolved (resolvedConfig) {
|
|
254
|
+
root = resolvedConfig.root;
|
|
255
|
+
},
|
|
256
|
+
resolveId: {
|
|
257
|
+
order: 'pre',
|
|
258
|
+
handler (id) {
|
|
259
|
+
return id === FILES_MODULE_ID ? RESOLVED_FILES_MODULE_ID : undefined;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
load (id, options) {
|
|
263
|
+
if (id !== RESOLVED_FILES_MODULE_ID) return undefined;
|
|
264
|
+
// Globs of virtual modules must be relative to the root. Recursive, so
|
|
265
|
+
// each also matches namespaces nested any number of levels deep.
|
|
266
|
+
const entries = sourcePatterns.map((pattern)=>JSON.stringify(toGlob(pattern, root))).map((pattern)=>(options === null || options === void 0 ? void 0 : options.ssr) ? `import.meta.glob(${pattern}, { eager: true })` : `import.meta.glob(${pattern})`).join(', ');
|
|
267
|
+
return `export default [${entries}];`;
|
|
268
|
+
},
|
|
269
|
+
config (userConfig) {
|
|
270
|
+
var _a;
|
|
271
|
+
// `config()` runs before `configResolved()`, so `root` is not yet the
|
|
272
|
+
// fully resolved one here -- close enough for source expansion, and
|
|
273
|
+
// `configResolved` overwrites it before `load()` ever runs.
|
|
274
|
+
root = path.resolve((_a = userConfig === null || userConfig === void 0 ? void 0 : userConfig.root) !== null && _a !== void 0 ? _a : process.cwd());
|
|
275
|
+
parseAll();
|
|
276
|
+
},
|
|
277
|
+
buildEnd: clean,
|
|
278
|
+
// `hotUpdate` (Vite 6+) is the modern hook; `handleHotUpdate` is kept for
|
|
279
|
+
// the `vite >= 5` peer range and is ignored by Vite whenever `hotUpdate`
|
|
280
|
+
// is also defined. Neither hook needs to invalidate the virtual module
|
|
281
|
+
// for an ADDED or DELETED json file, nor for a CHANGED one -- Vite's own
|
|
282
|
+
// `import.meta.glob` HMR already does that, virtual ids included. What
|
|
283
|
+
// is not free: a brand-new `.php` file has no sibling `.json` yet for
|
|
284
|
+
// that glob to notice, so it must be parsed here first.
|
|
285
|
+
hotUpdate (options) {
|
|
286
|
+
var _a;
|
|
287
|
+
if (!isTranslationSource(options.file)) return undefined;
|
|
288
|
+
if (options.file.endsWith('.php')) parseAll();
|
|
289
|
+
const filesModule = this.environment.moduleGraph.getModuleById(RESOLVED_FILES_MODULE_ID);
|
|
290
|
+
if (!filesModule) return undefined;
|
|
291
|
+
return [
|
|
292
|
+
...(_a = this.environment.moduleGraph.getModulesByFile(options.file)) !== null && _a !== void 0 ? _a : [],
|
|
293
|
+
filesModule
|
|
294
|
+
];
|
|
295
|
+
},
|
|
296
|
+
handleHotUpdate (ctx) {
|
|
297
|
+
if (isTranslationSource(ctx.file) && ctx.file.endsWith('.php')) parseAll();
|
|
298
|
+
},
|
|
299
|
+
configureServer () {
|
|
300
|
+
if (exitHandlersBound) return;
|
|
301
|
+
process.on('exit', clean);
|
|
302
|
+
process.on('SIGINT', process.exit);
|
|
303
|
+
process.on('SIGTERM', process.exit);
|
|
304
|
+
process.on('SIGHUP', process.exit);
|
|
305
|
+
exitHandlersBound = true;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export { FILES_MODULE_ID, i18n as default };
|
package/dist/vue.cjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var I18NextVue = require('i18next-vue');
|
|
4
|
+
var createI18n = require('./shared/create-i18n-WsDK4Z8L.cjs');
|
|
5
|
+
require('i18next');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vue plugin providing an i18next instance translating Laravel language files
|
|
9
|
+
* to `i18next-vue`'s `useTranslation()` composable and `$t`/`$i18next`.
|
|
10
|
+
*
|
|
11
|
+
* Every app instance owns its own i18next instance, so concurrent SSR requests
|
|
12
|
+
* do not share a language.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createApp } from 'vue';
|
|
16
|
+
* import { laravelVueI18n } from '@genrwork/laravel-i18next/vue';
|
|
17
|
+
*
|
|
18
|
+
* createApp(App)
|
|
19
|
+
* .use(laravelVueI18n({ locale: 'uk', fallbackLocale: 'en', files: import.meta.glob('/lang/**\/*.json') }))
|
|
20
|
+
* .mount('#app');
|
|
21
|
+
* ```
|
|
22
|
+
*/ function laravelVueI18n(options) {
|
|
23
|
+
return {
|
|
24
|
+
install (app) {
|
|
25
|
+
const i18next = createI18n.createI18n(options);
|
|
26
|
+
// Lives for the app's lifetime, so it is never unsubscribed.
|
|
27
|
+
createI18n.syncDocumentLang(i18next);
|
|
28
|
+
app.use(I18NextVue, {
|
|
29
|
+
i18next
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Object.defineProperty(exports, "useTranslation", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function () { return I18NextVue.useTranslation; }
|
|
38
|
+
});
|
|
39
|
+
exports.laravelVueI18n = laravelVueI18n;
|