Skip to content

Commit

Permalink
Merge pull request #90 from premieroctet/feature/i18n
Browse files Browse the repository at this point in the history
Add i18n support
  • Loading branch information
cregourd authored Jan 15, 2024
2 parents 201df27 + 1b5dd6d commit 112399c
Show file tree
Hide file tree
Showing 40 changed files with 1,096 additions and 175 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-peas-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": minor
---

🌐 add i18n support
1 change: 1 addition & 0 deletions apps/docs/pages/docs/_meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"getting-started": "Getting Started",
"api-docs": "API",
"i18n": "I18n",
"glossary": "Glossary",
"route": "Route name",
"edge-cases": "Edge cases"
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/pages/docs/api-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Tabs } from "nextra/components";
- `prisma`: your Prisma client instance
- `action`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to submit the form. It should be your own action, that wraps the `submitForm` action imported from `@premieroctet/next-admin/dist/actions`.
- `deleteAction`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to delete one or more records in a resource. It is optional, and should be your own action. This action takes 3 parameters: `model` (the model name) and `ids` (an array of ids to delete). Next Admin provides a default action for deletion, that you can call in your own action. Check the example app for more details.
- `getMessages`: a function with no parameters that returns translation messages. It is used to translate the default messages of the library. See [i18n](/docs/i18n) for more details.

</Tabs.Tab>
<Tabs.Tab>
Expand Down Expand Up @@ -144,6 +145,7 @@ import { Tabs } from "nextra/components";
- `AdminComponentProps`, which are passed by the [router function](#nextadminrouter-function) via getServerSideProps
- `options` used to customize the UI, like field formatters for example. Do not use with App router.
- `dashboard` used to customize the rendered dashboard
- `translations` used to customize some of the texts displayed in the UI. See [i18n](/docs/i18n) for more details.

> ⚠️ : Do not override these `AdminComponentProps` props, they are used internally by Next Admin.
Expand Down
50 changes: 50 additions & 0 deletions apps/docs/pages/docs/i18n.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# I18n

Next Admin supports i18n with the `translations` prop of the `NextAdmin` component.

The following keys are accepted:

| Name | Description | Default value |
| ------------------------------- | ------------------------------------------------------------------------------------- | ------------------------- |
| list.header.add.label | The "Add" button in the list header | Add |
| list.header.search.placeholder | The placeholder used in the search input | Search |
| list.footer.indicator.showing | The "Showing from" text in the list indicator, e.g: <u>Showing from</u> 1 to 10 of 25 | Showing from |
| list.footer.indicator.to | The "to" text in the list indicator, e.g: Showing from 1 <u>to</u> 10 of 25 | to |
| list.footer.indicator.of | The "of" text in the list indicator, e.g: Showing from 1 to 10 <u>of</u> 25 | of |
| list.row.actions.delete.label | The text in the delete button displayed at the end of each row | Delete |
| list.empty.label | The text displayed when there is no row in the list | No \{\{resource\}\} found |
| form.button.save.label | The text displayed in the form submit button | Submit |
| form.button.delete.label | The text displayed in the form delete button | Delete |
| form.widgets.file_upload.label | The text displayed in file upload widget to select a file | Choose a file |
| form.widgets.file_upload.delete | The text displayed in file upload widget to delete the current file | Delete |
| actions.label | The text displayed in the dropdown button for the actions list | Action |
| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete |

There is two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component.
> Note that the function way allows you to provide an object with a multiple level structure to translate the keys, while the `translations` props only allows you to provide a flat object (`form.widgets.file_upload.delete` ex.)
You can also pass your own set of translations. For example you can set a custom action name as a translation key, which will then be translated by the lib.

```js
actions: [
{
title: "actions.user.email",
action: async (...args) => {
"use server";
const { submitEmail } = await import("./actions/nextadmin");
await submitEmail(...args);
},
successMessage: "actions.user.email.success",
errorMessage: "actions.user.email.error",
},
],
```

Here, the `actions.user.email` key will be translated by the lib, and the value will be used as the action title, aswell as the success and error messages after the action's execution.

Currently, you can only translate the following:

- action title, success and error message
- field validation error message

Check the example app for more details on the usage.
2 changes: 1 addition & 1 deletion apps/example/.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

POSTGRES_PRISMA_URL="postgresql://next-admin:next-admin@localhost:5432/next-admin?schema=public"
POSTGRES_URL_NON_POOLING="postgresql://next-admin:next-admin@localhost:5432/next-admin?schema=public"
BASE_URL="http://localhost:3000/admin"
BASE_URL="http://localhost:3000/en/admin"
BASE_DOMAIN="http://localhost:3000"
38 changes: 38 additions & 0 deletions apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NextAdmin } from "@premieroctet/next-admin";
import { getPropsFromParams } from "@premieroctet/next-admin/dist/appRouter";
import { getMessages } from "next-intl/server";
import { deleteItem, submitFormAction } from "../../../../actions/nextadmin";
import Dashboard from "../../../../components/Dashboard";
import { options } from "../../../../options";
import { prisma } from "../../../../prisma";
import schema from "../../../../prisma/json-schema/json-schema.json";
import "../../../../styles.css";

export default async function AdminPage({
params,
searchParams,
}: {
params: { [key: string]: string[] | string };
searchParams: { [key: string]: string | string[] | undefined } | undefined;
}) {
const props = await getPropsFromParams({
params: params.nextadmin as string[],
searchParams,
options,
prisma,
schema,
action: submitFormAction,
deleteAction: deleteItem,
getMessages: () => getMessages({ locale: params.locale as string }).then((messages) => (messages.admin as Record<string, string>)),
locale: params.locale as string,
});


return (
<NextAdmin
{...props}
locale={params.locale as string}
dashboard={Dashboard}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MainLayout } from "@premieroctet/next-admin";
import { createRandomPost } from "../../../actions/posts";
import { createRandomPost } from "../../../../actions/posts";
import { getMainLayoutProps } from "@premieroctet/next-admin/dist/mainLayout";
import { prisma } from "../../../prisma";
import { options } from "../../../options";
import { prisma } from "../../../../prisma";
import { options } from "../../../../options";

const CustomPage = async () => {
const mainLayoutProps = getMainLayoutProps({ options, isAppDir: true });
Expand Down
26 changes: 26 additions & 0 deletions apps/example/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PropsWithChildren } from "react";
import "../../styles.css";
import { notFound } from "next/navigation";

type Props = {
params: {
locale: "en" | "fr";
};
};

const locales = ["en", "fr"];

export default function Layout({
children,
params: { locale },
}: PropsWithChildren<Props>) {
if (!locales.includes(locale)) {
notFound();
}

return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}
File renamed without changes.
28 changes: 0 additions & 28 deletions apps/example/app/admin/[[...nextadmin]]/page.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions apps/example/app/layout.tsx

This file was deleted.

15 changes: 15 additions & 0 deletions apps/example/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IntlErrorCode } from "next-intl";
import { getRequestConfig } from "next-intl/server";

export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default,
getMessageFallback: ({ namespace, key, error }) => {
const path = [namespace, key].filter((part) => part != null).join(".");

if (error.code === IntlErrorCode.MISSING_MESSAGE) {
return "";
} else {
return "Dear developer, please fix this message: " + path;
}
},
}));
20 changes: 20 additions & 0 deletions apps/example/messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"admin": {
"actions": {
"user": {
"email": {
"title": "Send email",
"success": "Email sent successfully",
"error": "Error while sending email"
}
}
},
"form": {
"user": {
"email": {
"error": "Invalid email"
}
}
}
}
}
65 changes: 65 additions & 0 deletions apps/example/messages/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"admin": {
"list": {
"header": {
"add": {
"label": "Ajouter"
},
"search": {
"placeholder": "Recherche"
}
},
"footer": {
"indicator": {
"showing": "Affichage de",
"to": "à",
"of": "sur"
}
},
"row": {
"actions": {
"delete": {
"label": "Supprimer"
}
}
},
"empty": {
"label": "Aucun(e) {{resource}} trouvée"
}
},
"form": {
"button": {
"delete": {
"label": "Supprimer"
},
"save": {
"label": "Enregistrer"
}
},
"widgets": {
"file_upload": {
"label": "Choisir un fichier",
"delete": "Supprimer"
}
},
"user": {
"email": {
"error": "Email invalide"
}
}
},
"actions": {
"user": {
"email": {
"title": "Envoyer un email",
"success": "Email envoyé avec succès",
"error": "Erreur lors de l'envoi de l'email"
}
},
"label": "Action",
"delete": {
"label": "Supprimer"
}
}
}
}
17 changes: 17 additions & 0 deletions apps/example/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import createMiddleware from "next-intl/middleware";

export default createMiddleware({
locales: ["en", "fr"],
defaultLocale: "en",
localeDetection: false,
localePrefix: "always",
alternateLinks: false,
});

export const config = {
matcher: [
"/",
"/(fr|en)/:path*",
"/((?!_next|_vercel|pagerouter|api|.*\\..*).*)",
],
};
6 changes: 4 additions & 2 deletions apps/example/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const withNextIntl = require("next-intl/plugin")("./i18n.ts");

/** @type {import('next').NextConfig} */
module.exports = {
module.exports = withNextIntl({
reactStrictMode: true,
experimental: {
swcPlugins: [
Expand All @@ -11,4 +13,4 @@ module.exports = {
],
],
},
};
});
22 changes: 11 additions & 11 deletions apps/example/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const options: NextAdminOptions = {
formatter: (date, context) => {
return new Date(date as unknown as string)
?.toLocaleString(context?.locale)
.split(" ")[0];
.split(/[\s,]+/)[0];
},
},
},
Expand All @@ -39,7 +39,7 @@ export const options: NextAdminOptions = {
],
fields: {
email: {
validate: (email) => email.includes("@") || "Invalid email",
validate: (email) => email.includes("@") || "form.user.email.error",
},
birthDate: {
input: <DatePicker />,
Expand All @@ -62,27 +62,27 @@ export const options: NextAdminOptions = {
validate: (value) => {
try {
if (!value) {
return true
return true;
}
JSON.parse(value as string)
return true
JSON.parse(value as string);
return true;
} catch {
return "Invalid JSON"
return "Invalid JSON";
}
}
}
},
},
},
},
actions: [
{
title: "Send email",
title: "actions.user.email.title",
action: async (...args) => {
"use server";
const { submitEmail } = await import("./actions/nextadmin");
await submitEmail(...args);
},
successMessage: "Email sent successfully",
errorMessage: "Error while sending email",
successMessage: "actions.user.email.success",
errorMessage: "actions.user.email.error",
},
],
},
Expand Down
Loading

0 comments on commit 112399c

Please sign in to comment.