From 717a2704e743ff48d1f4ef4f44bc66d0918cda44 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 22:46:17 +0200 Subject: [PATCH 01/11] refactor: move from next-international to next-intl --- apps/nextjs/next.config.mjs | 6 +- .../_client-providers/next-international.tsx | 12 --- apps/nextjs/src/app/[locale]/layout.tsx | 15 +++- .../groups/[id]/_reserved-group-alert.tsx | 4 +- .../components/language/language-combobox.tsx | 5 +- .../src/modes/command/children/language.tsx | 2 +- packages/translation/package.json | 8 +- packages/translation/src/client.ts | 45 +++++++++-- packages/translation/src/index.ts | 6 +- packages/translation/src/lang.ts | 2 +- packages/translation/src/lang/en.ts | 6 +- packages/translation/src/middleware.ts | 15 ++-- packages/translation/src/request.ts | 33 ++++++++ packages/translation/src/routing.ts | 14 ++++ packages/translation/src/server.ts | 12 +-- packages/translation/src/type.ts | 10 ++- packages/validation/src/form/i18n.ts | 11 +-- .../widgets/src/_inputs/widget-app-input.tsx | 4 +- .../widgets/src/widget-integration-select.tsx | 4 +- pnpm-lock.yaml | 78 +++++++++++++++++++ 20 files changed, 225 insertions(+), 67 deletions(-) delete mode 100644 apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx create mode 100644 packages/translation/src/request.ts create mode 100644 packages/translation/src/routing.ts diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs index 7d53a70db..7585f8456 100644 --- a/apps/nextjs/next.config.mjs +++ b/apps/nextjs/next.config.mjs @@ -2,9 +2,13 @@ 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 = { output: "standalone", @@ -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(config); diff --git a/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx b/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx deleted file mode 100644 index 8297a73bb..000000000 --- a/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index 22888ea0b..765eae8d7 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -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) => , (innerProps) => , - (innerProps) => , + (innerProps) => , (innerProps) => , (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 { return ( }> - {t("group.reservedNotice.message", { - checkoutDocs: ( + {t.rich("group.reservedNotice.message", { + checkoutDocs: () => ( { 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 ? : null} rightSection={} rightSectionPointerEvents="none" onClick={handleOnClick} diff --git a/packages/spotlight/src/modes/command/children/language.tsx b/packages/spotlight/src/modes/command/children/language.tsx index 848139b68..ad5e7f536 100644 --- a/packages/spotlight/src/modes/command/children/language.tsx +++ b/packages/spotlight/src/modes/command/children/language.tsx @@ -47,7 +47,7 @@ export const languageChildrenOptions = createChildrenOptions changeLocale(localeKey) }; }, diff --git a/packages/translation/package.json b/packages/translation/package.json index 0f4f728d3..378bd8531 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -8,7 +8,8 @@ ".": "./index.ts", "./client": "./src/client.ts", "./server": "./src/server.ts", - "./middleware": "./src/middleware.ts" + "./middleware": "./src/middleware.ts", + "./request": "./src/request.ts" }, "typesVersions": { "*": { @@ -27,7 +28,8 @@ "dependencies": { "dayjs": "^1.11.13", "mantine-react-table": "2.0.0-beta.7", - "next-international": "^1.2.4" + "next-international": "^1.2.4", + "next-intl": "3.23.2" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", @@ -36,4 +38,4 @@ "eslint": "^9.13.0", "typescript": "^5.6.3" } -} +} \ No newline at end of file diff --git a/packages/translation/src/client.ts b/packages/translation/src/client.ts index 8cbb49e73..622898f45 100644 --- a/packages/translation/src/client.ts +++ b/packages/translation/src/client.ts @@ -1,13 +1,42 @@ "use client"; -import { createI18nClient } from "next-international/client"; +import { useTransition } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; -import { languageMapping } from "./lang"; -import enTranslation from "./lang/en"; +import type { SupportedLanguage } from "."; +import type { TranslationObject } from "./type"; -export const { useI18n, useScopedI18n, useCurrentLocale, useChangeLocale, I18nProviderClient } = createI18nClient( - languageMapping(), - { - fallbackLocale: enTranslation, +export const { useI18n, useScopedI18n, useCurrentLocale, useChangeLocale } = { + useI18n: useTranslations, + useScopedI18n: useTranslations, + useCurrentLocale: () => useLocale() as SupportedLanguage, + useChangeLocale: () => { + const locale = useLocale() as SupportedLanguage; + const router = useRouter(); + const pathname = usePathname(); + const [isPending, startTransition] = useTransition(); + + return { + changeLocale: (newLocale: SupportedLanguage) => { + if (newLocale === locale) { + return; + } + + startTransition(() => { + router.replace("/" + newLocale + pathname); + }); + }, + isPending, + }; }, -); +}; + +declare global { + // Use type safe message keys with `next-intl` + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface IntlMessages extends RemoveReadonly {} +} +type RemoveReadonly = { + -readonly [P in keyof T]: T[P] extends Record ? RemoveReadonly : T[P]; +}; diff --git a/packages/translation/src/index.ts b/packages/translation/src/index.ts index 89f14b401..46bc10010 100644 --- a/packages/translation/src/index.ts +++ b/packages/translation/src/index.ts @@ -8,7 +8,7 @@ export type SupportedLanguage = (typeof supportedLanguages)[number]; export const defaultLocale = "en"; export { languageMapping } from "./lang"; -export type { TranslationKeys } from "./lang"; +export type { TranslationKeys, EnTranslation } from "./lang"; export const translateIfNecessary = (t: TranslationFunction, value: stringOrTranslation | undefined) => { if (typeof value === "function") { @@ -16,3 +16,7 @@ export const translateIfNecessary = (t: TranslationFunction, value: stringOrTran } return value; }; + +export const isLocaleSupported = (locale: string): locale is SupportedLanguage => { + return supportedLanguages.includes(locale as SupportedLanguage); +}; diff --git a/packages/translation/src/lang.ts b/packages/translation/src/lang.ts index 5874d7918..467eb83ef 100644 --- a/packages/translation/src/lang.ts +++ b/packages/translation/src/lang.ts @@ -1,7 +1,7 @@ import { supportedLanguages } from "."; const _enTranslations = () => import("./lang/en"); -type EnTranslation = typeof _enTranslations; +export type EnTranslation = typeof _enTranslations; export const languageMapping = () => { const mapping: Record = {}; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 670be6712..7eb599c8e 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -668,7 +668,7 @@ export default { }, }, }, - mantineReactTable: MRT_Localization_EN, + mantineReactTable: MRT_Localization_EN as Readonly>, }, section: { dynamic: { @@ -1167,11 +1167,11 @@ export default { }, integration: { noData: "No integration found", - description: "Click {here} to create a new integration", + description: "Click to create a new integration", }, app: { noData: "No app found", - description: "Click {here} to create a new app", + description: "Click to create a new app", }, error: { action: { diff --git a/packages/translation/src/middleware.ts b/packages/translation/src/middleware.ts index 77d2a138d..636c1c5ca 100644 --- a/packages/translation/src/middleware.ts +++ b/packages/translation/src/middleware.ts @@ -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*"], +}; diff --git a/packages/translation/src/request.ts b/packages/translation/src/request.ts new file mode 100644 index 000000000..1a9b753d4 --- /dev/null +++ b/packages/translation/src/request.ts @@ -0,0 +1,33 @@ +import deepmerge from "deepmerge"; +import { getRequestConfig } from "next-intl/server"; + +import { isLocaleSupported } from "."; +import type { SupportedLanguage } from "."; +import { languageMapping } from "./lang"; +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 = languageMapping(); + const currentMessages = (await languageMap[typedLocale]()).default; + + if (currentLocale !== routing.defaultLocale) { + const fallbackMessages = (await languageMap[routing.defaultLocale]()).default; + return { + locale: currentLocale, + messages: deepmerge(fallbackMessages, currentMessages), + }; + } + + return { + locale: currentLocale, + messages: currentMessages, + }; +}); diff --git a/packages/translation/src/routing.ts b/packages/translation/src/routing.ts new file mode 100644 index 000000000..515eaae88 --- /dev/null +++ b/packages/translation/src/routing.ts @@ -0,0 +1,14 @@ +import { createNavigation } from "next-intl/navigation"; +import { defineRouting } from "next-intl/routing"; + +import { defaultLocale, supportedLanguages } from "."; + +export const routing = defineRouting({ + locales: supportedLanguages, + defaultLocale, + localePrefix: { + mode: "never", + }, +}); + +export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); diff --git a/packages/translation/src/server.ts b/packages/translation/src/server.ts index c7b32793a..bfab90719 100644 --- a/packages/translation/src/server.ts +++ b/packages/translation/src/server.ts @@ -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"; diff --git a/packages/translation/src/type.ts b/packages/translation/src/type.ts index e2baf031f..7a31ee831 100644 --- a/packages/translation/src/type.ts +++ b/packages/translation/src/type.ts @@ -1,9 +1,11 @@ +import type { NamespaceKeys, NestedKeyOf } from "next-intl"; + import type { useI18n, useScopedI18n } from "./client"; import type enTranslation from "./lang/en"; -export type TranslationFunction = ReturnType; -export type ScopedTranslationFunction[0]> = ReturnType< - typeof useScopedI18n ->; +export type TranslationFunction = ReturnType>; +export type ScopedTranslationFunction< + NestedKey extends NamespaceKeys> = never, +> = ReturnType>; export type TranslationObject = typeof enTranslation; export type stringOrTranslation = string | ((t: TranslationFunction) => string); diff --git a/packages/validation/src/form/i18n.ts b/packages/validation/src/form/i18n.ts index c057fad57..54e2c3216 100644 --- a/packages/validation/src/form/i18n.ts +++ b/packages/validation/src/form/i18n.ts @@ -2,14 +2,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"; - -export const zodErrorMap = < - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TFunction extends (key: string, ...params: any[]) => string, ->( - t: TFunction, -) => { +import type { TranslationFunction, TranslationObject } from "@homarr/translation"; + +export const zodErrorMap = (t: TFunction) => { return (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => { const error = handleZodError(issue, ctx); if ("message" in error && error.message) { diff --git a/packages/widgets/src/_inputs/widget-app-input.tsx b/packages/widgets/src/_inputs/widget-app-input.tsx index ee22b08da..fd3cc0599 100644 --- a/packages/widgets/src/_inputs/widget-app-input.tsx +++ b/packages/widgets/src/_inputs/widget-app-input.tsx @@ -43,8 +43,8 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app"> inputWrapperOrder={["label", "input", "description", "error"]} description={ - {t("widget.common.app.description", { - here: ( + {t.rich("widget.common.app.description", { + here: () => ( {t("common.here")} diff --git a/packages/widgets/src/widget-integration-select.tsx b/packages/widgets/src/widget-integration-select.tsx index 3aa27fcd6..215b1ccf2 100644 --- a/packages/widgets/src/widget-integration-select.tsx +++ b/packages/widgets/src/widget-integration-select.tsx @@ -92,8 +92,8 @@ export const WidgetIntegrationSelect = ({ inputWrapperOrder={["label", "input", "description", "error"]} description={ - {t("widget.common.integration.description", { - here: ( + {t.rich("widget.common.integration.description", { + here: () => ( {t("common.here")} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ffccc7d0..e4c5d5eba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1398,6 +1398,9 @@ importers: next-international: specifier: ^1.2.4 version: 1.2.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) devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -2654,6 +2657,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==} @@ -5475,6 +5493,9 @@ packages: 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==} @@ -6131,6 +6152,12 @@ packages: 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==} engines: {node: '>=18.17.0'} @@ -7717,6 +7744,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 +8799,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': {} @@ -12171,6 +12228,13 @@ snapshots: 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: loose-envify: 1.4.0 @@ -12803,6 +12867,14 @@ snapshots: international-types: 0.8.1 server-only: 0.0.1 + 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: + '@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: '@next/env': 14.2.16 @@ -14632,6 +14704,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 From 4d5c2dff7f7d16e55e157d3f591368690e4ef7a5 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:30:14 +0200 Subject: [PATCH 02/11] refactor: restructure translation package, --- .../components/language/language-combobox.tsx | 8 ++-- packages/common/src/types.ts | 4 ++ .../src/modes/command/children/language.tsx | 26 ++++++------ packages/translation/package.json | 8 ++-- packages/translation/src/client.ts | 42 ------------------- packages/translation/src/client/index.ts | 11 +++++ .../src/client/use-change-locale.ts | 25 +++++++++++ .../src/client/use-current-locale.ts | 5 +++ packages/translation/src/config.ts | 26 ++++++++++++ packages/translation/src/index.ts | 13 +++--- packages/translation/src/locale-attributes.ts | 21 ---------- .../translation/src/{lang.ts => mapping.ts} | 6 +-- packages/translation/src/request.ts | 7 ++-- packages/translation/src/routing.ts | 7 +--- packages/translation/src/type.ts | 8 ++++ packages/widgets/src/test/translation.spec.ts | 6 +-- pnpm-lock.yaml | 31 ++++---------- 17 files changed, 127 insertions(+), 127 deletions(-) delete mode 100644 packages/translation/src/client.ts create mode 100644 packages/translation/src/client/index.ts create mode 100644 packages/translation/src/client/use-change-locale.ts create mode 100644 packages/translation/src/client/use-current-locale.ts create mode 100644 packages/translation/src/config.ts delete mode 100644 packages/translation/src/locale-attributes.ts rename packages/translation/src/{lang.ts => mapping.ts} (82%) diff --git a/apps/nextjs/src/components/language/language-combobox.tsx b/apps/nextjs/src/components/language/language-combobox.tsx index 6cd0a0fa9..b60e3a4fe 100644 --- a/apps/nextjs/src/components/language/language-combobox.tsx +++ b/apps/nextjs/src/components/language/language-combobox.tsx @@ -5,7 +5,7 @@ import { Combobox, Group, InputBase, Loader, Text, useCombobox } from "@mantine/ 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"; @@ -73,11 +73,11 @@ const OptionItem = ({ return ( - + - {localeAttributes[localeKey].name} + {localeConfigurations[localeKey].name} - ({localeAttributes[localeKey].translatedName}) + ({localeConfigurations[localeKey].translatedName}) diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index e29902783..45c9ad902 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -5,3 +5,7 @@ export type AtLeastOneOf = [T, ...T[]]; export type Modify>> = { [P in keyof (Omit & R)]: (Omit & R)[P]; }; + +export type RemoveReadonly = { + -readonly [P in keyof T]: T[P] extends Record ? RemoveReadonly : T[P]; +}; diff --git a/packages/spotlight/src/modes/command/children/language.tsx b/packages/spotlight/src/modes/command/children/language.tsx index ad5e7f536..f861746dd 100644 --- a/packages/spotlight/src/modes/command/children/language.tsx +++ b/packages/spotlight/src/modes/command/children/language.tsx @@ -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 ({ 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 ( - + - {attributes.name} + {configuration.name} - ({attributes.translatedName}) + ({configuration.translatedName}) diff --git a/packages/translation/package.json b/packages/translation/package.json index 378bd8531..417bd426c 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -6,7 +6,7 @@ "type": "module", "exports": { ".": "./index.ts", - "./client": "./src/client.ts", + "./client": "./src/client/index.ts", "./server": "./src/server.ts", "./middleware": "./src/middleware.ts", "./request": "./src/request.ts" @@ -26,10 +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-intl": "3.23.2" + "next-intl": "3.23.2", + "next": "^14.2.16", + "react": "^18.3.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/translation/src/client.ts b/packages/translation/src/client.ts deleted file mode 100644 index 622898f45..000000000 --- a/packages/translation/src/client.ts +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { useTransition } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { useLocale, useTranslations } from "next-intl"; - -import type { SupportedLanguage } from "."; -import type { TranslationObject } from "./type"; - -export const { useI18n, useScopedI18n, useCurrentLocale, useChangeLocale } = { - useI18n: useTranslations, - useScopedI18n: useTranslations, - useCurrentLocale: () => useLocale() as SupportedLanguage, - useChangeLocale: () => { - const locale = useLocale() as SupportedLanguage; - const router = useRouter(); - const pathname = usePathname(); - const [isPending, startTransition] = useTransition(); - - return { - changeLocale: (newLocale: SupportedLanguage) => { - if (newLocale === locale) { - return; - } - - startTransition(() => { - router.replace("/" + newLocale + pathname); - }); - }, - isPending, - }; - }, -}; - -declare global { - // Use type safe message keys with `next-intl` - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface IntlMessages extends RemoveReadonly {} -} -type RemoveReadonly = { - -readonly [P in keyof T]: T[P] extends Record ? RemoveReadonly : T[P]; -}; diff --git a/packages/translation/src/client/index.ts b/packages/translation/src/client/index.ts new file mode 100644 index 000000000..bc6a96953 --- /dev/null +++ b/packages/translation/src/client/index.ts @@ -0,0 +1,11 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +export { useChangeLocale } from "./use-change-locale"; +export { useCurrentLocale } from "./use-current-locale"; + +export const { useI18n, useScopedI18n } = { + useI18n: useTranslations, + useScopedI18n: useTranslations, +}; diff --git a/packages/translation/src/client/use-change-locale.ts b/packages/translation/src/client/use-change-locale.ts new file mode 100644 index 000000000..180cebaca --- /dev/null +++ b/packages/translation/src/client/use-change-locale.ts @@ -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, + }; +}; diff --git a/packages/translation/src/client/use-current-locale.ts b/packages/translation/src/client/use-current-locale.ts new file mode 100644 index 000000000..f05eff7e5 --- /dev/null +++ b/packages/translation/src/client/use-current-locale.ts @@ -0,0 +1,5 @@ +import { useLocale } from "next-intl"; + +import type { SupportedLanguage } from "../config"; + +export const useCurrentLocale = () => useLocale() as SupportedLanguage; diff --git a/packages/translation/src/config.ts b/packages/translation/src/config.ts new file mode 100644 index 000000000..6a88e640d --- /dev/null +++ b/packages/translation/src/config.ts @@ -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; diff --git a/packages/translation/src/index.ts b/packages/translation/src/index.ts index 46bc10010..fff1e95aa 100644 --- a/packages/translation/src/index.ts +++ b/packages/translation/src/index.ts @@ -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, EnTranslation } 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") { diff --git a/packages/translation/src/locale-attributes.ts b/packages/translation/src/locale-attributes.ts deleted file mode 100644 index 69967c003..000000000 --- a/packages/translation/src/locale-attributes.ts +++ /dev/null @@ -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", - }, -}; diff --git a/packages/translation/src/lang.ts b/packages/translation/src/mapping.ts similarity index 82% rename from packages/translation/src/lang.ts rename to packages/translation/src/mapping.ts index 467eb83ef..b65d7e7bb 100644 --- a/packages/translation/src/lang.ts +++ b/packages/translation/src/mapping.ts @@ -1,9 +1,9 @@ -import { supportedLanguages } from "."; +import { supportedLanguages } from "./config"; const _enTranslations = () => import("./lang/en"); -export type EnTranslation = typeof _enTranslations; +type EnTranslation = typeof _enTranslations; -export const languageMapping = () => { +export const createLanguageMapping = () => { const mapping: Record = {}; for (const language of supportedLanguages) { diff --git a/packages/translation/src/request.ts b/packages/translation/src/request.ts index 1a9b753d4..4436d7443 100644 --- a/packages/translation/src/request.ts +++ b/packages/translation/src/request.ts @@ -2,8 +2,8 @@ import deepmerge from "deepmerge"; import { getRequestConfig } from "next-intl/server"; import { isLocaleSupported } from "."; -import type { SupportedLanguage } from "."; -import { languageMapping } from "./lang"; +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 @@ -15,9 +15,10 @@ export default getRequestConfig(async ({ requestLocale }) => { } const typedLocale = currentLocale as SupportedLanguage; - const languageMap = languageMapping(); + 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 { diff --git a/packages/translation/src/routing.ts b/packages/translation/src/routing.ts index 515eaae88..a7e171bc5 100644 --- a/packages/translation/src/routing.ts +++ b/packages/translation/src/routing.ts @@ -1,14 +1,11 @@ -import { createNavigation } from "next-intl/navigation"; import { defineRouting } from "next-intl/routing"; -import { defaultLocale, supportedLanguages } from "."; +import { defaultLocale, supportedLanguages } from "./config"; export const routing = defineRouting({ locales: supportedLanguages, defaultLocale, localePrefix: { - mode: "never", + mode: "never", // Rewrite the URL with locale parameter but without shown in url }, }); - -export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); diff --git a/packages/translation/src/type.ts b/packages/translation/src/type.ts index 7a31ee831..6057069f0 100644 --- a/packages/translation/src/type.ts +++ b/packages/translation/src/type.ts @@ -1,5 +1,7 @@ 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"; @@ -9,3 +11,9 @@ export type ScopedTranslationFunction< > = ReturnType>; 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 {} +} diff --git a/packages/widgets/src/test/translation.spec.ts b/packages/widgets/src/test/translation.spec.ts index 5415fefcc..967c5de85 100644 --- a/packages/widgets/src/test/translation.spec.ts +++ b/packages/widgets/src/test/translation.spec.ts @@ -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`, () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4c5d5eba..730b29981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1389,18 +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 @@ -5490,9 +5496,6 @@ 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==} @@ -6149,9 +6152,6 @@ 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: @@ -7035,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==} @@ -12226,8 +12223,6 @@ 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 @@ -12861,12 +12856,6 @@ 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: - dependencies: - client-only: 0.0.1 - international-types: 0.8.1 - server-only: 0.0.1 - 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: '@formatjs/intl-localematcher': 0.5.5 @@ -13888,8 +13877,6 @@ snapshots: dependencies: randombytes: 2.1.0 - server-only@0.0.1: {} - set-blocking@2.0.0: {} set-function-length@1.2.2: From 603444e7c45cdc6abafcbef267ff7f06875bdd30 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:31:08 +0200 Subject: [PATCH 03/11] chore: change i18n-allay framework to next-intl --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0263499bb..28d5f189e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, From 7077f742d2ddfbd66a5c8edb44913bc507ed2331 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:31:44 +0200 Subject: [PATCH 04/11] fix: add missing bold html tag to translation --- .../modals-collection/src/invites/invite-copy-modal.tsx | 7 +++++-- packages/translation/src/lang/en.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/modals-collection/src/invites/invite-copy-modal.tsx b/packages/modals-collection/src/invites/invite-copy-modal.tsx index 2fa3890a3..08136af34 100644 --- a/packages/modals-collection/src/invites/invite-copy-modal.tsx +++ b/packages/modals-collection/src/invites/invite-copy-modal.tsx @@ -12,8 +12,11 @@ export const InviteCopyModal = createModal - {t("action.copy.description")} - {/* 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 */} + + {t.rich("action.copy.description", { + b: (children) => {children}, + })} + {t("action.copy.link")} {t("field.id.label")}: diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 7eb599c8e..836b6bd99 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -1800,7 +1800,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, 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.", link: "Invitation link", button: "Copy & close", }, From d9cc2f2ca45b602982a813df75176c5da403c2f7 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:34:06 +0200 Subject: [PATCH 05/11] fix: format issue --- packages/translation/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/translation/package.json b/packages/translation/package.json index 417bd426c..ac49e9c88 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -40,4 +40,4 @@ "eslint": "^9.13.0", "typescript": "^5.6.3" } -} \ No newline at end of file +} From 8e935d9146b2551ac55a34a9de522c25e24de94c Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:35:16 +0200 Subject: [PATCH 06/11] fix: address deepsource issues --- apps/nextjs/next.config.mjs | 4 ++-- packages/translation/src/client/use-change-locale.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs index 7585f8456..2467471d8 100644 --- a/apps/nextjs/next.config.mjs +++ b/apps/nextjs/next.config.mjs @@ -10,7 +10,7 @@ import "./src/env.mjs"; 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 */ @@ -38,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 withNextIntl(config); +export default withNextIntl(nextConfig); diff --git a/packages/translation/src/client/use-change-locale.ts b/packages/translation/src/client/use-change-locale.ts index 180cebaca..7d5ce06e1 100644 --- a/packages/translation/src/client/use-change-locale.ts +++ b/packages/translation/src/client/use-change-locale.ts @@ -17,7 +17,7 @@ export const useChangeLocale = () => { } startTransition(() => { - router.replace("/" + newLocale + pathname); + router.replace(`/${newLocale}/${pathname}`); }); }, isPending, From 3bec3e1d04f3b564ede18d2e1969aecb7645fbcd Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:39:57 +0200 Subject: [PATCH 07/11] fix: remove international-types dependency --- packages/validation/src/form/i18n.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/validation/src/form/i18n.ts b/packages/validation/src/form/i18n.ts index 54e2c3216..db53d1e95 100644 --- a/packages/validation/src/form/i18n.ts +++ b/packages/validation/src/form/i18n.ts @@ -1,4 +1,3 @@ -import type { ParamsObject } from "international-types"; import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod"; import { ZodIssueCode } from "zod"; @@ -134,7 +133,7 @@ type CustomErrorKey = keyof TranslationObject["common"]["zod"]["errors"]["custom export interface CustomErrorParams { i18n: { key: TKey; - params: ParamsObject; + params: Record; }; } From 26437d11716705f3df0325e9f68f157a4ba5923f Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:49:30 +0200 Subject: [PATCH 08/11] fix: lint and typecheck issues --- packages/translation/package.json | 2 +- packages/validation/src/form/i18n.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/translation/package.json b/packages/translation/package.json index ac49e9c88..4b5ef75ee 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -29,8 +29,8 @@ "@homarr/common": "workspace:^0.1.0", "dayjs": "^1.11.13", "mantine-react-table": "2.0.0-beta.7", - "next-intl": "3.23.2", "next": "^14.2.16", + "next-intl": "3.23.2", "react": "^18.3.1" }, "devDependencies": { diff --git a/packages/validation/src/form/i18n.ts b/packages/validation/src/form/i18n.ts index db53d1e95..39031a182 100644 --- a/packages/validation/src/form/i18n.ts +++ b/packages/validation/src/form/i18n.ts @@ -133,7 +133,7 @@ type CustomErrorKey = keyof TranslationObject["common"]["zod"]["errors"]["custom export interface CustomErrorParams { i18n: { key: TKey; - params: Record; + params?: Record; }; } From 6a294c3cf6848c3fdc09c9be16deca57aad13d41 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:52:30 +0200 Subject: [PATCH 09/11] fix: typecheck issue --- packages/validation/src/form/i18n.ts | 2 +- packages/validation/src/user.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/validation/src/form/i18n.ts b/packages/validation/src/form/i18n.ts index 39031a182..db53d1e95 100644 --- a/packages/validation/src/form/i18n.ts +++ b/packages/validation/src/form/i18n.ts @@ -133,7 +133,7 @@ type CustomErrorKey = keyof TranslationObject["common"]["zod"]["errors"]["custom export interface CustomErrorParams { i18n: { key: TKey; - params?: Record; + params: Record; }; } diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 566e43363..65bac9500 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -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]; From e0bfbe7d2bcb226ab4c27c456f933d846f5d4b2c Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 24 Oct 2024 23:53:08 +0200 Subject: [PATCH 10/11] fix: typecheck issue --- .../manage/users/create/_components/create-user-stepper.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx index 978f13144..fc1425f21 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx @@ -60,7 +60,10 @@ export const UserCreateStepperComponent = () => { }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], - params: createCustomErrorParams("passwordsDoNotMatch"), + params: createCustomErrorParams({ + key: "passwordsDoNotMatch", + params: {}, + }), }), { initialValues: { From f011c4eba5abd9f0caae4a9627082068e314c68d Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Fri, 25 Oct 2024 14:18:22 +0200 Subject: [PATCH 11/11] fix: issue with translations --- packages/translation/src/client/index.ts | 10 +++++++++- packages/ui/src/components/beta-badge.tsx | 6 ++++-- .../hooks/use-translated-mantine-react-table.ts | 14 +++----------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/translation/src/client/index.ts b/packages/translation/src/client/index.ts index bc6a96953..16d73330b 100644 --- a/packages/translation/src/client/index.ts +++ b/packages/translation/src/client/index.ts @@ -1,6 +1,8 @@ "use client"; -import { useTranslations } from "next-intl"; +import { useMessages, useTranslations } from "next-intl"; + +import type { TranslationObject } from "../type"; export { useChangeLocale } from "./use-change-locale"; export { useCurrentLocale } from "./use-current-locale"; @@ -9,3 +11,9 @@ export const { useI18n, useScopedI18n } = { useI18n: useTranslations, useScopedI18n: useTranslations, }; + +export const { useI18nMessages } = { + useI18nMessages: () => useMessages() as TranslationObject, +}; + +export { useTranslations }; diff --git a/packages/ui/src/components/beta-badge.tsx b/packages/ui/src/components/beta-badge.tsx index 4c7381196..8d9d36615 100644 --- a/packages/ui/src/components/beta-badge.tsx +++ b/packages/ui/src/components/beta-badge.tsx @@ -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 ( {t("common.beta")} diff --git a/packages/ui/src/hooks/use-translated-mantine-react-table.ts b/packages/ui/src/hooks/use-translated-mantine-react-table.ts index 1be41a63b..275f48b75 100644 --- a/packages/ui/src/hooks/use-translated-mantine-react-table.ts +++ b/packages/ui/src/hooks/use-translated-mantine-react-table.ts @@ -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 = ( tableOptions: Omit, "localization">, ) => { - const t = useScopedI18n("common.mantineReactTable"); + const messages = useI18nMessages(); return useMantineReactTable({ ...tableOptions, - localization: objectKeys(MRT_Localization_EN).reduce( - (acc, key) => { - acc[key] = t(key); - return acc; - }, - {} as typeof MRT_Localization_EN, - ), + localization: messages.common.mantineReactTable, }); };