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:
Meier Lukas
2024-10-26 22:46:14 +02:00
committed by GitHub
parent db198c6dab
commit 4502569223
33 changed files with 331 additions and 160 deletions

View File

@@ -27,7 +27,7 @@
"Umami"
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.enabledFrameworks": ["next-international"],
"i18n-ally.enabledFrameworks": ["next-intl"],
"i18n-ally.localesPaths": ["./packages/translation/src/lang/"],
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.extract.keyMaxLength": 0,

View File

@@ -2,11 +2,15 @@
import "@homarr/auth/env.mjs";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
import "./src/env.mjs";
// Package path does not work... so we need to use relative path
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
/** @type {import("next").NextConfig} */
const config = {
const nextConfig = {
output: "standalone",
reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */
@@ -34,4 +38,4 @@ const config = {
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
export default config;
export default withNextIntl(nextConfig);

View File

@@ -1,12 +0,0 @@
import type { PropsWithChildren } from "react";
import { defaultLocale } from "@homarr/translation";
import { I18nProviderClient } from "@homarr/translation/client";
export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
return (
<I18nProviderClient locale={locale} fallback={defaultLocale}>
{children}
</I18nProviderClient>
);
};

View File

@@ -7,18 +7,20 @@ import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { env } from "@homarr/auth/env.mjs";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { getScopedI18n } from "@homarr/translation/server";
import { isLocaleSupported } from "@homarr/translation";
import { getI18nMessages, getScopedI18n } from "@homarr/translation/server";
import { Analytics } from "~/components/layout/analytics";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { JotaiProvider } from "./_client-providers/jotai";
import { CustomMantineProvider } from "./_client-providers/mantine";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { AuthProvider } from "./_client-providers/session";
import { TRPCReactProvider } from "./_client-providers/trpc";
import { composeWrappers } from "./compose";
@@ -59,10 +61,15 @@ export const viewport: Viewport = {
};
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
if (!isLocaleSupported(props.params.locale)) {
notFound();
}
const session = await auth();
const colorScheme = getColorScheme();
const tCommon = await getScopedI18n("common");
const direction = tCommon("direction");
const i18nMessages = await getI18nMessages();
const StackedProvider = composeWrappers([
(innerProps) => {
@@ -70,7 +77,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
(innerProps) => <CustomMantineProvider {...innerProps} />,
(innerProps) => <ModalProvider {...innerProps} />,
]);
@@ -78,7 +85,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
return (
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
<html
lang="en"
lang={props.params.locale}
dir={direction}
data-mantine-color-scheme={colorScheme}
style={{

View File

@@ -60,7 +60,10 @@ export const UserCreateStepperComponent = () => {
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
params: createCustomErrorParams("passwordsDoNotMatch"),
params: createCustomErrorParams({
key: "passwordsDoNotMatch",
params: {},
}),
}),
{
initialValues: {

View File

@@ -10,8 +10,8 @@ export const ReservedGroupAlert = async () => {
return (
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
{t("group.reservedNotice.message", {
checkoutDocs: (
{t.rich("group.reservedNotice.message", {
checkoutDocs: () => (
<Anchor
size="sm"
component={Link}

View File

@@ -1,11 +1,11 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core";
import { Combobox, Group, InputBase, Loader, Text, useCombobox } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeAttributes, supportedLanguages } from "@homarr/translation";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import classes from "./language-combobox.module.css";
@@ -15,7 +15,7 @@ export const LanguageCombobox = () => {
onDropdownClose: () => combobox.resetSelectedOption(),
});
const currentLocale = useCurrentLocale();
const changeLocale = useChangeLocale();
const { changeLocale, isPending } = useChangeLocale();
const handleOnOptionSubmit = React.useCallback(
(value: string) => {
@@ -39,6 +39,7 @@ export const LanguageCombobox = () => {
component="button"
type="button"
pointer
leftSection={isPending ? <Loader size={16} /> : null}
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={handleOnClick}
@@ -72,11 +73,11 @@ const OptionItem = ({
return (
<Group wrap="nowrap" justify="space-between">
<Group wrap="nowrap">
<span className={`fi fi-${localeAttributes[localeKey].flagIcon} ${classes.flagIcon}`}></span>
<span className={`fi fi-${localeConfigurations[localeKey].flagIcon} ${classes.flagIcon}`}></span>
<Group wrap="nowrap" gap="xs">
<Text>{localeAttributes[localeKey].name}</Text>
<Text>{localeConfigurations[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit>
({localeAttributes[localeKey].translatedName})
({localeConfigurations[localeKey].translatedName})
</Text>
</Group>
</Group>

View File

@@ -5,3 +5,7 @@ export type AtLeastOneOf<T> = [T, ...T[]];
export type Modify<T, R extends Partial<Record<keyof T, unknown>>> = {
[P in keyof (Omit<T, keyof R> & R)]: (Omit<T, keyof R> & R)[P];
};
export type RemoveReadonly<T> = {
-readonly [P in keyof T]: T[P] extends Record<string, unknown> ? RemoveReadonly<T[P]> : T[P];
};

View File

@@ -12,8 +12,11 @@ export const InviteCopyModal = createModal<RouterOutputs["invite"]["createInvite
return (
<Stack>
<Text>{t("action.copy.description")}</Text>
{/* TODO: When next-international v2 is released the descriptions bold element can be implemented, see https://github.com/QuiiBz/next-international/pull/361 for progress */}
<Text>
{t.rich("action.copy.description", {
b: (children) => <b>{children}</b>,
})}
</Text>
<Link href={createPath(innerProps)}>{t("action.copy.link")}</Link>
<Stack gap="xs">
<Text fw="bold">{t("field.id.label")}:</Text>

View File

@@ -1,7 +1,7 @@
import { Group, Stack, Text } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import { localeAttributes, supportedLanguages } from "@homarr/translation";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
import { createChildrenOptions } from "../../../lib/children";
@@ -11,34 +11,34 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
const normalizedQuery = query.trim().toLowerCase();
const currentLocale = useCurrentLocale();
return supportedLanguages
.map((localeKey) => ({ localeKey, attributes: localeAttributes[localeKey] }))
.map((localeKey) => ({ localeKey, configuration: localeConfigurations[localeKey] }))
.filter(
({ attributes }) =>
attributes.name.toLowerCase().includes(normalizedQuery) ||
attributes.translatedName.toLowerCase().includes(normalizedQuery),
({ configuration }) =>
configuration.name.toLowerCase().includes(normalizedQuery) ||
configuration.translatedName.toLowerCase().includes(normalizedQuery),
)
.sort(
(languageA, languageB) =>
Math.min(
languageA.attributes.name.toLowerCase().indexOf(normalizedQuery),
languageA.attributes.translatedName.toLowerCase().indexOf(normalizedQuery),
languageA.configuration.name.toLowerCase().indexOf(normalizedQuery),
languageA.configuration.translatedName.toLowerCase().indexOf(normalizedQuery),
) -
Math.min(
languageB.attributes.name.toLowerCase().indexOf(normalizedQuery),
languageB.attributes.translatedName.toLowerCase().indexOf(normalizedQuery),
languageB.configuration.name.toLowerCase().indexOf(normalizedQuery),
languageB.configuration.translatedName.toLowerCase().indexOf(normalizedQuery),
),
)
.map(({ localeKey, attributes }) => ({
.map(({ localeKey, configuration }) => ({
key: localeKey,
Component() {
return (
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
<Group wrap="nowrap">
<span className={`fi fi-${attributes.flagIcon}`} style={{ borderRadius: 4 }}></span>
<span className={`fi fi-${configuration.flagIcon}`} style={{ borderRadius: 4 }}></span>
<Group wrap="nowrap" gap="xs">
<Text>{attributes.name}</Text>
<Text>{configuration.name}</Text>
<Text size="xs" c="dimmed" inherit>
({attributes.translatedName})
({configuration.translatedName})
</Text>
</Group>
</Group>
@@ -47,7 +47,7 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
);
},
useInteraction() {
const changeLocale = useChangeLocale();
const { changeLocale } = useChangeLocale();
return { type: "javaScript", onSelect: () => changeLocale(localeKey) };
},

View File

@@ -6,9 +6,10 @@
"type": "module",
"exports": {
".": "./index.ts",
"./client": "./src/client.ts",
"./client": "./src/client/index.ts",
"./server": "./src/server.ts",
"./middleware": "./src/middleware.ts"
"./middleware": "./src/middleware.ts",
"./request": "./src/request.ts"
},
"typesVersions": {
"*": {
@@ -25,9 +26,12 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.7",
"next-international": "^1.2.4"
"next": "^14.2.16",
"next-intl": "3.23.2",
"react": "^18.3.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -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,
},
);

View 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 };

View 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,
};
};

View File

@@ -0,0 +1,5 @@
import { useLocale } from "next-intl";
import type { SupportedLanguage } from "../config";
export const useCurrentLocale = () => useLocale() as SupportedLanguage;

View 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;

View File

@@ -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);
};

View File

@@ -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",
},

View File

@@ -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",
},
};

View File

@@ -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) {

View File

@@ -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*"],
};

View 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,
};
});

View 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
},
});

View File

@@ -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";

View File

@@ -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> {}
}

View File

@@ -1,14 +1,16 @@
"use client";
import type { BadgeProps } from "@mantine/core";
import { Badge } from "@mantine/core";
import { useI18n } from "@homarr/translation/client";
import { useTranslations } from "@homarr/translation/client";
interface BetaBadgeProps {
size: BadgeProps["size"];
}
export const BetaBadge = ({ size }: BetaBadgeProps) => {
const t = useI18n();
const t = useTranslations();
return (
<Badge size={size} color="green" variant="outline">
{t("common.beta")}

View File

@@ -1,22 +1,14 @@
import type { MRT_RowData, MRT_TableOptions } from "mantine-react-table";
import { useMantineReactTable } from "mantine-react-table";
import { MRT_Localization_EN } from "mantine-react-table/locales/en/index.cjs";
import { objectKeys } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
import { useI18nMessages } from "@homarr/translation/client";
export const useTranslatedMantineReactTable = <TData extends MRT_RowData>(
tableOptions: Omit<MRT_TableOptions<TData>, "localization">,
) => {
const t = useScopedI18n("common.mantineReactTable");
const messages = useI18nMessages();
return useMantineReactTable<TData>({
...tableOptions,
localization: objectKeys(MRT_Localization_EN).reduce(
(acc, key) => {
acc[key] = t(key);
return acc;
},
{} as typeof MRT_Localization_EN,
),
localization: messages.common.mantineReactTable,
});
};

View File

@@ -1,15 +1,9 @@
import type { ParamsObject } from "international-types";
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
import { ZodIssueCode } from "zod";
import type { TranslationObject } from "@homarr/translation";
import type { TranslationFunction, TranslationObject } from "@homarr/translation";
export const zodErrorMap = <
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TFunction extends (key: string, ...params: any[]) => string,
>(
t: TFunction,
) => {
export const zodErrorMap = <TFunction extends TranslationFunction>(t: TFunction) => {
return (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
const error = handleZodError(issue, ctx);
if ("message" in error && error.message) {
@@ -139,7 +133,7 @@ type CustomErrorKey = keyof TranslationObject["common"]["zod"]["errors"]["custom
export interface CustomErrorParams<TKey extends CustomErrorKey> {
i18n: {
key: TKey;
params: ParamsObject<TranslationObject["common"]["zod"]["errors"]["custom"][TKey]>;
params: Record<string, unknown>;
};
}

View File

@@ -30,7 +30,10 @@ const passwordSchema = z
return passwordRequirements.every((requirement) => requirement.check(value));
},
{
params: createCustomErrorParams("passwordRequirements"),
params: createCustomErrorParams({
key: "passwordRequirements",
params: {},
}),
},
);
@@ -38,7 +41,10 @@ const confirmPasswordRefine = [
(data: { password: string; confirmPassword: string }) => data.password === data.confirmPassword,
{
path: ["confirmPassword"],
params: createCustomErrorParams("passwordsDoNotMatch"),
params: createCustomErrorParams({
key: "passwordsDoNotMatch",
params: {},
}),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] satisfies [(args: any) => boolean, unknown];

View File

@@ -43,8 +43,8 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
inputWrapperOrder={["label", "input", "description", "error"]}
description={
<Text size="xs">
{t("widget.common.app.description", {
here: (
{t.rich("widget.common.app.description", {
here: () => (
<Anchor size="xs" component={Link} target="_blank" href="/manage/apps/new">
{t("common.here")}
</Anchor>

View File

@@ -1,12 +1,12 @@
import { describe, expect, it } from "vitest";
import { objectEntries } from "@homarr/common";
import { languageMapping } from "@homarr/translation";
import { createLanguageMapping } from "@homarr/translation";
import { widgetImports } from "..";
describe("Widget properties with description should have matching translations", async () => {
const enTranslation = await languageMapping().en();
const enTranslation = await createLanguageMapping().en();
objectEntries(widgetImports).forEach(([key, value]) => {
Object.entries(value.definition.options).forEach(
([optionKey, optionValue]: [string, { withDescription?: boolean }]) => {
@@ -25,7 +25,7 @@ describe("Widget properties with description should have matching translations",
});
describe("Widget properties should have matching name translations", async () => {
const enTranslation = await languageMapping().en();
const enTranslation = await createLanguageMapping().en();
objectEntries(widgetImports).forEach(([key, value]) => {
Object.keys(value.definition.options).forEach((optionKey) => {
it(`should have matching translations for ${optionKey} option name of ${key} widget`, () => {

View File

@@ -92,8 +92,8 @@ export const WidgetIntegrationSelect = ({
inputWrapperOrder={["label", "input", "description", "error"]}
description={
<Text size="xs">
{t("widget.common.integration.description", {
here: (
{t.rich("widget.common.integration.description", {
here: () => (
<Anchor size="xs" component={Link} target="_blank" href="/manage/integrations">
{t("common.here")}
</Anchor>

99
pnpm-lock.yaml generated
View File

@@ -1389,15 +1389,24 @@ importers:
packages/translation:
dependencies:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
dayjs:
specifier: ^1.11.13
version: 1.11.13
mantine-react-table:
specifier: 2.0.0-beta.7
version: 2.0.0-beta.7(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/dates@7.13.4(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(@tabler/icons-react@3.20.0(react@18.3.1))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-international:
specifier: ^1.2.4
version: 1.2.4
next:
specifier: ^14.2.16
version: 14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4)
next-intl:
specifier: 3.23.2
version: 3.23.2(next@14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4))(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
@@ -2654,6 +2663,21 @@ packages:
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
'@formatjs/ecma402-abstract@2.2.0':
resolution: {integrity: sha512-IpM+ev1E4QLtstniOE29W1rqH9eTdx5hQdNL8pzrflMj/gogfaoONZqL83LUeQScHAvyMbpqP5C9MzNf+fFwhQ==}
'@formatjs/fast-memoize@2.2.1':
resolution: {integrity: sha512-XS2RcOSyWxmUB7BUjj3mlPH0exsUzlf6QfhhijgI941WaJhVxXQ6mEWkdUFIdnKi3TuTYxRdelsgv3mjieIGIA==}
'@formatjs/icu-messageformat-parser@2.8.0':
resolution: {integrity: sha512-r2un3fmF9oJv3mOkH+wwQZ037VpqmdfahbcCZ9Lh+p6Sx+sNsonI7Zcr6jNMm1s+Si7ejQORS4Ezlh05mMPAXA==}
'@formatjs/icu-skeleton-parser@1.8.4':
resolution: {integrity: sha512-LMQ1+Wk1QSzU4zpd5aSu7+w5oeYhupRwZnMQckLPRYhSjf2/8JWQ882BauY9NyHxs5igpuQIXZDgfkaH3PoATg==}
'@formatjs/intl-localematcher@0.5.5':
resolution: {integrity: sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==}
'@hapi/bourne@3.0.0':
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
@@ -5472,8 +5496,8 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
international-types@0.8.1:
resolution: {integrity: sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA==}
intl-messageformat@10.7.1:
resolution: {integrity: sha512-xQuJW2WcyzNJZWUu5xTVPOmNSA1Sowuu/NKFdUid5Fxx/Yl6/s4DefTU/y7zy+irZLDmFGmTLtnM8FqpN05wlA==}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -6128,8 +6152,11 @@ packages:
nodemailer:
optional: true
next-international@1.2.4:
resolution: {integrity: sha512-JQvp+h2iSgA/t8hu5S/Lwow1ZErJutQRdpnplxjv4VTlCiND8T95fYih8BjkHcVhQbtM+Wu9Mb1CM32wD9hlWQ==}
next-intl@3.23.2:
resolution: {integrity: sha512-SCYEG2i0kYz+OupN6+qH9T+GDRfLCmJuT835uI9ac7AOlYCUbBizj28cti+oGhDkIjueZrweVw7iEiTkqCpKpQ==}
peerDependencies:
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
next@14.2.16:
resolution: {integrity: sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==}
@@ -7008,9 +7035,6 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -7717,6 +7741,11 @@ packages:
peerDependencies:
react: '>=16.13'
use-intl@3.23.2:
resolution: {integrity: sha512-lrKb5M6zr9YoHK+OuUsRApPPNEMHX8ntx0PDGZ0fxlMmj6W2u/3y++UB4uE/o0C8Jyn7oiHCjShYjgPjDaB1cg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
use-isomorphic-layout-effect@1.1.2:
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
@@ -8767,6 +8796,31 @@ snapshots:
'@floating-ui/utils@0.2.8': {}
'@formatjs/ecma402-abstract@2.2.0':
dependencies:
'@formatjs/fast-memoize': 2.2.1
'@formatjs/intl-localematcher': 0.5.5
tslib: 2.7.0
'@formatjs/fast-memoize@2.2.1':
dependencies:
tslib: 2.7.0
'@formatjs/icu-messageformat-parser@2.8.0':
dependencies:
'@formatjs/ecma402-abstract': 2.2.0
'@formatjs/icu-skeleton-parser': 1.8.4
tslib: 2.7.0
'@formatjs/icu-skeleton-parser@1.8.4':
dependencies:
'@formatjs/ecma402-abstract': 2.2.0
tslib: 2.7.0
'@formatjs/intl-localematcher@0.5.5':
dependencies:
tslib: 2.7.0
'@hapi/bourne@3.0.0': {}
'@homarr/gridstack@1.0.3': {}
@@ -12168,7 +12222,12 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
international-types@0.8.1: {}
intl-messageformat@10.7.1:
dependencies:
'@formatjs/ecma402-abstract': 2.2.0
'@formatjs/fast-memoize': 2.2.1
'@formatjs/icu-messageformat-parser': 2.8.0
tslib: 2.7.0
invariant@2.2.4:
dependencies:
@@ -12796,11 +12855,13 @@ snapshots:
next: 14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4)
react: 18.3.1
next-international@1.2.4:
next-intl@3.23.2(next@14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4))(react@18.3.1):
dependencies:
client-only: 0.0.1
international-types: 0.8.1
server-only: 0.0.1
'@formatjs/intl-localematcher': 0.5.5
negotiator: 0.6.3
next: 14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4)
react: 18.3.1
use-intl: 3.23.2(react@18.3.1)
next@14.2.16(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4):
dependencies:
@@ -13815,8 +13876,6 @@ snapshots:
dependencies:
randombytes: 2.1.0
server-only@0.0.1: {}
set-blocking@2.0.0: {}
set-function-length@1.2.2:
@@ -14631,6 +14690,12 @@ snapshots:
dequal: 2.0.3
react: 18.3.1
use-intl@3.23.2(react@18.3.1):
dependencies:
'@formatjs/fast-memoize': 2.2.1
intl-messageformat: 10.7.1
react: 18.3.1
use-isomorphic-layout-effect@1.1.2(@types/react@18.3.12)(react@18.3.1):
dependencies:
react: 18.3.1