zuby 1.0.67 → 1.0.69
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/commands/build.js +8 -0
- package/config.js +6 -0
- package/contexts/globalContext.d.ts +2 -0
- package/contexts/pageContext.d.ts +28 -0
- package/contexts/pageContext.js +50 -0
- package/hooks/useFetch.d.ts +28 -0
- package/hooks/useFetch.js +101 -0
- package/hooks/useProps.d.ts +1 -0
- package/hooks/useProps.js +8 -0
- package/i18n/index.d.ts +14 -0
- package/i18n/index.js +35 -0
- package/i18n/types.d.ts +1 -0
- package/i18n/types.js +1 -0
- package/package.json +1 -1
- package/plugins/contextPlugin/index.d.ts +4 -0
- package/plugins/contextPlugin/index.js +23 -1
- package/plugins/preloadPlugin/index.js +5 -1
- package/preload/index.d.ts +4 -0
- package/preload/index.js +22 -0
- package/server/index.js +48 -0
- package/types.d.ts +15 -1
package/commands/build.js
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'url';
|
|
|
12
12
|
import { getZubyPackageConfig } from '../packageConfig.js';
|
|
13
13
|
import { getTemplates } from '../templates/index.js';
|
|
14
14
|
import { CLIENT_CHUNKS_MANIFEST, SERVER_CHUNKS_MANIFEST } from '../constants.js';
|
|
15
|
+
import { getTranslationFiles } from '../i18n/index.js';
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
17
18
|
export default async function build(options) {
|
|
@@ -26,6 +27,8 @@ export default async function build(options) {
|
|
|
26
27
|
logger?.info(getTitle(chalk.gray(`building for production...`)));
|
|
27
28
|
// Load templates from the project directory
|
|
28
29
|
const templates = await getTemplates();
|
|
30
|
+
// Load translation files from the project directory
|
|
31
|
+
const translationFiles = await getTranslationFiles();
|
|
29
32
|
// Load the entry file from the project directory
|
|
30
33
|
// or jsxProvider
|
|
31
34
|
const entryFile = await getEntryFile(zubyInternalConfig);
|
|
@@ -63,10 +66,12 @@ export default async function build(options) {
|
|
|
63
66
|
clientViteBuildConfig,
|
|
64
67
|
serverViteBuildConfig,
|
|
65
68
|
templates,
|
|
69
|
+
translationFiles,
|
|
66
70
|
});
|
|
67
71
|
// Run build start hook
|
|
68
72
|
await executePlugins(zubyInternalConfig, PLUGIN_HOOKS.ZubyBuildStart, {
|
|
69
73
|
templates,
|
|
74
|
+
translationFiles,
|
|
70
75
|
});
|
|
71
76
|
// Clean build directory
|
|
72
77
|
if (existsSync(outDir || '')) {
|
|
@@ -80,6 +85,7 @@ export default async function build(options) {
|
|
|
80
85
|
// Run build client done hook
|
|
81
86
|
await executePlugins(zubyInternalConfig, PLUGIN_HOOKS.ZubyBuildClientDone, {
|
|
82
87
|
templates,
|
|
88
|
+
translationFiles,
|
|
83
89
|
});
|
|
84
90
|
// Server build
|
|
85
91
|
nextBuildStep('building server...');
|
|
@@ -87,6 +93,7 @@ export default async function build(options) {
|
|
|
87
93
|
// Run build server done hook
|
|
88
94
|
await executePlugins(zubyInternalConfig, PLUGIN_HOOKS.ZubyBuildServerDone, {
|
|
89
95
|
templates,
|
|
96
|
+
translationFiles,
|
|
90
97
|
});
|
|
91
98
|
// Add serialized zuby config to build directory
|
|
92
99
|
writeFileSync(normalizePath(join(outDir, 'zuby.config.json')), JSON.stringify(zubyInternalConfig, null, 2));
|
|
@@ -112,6 +119,7 @@ export default async function build(options) {
|
|
|
112
119
|
// to execute plugins that are part of the build process
|
|
113
120
|
await executePlugins(zubyInternalConfig, PLUGIN_HOOKS.ZubyBuildDone, {
|
|
114
121
|
templates,
|
|
122
|
+
translationFiles,
|
|
115
123
|
clientChunksManifest,
|
|
116
124
|
serverChunksManifest,
|
|
117
125
|
}, zubyPlugin => {
|
package/config.js
CHANGED
|
@@ -189,6 +189,12 @@ export const mergeDefaultConfig = async (config) => {
|
|
|
189
189
|
// Global props
|
|
190
190
|
config.props = config.props ?? {};
|
|
191
191
|
config.serverProps = config.serverProps ?? {};
|
|
192
|
+
// i18n
|
|
193
|
+
if (config.i18n?.locales && config.i18n.locales.length > 0) {
|
|
194
|
+
config.i18n.defaultLocale = config.i18n.defaultLocale ?? config.i18n.locales[0];
|
|
195
|
+
config.i18n.translationsPath = config.i18n.translationsPath ?? 'i18n';
|
|
196
|
+
config.i18n.translationsExtension = config.i18n.translationsExtension ?? 'json';
|
|
197
|
+
}
|
|
192
198
|
// Head elements
|
|
193
199
|
config.headElements = config.headElements ?? [];
|
|
194
200
|
// Inject scripts
|
|
@@ -188,6 +188,34 @@ export declare class PageContext {
|
|
|
188
188
|
* @example localizePath('/products/1', 'en') => /products/1
|
|
189
189
|
*/
|
|
190
190
|
localizePath(path: string, locale?: string | undefined): string;
|
|
191
|
+
/**
|
|
192
|
+
* Returns the detected locale for the given path.
|
|
193
|
+
* @param path The path to detect the locale
|
|
194
|
+
* @example getPathLocale('/products/1') => 'en'
|
|
195
|
+
* @example getPathLocale('/de/products/1') => 'de'
|
|
196
|
+
*/
|
|
197
|
+
getPathLocale(path: string): string | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* Localizes the given text for the current locale
|
|
200
|
+
* using the translations from the i18n config.
|
|
201
|
+
* If no translation is found, the backup text is returned.
|
|
202
|
+
* @param key The translation key
|
|
203
|
+
* @param backupText The backup text
|
|
204
|
+
* @param options The additional options
|
|
205
|
+
* @example localize('products.title', 'Products')
|
|
206
|
+
* @example localize('products.title', 'Produkte', { locale: 'de' })
|
|
207
|
+
*/
|
|
208
|
+
localize(key: string, backupText: string, options?: {
|
|
209
|
+
locale?: string;
|
|
210
|
+
}): Promise<string>;
|
|
211
|
+
/**
|
|
212
|
+
* Returns the translations for the given namespace.
|
|
213
|
+
* @param namespace The namespace
|
|
214
|
+
* @param locale The locale to use. If not specified, the current locale is used.
|
|
215
|
+
* @example getTranslations('products') => { title: 'Products' }
|
|
216
|
+
* @example getTranslations('products', 'de') => { title: 'Produkte' }
|
|
217
|
+
*/
|
|
218
|
+
getTranslations(namespace: string, locale?: string | undefined): Promise<Record<string, string>>;
|
|
191
219
|
/**
|
|
192
220
|
* Returns true if the current request
|
|
193
221
|
* was made by the Zuby.js pre-render build step.
|
package/contexts/pageContext.js
CHANGED
|
@@ -15,6 +15,17 @@ export class PageContext {
|
|
|
15
15
|
this._globalContext = options?.globalContext || getGlobalContext();
|
|
16
16
|
this._headElements = [...(this._globalContext?.headElements || [])];
|
|
17
17
|
this._bodyElements = [...(this._globalContext?.bodyElements || [])];
|
|
18
|
+
// Bind the methods to the current instance,
|
|
19
|
+
// to allow object destructuring of the methods.
|
|
20
|
+
this.getElement = this.getElement.bind(this);
|
|
21
|
+
this.getHeadElements = this.getHeadElements.bind(this);
|
|
22
|
+
this.getBodyElements = this.getBodyElements.bind(this);
|
|
23
|
+
this.addToHead = this.addToHead.bind(this);
|
|
24
|
+
this.addToBody = this.addToBody.bind(this);
|
|
25
|
+
this.localize = this.localize.bind(this);
|
|
26
|
+
this.localizePath = this.localizePath.bind(this);
|
|
27
|
+
this.getPathLocale = this.getPathLocale.bind(this);
|
|
28
|
+
this.getTranslations = this.getTranslations.bind(this);
|
|
18
29
|
}
|
|
19
30
|
/**
|
|
20
31
|
* The current URL of the page.
|
|
@@ -250,6 +261,45 @@ export class PageContext {
|
|
|
250
261
|
return path;
|
|
251
262
|
return `/${locale}/${path}`.replace(/\/+/g, '/');
|
|
252
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Returns the detected locale for the given path.
|
|
266
|
+
* @param path The path to detect the locale
|
|
267
|
+
* @example getPathLocale('/products/1') => 'en'
|
|
268
|
+
* @example getPathLocale('/de/products/1') => 'de'
|
|
269
|
+
*/
|
|
270
|
+
getPathLocale(path) {
|
|
271
|
+
return this.locales.find(locale => path.startsWith(`/${locale}`)) || this.defaultLocale;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Localizes the given text for the current locale
|
|
275
|
+
* using the translations from the i18n config.
|
|
276
|
+
* If no translation is found, the backup text is returned.
|
|
277
|
+
* @param key The translation key
|
|
278
|
+
* @param backupText The backup text
|
|
279
|
+
* @param options The additional options
|
|
280
|
+
* @example localize('products.title', 'Products')
|
|
281
|
+
* @example localize('products.title', 'Produkte', { locale: 'de' })
|
|
282
|
+
*/
|
|
283
|
+
async localize(key, backupText, options) {
|
|
284
|
+
const locale = options?.locale || this.locale;
|
|
285
|
+
const namespace = key.includes('.') ? key.replace(/\.(.+)$/, '.') : '';
|
|
286
|
+
const translations = await this.getTranslations(namespace, locale);
|
|
287
|
+
return translations?.[key] || backupText;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Returns the translations for the given namespace.
|
|
291
|
+
* @param namespace The namespace
|
|
292
|
+
* @param locale The locale to use. If not specified, the current locale is used.
|
|
293
|
+
* @example getTranslations('products') => { title: 'Products' }
|
|
294
|
+
* @example getTranslations('products', 'de') => { title: 'Produkte' }
|
|
295
|
+
*/
|
|
296
|
+
async getTranslations(namespace, locale = this.locale) {
|
|
297
|
+
const namespaceWithLocale = `${namespace}${locale}`;
|
|
298
|
+
const translationsImport = this._globalContext.i18n?.translations?.[namespaceWithLocale];
|
|
299
|
+
if (!translationsImport)
|
|
300
|
+
return {};
|
|
301
|
+
return translationsImport();
|
|
302
|
+
}
|
|
253
303
|
/**
|
|
254
304
|
* Returns true if the current request
|
|
255
305
|
* was made by the Zuby.js pre-render build step.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type FetchResponse = Object | string;
|
|
2
|
+
interface FetchResponseMetadata {
|
|
3
|
+
bodyUsed: boolean;
|
|
4
|
+
contentType: null | string;
|
|
5
|
+
headers: Headers;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
redirected: boolean;
|
|
8
|
+
response: FetchResponse;
|
|
9
|
+
status: number;
|
|
10
|
+
statusText: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
interface Options {
|
|
14
|
+
lifespan?: number;
|
|
15
|
+
metadata?: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface OptionsWithMetadata extends Options {
|
|
18
|
+
metadata: true;
|
|
19
|
+
}
|
|
20
|
+
interface OptionsWithoutMetadata extends Options {
|
|
21
|
+
metadata?: false;
|
|
22
|
+
}
|
|
23
|
+
interface UseFetch {
|
|
24
|
+
(input: RequestInfo, init?: RequestInit | undefined, options?: number | OptionsWithoutMetadata): FetchResponse;
|
|
25
|
+
(input: RequestInfo, init: RequestInit | undefined, options: OptionsWithMetadata): FetchResponseMetadata;
|
|
26
|
+
}
|
|
27
|
+
export declare const useFetch: UseFetch;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const getDefaultFetchFunction = () => {
|
|
2
|
+
if (typeof window === 'undefined') {
|
|
3
|
+
return () => {
|
|
4
|
+
return Promise.reject(new Error('Cannot find `window`. Use `createUseFetch` to provide a custom `fetch` function.'));
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
if (typeof window.fetch === 'undefined') {
|
|
8
|
+
return () => {
|
|
9
|
+
return Promise.reject(new Error('Cannot find `window.fetch`. Use `createUseFetch` to provide a custom `fetch` function.'));
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return window.fetch;
|
|
13
|
+
};
|
|
14
|
+
const createUseFetch = (fetch = getDefaultFetchFunction()) => {
|
|
15
|
+
// Create a set of caches for this hook.
|
|
16
|
+
const caches = [];
|
|
17
|
+
function useFetch(input, init, options = 0) {
|
|
18
|
+
if (typeof options === 'number') {
|
|
19
|
+
return useFetch(input, init, { lifespan: options });
|
|
20
|
+
}
|
|
21
|
+
const { metadata = false, lifespan = 0 } = options;
|
|
22
|
+
// Check each cache by this useFetch hook.
|
|
23
|
+
for (const cache of caches) {
|
|
24
|
+
// If this cache matches the request,
|
|
25
|
+
if (JSON.stringify(cache.init) === JSON.stringify(init) &&
|
|
26
|
+
JSON.stringify(cache.input) === JSON.stringify(input)) {
|
|
27
|
+
// If an error occurred, throw it so that componentDidCatch can handle
|
|
28
|
+
// it.
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(cache, 'error')) {
|
|
30
|
+
throw cache.error;
|
|
31
|
+
}
|
|
32
|
+
// If a response was successful, return it.
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(cache, 'response')) {
|
|
34
|
+
if (metadata) {
|
|
35
|
+
return {
|
|
36
|
+
bodyUsed: cache.bodyUsed,
|
|
37
|
+
contentType: cache.contentType,
|
|
38
|
+
headers: cache.headers,
|
|
39
|
+
ok: cache.ok,
|
|
40
|
+
redirected: cache.redirected,
|
|
41
|
+
response: cache.response,
|
|
42
|
+
status: cache.status,
|
|
43
|
+
statusText: cache.statusText,
|
|
44
|
+
url: cache.url,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return cache.response;
|
|
48
|
+
}
|
|
49
|
+
// If we are still waiting, throw the Promise so that Suspense can
|
|
50
|
+
// fallback.
|
|
51
|
+
throw cache.fetch;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// If no request in the cache matched this one, create a new cache entry.
|
|
55
|
+
const cache = {
|
|
56
|
+
// Make the fetch request.
|
|
57
|
+
fetch: fetch(input, init)
|
|
58
|
+
// Parse the response.
|
|
59
|
+
.then((response) => {
|
|
60
|
+
cache.contentType = response.headers.get('Content-Type');
|
|
61
|
+
if (metadata) {
|
|
62
|
+
cache.bodyUsed = response.bodyUsed;
|
|
63
|
+
cache.headers = response.headers;
|
|
64
|
+
cache.ok = response.ok;
|
|
65
|
+
cache.redirected = response.redirected;
|
|
66
|
+
cache.status = response.status;
|
|
67
|
+
cache.statusText = response.statusText;
|
|
68
|
+
}
|
|
69
|
+
if (cache.contentType && cache.contentType.indexOf('application/json') !== -1) {
|
|
70
|
+
return response.json();
|
|
71
|
+
}
|
|
72
|
+
return response.text();
|
|
73
|
+
})
|
|
74
|
+
// Cache the response.
|
|
75
|
+
.then((response) => {
|
|
76
|
+
cache.response = response;
|
|
77
|
+
})
|
|
78
|
+
// Handle an error.
|
|
79
|
+
.catch((e) => {
|
|
80
|
+
cache.error = e;
|
|
81
|
+
})
|
|
82
|
+
// Invalidate the cache.
|
|
83
|
+
.then(() => {
|
|
84
|
+
if (lifespan > 0) {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
const index = caches.indexOf(cache);
|
|
87
|
+
if (index !== -1) {
|
|
88
|
+
caches.splice(index, 1);
|
|
89
|
+
}
|
|
90
|
+
}, lifespan);
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
init,
|
|
94
|
+
input,
|
|
95
|
+
};
|
|
96
|
+
caches.push(cache);
|
|
97
|
+
throw cache.fetch;
|
|
98
|
+
}
|
|
99
|
+
return useFetch;
|
|
100
|
+
};
|
|
101
|
+
export const useFetch = createUseFetch();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useProps(path: string, priority?: 'low' | 'high' | 'auto'): string | Object;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useFetch } from './useFetch.js';
|
|
2
|
+
import { getGlobalContext } from '../contexts/index.js';
|
|
3
|
+
export function useProps(path, priority = 'auto') {
|
|
4
|
+
const { buildId } = getGlobalContext();
|
|
5
|
+
path = `/_props${path}/?${buildId}`;
|
|
6
|
+
path = path.replace(/\/+/g, '/');
|
|
7
|
+
return useFetch(path);
|
|
8
|
+
}
|
package/i18n/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects all translation files from the project
|
|
3
|
+
* and returns them as an object with the namespace
|
|
4
|
+
* as a key and the path to the file as a value.
|
|
5
|
+
* @example {
|
|
6
|
+
* 'en': '/project/i18n/en.json',
|
|
7
|
+
* 'cs': '/project/i18n/cs.json',
|
|
8
|
+
* 'products.en': '/project/i18n/products/en.json',
|
|
9
|
+
* 'products.cs': '/project/i18n/products/cs.json',
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export declare function getTranslationFiles(cache?: boolean): Promise<{
|
|
13
|
+
[key: string]: string;
|
|
14
|
+
}>;
|
package/i18n/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getZubyInternalConfig } from '../config.js';
|
|
2
|
+
import { normalizePath } from '../utils/pathUtils.js';
|
|
3
|
+
import { join, relative, resolve } from 'path';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
let translationFilesCache;
|
|
6
|
+
/**
|
|
7
|
+
* Collects all translation files from the project
|
|
8
|
+
* and returns them as an object with the namespace
|
|
9
|
+
* as a key and the path to the file as a value.
|
|
10
|
+
* @example {
|
|
11
|
+
* 'en': '/project/i18n/en.json',
|
|
12
|
+
* 'cs': '/project/i18n/cs.json',
|
|
13
|
+
* 'products.en': '/project/i18n/products/en.json',
|
|
14
|
+
* 'products.cs': '/project/i18n/products/cs.json',
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export async function getTranslationFiles(cache = true) {
|
|
18
|
+
if (cache && translationFilesCache)
|
|
19
|
+
return translationFilesCache;
|
|
20
|
+
const { i18n, srcDir } = await getZubyInternalConfig();
|
|
21
|
+
if (!i18n)
|
|
22
|
+
return {};
|
|
23
|
+
const { translationsPath = 'i18n', translationsExtension = 'json' } = i18n;
|
|
24
|
+
const translationsDir = normalizePath(join(srcDir, translationsPath));
|
|
25
|
+
const files = await glob(`${translationsDir}/**/*.${translationsExtension}`);
|
|
26
|
+
const entries = files.map(filename => {
|
|
27
|
+
filename = normalizePath(resolve(filename));
|
|
28
|
+
const namespace = normalizePath(relative(translationsDir, filename))
|
|
29
|
+
.replace(`.${translationsExtension}`, '')
|
|
30
|
+
.replace(/[\\\/]/g, '.');
|
|
31
|
+
return [namespace, filename];
|
|
32
|
+
});
|
|
33
|
+
translationFilesCache = Object.fromEntries(entries);
|
|
34
|
+
return translationFilesCache || {};
|
|
35
|
+
}
|
package/i18n/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/i18n/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -4,6 +4,10 @@ export default function index(): VitePlugin;
|
|
|
4
4
|
export declare function generateCompileTimeContextCode(ssr: boolean): Promise<string>;
|
|
5
5
|
export declare function generateTemplatesCode(ssr: boolean): Promise<string>;
|
|
6
6
|
export declare function generateTemplateCode(template: Template): Promise<string>;
|
|
7
|
+
export declare function generateI18nCode(): Promise<string>;
|
|
8
|
+
export declare function generateTranslationsCode(translationFiles?: {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
}): Promise<string>;
|
|
7
11
|
export declare function generateImportCode(template: Template): string;
|
|
8
12
|
export declare function generateRenderCode(ssr: boolean): Promise<string>;
|
|
9
13
|
export declare function generateImageCode(): Promise<string>;
|
|
@@ -4,6 +4,7 @@ import { relative } from 'path';
|
|
|
4
4
|
import { normalizePath } from '../../utils/pathUtils.js';
|
|
5
5
|
import { getZubyPackageConfig } from '../../packageConfig.js';
|
|
6
6
|
import { getApps, getErrors, getHandlers, getInnerLayouts, getLayouts, getLoaders, getPages, getTemplates, } from '../../templates/index.js';
|
|
7
|
+
import { getTranslationFiles } from '../../i18n/index.js';
|
|
7
8
|
let viteConfig;
|
|
8
9
|
let staticImports = [];
|
|
9
10
|
export default function index() {
|
|
@@ -40,7 +41,7 @@ export async function generateCompileTimeContextCode(ssr) {
|
|
|
40
41
|
serverProps: ${JSON.stringify(ssr ? serverProps : {})},
|
|
41
42
|
headElements: ${JSON.stringify(ssr ? headElements : [])},
|
|
42
43
|
bodyElements: ${JSON.stringify(ssr ? bodyElements : [])},
|
|
43
|
-
i18n: ${
|
|
44
|
+
i18n: ${await generateI18nCode()},
|
|
44
45
|
};`;
|
|
45
46
|
}
|
|
46
47
|
export async function generateTemplatesCode(ssr) {
|
|
@@ -82,6 +83,27 @@ export async function generateTemplateCode(template) {
|
|
|
82
83
|
component: () => ${generateImportCode(template)},
|
|
83
84
|
}`;
|
|
84
85
|
}
|
|
86
|
+
export async function generateI18nCode() {
|
|
87
|
+
const { i18n } = await getZubyInternalConfig();
|
|
88
|
+
if (!i18n)
|
|
89
|
+
return 'undefined';
|
|
90
|
+
const translationFiles = await getTranslationFiles();
|
|
91
|
+
return `{
|
|
92
|
+
defaultLocale: '${i18n.defaultLocale}',
|
|
93
|
+
locales: ${JSON.stringify(i18n.locales)},
|
|
94
|
+
translations: ${await generateTranslationsCode(translationFiles)},
|
|
95
|
+
translationsExtension: '${i18n.translationsExtension}',
|
|
96
|
+
}`;
|
|
97
|
+
}
|
|
98
|
+
export async function generateTranslationsCode(translationFiles = {}) {
|
|
99
|
+
return `{
|
|
100
|
+
${Object.entries(translationFiles)
|
|
101
|
+
.map(([key, value]) => {
|
|
102
|
+
return `'${key}': () => import("${value}")`;
|
|
103
|
+
})
|
|
104
|
+
.join(',')}
|
|
105
|
+
}`;
|
|
106
|
+
}
|
|
85
107
|
export function generateImportCode(template) {
|
|
86
108
|
// Sync templates are imported statically
|
|
87
109
|
if (Object.values(SYNC_TEMPLATES).includes(template.templateType)) {
|
|
@@ -12,7 +12,7 @@ export default function preloadPlugin() {
|
|
|
12
12
|
return {
|
|
13
13
|
name: 'zuby-preload-plugin',
|
|
14
14
|
hooks: {
|
|
15
|
-
'zuby:build:done': async ({ config, clientChunksManifest, templates }) => {
|
|
15
|
+
'zuby:build:done': async ({ config, clientChunksManifest, templates, translationFiles }) => {
|
|
16
16
|
const { srcDir, outDir } = config;
|
|
17
17
|
const preloadManifest = {};
|
|
18
18
|
const pages = await getPages(templates);
|
|
@@ -20,6 +20,10 @@ export default function preloadPlugin() {
|
|
|
20
20
|
const filename = normalizePath(relative(srcDir, page.filename));
|
|
21
21
|
preloadManifest[filename] = clientChunksManifest[filename] || [];
|
|
22
22
|
});
|
|
23
|
+
Object.values(translationFiles).forEach(file => {
|
|
24
|
+
const filename = normalizePath(relative(srcDir, file));
|
|
25
|
+
preloadManifest[filename] = clientChunksManifest[filename] || [];
|
|
26
|
+
});
|
|
23
27
|
writeFileSync(normalizePath(join(outDir, 'client', PREALOD_MANIFEST)), JSON.stringify(preloadManifest, null, 2));
|
|
24
28
|
},
|
|
25
29
|
},
|
package/preload/index.d.ts
CHANGED
|
@@ -14,3 +14,7 @@ export declare function preload(href: string, as?: string): void;
|
|
|
14
14
|
* @example preloadPage("/products/1")
|
|
15
15
|
*/
|
|
16
16
|
export declare function preloadPage(href: string, onHandle?: () => void | Promise<void>): void;
|
|
17
|
+
/**
|
|
18
|
+
* Preloads all required assets for given locale.
|
|
19
|
+
*/
|
|
20
|
+
export declare function preloadLocale(locale: string, onHandle?: () => void | Promise<void>): void;
|
package/preload/index.js
CHANGED
|
@@ -67,6 +67,28 @@ export function preloadPage(href, onHandle = () => { }) {
|
|
|
67
67
|
onHandle();
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Preloads all required assets for given locale.
|
|
72
|
+
*/
|
|
73
|
+
export function preloadLocale(locale, onHandle = () => { }) {
|
|
74
|
+
// Do nothing on server
|
|
75
|
+
if (typeof window === 'undefined')
|
|
76
|
+
return;
|
|
77
|
+
const { i18n } = getGlobalContext();
|
|
78
|
+
const { translationsExtension = 'json' } = i18n || {};
|
|
79
|
+
window.requestIdleCallback(async () => {
|
|
80
|
+
const preloadManifest = await getPreloadManifest();
|
|
81
|
+
// Preload assets such as scripts and styles
|
|
82
|
+
const preloadAssets = [];
|
|
83
|
+
Object.entries(preloadManifest).forEach(([filename, assets]) => {
|
|
84
|
+
if (filename.endsWith(`${locale}.${translationsExtension}`)) {
|
|
85
|
+
preloadAssets.push(...assets);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
preloadAssets.forEach(href => preload(href));
|
|
89
|
+
onHandle();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
70
92
|
/**
|
|
71
93
|
* Appends preload link into head element of page.
|
|
72
94
|
* For example:
|
package/server/index.js
CHANGED
|
@@ -1859,6 +1859,15 @@ var PageContext = class {
|
|
|
1859
1859
|
this._globalContext = options?.globalContext || getGlobalContext();
|
|
1860
1860
|
this._headElements = [...this._globalContext?.headElements || []];
|
|
1861
1861
|
this._bodyElements = [...this._globalContext?.bodyElements || []];
|
|
1862
|
+
this.getElement = this.getElement.bind(this);
|
|
1863
|
+
this.getHeadElements = this.getHeadElements.bind(this);
|
|
1864
|
+
this.getBodyElements = this.getBodyElements.bind(this);
|
|
1865
|
+
this.addToHead = this.addToHead.bind(this);
|
|
1866
|
+
this.addToBody = this.addToBody.bind(this);
|
|
1867
|
+
this.localize = this.localize.bind(this);
|
|
1868
|
+
this.localizePath = this.localizePath.bind(this);
|
|
1869
|
+
this.getPathLocale = this.getPathLocale.bind(this);
|
|
1870
|
+
this.getTranslations = this.getTranslations.bind(this);
|
|
1862
1871
|
}
|
|
1863
1872
|
/**
|
|
1864
1873
|
* The current URL of the page.
|
|
@@ -2094,6 +2103,45 @@ var PageContext = class {
|
|
|
2094
2103
|
return path;
|
|
2095
2104
|
return `/${locale}/${path}`.replace(/\/+/g, "/");
|
|
2096
2105
|
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Returns the detected locale for the given path.
|
|
2108
|
+
* @param path The path to detect the locale
|
|
2109
|
+
* @example getPathLocale('/products/1') => 'en'
|
|
2110
|
+
* @example getPathLocale('/de/products/1') => 'de'
|
|
2111
|
+
*/
|
|
2112
|
+
getPathLocale(path) {
|
|
2113
|
+
return this.locales.find((locale) => path.startsWith(`/${locale}`)) || this.defaultLocale;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Localizes the given text for the current locale
|
|
2117
|
+
* using the translations from the i18n config.
|
|
2118
|
+
* If no translation is found, the backup text is returned.
|
|
2119
|
+
* @param key The translation key
|
|
2120
|
+
* @param backupText The backup text
|
|
2121
|
+
* @param options The additional options
|
|
2122
|
+
* @example localize('products.title', 'Products')
|
|
2123
|
+
* @example localize('products.title', 'Produkte', { locale: 'de' })
|
|
2124
|
+
*/
|
|
2125
|
+
async localize(key, backupText, options) {
|
|
2126
|
+
const locale = options?.locale || this.locale;
|
|
2127
|
+
const namespace = key.includes(".") ? key.replace(/\.(.+)$/, ".") : "";
|
|
2128
|
+
const translations = await this.getTranslations(namespace, locale);
|
|
2129
|
+
return translations?.[key] || backupText;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Returns the translations for the given namespace.
|
|
2133
|
+
* @param namespace The namespace
|
|
2134
|
+
* @param locale The locale to use. If not specified, the current locale is used.
|
|
2135
|
+
* @example getTranslations('products') => { title: 'Products' }
|
|
2136
|
+
* @example getTranslations('products', 'de') => { title: 'Produkte' }
|
|
2137
|
+
*/
|
|
2138
|
+
async getTranslations(namespace, locale = this.locale) {
|
|
2139
|
+
const namespaceWithLocale = `${namespace}${locale}`;
|
|
2140
|
+
const translationsImport = this._globalContext.i18n?.translations?.[namespaceWithLocale];
|
|
2141
|
+
if (!translationsImport)
|
|
2142
|
+
return {};
|
|
2143
|
+
return translationsImport();
|
|
2144
|
+
}
|
|
2097
2145
|
/**
|
|
2098
2146
|
* Returns true if the current request
|
|
2099
2147
|
* was made by the Zuby.js pre-render build step.
|
package/types.d.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface ZubyConfig {
|
|
|
44
44
|
output?: Output;
|
|
45
45
|
/**
|
|
46
46
|
* The internalization config. If this is set, the site will be generated in multiple locales.
|
|
47
|
+
* The defaultLocale is optional and defaults to the first locale in the locales array.
|
|
48
|
+
*
|
|
49
|
+
* The translationsPath is optional and defaults to './i18n' folder with your translations.
|
|
50
|
+
* For example: ./i18n/en.json, ./i18n/de.json, ./i18n/cs.json, ./i18n/pl.json.
|
|
51
|
+
* It also supports splitting into namespaces.
|
|
52
|
+
* For example: ./i18n/products/pl.json., ./i18n/products/en.json.
|
|
53
|
+
*
|
|
54
|
+
* The translationsExtension is optional and defaults to 'json' file extension.
|
|
55
|
+
*
|
|
47
56
|
* @default undefined
|
|
48
57
|
* @example {
|
|
49
58
|
* locales: ['en', 'de', 'cs', 'pl'],
|
|
@@ -52,7 +61,9 @@ export interface ZubyConfig {
|
|
|
52
61
|
*/
|
|
53
62
|
i18n?: {
|
|
54
63
|
locales: string[];
|
|
55
|
-
defaultLocale
|
|
64
|
+
defaultLocale?: string;
|
|
65
|
+
translationsPath?: string;
|
|
66
|
+
translationsExtension?: string;
|
|
56
67
|
};
|
|
57
68
|
/**
|
|
58
69
|
* The URL of the site
|
|
@@ -466,6 +477,9 @@ export interface ZubyBuildHookParams {
|
|
|
466
477
|
config: ZubyInternalConfig;
|
|
467
478
|
logger: ZubyLogger;
|
|
468
479
|
templates: Template[];
|
|
480
|
+
translationFiles: {
|
|
481
|
+
[key: string]: string;
|
|
482
|
+
};
|
|
469
483
|
}
|
|
470
484
|
export interface ZubyBuildSetupHookParams extends ZubyBuildHookParams {
|
|
471
485
|
clientViteBuildConfig: ViteInlineConfig;
|