Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move from next-international to next-intl #1368

Merged
merged 11 commits into from
Oct 26, 2024
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions apps/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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);

This file was deleted.

15 changes: 11 additions & 4 deletions apps/nextjs/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -59,26 +61,31 @@ 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) => {
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
(innerProps) => <CustomMantineProvider {...innerProps} />,
(innerProps) => <ModalProvider {...innerProps} />,
]);

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={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export const UserCreateStepperComponent = () => {
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
params: createCustomErrorParams("passwordsDoNotMatch"),
params: createCustomErrorParams({
key: "passwordsDoNotMatch",
params: {},
}),
}),
{
initialValues: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
13 changes: 7 additions & 6 deletions apps/nextjs/src/components/language/language-combobox.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) => {
Expand All @@ -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}
Expand Down Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
28 changes: 14 additions & 14 deletions packages/spotlight/src/modes/command/children/language.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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>
Expand All @@ -47,7 +47,7 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
);
},
useInteraction() {
const changeLocale = useChangeLocale();
const { changeLocale } = useChangeLocale();

return { type: "javaScript", onSelect: () => changeLocale(localeKey) };
},
Expand Down
10 changes: 7 additions & 3 deletions packages/translation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
"*": {
Expand All @@ -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",
Expand Down
13 changes: 0 additions & 13 deletions packages/translation/src/client.ts

This file was deleted.

19 changes: 19 additions & 0 deletions packages/translation/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions packages/translation/src/client/use-change-locale.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions packages/translation/src/client/use-current-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useLocale } from "next-intl";

import type { SupportedLanguage } from "../config";

export const useCurrentLocale = () => useLocale() as SupportedLanguage;
26 changes: 26 additions & 0 deletions packages/translation/src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading