cumstack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * cumstack Server Module
3
+ * server-side rendering with hono
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { renderToString } from './server/jsx.js';
8
+ import { h } from './server/jsx.js';
9
+ import { initI18n, setLanguage, extractLanguageFromRoute } from './shared/i18n.js';
10
+
11
+ /**
12
+ * escape html to prevent xss
13
+ * @param {string} str - String to escape
14
+ * @returns {string} Escaped string
15
+ */
16
+ function escapeHtml(str) {
17
+ if (!str) return '';
18
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
19
+ }
20
+
21
+ /**
22
+ * create a new server context to avoid global state pollution
23
+ */
24
+ function createServerContext() {
25
+ return {
26
+ routeRegistry: new Map(),
27
+ i18nConfig: null,
28
+ initialized: false,
29
+ };
30
+ }
31
+
32
+ // server context - will be reset per foxgirl call
33
+ let serverContext = createServerContext();
34
+
35
+ /**
36
+ * detect language from request
37
+ * @param {Request} request - HTTP request
38
+ * @param {Object} config - i18n configuration
39
+ * @returns {string} Detected language code
40
+ */
41
+ function detectLanguageFromRequest(request, config) {
42
+ const url = new URL(request.url);
43
+
44
+ // check url path first if explicit routing
45
+ if (config.explicitRouting) {
46
+ const { language } = extractLanguageFromRoute(url.pathname);
47
+ if (language) return language;
48
+ }
49
+
50
+ // check accept-language header
51
+ if (config.defaultLng === 'auto') {
52
+ const acceptLang = request.headers.get('accept-language');
53
+ if (acceptLang) {
54
+ const detectedLang = acceptLang
55
+ .split(',')
56
+ .map((l) => l.split(';')[0].trim().split('-')[0])
57
+ .find((l) => config.supportedLanguages.includes(l));
58
+ if (detectedLang) return detectedLang;
59
+ }
60
+ }
61
+
62
+ // use default or fallback
63
+ return config.defaultLng === 'auto' ? config.fallbackLng : config.defaultLng;
64
+ }
65
+
66
+ /**
67
+ * router component (server)
68
+ * @param {Object} props - Component props
69
+ * @param {Object} [props.i18nOpt] - i18n configuration
70
+ * @param {*} props.children - Child elements
71
+ * @returns {*} Children
72
+ */
73
+ export function Router({ i18nOpt, children }) {
74
+ if (i18nOpt) {
75
+ serverContext.i18nConfig = i18nOpt;
76
+ // initialize i18n on server
77
+ if (!serverContext.initialized) {
78
+ initI18n({
79
+ defaultLanguage: i18nOpt.defaultLng || i18nOpt.fallbackLng || 'en',
80
+ detectBrowser: false,
81
+ });
82
+ serverContext.initialized = true;
83
+ }
84
+ }
85
+ return children;
86
+ }
87
+
88
+ /**
89
+ * route component (server)
90
+ * @param {Object} props - Component props
91
+ * @param {string} props.path - Route path
92
+ * @param {Function} [props.component] - Component function
93
+ * @param {*} [props.element] - Element to render
94
+ * @param {*} [props.children] - Child elements
95
+ * @returns {null}
96
+ */
97
+ export function Route({ path, component, element, children }) {
98
+ // register route during server initialization
99
+ // only register if not already registered to prevent duplicates
100
+ if (!serverContext.routeRegistry.has(path)) {
101
+ if (component) serverContext.routeRegistry.set(path, component);
102
+ else if (element) serverContext.routeRegistry.set(path, () => element);
103
+ else if (children) serverContext.routeRegistry.set(path, () => children);
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * FoxgirlCreampie component
110
+ * @param {Object} props - Component props
111
+ * @param {*} props.children - Child elements
112
+ * @returns {*} Children
113
+ */
114
+ export function FoxgirlCreampie({ children }) {
115
+ return children;
116
+ }
117
+
118
+ /**
119
+ * html document template
120
+ * @param {Object} props - Document props
121
+ * @param {string} [props.title] - Page title
122
+ * @param {string} props.content - Rendered HTML content
123
+ * @param {string} props.language - Language code
124
+ * @param {string} [props.theme] - Theme name
125
+ * @param {Array<string>} [props.scripts] - Additional scripts
126
+ * @param {Array<string>} [props.styles] - Additional stylesheets
127
+ * @param {string} [props.appName] - Application name
128
+ * @returns {Object} JSX element
129
+ */
130
+ function Document({ content, language, theme = 'dark', scripts = [], styles = [], appName = 'cumstack App' }) {
131
+ // sanitize data for json embedding to prevent xss
132
+ const sanitizedData = {
133
+ language: escapeHtml(language),
134
+ theme: escapeHtml(theme),
135
+ };
136
+
137
+ return h(
138
+ 'html',
139
+ { lang: language },
140
+ h(
141
+ 'head',
142
+ {},
143
+ h('meta', { charset: 'utf-8' }),
144
+ h('meta', {
145
+ name: 'viewport',
146
+ content: 'width=device-width, initial-scale=1',
147
+ })
148
+ ),
149
+ h(
150
+ 'body',
151
+ { className: `theme-${theme}`, style: 'margin: 0; padding: 0' },
152
+ h('div', { id: 'app', 'data-cumstack-ssr': 'true', innerHTML: content }),
153
+ h('script', {
154
+ id: 'cumstack-data',
155
+ type: 'application/json',
156
+ innerHTML: JSON.stringify(sanitizedData),
157
+ }),
158
+ h('script', { type: 'module', src: '/main.client.js' }),
159
+ ...scripts.map((src) => h('script', { type: 'module', src }))
160
+ )
161
+ );
162
+ }
163
+
164
+ /**
165
+ * match and render route
166
+ * @param {string} pathname - URL pathname
167
+ * @param {string} language - Language code
168
+ * @returns {Object|null} Rendered component and extracted params
169
+ */
170
+ function matchAndRenderRoute(pathname, language) {
171
+ // remove language prefix if present
172
+ let cleanPath = pathname;
173
+ if (serverContext.i18nConfig?.explicitRouting) {
174
+ const { path } = extractLanguageFromRoute(pathname);
175
+ cleanPath = path || '/';
176
+ }
177
+
178
+ // try exact match first
179
+ if (serverContext.routeRegistry.has(cleanPath)) {
180
+ const component = serverContext.routeRegistry.get(cleanPath);
181
+ return {
182
+ content: component ? component({ params: {} }) : null,
183
+ params: {},
184
+ };
185
+ }
186
+
187
+ // try wildcard match with param extraction
188
+ for (const [pattern, component] of serverContext.routeRegistry.entries()) {
189
+ if (pattern.includes(':') || pattern === '*') {
190
+ // extract parameter names
191
+ const paramNames = [];
192
+ const regexPattern = pattern
193
+ .replace(/:(\w+)/g, (_, name) => {
194
+ paramNames.push(name);
195
+ return '([^/]+)';
196
+ })
197
+ .replace('*', '(.*)');
198
+ const regex = new RegExp('^' + regexPattern + '$');
199
+ const match = cleanPath.match(regex);
200
+ if (match) {
201
+ // extract params
202
+ const params = {};
203
+ paramNames.forEach((name, i) => (params[name] = match[i + 1]));
204
+ return { content: component ? component({ params }) : null, params };
205
+ }
206
+ }
207
+ }
208
+
209
+ // 404
210
+ return {
211
+ content: h(
212
+ 'div',
213
+ { className: 'page not-found' },
214
+ h('h1', {}, '404 - Not Found'),
215
+ h('p', {}, 'The page you are looking for does not exist'),
216
+ h('a', { href: '/' }, 'Go Home')
217
+ ),
218
+ params: {},
219
+ };
220
+ }
221
+
222
+ /**
223
+ * create server handler with hono
224
+ * @param {Function} app - App component function
225
+ * @param {Object} [options] - Server options
226
+ * @param {Function} [options.middlewares] - Custom middleware function
227
+ * @param {Function} [options.routes] - Custom API routes function
228
+ * @param {string} [options.appName] - Application name
229
+ * @param {string} [options.theme] - Default theme
230
+ * @param {Array<string>} [options.scripts] - Additional scripts
231
+ * @param {Array<string>} [options.styles] - Additional stylesheets
232
+ * @returns {Function} Hono fetch handler
233
+ */
234
+ export function foxgirl(app, options = {}) {
235
+ serverContext = createServerContext();
236
+ const honoApp = new Hono();
237
+ const { appName = 'cumstack App', theme = 'dark', scripts = [], styles = [] } = options;
238
+ app();
239
+ // middleware: set request context (avoid global pollution)
240
+ honoApp.use('*', async (c, next) => {
241
+ // store env in request context instead of global
242
+ c.set('deployment', c.env?.CF_VERSION_METADATA ?? null);
243
+ c.set('env', c.env ?? {});
244
+ c.set('environment', c.env?.ENVIRONMENT ?? 'development');
245
+ await next();
246
+ });
247
+ if (options.middlewares && typeof options.middlewares === 'function') options.middlewares(honoApp);
248
+ honoApp.use('*', async (c, next) => {
249
+ const language = detectLanguageFromRequest(
250
+ c.req.raw,
251
+ serverContext.i18nConfig || {
252
+ supportedLanguages: ['en'],
253
+ explicitRouting: false,
254
+ defaultLng: 'en',
255
+ fallbackLng: 'en',
256
+ }
257
+ );
258
+ setLanguage(language);
259
+ c.set('language', language);
260
+ await next();
261
+ });
262
+ // register all routes with hono
263
+ const registeredPatterns = new Set();
264
+ for (const [path, component] of serverContext.routeRegistry.entries()) {
265
+ // handle both explicit language routes and base routes
266
+ const patterns = [];
267
+ if (serverContext.i18nConfig?.explicitRouting) {
268
+ serverContext.i18nConfig.supportedLanguages.forEach((lang) => {
269
+ patterns.push(`/${lang}${path === '/' ? '' : path}`);
270
+ if (path === '/') patterns.push(`/${lang}`);
271
+ });
272
+ }
273
+ patterns.push(path);
274
+ patterns.forEach((pattern) => {
275
+ if (!registeredPatterns.has(pattern)) {
276
+ registeredPatterns.add(pattern);
277
+ honoApp.get(pattern, async (c) => {
278
+ try {
279
+ const language = c.get('language');
280
+ const url = new URL(c.req.url);
281
+ const { content, params } = matchAndRenderRoute(url.pathname, language);
282
+ const appContent = h('div', { className: 'app-root' }, content);
283
+ const html = renderToString(
284
+ Document({
285
+ title: appName,
286
+ content: renderToString(appContent),
287
+ language,
288
+ theme,
289
+ scripts,
290
+ styles,
291
+ appName,
292
+ })
293
+ );
294
+ const response = c.html('<!DOCTYPE html>' + html);
295
+ response.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
296
+ return response;
297
+ } catch (error) {
298
+ console.error(`Route error [${c.req.method} ${c.req.url}]:`, error);
299
+ const errorHtml = `
300
+ <!DOCTYPE html>
301
+ <html>
302
+ <head><title>500 - Server Error</title></head>
303
+ <body>
304
+ <h1>Internal Server Error</h1>
305
+ <p>An error occurred while processing your request.</p>
306
+ ${c.get('environment') !== 'production' ? `<pre>${escapeHtml(error.stack)}</pre>` : ''}
307
+ </body>
308
+ </html>
309
+ `;
310
+ return c.html(errorHtml, 500);
311
+ }
312
+ });
313
+ }
314
+ });
315
+ }
316
+ if (options.routes && typeof options.routes === 'function') options.routes(honoApp);
317
+ // 404 handler
318
+ honoApp.notFound((c) => {
319
+ const language = c.get('language') || 'en';
320
+ const notFoundContent = h(
321
+ 'div',
322
+ { className: 'page not-found' },
323
+ h('h1', {}, '404 - Not Found'),
324
+ h('p', {}, 'The page you are looking for does not exist'),
325
+ h('a', { href: '/' }, 'Go Home')
326
+ );
327
+ const html = renderToString(
328
+ Document({
329
+ title: '404 - Not Found',
330
+ content: renderToString(notFoundContent),
331
+ language,
332
+ theme,
333
+ appName,
334
+ })
335
+ );
336
+ return c.html('<!DOCTYPE html>' + html, 404);
337
+ });
338
+
339
+ // error handler
340
+ honoApp.onError((err, c) => {
341
+ console.error(`Server error [${c.req.method} ${c.req.url}]:`, err);
342
+ const errorHtml = `
343
+ <!DOCTYPE html>
344
+ <html>
345
+ <head><title>500 - Server Error</title></head>
346
+ <body>
347
+ <h1>Internal Server Error</h1>
348
+ <p>An unexpected error occurred.</p>
349
+ ${c.get('environment') !== 'production' ? `<pre>${escapeHtml(err.stack || err.message)}</pre>` : ''}
350
+ </body>
351
+ </html>
352
+ `;
353
+ return c.html(errorHtml, 500);
354
+ });
355
+ // return hono's fetch handler
356
+ return honoApp.fetch.bind(honoApp);
357
+ }
358
+
359
+ /**
360
+ * export route registry for client access
361
+ * @returns {Map} Current route registry
362
+ */
363
+ export function getRouteRegistry() {
364
+ return serverContext.routeRegistry;
365
+ }
366
+
367
+ /**
368
+ * get i18n configuration
369
+ * @returns {Object|null} Current i18n config
370
+ */
371
+ export function getI18nConfig() {
372
+ return serverContext.i18nConfig;
373
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * cumstack i18n System
3
+ * built-in internationalization support
4
+ */
5
+
6
+ import { createMoan } from './reactivity.js';
7
+ import { isValidLanguageCode } from './language-codes.js';
8
+
9
+ // translation store
10
+ const translations = new Map();
11
+ let i18nConfiguration = {
12
+ supportedLanguages: null, // null means use all registered
13
+ explicitRouting: false,
14
+ defaultLanguage: null,
15
+ fallbackLanguage: null,
16
+ storageKeys: {
17
+ page: 'pLng',
18
+ user: 'uLng',
19
+ },
20
+ };
21
+ let [currentLanguage, setCurrentLanguage] = createMoan(null);
22
+
23
+ /**
24
+ * get supported languages from config or all registered translations
25
+ * @returns {string[]}
26
+ */
27
+ function getSupportedLanguages() {
28
+ const registered = Array.from(translations.keys());
29
+ // if supportedLanguages is configured, filter registered translations
30
+ if (i18nConfiguration.supportedLanguages && Array.isArray(i18nConfiguration.supportedLanguages)) {
31
+ return registered.filter((lang) => i18nConfiguration.supportedLanguages.includes(lang));
32
+ }
33
+ return registered;
34
+ }
35
+
36
+ /**
37
+ * register translations for a language
38
+ * @param {string} lang - Language code (must be valid ISO 639-1)
39
+ * @param {Record<string, string>} messages - Translation messages
40
+ */
41
+ export function registerTranslations(lang, messages) {
42
+ if (!isValidLanguageCode(lang)) {
43
+ console.warn(`Invalid language code: ${lang}. Must be a valid ISO 639-1 code.`);
44
+ return;
45
+ }
46
+ translations.set(lang, messages);
47
+ }
48
+
49
+ /**
50
+ * get translation for a key
51
+ * @param {string} key - Translation key
52
+ * @param {Record<string, any>} params - Interpolation params
53
+ * @returns {string}
54
+ */
55
+ export function t(key, params = {}) {
56
+ const lang = currentLanguage();
57
+ const fallback = i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0];
58
+ const messages = translations.get(lang) || translations.get(fallback) || {};
59
+ let message = messages[key] || key;
60
+ // interpolate params
61
+ Object.keys(params).forEach((param) => (message = message.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param])));
62
+ return message;
63
+ }
64
+
65
+ /**
66
+ * set current language
67
+ * @param {string} lang - Language code
68
+ * @param {boolean} isExplicitRoute - Whether this is from an explicit language route
69
+ */
70
+ export function setLanguage(lang, isExplicitRoute = false) {
71
+ const supportedLanguages = getSupportedLanguages();
72
+ if (supportedLanguages.includes(lang)) {
73
+ setCurrentLanguage(lang);
74
+ // store in localstorage if available
75
+ if (typeof window !== 'undefined') {
76
+ const pageKey = i18nConfiguration.storageKeys?.page || 'pLng';
77
+ const userKey = i18nConfiguration.storageKeys?.user || 'uLng';
78
+ localStorage.setItem('language', lang);
79
+ // if it's an explicit route, store as preferred language
80
+ if (isExplicitRoute) localStorage.setItem(pageKey, lang);
81
+ // always update user language
82
+ localStorage.setItem(userKey, lang);
83
+ document.documentElement.lang = lang;
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * get current language
90
+ * @returns {string}
91
+ */
92
+ export function getLanguage() {
93
+ return currentLanguage();
94
+ }
95
+
96
+ /**
97
+ * get all translations for a language
98
+ * @param {string} lang - Language code
99
+ * @returns {Record<string, string>}
100
+ */
101
+ export function getTranslations(lang = null) {
102
+ const targetLang = lang || currentLanguage();
103
+ return translations.get(targetLang) || {};
104
+ }
105
+
106
+ /**
107
+ * detect language from browser
108
+ * @returns {string}
109
+ */
110
+ export function detectBrowserLanguage() {
111
+ if (typeof window === 'undefined') return i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0];
112
+ const supportedLanguages = getSupportedLanguages();
113
+ // check localstorage first
114
+ const stored = localStorage.getItem('language');
115
+ if (stored && supportedLanguages.includes(stored)) return stored;
116
+ // check navigator language
117
+ const browserLang = navigator.language.split('-')[0];
118
+ return supportedLanguages.includes(browserLang) ? browserLang : i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0];
119
+ }
120
+
121
+ /**
122
+ * get user language from localstorage
123
+ * @returns {string|null}
124
+ */
125
+ export function getUserLanguage() {
126
+ if (typeof window === 'undefined') return null;
127
+ const supportedLanguages = getSupportedLanguages();
128
+ const userKey = i18nConfiguration.storageKeys?.user || 'uLng';
129
+ const userLang = localStorage.getItem(userKey);
130
+ return userLang && supportedLanguages.includes(userLang) ? userLang : null;
131
+ }
132
+
133
+ /**
134
+ * get preferred language from localstorage
135
+ * @returns {string|null}
136
+ */
137
+ export function getPreferredLanguage() {
138
+ if (typeof window === 'undefined') return null;
139
+ const supportedLanguages = getSupportedLanguages();
140
+ const pageKey = i18nConfiguration.storageKeys?.page || 'pLng';
141
+ const prefLang = localStorage.getItem(pageKey);
142
+ return prefLang && supportedLanguages.includes(prefLang) ? prefLang : null;
143
+ }
144
+
145
+ /**
146
+ * set preferred language
147
+ * @param {string} lang - Language code
148
+ */
149
+ export function setPreferredLanguage(lang) {
150
+ const supportedLanguages = getSupportedLanguages();
151
+ const pageKey = i18nConfiguration.storageKeys?.page || 'pLng';
152
+ if (typeof window !== 'undefined' && supportedLanguages.includes(lang)) localStorage.setItem(pageKey, lang);
153
+ }
154
+
155
+ /**
156
+ * clear preferred language from localstorage
157
+ */
158
+ export function clearPreferredLanguage() {
159
+ if (typeof window !== 'undefined') {
160
+ const pageKey = i18nConfiguration.storageKeys?.page || 'pLng';
161
+ localStorage.removeItem(pageKey);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * initialize i18n system
167
+ * @param {Object} options - Initialization options
168
+ * @param {Array<string>} [options.supportedLanguages] - List of supported language codes
169
+ * @param {boolean} [options.explicitRouting] - Enable language prefix in routes
170
+ * @param {string} [options.defaultLanguage] - Default language ('auto' for detection)
171
+ * @param {string} [options.fallbackLanguage] - Fallback language
172
+ * @param {Object} [options.storageKeys] - Custom localStorage keys
173
+ * @param {boolean} [options.detectBrowser] - Enable browser language detection
174
+ * @returns {Object} i18n utilities (language signal, setLanguage, t)
175
+ */
176
+ export function initI18n(options = {}) {
177
+ // merge configuration
178
+ i18nConfiguration = {
179
+ ...i18nConfiguration,
180
+ supportedLanguages: options.supportedLanguages || null,
181
+ explicitRouting: options.explicitRouting !== undefined ? options.explicitRouting : false,
182
+ defaultLanguage: options.defaultLanguage || options.defaultLng || null,
183
+ fallbackLanguage: options.fallbackLanguage || options.fallbackLng || null,
184
+ storageKeys: {
185
+ page: options.storageKeys?.page || options.storageKeys?.preferred || 'pLng',
186
+ user: options.storageKeys?.user || 'uLng',
187
+ },
188
+ };
189
+ const supportedLanguages = getSupportedLanguages();
190
+ const configDefaultLang = i18nConfiguration.defaultLanguage;
191
+ const fallbackLang = i18nConfiguration.fallbackLanguage || supportedLanguages[0];
192
+ const { detectBrowser = true } = options;
193
+ let initialLang = fallbackLang;
194
+ // handle 'auto' detection or browser detection
195
+ if (configDefaultLang === 'auto' || (detectBrowser && typeof window !== 'undefined')) initialLang = detectBrowserLanguage();
196
+ else if (configDefaultLang && configDefaultLang !== 'auto') initialLang = configDefaultLang;
197
+ setCurrentLanguage(initialLang);
198
+ if (typeof window !== 'undefined') document.documentElement.lang = initialLang;
199
+ return {
200
+ language: currentLanguage,
201
+ setLanguage,
202
+ t,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * localize a route path with language prefix
208
+ * @param {string} path - Route path
209
+ * @param {string|null} [lang] - Language code (defaults to current language)
210
+ * @returns {string} Localized route path
211
+ */
212
+ export function localizeRoute(path, lang = null) {
213
+ const language = lang || currentLanguage();
214
+ const defaultLang = i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0];
215
+ if (!i18nConfiguration.explicitRouting && language === defaultLang) return path;
216
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
217
+ return `/${language}${normalizedPath}`;
218
+ }
219
+
220
+ /**
221
+ * extract language from localized route
222
+ * @param {string} path - Route path
223
+ * @returns {{ language: string, path: string }}
224
+ */
225
+ export function extractLanguageFromRoute(path) {
226
+ const segments = path.split('/').filter(Boolean);
227
+ const supportedLanguages = getSupportedLanguages();
228
+ if (segments.length > 0 && supportedLanguages.includes(segments[0])) {
229
+ return {
230
+ language: segments[0],
231
+ path: '/' + segments.slice(1).join('/'),
232
+ };
233
+ }
234
+ return {
235
+ language: i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0],
236
+ path,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * create language switcher helper
242
+ * @param {Function} navigate - Navigation function
243
+ * @returns {Function} Language switcher function
244
+ */
245
+ export function createLanguageSwitcher(navigate) {
246
+ return (lang) => {
247
+ const currentPath = typeof window !== 'undefined' ? window.location.pathname : '/';
248
+ const { path } = extractLanguageFromRoute(currentPath);
249
+ const newPath = localizeRoute(path, lang);
250
+ setLanguage(lang);
251
+ navigate?.(newPath);
252
+ };
253
+ }
254
+
255
+ /**
256
+ * get the configured default language
257
+ * @returns {string}
258
+ */
259
+ export function getDefaultLanguage() {
260
+ return i18nConfiguration.fallbackLanguage || getSupportedLanguages()[0];
261
+ }
262
+
263
+ /**
264
+ * get the i18n configuration
265
+ * @returns {Object}
266
+ */
267
+ export function getI18nConfiguration() {
268
+ return i18nConfiguration;
269
+ }
270
+
271
+ export { currentLanguage };