refactor: move from next-international to next-intl (#1368)
* refactor: move from next-international to next-intl * refactor: restructure translation package, * chore: change i18n-allay framework to next-intl * fix: add missing bold html tag to translation * fix: format issue * fix: address deepsource issues * fix: remove international-types dependency * fix: lint and typecheck issues * fix: typecheck issue * fix: typecheck issue * fix: issue with translations
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createI18nClient } from "next-international/client";
|
||||
|
||||
import { languageMapping } from "./lang";
|
||||
import enTranslation from "./lang/en";
|
||||
|
||||
export const { useI18n, useScopedI18n, useCurrentLocale, useChangeLocale, I18nProviderClient } = createI18nClient(
|
||||
languageMapping(),
|
||||
{
|
||||
fallbackLocale: enTranslation,
|
||||
},
|
||||
);
|
||||
19
packages/translation/src/client/index.ts
Normal file
19
packages/translation/src/client/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useMessages, useTranslations } from "next-intl";
|
||||
|
||||
import type { TranslationObject } from "../type";
|
||||
|
||||
export { useChangeLocale } from "./use-change-locale";
|
||||
export { useCurrentLocale } from "./use-current-locale";
|
||||
|
||||
export const { useI18n, useScopedI18n } = {
|
||||
useI18n: useTranslations,
|
||||
useScopedI18n: useTranslations,
|
||||
};
|
||||
|
||||
export const { useI18nMessages } = {
|
||||
useI18nMessages: () => useMessages() as TranslationObject,
|
||||
};
|
||||
|
||||
export { useTranslations };
|
||||
25
packages/translation/src/client/use-change-locale.ts
Normal file
25
packages/translation/src/client/use-change-locale.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useTransition } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import type { SupportedLanguage } from "../config";
|
||||
import { useCurrentLocale } from "./use-current-locale";
|
||||
|
||||
export const useChangeLocale = () => {
|
||||
const currentLocale = useCurrentLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return {
|
||||
changeLocale: (newLocale: SupportedLanguage) => {
|
||||
if (newLocale === currentLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(`/${newLocale}/${pathname}`);
|
||||
});
|
||||
},
|
||||
isPending,
|
||||
};
|
||||
};
|
||||
5
packages/translation/src/client/use-current-locale.ts
Normal file
5
packages/translation/src/client/use-current-locale.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
import type { SupportedLanguage } from "../config";
|
||||
|
||||
export const useCurrentLocale = () => useLocale() as SupportedLanguage;
|
||||
26
packages/translation/src/config.ts
Normal file
26
packages/translation/src/config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { objectKeys } from "@homarr/common";
|
||||
|
||||
export const localeConfigurations = {
|
||||
de: {
|
||||
name: "Deutsch",
|
||||
translatedName: "German",
|
||||
flagIcon: "de",
|
||||
},
|
||||
en: {
|
||||
name: "English",
|
||||
translatedName: "English",
|
||||
flagIcon: "us",
|
||||
},
|
||||
} satisfies Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
translatedName: string;
|
||||
flagIcon: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const supportedLanguages = objectKeys(localeConfigurations);
|
||||
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||
|
||||
export const defaultLocale = "en" satisfies SupportedLanguage;
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { SupportedLanguage } from "./config";
|
||||
import { supportedLanguages } from "./config";
|
||||
import type { stringOrTranslation, TranslationFunction } from "./type";
|
||||
|
||||
export * from "./type";
|
||||
export * from "./locale-attributes";
|
||||
|
||||
export const supportedLanguages = ["en", "de"] as const;
|
||||
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||
|
||||
export const defaultLocale = "en";
|
||||
export { languageMapping } from "./lang";
|
||||
export type { TranslationKeys } from "./lang";
|
||||
export * from "./config";
|
||||
export { createLanguageMapping } from "./mapping";
|
||||
export type { TranslationKeys } from "./mapping";
|
||||
|
||||
export const translateIfNecessary = (t: TranslationFunction, value: stringOrTranslation | undefined) => {
|
||||
if (typeof value === "function") {
|
||||
@@ -16,3 +13,7 @@ export const translateIfNecessary = (t: TranslationFunction, value: stringOrTran
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const isLocaleSupported = (locale: string): locale is SupportedLanguage => {
|
||||
return supportedLanguages.includes(locale as SupportedLanguage);
|
||||
};
|
||||
|
||||
@@ -706,7 +706,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mantineReactTable: MRT_Localization_EN,
|
||||
mantineReactTable: MRT_Localization_EN as Readonly<Record<keyof typeof MRT_Localization_EN, string>>,
|
||||
},
|
||||
section: {
|
||||
dynamic: {
|
||||
@@ -1205,11 +1205,11 @@ export default {
|
||||
},
|
||||
integration: {
|
||||
noData: "No integration found",
|
||||
description: "Click {here} to create a new integration",
|
||||
description: "Click <here></here> to create a new integration",
|
||||
},
|
||||
app: {
|
||||
noData: "No app found",
|
||||
description: "Click {here} to create a new app",
|
||||
description: "Click <here></here> to create a new app",
|
||||
},
|
||||
error: {
|
||||
action: {
|
||||
@@ -1842,7 +1842,7 @@ export default {
|
||||
copy: {
|
||||
title: "Copy invite",
|
||||
description:
|
||||
"Your invitation has been generated. After this modal closes, you'll not be able to copy this link anymore. If you do no longer wish to invite said person, you can delete this invitation any time.",
|
||||
"Your invitation has been generated. After this modal closes, <b>you'll not be able to copy this link anymore.</b> If you do no longer wish to invite said person, you can delete this invitation any time.",
|
||||
link: "Invitation link",
|
||||
button: "Copy & close",
|
||||
},
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { SupportedLanguage } from ".";
|
||||
|
||||
export const localeAttributes: Record<
|
||||
SupportedLanguage,
|
||||
{
|
||||
name: string;
|
||||
translatedName: string;
|
||||
flagIcon: string;
|
||||
}
|
||||
> = {
|
||||
de: {
|
||||
name: "Deutsch",
|
||||
translatedName: "German",
|
||||
flagIcon: "de",
|
||||
},
|
||||
en: {
|
||||
name: "English",
|
||||
translatedName: "English",
|
||||
flagIcon: "us",
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { supportedLanguages } from ".";
|
||||
import { supportedLanguages } from "./config";
|
||||
|
||||
const _enTranslations = () => import("./lang/en");
|
||||
type EnTranslation = typeof _enTranslations;
|
||||
|
||||
export const languageMapping = () => {
|
||||
export const createLanguageMapping = () => {
|
||||
const mapping: Record<string, unknown> = {};
|
||||
|
||||
for (const language of supportedLanguages) {
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createI18nMiddleware } from "next-international/middleware";
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
|
||||
import { defaultLocale, supportedLanguages } from ".";
|
||||
import { routing } from "./routing";
|
||||
|
||||
export const I18nMiddleware = createI18nMiddleware({
|
||||
locales: supportedLanguages,
|
||||
defaultLocale,
|
||||
urlMappingStrategy: "rewrite",
|
||||
});
|
||||
export const I18nMiddleware = createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: ["/", "/(de|en)/:path*"],
|
||||
};
|
||||
|
||||
34
packages/translation/src/request.ts
Normal file
34
packages/translation/src/request.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
|
||||
import { isLocaleSupported } from ".";
|
||||
import type { SupportedLanguage } from "./config";
|
||||
import { createLanguageMapping } from "./mapping";
|
||||
import { routing } from "./routing";
|
||||
|
||||
// This file is referenced in the `next.config.js` file. See https://next-intl-docs.vercel.app/docs/usage/configuration
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let currentLocale = await requestLocale;
|
||||
|
||||
if (!currentLocale || !isLocaleSupported(currentLocale)) {
|
||||
currentLocale = routing.defaultLocale;
|
||||
}
|
||||
const typedLocale = currentLocale as SupportedLanguage;
|
||||
|
||||
const languageMap = createLanguageMapping();
|
||||
const currentMessages = (await languageMap[typedLocale]()).default;
|
||||
|
||||
// Fallback to default locale if the current locales messages if not all messages are present
|
||||
if (currentLocale !== routing.defaultLocale) {
|
||||
const fallbackMessages = (await languageMap[routing.defaultLocale]()).default;
|
||||
return {
|
||||
locale: currentLocale,
|
||||
messages: deepmerge(fallbackMessages, currentMessages),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
locale: currentLocale,
|
||||
messages: currentMessages,
|
||||
};
|
||||
});
|
||||
11
packages/translation/src/routing.ts
Normal file
11
packages/translation/src/routing.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
import { defaultLocale, supportedLanguages } from "./config";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: supportedLanguages,
|
||||
defaultLocale,
|
||||
localePrefix: {
|
||||
mode: "never", // Rewrite the URL with locale parameter but without shown in url
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createI18nServer } from "next-international/server";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { languageMapping } from "./lang";
|
||||
import enTranslation from "./lang/en";
|
||||
export const { getI18n, getScopedI18n } = {
|
||||
getI18n: getTranslations,
|
||||
getScopedI18n: getTranslations,
|
||||
};
|
||||
|
||||
export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer(languageMapping(), {
|
||||
fallbackLocale: enTranslation,
|
||||
});
|
||||
export { getMessages as getI18nMessages } from "next-intl/server";
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import type { NamespaceKeys, NestedKeyOf } from "next-intl";
|
||||
|
||||
import type { RemoveReadonly } from "@homarr/common/types";
|
||||
|
||||
import type { useI18n, useScopedI18n } from "./client";
|
||||
import type enTranslation from "./lang/en";
|
||||
|
||||
export type TranslationFunction = ReturnType<typeof useI18n>;
|
||||
export type ScopedTranslationFunction<T extends Parameters<typeof useScopedI18n>[0]> = ReturnType<
|
||||
typeof useScopedI18n<T>
|
||||
>;
|
||||
export type TranslationFunction = ReturnType<typeof useI18n<never>>;
|
||||
export type ScopedTranslationFunction<
|
||||
NestedKey extends NamespaceKeys<IntlMessages, NestedKeyOf<IntlMessages>> = never,
|
||||
> = ReturnType<typeof useScopedI18n<NestedKey>>;
|
||||
export type TranslationObject = typeof enTranslation;
|
||||
export type stringOrTranslation = string | ((t: TranslationFunction) => string);
|
||||
|
||||
declare global {
|
||||
// Use type safe message keys with `next-intl`
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface IntlMessages extends RemoveReadonly<TranslationObject> {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user