B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

31
lib/i18n/i18n.resolver.ts Normal file
View File

@@ -0,0 +1,31 @@
import { getLogger } from '@kit/shared/logger';
/**
* @name i18nResolver
* @description Resolve the translation file for the given language and namespace in the current application.
* @param language
* @param namespace
*/
export async function i18nResolver(language: string, namespace: string) {
const logger = await getLogger();
try {
const data = await import(
`../../public/locales/${language}/${namespace}.json`
);
return data as Record<string, string>;
} catch (error) {
console.group(
`Error while loading translation file: ${language}/${namespace}`,
);
logger.error(error instanceof Error ? error.message : error);
logger.warn(
`Please create a translation file for this language at "public/locales/${language}/${namespace}.json"`,
);
console.groupEnd();
// return an empty object if the file could not be loaded to avoid loops
return {};
}
}

98
lib/i18n/i18n.server.ts Normal file
View File

@@ -0,0 +1,98 @@
import 'server-only';
import { cache } from 'react';
import { cookies, headers } from 'next/headers';
import { z } from 'zod';
import {
initializeServerI18n,
parseAcceptLanguageHeader,
} from '@kit/i18n/server';
import featuresFlagConfig from '~/config/feature-flags.config';
import {
I18N_COOKIE_NAME,
getI18nSettings,
languages,
} from '~/lib/i18n/i18n.settings';
import { i18nResolver } from './i18n.resolver';
/**
* @name priority
* @description The language priority setting from the feature flag configuration.
*/
const priority = featuresFlagConfig.languagePriority;
/**
* @name createI18nServerInstance
* @description Creates an instance of the i18n server.
* It uses the language from the cookie if it exists, otherwise it uses the language from the accept-language header.
* If neither is available, it will default to the provided environment variable.
*
* Initialize the i18n instance for every RSC server request (eg. each page/layout)
*/
async function createInstance() {
const cookieStore = await cookies();
const langCookieValue = cookieStore.get(I18N_COOKIE_NAME)?.value;
let selectedLanguage: string | undefined = undefined;
// if the cookie is set, use the language from the cookie
if (langCookieValue) {
selectedLanguage = getLanguageOrFallback(langCookieValue);
}
// if not, check if the language priority is set to user and
// use the user's preferred language
if (!selectedLanguage && priority === 'user') {
const userPreferredLanguage = await getPreferredLanguageFromBrowser();
selectedLanguage = getLanguageOrFallback(userPreferredLanguage);
}
const settings = getI18nSettings(selectedLanguage);
return initializeServerI18n(settings, i18nResolver);
}
export const createI18nServerInstance = cache(createInstance);
/**
* @name getPreferredLanguageFromBrowser
* Get the user's preferred language from the accept-language header.
*/
async function getPreferredLanguageFromBrowser() {
const headersStore = await headers();
const acceptLanguage = headersStore.get('accept-language');
// no accept-language header, return
if (!acceptLanguage) {
return;
}
return parseAcceptLanguageHeader(acceptLanguage, languages)[0];
}
/**
* @name getLanguageOrFallback
* Get the language or fallback to the default language.
* @param selectedLanguage
*/
function getLanguageOrFallback(selectedLanguage: string | undefined) {
const language = z
.enum(languages as [string, ...string[]])
.safeParse(selectedLanguage);
if (language.success) {
return language.data;
}
console.warn(
`The language passed is invalid. Defaulted back to "${languages[0]}"`,
);
return languages[0];
}

62
lib/i18n/i18n.settings.ts Normal file
View File

@@ -0,0 +1,62 @@
import { createI18nSettings } from '@kit/i18n';
/**
* The default language of the application.
* This is used as a fallback language when the selected language is not supported.
*
*/
const defaultLanguage = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
/**
* The list of supported languages.
* By default, only the default language is supported.
* Add more languages here if needed.
*/
export const languages: string[] = [defaultLanguage];
/**
* The name of the cookie that stores the selected language.
*/
export const I18N_COOKIE_NAME = 'lang';
/**
* The default array of Internationalization (i18n) namespaces.
* These namespaces are commonly used in the application for translation purposes.
*
* Add your own namespaces here
**/
export const defaultI18nNamespaces = [
'common',
'auth',
'account',
'teams',
'billing',
'marketing',
];
/**
* Get the i18n settings for the given language and namespaces.
* If the language is not supported, it will fall back to the default language.
* @param language
* @param ns
*/
export function getI18nSettings(
language: string | undefined,
ns: string | string[] = defaultI18nNamespaces,
) {
let lng = language ?? defaultLanguage;
if (!languages.includes(lng)) {
console.warn(
`Language "${lng}" is not supported. Falling back to "${defaultLanguage}"`,
);
lng = defaultLanguage;
}
return createI18nSettings({
language: lng,
namespaces: ns,
languages,
});
}

13
lib/i18n/with-i18n.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { createI18nServerInstance } from './i18n.server';
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
export function withI18n<Params extends object>(
Component: LayoutOrPageComponent<Params>,
) {
return async function I18nServerComponentWrapper(params: Params) {
await createI18nServerInstance();
return <Component {...params} />;
};
}