@nuraly/lumenjs 0.1.0 → 0.1.1
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/dist/build/build.js +19 -1
- package/dist/build/serve-i18n.d.ts +6 -0
- package/dist/build/serve-i18n.js +26 -0
- package/dist/build/serve-loaders.js +7 -2
- package/dist/build/serve-ssr.js +3 -3
- package/dist/build/serve.js +20 -4
- package/dist/dev-server/config.d.ts +6 -0
- package/dist/dev-server/config.js +27 -1
- package/dist/dev-server/index-html.d.ts +7 -0
- package/dist/dev-server/index-html.js +15 -2
- package/dist/dev-server/middleware/locale.d.ts +20 -0
- package/dist/dev-server/middleware/locale.js +55 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.d.ts +15 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +71 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +16 -2
- package/dist/dev-server/plugins/vite-plugin-routes.js +1 -11
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +8 -1
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +52 -22
- package/dist/dev-server/server.d.ts +1 -1
- package/dist/dev-server/server.js +37 -10
- package/dist/dev-server/ssr-render.d.ts +1 -1
- package/dist/dev-server/ssr-render.js +23 -8
- package/dist/editor/editor-bridge.d.ts +1 -1
- package/dist/editor/inline-text-edit.js +15 -4
- package/dist/runtime/i18n.d.ts +56 -0
- package/dist/runtime/i18n.js +100 -0
- package/dist/runtime/router-data.js +9 -0
- package/dist/runtime/router-hydration.js +15 -1
- package/dist/runtime/router.d.ts +4 -0
- package/dist/runtime/router.js +20 -4
- package/dist/shared/types.d.ts +7 -0
- package/dist/shared/utils.d.ts +7 -0
- package/dist/shared/utils.js +16 -0
- package/package.json +1 -1
package/dist/build/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import { getSharedViteConfig } from '../dev-server/server.js';
|
|
5
5
|
import { readProjectConfig } from '../dev-server/config.js';
|
|
6
6
|
import { generateIndexHtml } from '../dev-server/index-html.js';
|
|
7
|
+
import { filePathToTagName } from '../shared/utils.js';
|
|
7
8
|
import { scanPages, scanLayouts, scanApiRoutes, getLayoutDirsForPage } from './scan.js';
|
|
8
9
|
export async function buildProject(options) {
|
|
9
10
|
const { projectDir } = options;
|
|
@@ -18,7 +19,7 @@ export async function buildProject(options) {
|
|
|
18
19
|
fs.rmSync(outDir, { recursive: true });
|
|
19
20
|
}
|
|
20
21
|
fs.mkdirSync(outDir, { recursive: true });
|
|
21
|
-
const { title, integrations } = readProjectConfig(projectDir);
|
|
22
|
+
const { title, integrations, i18n: i18nConfig } = readProjectConfig(projectDir);
|
|
22
23
|
const shared = getSharedViteConfig(projectDir, { mode: 'production', integrations });
|
|
23
24
|
// Scan pages, layouts, and API routes for the manifest
|
|
24
25
|
const pageEntries = scanPages(pagesDir);
|
|
@@ -141,14 +142,30 @@ export async function buildProject(options) {
|
|
|
141
142
|
fs.unlinkSync(ssrEntryPath);
|
|
142
143
|
}
|
|
143
144
|
}
|
|
145
|
+
// --- Copy locales ---
|
|
146
|
+
if (i18nConfig) {
|
|
147
|
+
const localesDir = path.join(projectDir, 'locales');
|
|
148
|
+
const outLocalesDir = path.join(outDir, 'locales');
|
|
149
|
+
if (fs.existsSync(localesDir)) {
|
|
150
|
+
fs.mkdirSync(outLocalesDir, { recursive: true });
|
|
151
|
+
for (const file of fs.readdirSync(localesDir)) {
|
|
152
|
+
if (file.endsWith('.json')) {
|
|
153
|
+
fs.copyFileSync(path.join(localesDir, file), path.join(outLocalesDir, file));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log(`[LumenJS] Copied ${i18nConfig.locales.length} locale(s) to output.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
144
159
|
// --- Write manifest ---
|
|
145
160
|
const manifest = {
|
|
146
161
|
routes: pageEntries.map(e => {
|
|
147
162
|
const routeLayouts = getLayoutDirsForPage(e.filePath, pagesDir, layoutEntries);
|
|
163
|
+
const relPath = path.relative(pagesDir, e.filePath).replace(/\\/g, '/');
|
|
148
164
|
return {
|
|
149
165
|
path: e.routePath,
|
|
150
166
|
module: e.hasLoader ? `pages/${e.name}.js` : '',
|
|
151
167
|
hasLoader: e.hasLoader,
|
|
168
|
+
tagName: filePathToTagName(relPath),
|
|
152
169
|
...(routeLayouts.length > 0 ? { layouts: routeLayouts } : {}),
|
|
153
170
|
};
|
|
154
171
|
}),
|
|
@@ -162,6 +179,7 @@ export async function buildProject(options) {
|
|
|
162
179
|
module: e.hasLoader ? (e.dir ? `layouts/${e.dir}/_layout.js` : 'layouts/_layout.js') : '',
|
|
163
180
|
hasLoader: e.hasLoader,
|
|
164
181
|
})),
|
|
182
|
+
...(i18nConfig ? { i18n: i18nConfig } : {}),
|
|
165
183
|
};
|
|
166
184
|
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
167
185
|
console.log('[LumenJS] Build complete.');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
/**
|
|
3
|
+
* Handle `/__nk_i18n/<locale>.json` requests in production.
|
|
4
|
+
* Reads from the built `locales/` directory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handleI18nRequest(localesDir: string, locales: string[], pathname: string, req: http.IncomingMessage, res: http.ServerResponse): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { sendCompressed } from './serve-static.js';
|
|
4
|
+
/**
|
|
5
|
+
* Handle `/__nk_i18n/<locale>.json` requests in production.
|
|
6
|
+
* Reads from the built `locales/` directory.
|
|
7
|
+
*/
|
|
8
|
+
export function handleI18nRequest(localesDir, locales, pathname, req, res) {
|
|
9
|
+
const match = pathname.match(/^\/__nk_i18n\/([a-z]{2}(?:-[a-zA-Z]+)?)\.json$/);
|
|
10
|
+
if (!match)
|
|
11
|
+
return false;
|
|
12
|
+
const locale = match[1];
|
|
13
|
+
if (!locales.includes(locale)) {
|
|
14
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
15
|
+
res.end(JSON.stringify({ error: 'Unknown locale' }));
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const filePath = path.join(localesDir, `${locale}.json`);
|
|
19
|
+
if (!fs.existsSync(filePath)) {
|
|
20
|
+
sendCompressed(req, res, 200, 'application/json', '{}');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
24
|
+
sendCompressed(req, res, 200, 'application/json', content);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
@@ -31,7 +31,10 @@ export async function handleLayoutLoaderRequest(manifest, serverDir, queryString
|
|
|
31
31
|
res.end(JSON.stringify({ __nk_no_loader: true }));
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
// Parse locale from query for layout loader
|
|
35
|
+
const locale = query.__locale;
|
|
36
|
+
delete query.__locale;
|
|
37
|
+
const result = await mod.loader({ params: {}, query: {}, url: `/__layout/${dir}`, headers, locale });
|
|
35
38
|
if (isRedirectResponse(result)) {
|
|
36
39
|
res.writeHead(result.status || 302, { Location: result.location });
|
|
37
40
|
res.end();
|
|
@@ -91,7 +94,9 @@ export async function handleLoaderRequest(manifest, serverDir, pagesDir, pathnam
|
|
|
91
94
|
res.end(JSON.stringify({ __nk_no_loader: true }));
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
|
-
const
|
|
97
|
+
const locale = query.__locale;
|
|
98
|
+
delete query.__locale;
|
|
99
|
+
const result = await mod.loader({ params: matched.params, query, url: pagePath, headers, locale });
|
|
95
100
|
if (isRedirectResponse(result)) {
|
|
96
101
|
res.writeHead(result.status || 302, { Location: result.location });
|
|
97
102
|
res.end();
|
package/dist/build/serve-ssr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { stripOuterLitMarkers, dirToLayoutTagName,
|
|
3
|
+
import { stripOuterLitMarkers, dirToLayoutTagName, isRedirectResponse } from '../shared/utils.js';
|
|
4
4
|
import { matchRoute } from '../shared/route-matching.js';
|
|
5
5
|
import { sendCompressed } from './serve-static.js';
|
|
6
6
|
export async function handlePageRoute(manifest, serverDir, pagesDir, pathname, queryString, indexHtmlShell, title, ssrRuntime, req, res) {
|
|
@@ -23,8 +23,8 @@ export async function handlePageRoute(manifest, serverDir, pagesDir, pathname, q
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
//
|
|
27
|
-
const tagName =
|
|
26
|
+
// Use tag name from route manifest (matches client router)
|
|
27
|
+
const tagName = matched.route.tagName;
|
|
28
28
|
// Run layout loaders
|
|
29
29
|
const layoutDirs = (allMatched || matched).route.layouts || [];
|
|
30
30
|
const layoutsData = [];
|
package/dist/build/serve.js
CHANGED
|
@@ -8,6 +8,8 @@ import { handleApiRoute } from './serve-api.js';
|
|
|
8
8
|
import { handleLoaderRequest, handleLayoutLoaderRequest } from './serve-loaders.js';
|
|
9
9
|
import { handlePageRoute } from './serve-ssr.js';
|
|
10
10
|
import { renderErrorPage } from './error-page.js';
|
|
11
|
+
import { handleI18nRequest } from './serve-i18n.js';
|
|
12
|
+
import { resolveLocale } from '../dev-server/middleware/locale.js';
|
|
11
13
|
export async function serveProject(options) {
|
|
12
14
|
const { projectDir } = options;
|
|
13
15
|
const port = options.port || 3000;
|
|
@@ -21,6 +23,7 @@ export async function serveProject(options) {
|
|
|
21
23
|
}
|
|
22
24
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
23
25
|
const { title } = readProjectConfig(projectDir);
|
|
26
|
+
const localesDir = path.join(outDir, 'locales');
|
|
24
27
|
// Read the built index.html shell
|
|
25
28
|
const indexHtmlPath = path.join(clientDir, 'index.html');
|
|
26
29
|
if (!fs.existsSync(indexHtmlPath)) {
|
|
@@ -54,18 +57,31 @@ export async function serveProject(options) {
|
|
|
54
57
|
if (served)
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
|
-
// 3.
|
|
60
|
+
// 3. i18n translation endpoint
|
|
61
|
+
if (pathname.startsWith('/__nk_i18n/') && manifest.i18n) {
|
|
62
|
+
handleI18nRequest(localesDir, manifest.i18n.locales, pathname, req, res);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// 4. Layout loader endpoint
|
|
58
66
|
if (pathname === '/__nk_loader/__layout/' || pathname === '/__nk_loader/__layout') {
|
|
59
67
|
await handleLayoutLoaderRequest(manifest, serverDir, queryString, req.headers, res);
|
|
60
68
|
return;
|
|
61
69
|
}
|
|
62
|
-
//
|
|
70
|
+
// 5. Loader endpoint for client-side navigation
|
|
63
71
|
if (pathname.startsWith('/__nk_loader/')) {
|
|
64
72
|
await handleLoaderRequest(manifest, serverDir, pagesDir, pathname, queryString, req.headers, res);
|
|
65
73
|
return;
|
|
66
74
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
75
|
+
// 6. Resolve locale and strip prefix for page routing
|
|
76
|
+
let resolvedPathname = pathname;
|
|
77
|
+
let locale;
|
|
78
|
+
if (manifest.i18n) {
|
|
79
|
+
const result = resolveLocale(pathname, manifest.i18n, req.headers);
|
|
80
|
+
resolvedPathname = result.pathname;
|
|
81
|
+
locale = result.locale;
|
|
82
|
+
}
|
|
83
|
+
// 7. Page routes — SSR render
|
|
84
|
+
await handlePageRoute(manifest, serverDir, pagesDir, resolvedPathname, queryString, indexHtmlShell, title, ssrRuntime, req, res);
|
|
69
85
|
}
|
|
70
86
|
catch (err) {
|
|
71
87
|
console.error('[LumenJS] Request error:', err);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
export interface I18nConfig {
|
|
2
|
+
locales: string[];
|
|
3
|
+
defaultLocale: string;
|
|
4
|
+
prefixDefault: boolean;
|
|
5
|
+
}
|
|
1
6
|
export interface ProjectConfig {
|
|
2
7
|
title: string;
|
|
3
8
|
integrations: string[];
|
|
9
|
+
i18n?: I18nConfig;
|
|
4
10
|
}
|
|
5
11
|
/**
|
|
6
12
|
* Reads the project config from lumenjs.config.ts.
|
|
@@ -26,7 +26,33 @@ export function readProjectConfig(projectDir) {
|
|
|
26
26
|
}
|
|
27
27
|
catch { /* use defaults */ }
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
// Parse i18n config (reuse the same file read)
|
|
30
|
+
let i18n;
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
+
const i18nMatch = configContent.match(/i18n\s*:\s*\{([\s\S]*?)\}/);
|
|
35
|
+
if (i18nMatch) {
|
|
36
|
+
const block = i18nMatch[1];
|
|
37
|
+
const localesMatch = block.match(/locales\s*:\s*\[([^\]]*)\]/);
|
|
38
|
+
const defaultMatch = block.match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
39
|
+
const prefixMatch = block.match(/prefixDefault\s*:\s*(true|false)/);
|
|
40
|
+
if (localesMatch && defaultMatch) {
|
|
41
|
+
const locales = localesMatch[1]
|
|
42
|
+
.split(',')
|
|
43
|
+
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
i18n = {
|
|
46
|
+
locales,
|
|
47
|
+
defaultLocale: defaultMatch[1],
|
|
48
|
+
prefixDefault: prefixMatch ? prefixMatch[1] === 'true' : false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
return { title, integrations, ...(i18n ? { i18n } : {}) };
|
|
30
56
|
}
|
|
31
57
|
/**
|
|
32
58
|
* Reads the project title from lumenjs.config.ts (or returns default).
|
|
@@ -8,6 +8,13 @@ export interface IndexHtmlOptions {
|
|
|
8
8
|
data: any;
|
|
9
9
|
}>;
|
|
10
10
|
integrations?: string[];
|
|
11
|
+
locale?: string;
|
|
12
|
+
i18nConfig?: {
|
|
13
|
+
locales: string[];
|
|
14
|
+
defaultLocale: string;
|
|
15
|
+
prefixDefault: boolean;
|
|
16
|
+
};
|
|
17
|
+
translations?: Record<string, string>;
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
20
|
* Generates the index.html shell that loads the LumenJS app.
|
|
@@ -19,16 +19,28 @@ export function generateIndexHtml(options) {
|
|
|
19
19
|
: options.loaderData;
|
|
20
20
|
loaderDataScript = `<script type="application/json" id="__nk_ssr_data__">${JSON.stringify(ssrData).replace(/</g, '\\u003c')}</script>`;
|
|
21
21
|
}
|
|
22
|
+
// i18n: inline translations and config for client hydration
|
|
23
|
+
let i18nScript = '';
|
|
24
|
+
if (options.i18nConfig && options.locale && options.translations) {
|
|
25
|
+
const i18nData = {
|
|
26
|
+
config: options.i18nConfig,
|
|
27
|
+
locale: options.locale,
|
|
28
|
+
translations: options.translations,
|
|
29
|
+
};
|
|
30
|
+
i18nScript = `<script type="application/json" id="__nk_i18n__">${JSON.stringify(i18nData).replace(/</g, '\\u003c')}</script>`;
|
|
31
|
+
}
|
|
32
|
+
// i18n module is loaded via imports from router-hydration, no separate script needed
|
|
22
33
|
const hydrateScript = isSSR
|
|
23
34
|
? `<script type="module">import '@lit-labs/ssr-client/lit-element-hydrate-support.js';</script>`
|
|
24
35
|
: '';
|
|
36
|
+
const htmlLang = options.locale || 'en';
|
|
25
37
|
return `<!DOCTYPE html>
|
|
26
|
-
<html lang="
|
|
38
|
+
<html lang="${htmlLang}">
|
|
27
39
|
<head>
|
|
28
40
|
<meta charset="UTF-8" />
|
|
29
41
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
30
42
|
<title>${escapeHtml(options.title)}</title>
|
|
31
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@nuralyui/themes@latest/dist/default.css"
|
|
43
|
+
${options.integrations?.includes('nuralyui') ? '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@nuralyui/themes@latest/dist/default.css">' : ''}${options.integrations?.includes('tailwind') ? '\n <script type="module">import "/styles/tailwind.css";</script>' : ''}
|
|
32
44
|
<style>
|
|
33
45
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
34
46
|
body { font-family: system-ui, -apple-system, sans-serif; min-height: 100vh; }
|
|
@@ -36,6 +48,7 @@ export function generateIndexHtml(options) {
|
|
|
36
48
|
</style>
|
|
37
49
|
</head>
|
|
38
50
|
<body>
|
|
51
|
+
${i18nScript}
|
|
39
52
|
${loaderDataScript}
|
|
40
53
|
${appTag}
|
|
41
54
|
${hydrateScript}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface I18nConfig {
|
|
2
|
+
locales: string[];
|
|
3
|
+
defaultLocale: string;
|
|
4
|
+
prefixDefault: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface LocaleResult {
|
|
7
|
+
locale: string;
|
|
8
|
+
pathname: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract the locale from the request URL, cookie, or Accept-Language header.
|
|
12
|
+
* Returns the resolved locale and the pathname with the locale prefix stripped.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order:
|
|
15
|
+
* 1. URL prefix: /fr/about → locale "fr", pathname "/about"
|
|
16
|
+
* 2. Cookie "nk-locale"
|
|
17
|
+
* 3. Accept-Language header
|
|
18
|
+
* 4. Config defaultLocale
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveLocale(pathname: string, config: I18nConfig, headers?: Record<string, string | string[] | undefined>): LocaleResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the locale from the request URL, cookie, or Accept-Language header.
|
|
3
|
+
* Returns the resolved locale and the pathname with the locale prefix stripped.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. URL prefix: /fr/about → locale "fr", pathname "/about"
|
|
7
|
+
* 2. Cookie "nk-locale"
|
|
8
|
+
* 3. Accept-Language header
|
|
9
|
+
* 4. Config defaultLocale
|
|
10
|
+
*/
|
|
11
|
+
export function resolveLocale(pathname, config, headers) {
|
|
12
|
+
// 1. URL prefix
|
|
13
|
+
for (const loc of config.locales) {
|
|
14
|
+
if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) {
|
|
15
|
+
return { locale: loc, pathname: pathname.slice(loc.length + 1) || '/' };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// 2. Cookie
|
|
19
|
+
const cookieHeader = headers?.cookie;
|
|
20
|
+
if (typeof cookieHeader === 'string') {
|
|
21
|
+
const match = cookieHeader.match(/(?:^|;\s*)nk-locale=([^;]+)/);
|
|
22
|
+
if (match && config.locales.includes(match[1])) {
|
|
23
|
+
return { locale: match[1], pathname };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 3. Accept-Language
|
|
27
|
+
const acceptLang = headers?.['accept-language'];
|
|
28
|
+
if (typeof acceptLang === 'string') {
|
|
29
|
+
const preferred = parseAcceptLanguage(acceptLang);
|
|
30
|
+
for (const lang of preferred) {
|
|
31
|
+
const short = lang.split('-')[0];
|
|
32
|
+
if (config.locales.includes(short)) {
|
|
33
|
+
return { locale: short, pathname };
|
|
34
|
+
}
|
|
35
|
+
if (config.locales.includes(lang)) {
|
|
36
|
+
return { locale: lang, pathname };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 4. Default
|
|
41
|
+
return { locale: config.defaultLocale, pathname };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse the Accept-Language header into a sorted list of language codes.
|
|
45
|
+
*/
|
|
46
|
+
function parseAcceptLanguage(header) {
|
|
47
|
+
return header
|
|
48
|
+
.split(',')
|
|
49
|
+
.map(part => {
|
|
50
|
+
const [lang, q] = part.trim().split(';q=');
|
|
51
|
+
return { lang: lang.trim().toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => b.q - a.q)
|
|
54
|
+
.map(e => e.lang);
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import type { I18nConfig } from '../middleware/locale.js';
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin for LumenJS i18n support.
|
|
5
|
+
*
|
|
6
|
+
* - Serves `/__nk_i18n/<locale>.json` with translation files
|
|
7
|
+
* - Provides a virtual module `@lumenjs/i18n` that re-exports the runtime
|
|
8
|
+
* - Watches locale files for HMR
|
|
9
|
+
*/
|
|
10
|
+
export declare function i18nPlugin(projectDir: string, config: I18nConfig): Plugin;
|
|
11
|
+
/**
|
|
12
|
+
* Load translations for a locale from the project's locales/ directory.
|
|
13
|
+
* Used during SSR to inline translations in the HTML.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadTranslationsFromDisk(projectDir: string, locale: string): Record<string, string>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin for LumenJS i18n support.
|
|
5
|
+
*
|
|
6
|
+
* - Serves `/__nk_i18n/<locale>.json` with translation files
|
|
7
|
+
* - Provides a virtual module `@lumenjs/i18n` that re-exports the runtime
|
|
8
|
+
* - Watches locale files for HMR
|
|
9
|
+
*/
|
|
10
|
+
export function i18nPlugin(projectDir, config) {
|
|
11
|
+
const localesDir = path.join(projectDir, 'locales');
|
|
12
|
+
return {
|
|
13
|
+
name: 'lumenjs-i18n',
|
|
14
|
+
configureServer(server) {
|
|
15
|
+
// Serve translation JSON files
|
|
16
|
+
server.middlewares.use((req, res, next) => {
|
|
17
|
+
if (!req.url?.startsWith('/__nk_i18n/'))
|
|
18
|
+
return next();
|
|
19
|
+
const match = req.url.match(/^\/__nk_i18n\/([a-z]{2}(?:-[a-zA-Z]+)?)\.json$/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return next();
|
|
22
|
+
const locale = match[1];
|
|
23
|
+
if (!config.locales.includes(locale)) {
|
|
24
|
+
res.statusCode = 404;
|
|
25
|
+
res.end(JSON.stringify({ error: 'Unknown locale' }));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const filePath = path.join(localesDir, `${locale}.json`);
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
res.statusCode = 200;
|
|
31
|
+
res.setHeader('Content-Type', 'application/json');
|
|
32
|
+
res.end('{}');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
37
|
+
// Validate JSON
|
|
38
|
+
JSON.parse(content);
|
|
39
|
+
res.statusCode = 200;
|
|
40
|
+
res.setHeader('Content-Type', 'application/json');
|
|
41
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
42
|
+
res.end(content);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
res.statusCode = 500;
|
|
46
|
+
res.setHeader('Content-Type', 'application/json');
|
|
47
|
+
res.end(JSON.stringify({ error: 'Invalid locale file' }));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// Watch locale directory for changes and trigger HMR
|
|
51
|
+
if (fs.existsSync(localesDir)) {
|
|
52
|
+
server.watcher.add(localesDir);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load translations for a locale from the project's locales/ directory.
|
|
59
|
+
* Used during SSR to inline translations in the HTML.
|
|
60
|
+
*/
|
|
61
|
+
export function loadTranslationsFromDisk(projectDir, locale) {
|
|
62
|
+
const filePath = path.join(projectDir, 'locales', `${locale}.json`);
|
|
63
|
+
if (!fs.existsSync(filePath))
|
|
64
|
+
return {};
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -83,7 +83,10 @@ export function lumenLoadersPlugin(pagesDir) {
|
|
|
83
83
|
res.end(JSON.stringify({ __nk_no_loader: true }));
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// Extract locale from query if provided by the client router
|
|
87
|
+
const locale = query.__locale;
|
|
88
|
+
delete query.__locale;
|
|
89
|
+
const result = await mod.loader({ params, query, url: pagePath, headers: req.headers, locale });
|
|
87
90
|
if (isRedirectResponse(result)) {
|
|
88
91
|
res.statusCode = result.status || 302;
|
|
89
92
|
res.setHeader('Location', result.location);
|
|
@@ -195,7 +198,18 @@ async function handleLayoutLoader(server, pagesDir, dir, req, res) {
|
|
|
195
198
|
res.end(JSON.stringify({ __nk_no_loader: true }));
|
|
196
199
|
return;
|
|
197
200
|
}
|
|
198
|
-
|
|
201
|
+
// Parse locale from query for layout loader requests
|
|
202
|
+
const query = {};
|
|
203
|
+
const reqUrl = req.url || '';
|
|
204
|
+
const qs = reqUrl.split('?')[1];
|
|
205
|
+
if (qs) {
|
|
206
|
+
for (const pair of qs.split('&')) {
|
|
207
|
+
const [key, val] = pair.split('=');
|
|
208
|
+
query[decodeURIComponent(key)] = decodeURIComponent(val || '');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const locale = query.__locale;
|
|
212
|
+
const result = await mod.loader({ params: {}, query: {}, url: `/__layout/${dir}`, headers: req.headers, locale });
|
|
199
213
|
if (isRedirectResponse(result)) {
|
|
200
214
|
res.statusCode = result.status || 302;
|
|
201
215
|
res.setHeader('Location', result.location);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { dirToLayoutTagName, fileHasLoader, filePathToRoute } from '../../shared/utils.js';
|
|
3
|
+
import { dirToLayoutTagName, fileHasLoader, filePathToRoute, filePathToTagName } from '../../shared/utils.js';
|
|
4
4
|
const VIRTUAL_MODULE_ID = 'virtual:lumenjs-routes';
|
|
5
5
|
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
6
6
|
/**
|
|
@@ -68,16 +68,6 @@ export function lumenRoutesPlugin(pagesDir) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
function filePathToTagName(filePath) {
|
|
72
|
-
const name = filePath
|
|
73
|
-
.replace(/\.(ts|js)$/, '')
|
|
74
|
-
.replace(/\\/g, '-')
|
|
75
|
-
.replace(/\//g, '-')
|
|
76
|
-
.replace(/\[\.\.\.([^\]]+)\]/g, '$1')
|
|
77
|
-
.replace(/\[([^\]]+)\]/g, '$1')
|
|
78
|
-
.toLowerCase();
|
|
79
|
-
return `page-${name}`;
|
|
80
|
-
}
|
|
81
71
|
/** Build the layout chain for a route based on its file path within pages/ */
|
|
82
72
|
function getLayoutChain(componentPath, layouts) {
|
|
83
73
|
const relativeToPages = path.relative(pagesDir, componentPath).replace(/\\/g, '/');
|
|
@@ -29,7 +29,14 @@ export function sourceAnnotatorPlugin(projectDir) {
|
|
|
29
29
|
const escaped = content.trim().replace(/"/g, '"').replace(/\$\{/g, '__NK_EXPR__');
|
|
30
30
|
return `<${tag}${attrStr} data-nk-dynamic="${escaped}">${content}</`;
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
// Detect t('key') calls inside template expressions and add data-nk-i18n-key
|
|
33
|
+
const i18nAnnotated = dynamicAnnotated.replace(/<(h[1-6]|p|span|a|label|li|button|div)(\s[^>]*)?>([^<]*\$\{t\(['"]([^'"]+)['"]\)\}[^<]*)<\//gi, (m, tag, attrs, content, key) => {
|
|
34
|
+
const attrStr = attrs || '';
|
|
35
|
+
if (attrStr.includes('data-nk-i18n-key'))
|
|
36
|
+
return m;
|
|
37
|
+
return `<${tag}${attrStr} data-nk-i18n-key="${key}">${content}</`;
|
|
38
|
+
});
|
|
39
|
+
return 'html`' + i18nAnnotated + '`';
|
|
33
40
|
});
|
|
34
41
|
if (transformed !== code) {
|
|
35
42
|
return { code: transformed, map: null };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
/**
|
|
3
3
|
* Virtual module plugin — serves compiled LumenJS runtime and editor modules.
|
|
4
|
+
* Rewrites relative imports between split sub-modules to virtual module paths.
|
|
5
|
+
*
|
|
6
|
+
* i18n is resolved via resolve.alias (physical file) rather than as a virtual
|
|
7
|
+
* module, because Vite's import-analysis rejects bare @-prefixed specifiers
|
|
8
|
+
* that go through the virtual module path.
|
|
4
9
|
*/
|
|
5
10
|
export declare function virtualModulesPlugin(runtimeDir: string, editorDir: string): Plugin;
|
|
@@ -2,36 +2,66 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
/**
|
|
4
4
|
* Virtual module plugin — serves compiled LumenJS runtime and editor modules.
|
|
5
|
+
* Rewrites relative imports between split sub-modules to virtual module paths.
|
|
6
|
+
*
|
|
7
|
+
* i18n is resolved via resolve.alias (physical file) rather than as a virtual
|
|
8
|
+
* module, because Vite's import-analysis rejects bare @-prefixed specifiers
|
|
9
|
+
* that go through the virtual module path.
|
|
5
10
|
*/
|
|
6
11
|
export function virtualModulesPlugin(runtimeDir, editorDir) {
|
|
12
|
+
const runtimeModules = {
|
|
13
|
+
'app-shell': 'app-shell.js',
|
|
14
|
+
'router': 'router.js',
|
|
15
|
+
'router-data': 'router-data.js',
|
|
16
|
+
'router-hydration': 'router-hydration.js',
|
|
17
|
+
'i18n': 'i18n.js',
|
|
18
|
+
};
|
|
19
|
+
// Modules resolved via resolve.alias instead of virtual module.
|
|
20
|
+
// They still appear in the map so relative import rewrites work.
|
|
21
|
+
const aliasedModules = new Set(['i18n']);
|
|
22
|
+
const editorModules = {
|
|
23
|
+
'editor-bridge': 'editor-bridge.js',
|
|
24
|
+
'element-annotator': 'element-annotator.js',
|
|
25
|
+
'click-select': 'click-select.js',
|
|
26
|
+
'hover-detect': 'hover-detect.js',
|
|
27
|
+
'inline-text-edit': 'inline-text-edit.js',
|
|
28
|
+
};
|
|
29
|
+
function rewriteRelativeImports(code, modules) {
|
|
30
|
+
for (const name of Object.keys(modules)) {
|
|
31
|
+
const file = modules[name];
|
|
32
|
+
// Aliased modules use @lumenjs/name (resolved by Vite alias).
|
|
33
|
+
// Virtual modules use /@lumenjs/name (resolved by this plugin).
|
|
34
|
+
const prefix = aliasedModules.has(name) ? '@lumenjs' : '/@lumenjs';
|
|
35
|
+
code = code.replace(new RegExp(`from\\s+['"]\\.\\/${file.replace('.', '\\.')}['"]`, 'g'), `from '${prefix}/${name}'`);
|
|
36
|
+
}
|
|
37
|
+
return code;
|
|
38
|
+
}
|
|
7
39
|
return {
|
|
8
40
|
name: 'lumenjs-virtual-modules',
|
|
41
|
+
enforce: 'pre',
|
|
9
42
|
resolveId(id) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
const match = id.match(/^\/@lumenjs\/(.+)$/);
|
|
44
|
+
if (!match)
|
|
45
|
+
return;
|
|
46
|
+
const name = match[1];
|
|
47
|
+
// Skip aliased modules — they're resolved via resolve.alias
|
|
48
|
+
if (aliasedModules.has(name))
|
|
49
|
+
return;
|
|
50
|
+
if (runtimeModules[name] || editorModules[name]) {
|
|
51
|
+
return `\0lumenjs:${name}`;
|
|
52
|
+
}
|
|
18
53
|
},
|
|
19
54
|
load(id) {
|
|
20
|
-
if (id
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return fs.readFileSync(path.join(runtimeDir, 'router.js'), 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
if (id === '\0lumenjs:editor-bridge') {
|
|
29
|
-
let code = fs.readFileSync(path.join(editorDir, 'editor-bridge.js'), 'utf-8');
|
|
30
|
-
code = code.replace(/from\s+['"]\.\/element-annotator\.js['"]/g, "from '/@lumenjs/element-annotator'");
|
|
31
|
-
return code;
|
|
55
|
+
if (!id.startsWith('\0lumenjs:'))
|
|
56
|
+
return;
|
|
57
|
+
const name = id.slice('\0lumenjs:'.length);
|
|
58
|
+
if (runtimeModules[name]) {
|
|
59
|
+
const code = fs.readFileSync(path.join(runtimeDir, runtimeModules[name]), 'utf-8');
|
|
60
|
+
return rewriteRelativeImports(code, runtimeModules);
|
|
32
61
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
62
|
+
if (editorModules[name]) {
|
|
63
|
+
const code = fs.readFileSync(path.join(editorDir, editorModules[name]), 'utf-8');
|
|
64
|
+
return rewriteRelativeImports(code, editorModules);
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
67
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ViteDevServer, UserConfig, Plugin } from 'vite';
|
|
2
2
|
export { readProjectConfig, readProjectTitle, getLumenJSNodeModules, getLumenJSDirs } from './config.js';
|
|
3
|
-
export type { ProjectConfig } from './config.js';
|
|
3
|
+
export type { ProjectConfig, I18nConfig } from './config.js';
|
|
4
4
|
export { getNuralyUIAliases, resolveNuralyUIPaths } from './nuralyui-aliases.js';
|
|
5
5
|
export interface DevServerOptions {
|
|
6
6
|
projectDir: string;
|
|
@@ -15,6 +15,8 @@ import { autoImportPlugin } from './plugins/vite-plugin-auto-import.js';
|
|
|
15
15
|
import { litHmrPlugin } from './plugins/vite-plugin-lit-hmr.js';
|
|
16
16
|
import { sourceAnnotatorPlugin } from './plugins/vite-plugin-source-annotator.js';
|
|
17
17
|
import { virtualModulesPlugin } from './plugins/vite-plugin-virtual-modules.js';
|
|
18
|
+
import { i18nPlugin, loadTranslationsFromDisk } from './plugins/vite-plugin-i18n.js';
|
|
19
|
+
import { resolveLocale } from './middleware/locale.js';
|
|
18
20
|
// Re-export for backwards compatibility
|
|
19
21
|
export { readProjectConfig, readProjectTitle, getLumenJSNodeModules, getLumenJSDirs } from './config.js';
|
|
20
22
|
export { getNuralyUIAliases, resolveNuralyUIPaths } from './nuralyui-aliases.js';
|
|
@@ -28,14 +30,21 @@ export function getSharedViteConfig(projectDir, options) {
|
|
|
28
30
|
const pagesDir = path.join(projectDir, 'pages');
|
|
29
31
|
const lumenNodeModules = getLumenJSNodeModules();
|
|
30
32
|
const { runtimeDir, editorDir } = getLumenJSDirs();
|
|
31
|
-
// Resolve NuralyUI paths for aliases
|
|
32
|
-
const nuralyUIPaths = resolveNuralyUIPaths(projectDir);
|
|
33
|
+
// Resolve NuralyUI paths for aliases (only when nuralyui integration is enabled)
|
|
33
34
|
const aliases = {};
|
|
34
|
-
if (
|
|
35
|
-
|
|
35
|
+
if (options?.integrations?.includes('nuralyui')) {
|
|
36
|
+
const nuralyUIPaths = resolveNuralyUIPaths(projectDir);
|
|
37
|
+
if (nuralyUIPaths) {
|
|
38
|
+
Object.assign(aliases, getNuralyUIAliases(nuralyUIPaths.componentsPath, nuralyUIPaths.commonPath));
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
const resolve = {
|
|
38
|
-
alias: {
|
|
42
|
+
alias: {
|
|
43
|
+
...aliases,
|
|
44
|
+
// Map @lumenjs/i18n to the physical dist file so Vite resolves it
|
|
45
|
+
// without going through node_modules (it's not an npm package).
|
|
46
|
+
'@lumenjs/i18n': path.join(runtimeDir, 'i18n.js'),
|
|
47
|
+
},
|
|
39
48
|
conditions: isDev ? ['development', 'browser'] : ['browser'],
|
|
40
49
|
dedupe: ['lit', 'lit-html', 'lit-element', '@lit/reactive-element'],
|
|
41
50
|
};
|
|
@@ -51,9 +60,12 @@ export function getSharedViteConfig(projectDir, options) {
|
|
|
51
60
|
lumenRoutesPlugin(pagesDir),
|
|
52
61
|
lumenLoadersPlugin(pagesDir),
|
|
53
62
|
litDedupPlugin(lumenNodeModules, isDev),
|
|
54
|
-
autoImportPlugin(projectDir),
|
|
55
63
|
virtualModulesPlugin(runtimeDir, editorDir),
|
|
56
64
|
];
|
|
65
|
+
// Conditionally add NuralyUI auto-import plugin
|
|
66
|
+
if (options?.integrations?.includes('nuralyui')) {
|
|
67
|
+
plugins.push(autoImportPlugin(projectDir));
|
|
68
|
+
}
|
|
57
69
|
// Conditionally add Tailwind plugin from the project's node_modules
|
|
58
70
|
if (options?.integrations?.includes('tailwind')) {
|
|
59
71
|
try {
|
|
@@ -74,7 +86,7 @@ export async function createDevServer(options) {
|
|
|
74
86
|
const apiDir = path.join(projectDir, 'api');
|
|
75
87
|
const publicDir = path.join(projectDir, 'public');
|
|
76
88
|
const config = readProjectConfig(projectDir);
|
|
77
|
-
const { title, integrations } = config;
|
|
89
|
+
const { title, integrations, i18n: i18nConfig } = config;
|
|
78
90
|
const shared = getSharedViteConfig(projectDir, { integrations });
|
|
79
91
|
const server = await createViteServer({
|
|
80
92
|
root: projectDir,
|
|
@@ -92,6 +104,7 @@ export async function createDevServer(options) {
|
|
|
92
104
|
...shared.plugins,
|
|
93
105
|
lumenApiRoutesPlugin(apiDir, projectDir),
|
|
94
106
|
litHmrPlugin(projectDir),
|
|
107
|
+
...(i18nConfig ? [i18nPlugin(projectDir, i18nConfig)] : []),
|
|
95
108
|
...(editorMode ? [sourceAnnotatorPlugin(projectDir)] : []),
|
|
96
109
|
{
|
|
97
110
|
name: 'lumenjs-index-html',
|
|
@@ -99,10 +112,20 @@ export async function createDevServer(options) {
|
|
|
99
112
|
server.middlewares.use((req, res, next) => {
|
|
100
113
|
if (req.url && !req.url.startsWith('/@') && !req.url.startsWith('/node_modules') &&
|
|
101
114
|
!req.url.startsWith('/api/') && !req.url.startsWith('/__nk_loader/') &&
|
|
115
|
+
!req.url.startsWith('/__nk_i18n/') &&
|
|
102
116
|
!req.url.includes('.') && req.method === 'GET') {
|
|
103
|
-
|
|
117
|
+
let pathname = req.url.split('?')[0];
|
|
118
|
+
// Resolve locale from URL/cookie/header
|
|
119
|
+
let locale;
|
|
120
|
+
let translations;
|
|
121
|
+
if (i18nConfig) {
|
|
122
|
+
const localeResult = resolveLocale(pathname, i18nConfig, req.headers);
|
|
123
|
+
locale = localeResult.locale;
|
|
124
|
+
pathname = localeResult.pathname;
|
|
125
|
+
translations = loadTranslationsFromDisk(projectDir, locale);
|
|
126
|
+
}
|
|
104
127
|
const SSR_PLACEHOLDER = '<!--__NK_SSR_CONTENT__-->';
|
|
105
|
-
ssrRenderPage(server, pagesDir, pathname, req.headers).then(async (ssrResult) => {
|
|
128
|
+
ssrRenderPage(server, pagesDir, pathname, req.headers, locale).then(async (ssrResult) => {
|
|
106
129
|
if (ssrResult?.redirect) {
|
|
107
130
|
res.writeHead(ssrResult.redirect.status, { Location: ssrResult.redirect.location });
|
|
108
131
|
res.end();
|
|
@@ -115,6 +138,9 @@ export async function createDevServer(options) {
|
|
|
115
138
|
loaderData: ssrResult?.loaderData,
|
|
116
139
|
layoutsData: ssrResult?.layoutsData,
|
|
117
140
|
integrations,
|
|
141
|
+
locale,
|
|
142
|
+
i18nConfig: i18nConfig || undefined,
|
|
143
|
+
translations,
|
|
118
144
|
});
|
|
119
145
|
const transformed = await server.transformIndexHtml(req.url, shellHtml);
|
|
120
146
|
const finalHtml = ssrResult
|
|
@@ -125,7 +151,7 @@ export async function createDevServer(options) {
|
|
|
125
151
|
res.end(finalHtml);
|
|
126
152
|
}).catch(err => {
|
|
127
153
|
console.error('[LumenJS] SSR/HTML generation error:', err);
|
|
128
|
-
const html = generateIndexHtml({ title, editorMode, integrations });
|
|
154
|
+
const html = generateIndexHtml({ title, editorMode, integrations, locale, i18nConfig: i18nConfig || undefined, translations });
|
|
129
155
|
server.transformIndexHtml(req.url, html).then(transformed => {
|
|
130
156
|
res.setHeader('Content-Type', 'text/html');
|
|
131
157
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -142,6 +168,7 @@ export async function createDevServer(options) {
|
|
|
142
168
|
esbuild: shared.esbuild,
|
|
143
169
|
optimizeDeps: {
|
|
144
170
|
include: ['lit', 'lit/decorators.js', 'lit/directive.js', 'lit/directive-helpers.js', 'lit/async-directive.js', 'lit-html', 'lit-element', '@lit/reactive-element'],
|
|
171
|
+
exclude: ['@lumenjs/i18n'],
|
|
145
172
|
},
|
|
146
173
|
ssr: {
|
|
147
174
|
noExternal: true,
|
|
@@ -9,7 +9,7 @@ export interface LayoutSSRData {
|
|
|
9
9
|
*
|
|
10
10
|
* Returns pre-rendered HTML and loader data, or null on failure (falls back to CSR).
|
|
11
11
|
*/
|
|
12
|
-
export declare function ssrRenderPage(server: ViteDevServer, pagesDir: string, pathname: string, headers?: Record<string, string | string[] | undefined
|
|
12
|
+
export declare function ssrRenderPage(server: ViteDevServer, pagesDir: string, pathname: string, headers?: Record<string, string | string[] | undefined>, locale?: string): Promise<{
|
|
13
13
|
html: string;
|
|
14
14
|
loaderData: any;
|
|
15
15
|
layoutsData?: LayoutSSRData[];
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { resolvePageFile, extractRouteParams } from './plugins/vite-plugin-loaders.js';
|
|
4
|
-
import { stripOuterLitMarkers, dirToLayoutTagName,
|
|
4
|
+
import { stripOuterLitMarkers, dirToLayoutTagName, filePathToTagName } from '../shared/utils.js';
|
|
5
5
|
import { installDomShims } from '../shared/dom-shims.js';
|
|
6
|
+
import { loadTranslationsFromDisk } from './plugins/vite-plugin-i18n.js';
|
|
6
7
|
/**
|
|
7
8
|
* Server-side render a LumenJS page using @lit-labs/ssr.
|
|
8
9
|
* Wraps the page in its layout chain if layouts exist.
|
|
9
10
|
*
|
|
10
11
|
* Returns pre-rendered HTML and loader data, or null on failure (falls back to CSR).
|
|
11
12
|
*/
|
|
12
|
-
export async function ssrRenderPage(server, pagesDir, pathname, headers) {
|
|
13
|
+
export async function ssrRenderPage(server, pagesDir, pathname, headers, locale) {
|
|
13
14
|
try {
|
|
14
15
|
const filePath = resolvePageFile(pagesDir, pathname);
|
|
15
16
|
if (!filePath)
|
|
@@ -19,6 +20,21 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers) {
|
|
|
19
20
|
await server.ssrLoadModule('@lit-labs/ssr/lib/install-global-dom-shim.js');
|
|
20
21
|
// Patch missing DOM APIs that NuralyUI components may use during SSR
|
|
21
22
|
installDomShims();
|
|
23
|
+
// Initialize i18n in the SSR context so t() works during render
|
|
24
|
+
if (locale) {
|
|
25
|
+
const projectDir = path.resolve(pagesDir, '..');
|
|
26
|
+
const translations = loadTranslationsFromDisk(projectDir, locale);
|
|
27
|
+
try {
|
|
28
|
+
// Load the same i18n module the page will import (via resolve.alias)
|
|
29
|
+
const i18nMod = await server.ssrLoadModule('@lumenjs/i18n');
|
|
30
|
+
if (i18nMod?.initI18n) {
|
|
31
|
+
i18nMod.initI18n({ locales: [], defaultLocale: locale, prefixDefault: false }, locale, translations);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// i18n module not available — translations will show keys
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
// Invalidate SSR module cache so we always get fresh content after file edits.
|
|
23
39
|
// Also clear the custom element from the SSR registry so the new class is used.
|
|
24
40
|
const g = globalThis;
|
|
@@ -29,15 +45,14 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers) {
|
|
|
29
45
|
// Run loader if present
|
|
30
46
|
let loaderData = undefined;
|
|
31
47
|
if (mod.loader && typeof mod.loader === 'function') {
|
|
32
|
-
loaderData = await mod.loader({ params, query: {}, url: pathname, headers: headers || {} });
|
|
48
|
+
loaderData = await mod.loader({ params, query: {}, url: pathname, headers: headers || {}, locale });
|
|
33
49
|
if (loaderData && typeof loaderData === 'object' && loaderData.__nk_redirect) {
|
|
34
50
|
return { html: '', loaderData: null, redirect: { location: loaderData.location, status: loaderData.status || 302 } };
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
|
-
// Determine the custom element tag name
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
return null;
|
|
53
|
+
// Determine the custom element tag name from file path (matches client router)
|
|
54
|
+
const relPath = path.relative(pagesDir, filePath).replace(/\\/g, '/');
|
|
55
|
+
const tagName = filePathToTagName(relPath);
|
|
41
56
|
// Discover layout chain for this page
|
|
42
57
|
const layoutChain = discoverLayoutChain(pagesDir, filePath);
|
|
43
58
|
const layoutsData = [];
|
|
@@ -50,7 +65,7 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers) {
|
|
|
50
65
|
const layoutMod = await server.ssrLoadModule(layout.filePath);
|
|
51
66
|
let layoutLoaderData = undefined;
|
|
52
67
|
if (layoutMod.loader && typeof layoutMod.loader === 'function') {
|
|
53
|
-
layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: headers || {} });
|
|
68
|
+
layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: headers || {}, locale });
|
|
54
69
|
if (layoutLoaderData && typeof layoutLoaderData === 'object' && layoutLoaderData.__nk_redirect) {
|
|
55
70
|
return { html: '', loaderData: null, redirect: { location: layoutLoaderData.location, status: layoutLoaderData.status || 302 } };
|
|
56
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface NkEditorMessage {
|
|
2
|
-
type: 'NK_READY' | 'NK_ELEMENT_CLICKED' | 'NK_ELEMENT_HOVERED' | 'NK_SELECT_ELEMENT' | 'NK_HIGHLIGHT_ELEMENT' | 'NK_TEXT_CHANGED' | 'NK_SET_PREVIEW_MODE';
|
|
2
|
+
type: 'NK_READY' | 'NK_ELEMENT_CLICKED' | 'NK_ELEMENT_HOVERED' | 'NK_SELECT_ELEMENT' | 'NK_HIGHLIGHT_ELEMENT' | 'NK_TEXT_CHANGED' | 'NK_TRANSLATION_CHANGED' | 'NK_SET_PREVIEW_MODE';
|
|
3
3
|
payload?: any;
|
|
4
4
|
}
|
|
5
5
|
export declare function isPreviewMode(): boolean;
|
|
@@ -93,10 +93,21 @@ export function setupInlineTextEdit() {
|
|
|
93
93
|
editingEl.style.minWidth = '';
|
|
94
94
|
editingEl = null;
|
|
95
95
|
if (newText !== originalText) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
// Check if this element has an i18n key — if so, send a translation change
|
|
97
|
+
const i18nKey = textEl.getAttribute('data-nk-i18n-key');
|
|
98
|
+
if (i18nKey) {
|
|
99
|
+
const locale = document.documentElement.lang || 'en';
|
|
100
|
+
sendToHost({
|
|
101
|
+
type: 'NK_TRANSLATION_CHANGED',
|
|
102
|
+
payload: { key: i18nKey, locale, originalText, newText }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
sendToHost({
|
|
107
|
+
type: 'NK_TEXT_CHANGED',
|
|
108
|
+
payload: { sourceFile, line, originalText, newText }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
100
111
|
}
|
|
101
112
|
};
|
|
102
113
|
textEl.addEventListener('blur', commitEdit, { once: true });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LumenJS i18n runtime — provides translation lookup, locale management,
|
|
3
|
+
* and translation loading for both SSR and client-side navigation.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Look up a translation key. Returns the translated string, or the key itself
|
|
7
|
+
* if no translation is found.
|
|
8
|
+
*/
|
|
9
|
+
export declare function t(key: string): string;
|
|
10
|
+
/** Returns the current locale. */
|
|
11
|
+
export declare function getLocale(): string;
|
|
12
|
+
/** Returns the i18n config, or null if i18n is not enabled. */
|
|
13
|
+
export declare function getI18nConfig(): {
|
|
14
|
+
locales: string[];
|
|
15
|
+
defaultLocale: string;
|
|
16
|
+
prefixDefault: boolean;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Switch to a new locale. Navigates to the same pathname under the new
|
|
20
|
+
* locale prefix and sets the `nk-locale` cookie.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setLocale(locale: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Initialise the i18n runtime with config and translations.
|
|
25
|
+
* Called during hydration (from SSR data) or on first load.
|
|
26
|
+
*/
|
|
27
|
+
export declare function initI18n(config: {
|
|
28
|
+
locales: string[];
|
|
29
|
+
defaultLocale: string;
|
|
30
|
+
prefixDefault: boolean;
|
|
31
|
+
}, locale: string, trans: Record<string, string>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Load translations for a locale from the server and swap them in.
|
|
34
|
+
* Used during client-side locale switches.
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadTranslations(locale: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Strip the locale prefix from a pathname.
|
|
39
|
+
* /fr/about → /about
|
|
40
|
+
* /about → /about
|
|
41
|
+
*/
|
|
42
|
+
export declare function stripLocalePrefix(pathname: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Prepend the locale prefix to a pathname.
|
|
45
|
+
* (fr, /about) → /fr/about
|
|
46
|
+
* (en, /about) → /about (when prefixDefault=false)
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildLocalePath(locale: string, pathname: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Detect the locale from a URL pathname.
|
|
51
|
+
* Returns the locale and the pathname with the prefix stripped.
|
|
52
|
+
*/
|
|
53
|
+
export declare function detectLocaleFromPath(pathname: string): {
|
|
54
|
+
locale: string;
|
|
55
|
+
pathname: string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LumenJS i18n runtime — provides translation lookup, locale management,
|
|
3
|
+
* and translation loading for both SSR and client-side navigation.
|
|
4
|
+
*/
|
|
5
|
+
let currentLocale = 'en';
|
|
6
|
+
let translations = {};
|
|
7
|
+
let i18nConfig = null;
|
|
8
|
+
/**
|
|
9
|
+
* Look up a translation key. Returns the translated string, or the key itself
|
|
10
|
+
* if no translation is found.
|
|
11
|
+
*/
|
|
12
|
+
export function t(key) {
|
|
13
|
+
return translations[key] ?? key;
|
|
14
|
+
}
|
|
15
|
+
/** Returns the current locale. */
|
|
16
|
+
export function getLocale() {
|
|
17
|
+
return currentLocale;
|
|
18
|
+
}
|
|
19
|
+
/** Returns the i18n config, or null if i18n is not enabled. */
|
|
20
|
+
export function getI18nConfig() {
|
|
21
|
+
return i18nConfig;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Switch to a new locale. Navigates to the same pathname under the new
|
|
25
|
+
* locale prefix and sets the `nk-locale` cookie.
|
|
26
|
+
*/
|
|
27
|
+
export function setLocale(locale) {
|
|
28
|
+
if (!i18nConfig || !i18nConfig.locales.includes(locale))
|
|
29
|
+
return;
|
|
30
|
+
document.cookie = `nk-locale=${locale};path=/;max-age=${60 * 60 * 24 * 365};SameSite=Lax`;
|
|
31
|
+
const pathname = stripLocalePrefix(location.pathname);
|
|
32
|
+
const newPath = buildLocalePath(locale, pathname);
|
|
33
|
+
location.href = newPath;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialise the i18n runtime with config and translations.
|
|
37
|
+
* Called during hydration (from SSR data) or on first load.
|
|
38
|
+
*/
|
|
39
|
+
export function initI18n(config, locale, trans) {
|
|
40
|
+
i18nConfig = config;
|
|
41
|
+
currentLocale = locale;
|
|
42
|
+
translations = trans;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load translations for a locale from the server and swap them in.
|
|
46
|
+
* Used during client-side locale switches.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadTranslations(locale) {
|
|
49
|
+
const res = await fetch(`/__nk_i18n/${locale}.json`);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
console.error(`[i18n] Failed to load translations for locale "${locale}"`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
translations = await res.json();
|
|
55
|
+
currentLocale = locale;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Strip the locale prefix from a pathname.
|
|
59
|
+
* /fr/about → /about
|
|
60
|
+
* /about → /about
|
|
61
|
+
*/
|
|
62
|
+
export function stripLocalePrefix(pathname) {
|
|
63
|
+
if (!i18nConfig)
|
|
64
|
+
return pathname;
|
|
65
|
+
for (const loc of i18nConfig.locales) {
|
|
66
|
+
if (loc === i18nConfig.defaultLocale && !i18nConfig.prefixDefault)
|
|
67
|
+
continue;
|
|
68
|
+
if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) {
|
|
69
|
+
return pathname.slice(loc.length + 1) || '/';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return pathname;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Prepend the locale prefix to a pathname.
|
|
76
|
+
* (fr, /about) → /fr/about
|
|
77
|
+
* (en, /about) → /about (when prefixDefault=false)
|
|
78
|
+
*/
|
|
79
|
+
export function buildLocalePath(locale, pathname) {
|
|
80
|
+
if (!i18nConfig)
|
|
81
|
+
return pathname;
|
|
82
|
+
if (locale === i18nConfig.defaultLocale && !i18nConfig.prefixDefault) {
|
|
83
|
+
return pathname;
|
|
84
|
+
}
|
|
85
|
+
return `/${locale}${pathname === '/' ? '' : pathname}` || `/${locale}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Detect the locale from a URL pathname.
|
|
89
|
+
* Returns the locale and the pathname with the prefix stripped.
|
|
90
|
+
*/
|
|
91
|
+
export function detectLocaleFromPath(pathname) {
|
|
92
|
+
if (!i18nConfig)
|
|
93
|
+
return { locale: 'en', pathname };
|
|
94
|
+
for (const loc of i18nConfig.locales) {
|
|
95
|
+
if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) {
|
|
96
|
+
return { locale: loc, pathname: pathname.slice(loc.length + 1) || '/' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { locale: i18nConfig.defaultLocale, pathname };
|
|
100
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { getI18nConfig, getLocale } from './i18n.js';
|
|
1
2
|
export async function fetchLoaderData(pathname, params) {
|
|
2
3
|
const url = new URL(`/__nk_loader${pathname}`, location.origin);
|
|
3
4
|
if (Object.keys(params).length > 0) {
|
|
4
5
|
url.searchParams.set('__params', JSON.stringify(params));
|
|
5
6
|
}
|
|
7
|
+
const config = getI18nConfig();
|
|
8
|
+
if (config) {
|
|
9
|
+
url.searchParams.set('__locale', getLocale());
|
|
10
|
+
}
|
|
6
11
|
const res = await fetch(url.toString());
|
|
7
12
|
if (!res.ok) {
|
|
8
13
|
throw new Error(`Loader returned ${res.status}`);
|
|
@@ -15,6 +20,10 @@ export async function fetchLoaderData(pathname, params) {
|
|
|
15
20
|
export async function fetchLayoutLoaderData(dir) {
|
|
16
21
|
const url = new URL(`/__nk_loader/__layout/`, location.origin);
|
|
17
22
|
url.searchParams.set('__dir', dir);
|
|
23
|
+
const config = getI18nConfig();
|
|
24
|
+
if (config) {
|
|
25
|
+
url.searchParams.set('__locale', getLocale());
|
|
26
|
+
}
|
|
18
27
|
const res = await fetch(url.toString());
|
|
19
28
|
if (!res.ok) {
|
|
20
29
|
throw new Error(`Layout loader returned ${res.status}`);
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
import { initI18n, stripLocalePrefix, getI18nConfig } from './i18n.js';
|
|
1
2
|
/**
|
|
2
3
|
* Hydrate the initial SSR-rendered route.
|
|
3
4
|
* Sets loaderData on existing DOM elements BEFORE loading modules to avoid
|
|
4
5
|
* hydration mismatches with Lit's microtask-based hydration.
|
|
5
6
|
*/
|
|
6
7
|
export async function hydrateInitialRoute(routes, outlet, matchRoute, onHydrated) {
|
|
7
|
-
|
|
8
|
+
// Read i18n data and init before anything else
|
|
9
|
+
const i18nScript = document.getElementById('__nk_i18n__');
|
|
10
|
+
if (i18nScript) {
|
|
11
|
+
try {
|
|
12
|
+
const i18nData = JSON.parse(i18nScript.textContent || '');
|
|
13
|
+
initI18n(i18nData.config, i18nData.locale, i18nData.translations);
|
|
14
|
+
}
|
|
15
|
+
catch { /* ignore */ }
|
|
16
|
+
i18nScript.remove();
|
|
17
|
+
}
|
|
18
|
+
// Strip locale prefix for route matching (routes are locale-agnostic)
|
|
19
|
+
const config = getI18nConfig();
|
|
20
|
+
const matchPath = config ? stripLocalePrefix(location.pathname) : location.pathname;
|
|
21
|
+
const match = matchRoute(matchPath);
|
|
8
22
|
if (!match)
|
|
9
23
|
return;
|
|
10
24
|
const layouts = match.route.layouts || [];
|
package/dist/runtime/router.d.ts
CHANGED
|
@@ -32,4 +32,8 @@ export declare class NkRouter {
|
|
|
32
32
|
private buildLayoutTree;
|
|
33
33
|
private createPageElement;
|
|
34
34
|
private handleLinkClick;
|
|
35
|
+
/** Strip locale prefix from a path for internal route matching. */
|
|
36
|
+
private stripLocale;
|
|
37
|
+
/** Prepend locale prefix for browser-facing URLs. */
|
|
38
|
+
private withLocale;
|
|
35
39
|
}
|
package/dist/runtime/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchLoaderData, fetchLayoutLoaderData, render404 } from './router-data.js';
|
|
2
2
|
import { hydrateInitialRoute } from './router-hydration.js';
|
|
3
|
+
import { getI18nConfig, getLocale, stripLocalePrefix, buildLocalePath } from './i18n.js';
|
|
3
4
|
/**
|
|
4
5
|
* Simple client-side router for LumenJS pages.
|
|
5
6
|
* Handles popstate and link clicks for SPA navigation.
|
|
@@ -17,7 +18,10 @@ export class NkRouter {
|
|
|
17
18
|
...r,
|
|
18
19
|
...this.compilePattern(r.path),
|
|
19
20
|
}));
|
|
20
|
-
window.addEventListener('popstate', () =>
|
|
21
|
+
window.addEventListener('popstate', () => {
|
|
22
|
+
const path = this.stripLocale(location.pathname);
|
|
23
|
+
this.navigate(path, false);
|
|
24
|
+
});
|
|
21
25
|
document.addEventListener('click', (e) => this.handleLinkClick(e));
|
|
22
26
|
if (hydrate) {
|
|
23
27
|
hydrateInitialRoute(this.routes, this.outlet, (p) => this.matchRoute(p), (tag, layoutTags, params) => {
|
|
@@ -27,7 +31,8 @@ export class NkRouter {
|
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
else {
|
|
30
|
-
this.
|
|
34
|
+
const path = this.stripLocale(location.pathname);
|
|
35
|
+
this.navigate(path, false);
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
compilePattern(path) {
|
|
@@ -48,7 +53,8 @@ export class NkRouter {
|
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
55
|
if (pushState) {
|
|
51
|
-
|
|
56
|
+
const localePath = this.withLocale(pathname);
|
|
57
|
+
history.pushState(null, '', localePath);
|
|
52
58
|
}
|
|
53
59
|
this.params = match.params;
|
|
54
60
|
// Lazy-load the page component if not yet registered
|
|
@@ -197,6 +203,16 @@ export class NkRouter {
|
|
|
197
203
|
if (!href || href.startsWith('http') || href.startsWith('#') || anchor.hasAttribute('target'))
|
|
198
204
|
return;
|
|
199
205
|
event.preventDefault();
|
|
200
|
-
this.navigate(href);
|
|
206
|
+
this.navigate(this.stripLocale(href));
|
|
207
|
+
}
|
|
208
|
+
/** Strip locale prefix from a path for internal route matching. */
|
|
209
|
+
stripLocale(path) {
|
|
210
|
+
const config = getI18nConfig();
|
|
211
|
+
return config ? stripLocalePrefix(path) : path;
|
|
212
|
+
}
|
|
213
|
+
/** Prepend locale prefix for browser-facing URLs. */
|
|
214
|
+
withLocale(path) {
|
|
215
|
+
const config = getI18nConfig();
|
|
216
|
+
return config ? buildLocalePath(getLocale(), path) : path;
|
|
201
217
|
}
|
|
202
218
|
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -7,10 +7,17 @@ export interface ManifestRoute {
|
|
|
7
7
|
path: string;
|
|
8
8
|
module: string;
|
|
9
9
|
hasLoader: boolean;
|
|
10
|
+
tagName?: string;
|
|
10
11
|
layouts?: string[];
|
|
11
12
|
}
|
|
13
|
+
export interface I18nManifest {
|
|
14
|
+
locales: string[];
|
|
15
|
+
defaultLocale: string;
|
|
16
|
+
prefixDefault: boolean;
|
|
17
|
+
}
|
|
12
18
|
export interface BuildManifest {
|
|
13
19
|
routes: ManifestRoute[];
|
|
14
20
|
apiRoutes: ManifestRoute[];
|
|
15
21
|
layouts: ManifestLayout[];
|
|
22
|
+
i18n?: I18nManifest;
|
|
16
23
|
}
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export declare function dirToLayoutTagName(dir: string): string;
|
|
|
17
17
|
* Pages use @customElement('page-xxx') which registers the element.
|
|
18
18
|
*/
|
|
19
19
|
export declare function findTagName(mod: Record<string, any>): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Convert a relative file path within pages/ to a page tag name.
|
|
22
|
+
* 'index.ts' → 'page-index'
|
|
23
|
+
* 'docs/api-routes.ts' → 'page-docs-api-routes'
|
|
24
|
+
* 'blog/[slug].ts' → 'page-blog-slug'
|
|
25
|
+
*/
|
|
26
|
+
export declare function filePathToTagName(filePath: string): string;
|
|
20
27
|
/**
|
|
21
28
|
* Check if a redirect response was returned from a loader.
|
|
22
29
|
*/
|
package/dist/shared/utils.js
CHANGED
|
@@ -49,6 +49,22 @@ export function findTagName(mod) {
|
|
|
49
49
|
}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Convert a relative file path within pages/ to a page tag name.
|
|
54
|
+
* 'index.ts' → 'page-index'
|
|
55
|
+
* 'docs/api-routes.ts' → 'page-docs-api-routes'
|
|
56
|
+
* 'blog/[slug].ts' → 'page-blog-slug'
|
|
57
|
+
*/
|
|
58
|
+
export function filePathToTagName(filePath) {
|
|
59
|
+
const name = filePath
|
|
60
|
+
.replace(/\.(ts|js)$/, '')
|
|
61
|
+
.replace(/\\/g, '-')
|
|
62
|
+
.replace(/\//g, '-')
|
|
63
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, '$1')
|
|
64
|
+
.replace(/\[([^\]]+)\]/g, '$1')
|
|
65
|
+
.toLowerCase();
|
|
66
|
+
return `page-${name}`;
|
|
67
|
+
}
|
|
52
68
|
/**
|
|
53
69
|
* Check if a redirect response was returned from a loader.
|
|
54
70
|
*/
|
package/package.json
CHANGED