i18n by next-intl

This commit is contained in:
Takeshi Kimata
2024-05-03 11:44:02 +09:00
parent 7af70399a1
commit 1e01535134
51 changed files with 194 additions and 87 deletions

View File

@@ -1,13 +0,0 @@
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
{children}
</div>
</section>
);
}

View File

@@ -1,9 +0,0 @@
import { title } from "@/components/primitives";
export default function DocsPage() {
return (
<div>
<h1 className={title()}>Docs</h1>
</div>
);
}

View File

@@ -1,20 +1,15 @@
import { import {
Navbar as NextUINavbar, Navbar as NextUINavbar,
NavbarContent, NavbarContent,
NavbarMenu,
NavbarMenuToggle, NavbarMenuToggle,
NavbarBrand, NavbarBrand,
NavbarItem, NavbarItem,
NavbarMenuItem,
Kbd, Kbd,
Link, Link as NextUiLink,
Input, Input,
link as linkStyles,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { Link } from "@/src/navigation";
import { siteConfig } from "@/config/site"; import { siteConfig } from "@/config/site";
import NextLink from "next/link";
import { ThemeSwitch } from "@/components/theme-switch"; import { ThemeSwitch } from "@/components/theme-switch";
import { GithubIcon, SearchIcon } from "@/components/icons"; import { GithubIcon, SearchIcon } from "@/components/icons";
@@ -46,7 +41,7 @@ export const Navbar = () => {
<NextUINavbar maxWidth="xl" position="sticky" className="bg-inherit"> <NextUINavbar maxWidth="xl" position="sticky" className="bg-inherit">
<NavbarContent className="basis-1/5 sm:basis-full" justify="start"> <NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand as="li" className="gap-3 max-w-fit"> <NavbarBrand as="li" className="gap-3 max-w-fit">
<NextLink className="flex justify-start items-center gap-1" href="/"> <Link className="flex justify-start items-center gap-1" href="/">
<Image <Image
src="/favicon/android-chrome-192x192.png" src="/favicon/android-chrome-192x192.png"
width={32} width={32}
@@ -54,17 +49,17 @@ export const Navbar = () => {
alt="Logo" alt="Logo"
/> />
<p className="font-bold text-inherit">Test Case Manager</p> <p className="font-bold text-inherit">Test Case Manager</p>
</NextLink> </Link>
</NavbarBrand> </NavbarBrand>
<ul className="hidden lg:flex gap-4 justify-start ml-2"> <ul className="hidden lg:flex gap-4 justify-start ml-2">
{siteConfig.navItems.map((item) => ( {siteConfig.navItems.map((item) => (
<NavbarItem key={item.href}> <NavbarItem key={item.href}>
<NextLink <Link
className="data-[active=true]:text-primary data-[active=true]:font-medium" className="data-[active=true]:text-primary data-[active=true]:font-medium"
href={item.href} href={item.href}
> >
{item.label} {item.label}
</NextLink> </Link>
</NavbarItem> </NavbarItem>
))} ))}
</ul> </ul>
@@ -75,62 +70,29 @@ export const Navbar = () => {
justify="end" justify="end"
> >
<NavbarItem className="hidden sm:flex gap-2"> <NavbarItem className="hidden sm:flex gap-2">
{/* <Link isExternal href={siteConfig.links.twitter} aria-label="Twitter"> <NextUiLink
<TwitterIcon className="text-default-500" /> isExternal
</Link> */} href={siteConfig.links.github}
{/* <Link isExternal href={siteConfig.links.discord} aria-label="Discord"> aria-label="Github"
<DiscordIcon className="text-default-500" /> >
</Link> */}
<Link isExternal href={siteConfig.links.github} aria-label="Github">
<GithubIcon className="text-default-500" /> <GithubIcon className="text-default-500" />
</Link> </NextUiLink>
<ThemeSwitch /> <ThemeSwitch />
</NavbarItem> </NavbarItem>
<NavbarItem className="hidden lg:flex">{searchInput}</NavbarItem> <NavbarItem className="hidden lg:flex">{searchInput}</NavbarItem>
{/* <NavbarItem className="hidden md:flex">
<Button
isExternal
as={Link}
className="text-sm font-normal text-default-600 bg-default-100"
href={siteConfig.links.sponsor}
startContent={<HeartFilledIcon className="text-danger" />}
variant="flat"
>
Sponsor
</Button>
</NavbarItem> */}
</NavbarContent> </NavbarContent>
<NavbarContent className="sm:hidden basis-1 pl-4" justify="end"> <NavbarContent className="sm:hidden basis-1 pl-4" justify="end">
<Link isExternal href={siteConfig.links.github} aria-label="Github"> <NextUiLink
isExternal
href={siteConfig.links.github}
aria-label="Github"
>
<GithubIcon className="text-default-500" /> <GithubIcon className="text-default-500" />
</Link> </NextUiLink>
<ThemeSwitch /> <ThemeSwitch />
<NavbarMenuToggle /> <NavbarMenuToggle />
</NavbarContent> </NavbarContent>
<NavbarMenu>
{searchInput}
<div className="mx-4 mt-2 flex flex-col gap-2">
{siteConfig.navMenuItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
color={
index === 2
? "primary"
: index === siteConfig.navMenuItems.length - 1
? "danger"
: "foreground"
}
href="#"
size="lg"
>
{item.label}
</Link>
</NavbarMenuItem>
))}
</div>
</NavbarMenu>
</NextUINavbar> </NextUINavbar>
); );
}; };

View File

@@ -0,0 +1,9 @@
{
"Index": {
"title": "Hello world!"
},
"ProjectPage": {
"projects": "Projects",
"newProject": "New Project"
}
}

View File

@@ -0,0 +1,9 @@
{
"Index": {
"title": "はろー"
},
"ProjectPage": {
"projects": "プロジェクト",
"newProject": "プロジェクトの作成"
}
}

View File

@@ -1,3 +1,6 @@
const createNextIntlPlugin = require("next-intl/plugin");
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
typescript: { typescript: {
@@ -7,6 +10,6 @@ const nextConfig = {
// !! WARN !! // !! WARN !!
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
} };
module.exports = nextConfig module.exports = withNextIntl(nextConfig);

View File

@@ -24,6 +24,7 @@
"intl-messageformat": "^10.5.0", "intl-messageformat": "^10.5.0",
"lucide-react": "^0.372.0", "lucide-react": "^0.372.0",
"next": "14.0.2", "next": "14.0.2",
"next-intl": "^3.12.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"react": "18.2.0", "react": "18.2.0",
@@ -5204,6 +5205,14 @@
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
}, },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "14.0.2", "version": "14.0.2",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.2.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.0.2.tgz",
@@ -5249,6 +5258,34 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.12.0.tgz",
"integrity": "sha512-N3DHT6ce6K4VHVA3y2p3U7wfBx4c31qEgQSTFCFJuNnE7XYzy49O576ewEz7/k2YaB/U5bfxaWWaMMkskofwoQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"dependencies": {
"@formatjs/intl-localematcher": "^0.2.32",
"negotiator": "^0.6.3",
"use-intl": "^3.12.0"
},
"peerDependencies": {
"next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/next-intl/node_modules/@formatjs/intl-localematcher": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/next-themes": { "node_modules/next-themes": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
@@ -6683,6 +6720,73 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/use-intl": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.12.0.tgz",
"integrity": "sha512-tTJBSaxpVF1ZHqJ5+1JOaBsPmvBPscXHR0soMNQFWIURZspOueLaMXx1XHNdBv9KZGwepBju5aWXkJ0PM6ztXg==",
"dependencies": {
"@formatjs/ecma402-abstract": "^1.11.4",
"intl-messageformat": "^9.3.18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/use-intl/node_modules/@formatjs/ecma402-abstract": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
"dependencies": {
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
}
},
"node_modules/use-intl/node_modules/@formatjs/fast-memoize": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/use-intl/node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/icu-skeleton-parser": "1.3.6",
"tslib": "^2.1.0"
}
},
"node_modules/use-intl/node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"tslib": "^2.1.0"
}
},
"node_modules/use-intl/node_modules/@formatjs/intl-localematcher": {
"version": "0.2.25",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/use-intl/node_modules/intl-messageformat": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.1.0",
"tslib": "^2.1.0"
}
},
"node_modules/use-isomorphic-layout-effect": { "node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",

View File

@@ -25,6 +25,7 @@
"intl-messageformat": "^10.5.0", "intl-messageformat": "^10.5.0",
"lucide-react": "^0.372.0", "lucide-react": "^0.372.0",
"next": "14.0.2", "next": "14.0.2",
"next-intl": "^3.12.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"react": "18.2.0", "react": "18.2.0",

View File

@@ -25,11 +25,13 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
params: {locale}
}: { }: {
children: React.ReactNode; children: React.ReactNode;
params: {locale: string};
}) { }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
<head /> <head />
<body <body
className={clsx( className={clsx(

View File

@@ -1,7 +1,11 @@
import { Link, button as buttonStyles } from "@nextui-org/react"; import { button as buttonStyles } from "@nextui-org/react";
import { title, subtitle } from "@/components/primitives"; import { title, subtitle } from "@/components/primitives";
import { useTranslations } from "next-intl";
import { Link } from "@/src/navigation";
export default function Home() { export default function Home() {
const t = useTranslations("Index");
return ( return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10"> <section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center"> <div className="inline-block max-w-lg text-center justify-center">
@@ -13,6 +17,8 @@ export default function Home() {
</h2> </h2>
</div> </div>
<h1>{t("title")}</h1>
<div className="flex gap-3"> <div className="flex gap-3">
<Link <Link
href="/projects" href="/projects"

View File

@@ -1,4 +1,4 @@
"use client"; // "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
@@ -11,8 +11,10 @@ import {
updateProject, updateProject,
deleteProject, deleteProject,
} from "./projectsControl"; } from "./projectsControl";
import { useTranslations } from "next-intl";
export default function ProjectsPage() { export default function ProjectsPage() {
const t = useTranslations("ProjectsPage");
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
useEffect(() => { useEffect(() => {
@@ -78,7 +80,7 @@ export default function ProjectsPage() {
return ( return (
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow"> <div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
<div className="w-full p-3 flex items-center justify-between"> <div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">Projects</h3> <h3 className="font-bold">{t("project.project")}</h3>
<div> <div>
<Button <Button
startContent={<Plus size={16} />} startContent={<Plus size={16} />}
@@ -86,7 +88,7 @@ export default function ProjectsPage() {
color="primary" color="primary"
onClick={openDialogForCreate} onClick={openDialogForCreate}
> >
New Project {t("newProject")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { Image, Button, Tooltip, Card, CardBody } from "@nextui-org/react"; import { Image, Button, Tooltip, Card, CardBody } from "@nextui-org/react";
import { AttachmentType } from "../../../../../../../types/case"; import { AttachmentType } from "../../../../../../../../types/case";
import { Trash, ArrowDownToLine } from "lucide-react"; import { Trash, ArrowDownToLine } from "lucide-react";
type Props = { type Props = {

View File

@@ -1,5 +1,5 @@
import { Textarea, Button, Tooltip } from "@nextui-org/react"; import { Textarea, Button, Tooltip } from "@nextui-org/react";
import { StepType } from "../../../../../../../types/case"; import { StepType } from "../../../../../../../../types/case";
import { Plus, Trash } from "lucide-react"; import { Plus, Trash } from "lucide-react";
type Props = { type Props = {

12
frontend/src/i18n.ts Normal file
View File

@@ -0,0 +1,12 @@
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
const locales = ["en", "ja"];
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});

View File

@@ -0,0 +1,12 @@
import createMiddleware from "next-intl/middleware";
import { locales, localePrefix } from "./navigation";
export default createMiddleware({
defaultLocale: "en",
localePrefix,
locales,
});
export const config = {
matcher: ["/", "/(ja|en)/:path*"],
};

View File

@@ -0,0 +1,7 @@
import { createSharedPathnamesNavigation } from "next-intl/navigation";
export const locales = ["en", "ja"] as const;
export const localePrefix = "always";
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });