Open30

Next.js app router 多言語対応する。

oo

MEMO

言語ファイル作成
上記で使用したキーを定義して行きます。
長すぎるのはダメですが、キーに日本語も使えます。
コードとメッセージが視覚的に区別できるので、解りやすいと思います。

messages/ja.json
{
"app_name": "NextAppOrigin",
"sub_title": ": ベースアプリケーション",
"env_name": {
"development": "【開発環境】",
"test": "【テスト環境】",
"staging": "【STG環境】",
"production": ""
},
"my_name": "My name",
"my_url": "https://example.com",
"サービス概要": "Next.js(React/Material UI)のベースアプリケーションです。(サービス概要に差し替え)",
"サービス説明": "サービスを迅速に立ち上げられるように、よく使う機能を予め開発しています。(サービス説明に差し替え)",
"リポジトリ": "リポジトリ",
"development": "development",
"テーマカラー確認": "テーマカラー確認",
"大切なお知らせ": "大切なお知らせ",
"アカウント登録": "アカウント登録",
"無料で始める": "無料で始める",
"ログイン": "ログイン"
}

oo

next.config

公式の通り何も考えずに書けば良さそう。

next.config.mjs
const withNextIntl = require('next-intl/plugin')();
 
module.exports = withNextIntl({
  // next.config.tsに記載していた元々の設定
});
oo

i18n.ts

対応する言語だけ気をつけてたらそのままで良さそう。

src/i18n.ts
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
 
// 対応する言語一覧
const locales = ['en', 'de'];
 
export default getRequestConfig(async ({locale}) => {
 // 現在のリクエストのロケールがサポートされていない場合は、404エラーを返します。
  if (!locales.includes(locale as any)) notFound();
 
  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});
oo

middleware.ts

middleware.ts
import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  // サポートする言語の一覧
  locales: ["ja", "en"],

  // defaultの言語
  defaultLocale: "ja",
});

export const config = {
  // 国際化対応を行うpath
  // ロケールを含むURLパスにマッチさせるためのパターン
  matcher: ["/", "/(ja|en)/:path*"],
};
oo

app/[locale]/layout.tsx

app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";

export default async function LocaleLayout({
  children, // 子コンポーネント
  params: { locale }, // パラメータからロケールを取得
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  // Providing all messages to the client
  // side is the easiest way to get started
  // メッセージを非同期で取得
  const messages = await getMessages();

  return (
    // htmlタグのlang属性を動的に設定
    <html lang={locale}>
      <body>
        {/* // メッセージ情報をクライアントに提供 */}
        <NextIntlClientProvider messages={messages}>
          // メッセージ情報をクライアントに提供
          <div className="container mx-auto py-8 max-w-2xl">{children}</div>
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

// Providing all messages to the client
// side is the easiest way to get started
このコメントの意味は、クライアントサイドで翻訳メッセージを使用するのが簡単ですよと。
国際化対応を始める際に、翻訳データをクライアントサイドに一括で渡すアプローチは、設定や管理が比較的簡単であるため、初めて国際化対応を行う場合に推奨される方法です。特に、アプリケーションがそれほど大きくない場合や、サーバーサイドレンダリングを行っている場合に有効です。と。

oo

Next.js App Routerを使用して多言語対応を行うためのnext-intlの設定とその流れを解説します。ここでは、公式ドキュメントを基に、ステップごとに必要な理由とともに説明します。

Next.js App Routerとは?

Next.jsのApp Routerは、従来のpagesディレクトリに代わる新しいルーティング方式です。これを使用することで、より動的で柔軟なルーティングが可能になり、ファイルベースのルーティングと比較して、パフォーマンスの向上や開発体験の向上が期待できます。

next-intlとは?

next-intlは、Next.jsアプリケーションにおける国際化(i18n)を簡単に実装するためのライブラリです。多言語対応を行う際に、翻訳メッセージの管理や日時・数値のフォーマットなど、多くの便利な機能を提供します。

ステップバイステップでの設定

Step 1: 必要なパッケージのインストール

まずはnext-intlをインストールします。

npm install next-intl

Step 2: ファイル構造の作成

多言語対応のために、以下のようなファイル構造を作成します。

├── messages
│   ├── en.json
│   └── ...
├── next.config.mjs
└── src
    ├── i18n.ts
    ├── middleware.ts
    └── app
        └── [locale]
            ├── layout.tsx
            └── page.tsx
  • messages/en.json: 各ロケールに対応するメッセージを定義します。例えば、英語では以下のようにします。

    {
      "Index": {
        "title": "Hello world!"
      }
    }
    

Step 3: Next.jsの設定

next.config.mjsファイルでnext-intlのプラグインを設定します。

// next.config.mjs
import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin();
 
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
export default withNextIntl(nextConfig);

ここでcreateNextIntlPluginを使用する理由は、Next.jsのサーバーコンポーネントで国際化設定を利用するために、必要な設定を自動的に行うためです。

Step 4: 国際化設定ファイル i18n.ts

src/i18n.tsでは、リクエストごとにどの言語設定を使用するかを定義します。

// src/i18n.ts
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
 
const locales = ['en', 'de'];
 
export default getRequestConfig(async ({locale}) => {
  if (!locales.includes(locale as any)) notFound();
 
  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});

ここでgetRequestConfigを使う理由は、リクエストごとに動的に言語設定を決定し、必要なメッセージファイルを読み込むためです。これにより、ユーザーのロケールに応じて適切なメッセージが表示されます。

Step 5: ミドルウェア middleware.ts

src/middleware.tsで、どのロケールを使うかをURLから判断し、適切にリダイレクトやリライトを行います。

// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  locales: ['en', 'de'],
  defaultLocale: 'en'
});
 
export const config = {
  matcher: ['/', '/(de|en)/:path*']
};

createMiddlewareを使用する理由は、リクエストが来たときにURLに基づいてロケールを自動的に判断し、その情報をリクエスト処理の流れに組み込むためです。

Step 6: レイアウトとページコンポーネント

app/[locale]/layout.tsx

各ロケールに基づいたレイアウトを定義します。

// app/[locale]/layout.tsx
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
 
export default async function LocaleLayout({
  children,
  params: {locale}
}) {
  const messages = await getMessages();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

NextIntlClientProviderを使用する理由は、クライアントサイドでの国際化設定を提供し、各コンポーネントでuseTranslationsなどのフックを使えるようにするためです。

app/[locale]/page.tsx

各ページでどのようにメッセージを使用するかを示します。

// app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
 
export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}

ここでuseTranslationsを使う理由は、定義されたメッセージから適切な翻訳を取得して表示するためです。

静的レンダリングの設定

静的レンダリングを可能にするため、generateStaticParamsunstable_setRequestLocaleを使用します。

app/[locale]/layout.tsxの更新

// app/[locale]/layout.tsx
import {unstable_setRequestLocale} from 'next-intl/server';
const locales = ['en', 'de'];
 
export function generateStaticParams() {
  return locales.map((locale) => ({locale}));
}
 
export default async function LocaleLayout({children, params: {locale}}) {
  unstable_setRequestLocale(locale);
  return (
    // ...
  );
}

unstable_setRequestLocaleを使用する理由は、サーバーコンポーネントがリクエストのロケール情報を正確に知ることができるようにするためです。これにより、静的レンダリング時にもロケールに基づいた正しい内容をレンダリングできます。

app/[locale]/page.tsxの更新

// app/[locale]/page.tsx
import {unstable_setRequestLocale} from 'next-intl/server';
 
export default function IndexPage({params: {locale}}) {
  unstable_setRequestLocale(locale);
  const t = useTranslations('IndexPage');
  return (
    // ...
  );
}

まとめ

この設定により、Next.jsのApp Routerを使った多言語対応が完成します。これにより、各ページとレイアウトで動的にロケールを切り替え、適切な言語でコンテンツを提供できるようになります。next-intlを使うことで、開発者は翻訳メッセージの管理や日時・数値の国際化などを簡単に行えるようになります。

oo

TODO:静的レンダリング

oo

質問: messagesディレクトリは、srcの中に入れてはいけませんか?
回答: messagesディレクトリをsrcディレクトリ内に配置することは技術的に可能ですが、公式の設定や多くのNext.jsのプロジェクトではルートディレクトリに配置することが一般的です。ただし、特定の理由がある場合(例えば、特定のビルドツールやプロジェクトの構成ポリシーに従うなど)はsrc内に配置することもできます。

ルートに配置するメリット:
Next.jsの標準的なフォルダ構成に従うことで、他の開発者がプロジェクトを理解しやすくなる。
next-intlや他の国際化ライブラリがデフォルトでルートディレクトリを参照する設定が多いため、設定がシンプルになる。

src内に配置する場合:
import文でのパスが少し長くなる可能性があります(例: import ... from '../messages/...'のようになる場合)。
プロジェクトのソースコードをsrc内に統一したい場合に選択することがあります。
実装例: messagesをsrc内に配置する場合、next.config.mjsでパスを指定するか、i18n.tsのimportパスを調整します。

// src/i18n.ts (messagesがsrc内にある場合の例)
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';

const locales = ['en', 'de'];

export default getRequestConfig(async ({ locale }) => {
  if (!locales.includes(locale as any)) notFound();
  // メッセージをsrc/messagesから読み込む
  return {
    messages: (await import(`../src/messages/${locale}.json`)).default
  };
});
oo

質問: 元々Topページを表示するために用意していたsrc/app/page.tsxはどうしたらいいですか?
回答: 国際化をApp Routerで実装する場合、各ロケールごとにページを用意するのが一般的です。そのため、src/app/page.tsxの内容をsrc/app/[locale]/page.tsxに移動させるのが良いでしょう。

移動先: src/app/[locale]/page.tsx

手順:

src/app/page.tsxの内容をsrc/app/[locale]/page.tsxに移動します。
移動後、src/app/page.tsxは削除します。
ロケールデータの読み込み: 国際化対応のために、ページ内でuseTranslationsを使用して適切な翻訳を表示するようにします。

移動と更新後の
src/app/[locale]/page.tsx
以下は、src/app/page.tsxの内容をsrc/app/[locale]/page.tsxに移動させ、国際化対応を行う例です。

src/app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
import FAQComponent from "@/features/home/components/FAQ";
import { FAQData } from "@/features/home/types/faq";
import { getFAQData } from "@/lib/faq";
import CTA from "@/features/home/components/CTA";
import Title from "@/features/home/components/Title";
import Feature from "@/features/home/components/Feature";
import Copy from "@/features/home/components/Copy";
import ShareButtons from "@/components/elements/ShareButtons/ShareButtons";

export default async function Home() {
  const t = useTranslations('Home'); // Home名前空間で翻訳を取得
  const faqData: FAQData = await getFAQData();

  return (
    <main className="flex min-h-screen flex-col items-center justify-between">
      <div className="w-full">
        <section className="hero bg-muted py-16">
          <Title />
          <div className="mt-16">
            <CTA />
          </div>
          <div className="mt-16 flex items-center justify-center flex-col">
            <p className="mr-4 mb-4 text-sm">{t('share')}</p>  // 翻訳を使用
            <ShareButtons />
          </div>
        </section>
        <section className="feature py-16">
          <Feature />
        </section>
        <div className="py-4">
          <CTA />
        </div>
        <section className="copy py-16">
          <Copy />
        </section>
        <section className="faq">
          <FAQComponent faqs={faqData} />
        </section>
      </div>
    </main>
  );
}
oo

src/config.ts

import {Pathnames} from 'next-intl/navigation';

export const port = process.env.PORT || 3000;
export const host = process.env.VERCEL_URL
 ? `https://${process.env.VERCEL_URL}`
 : `http://localhost:${port}`;

export const defaultLocale = 'en' as const;
export const locales = ['en', 'de'] as const;

export const pathnames = {
 '/': '/',
 '/pathnames': {
   en: '/pathnames',
   de: '/pfadnamen'
 }
} satisfies Pathnames<typeof locales>;

// Use the default: `always`
export const localePrefix = undefined;

export type AppPathnames = keyof typeof pathnames;
oo
import { Pathnames } from "next-intl/navigation";

export const port = process.env.PORT || 3000;
export const host = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : `http://localhost:${port}`;

export const locales = ["en", "ja"] as const;

export const pathnames = {
  "/": "/",
  // "/pathnames": {
  //   en: "/pathnames",
  //   ja: "/pfadnamen",
  // },
} satisfies Pathnames<typeof locales>;

// Use the default: `always`
export const localePrefix = "always"; // https://next-intl-docs.vercel.app/docs/routing/middleware#locale-prefix

export type AppPathnames = keyof typeof pathnames;

// "/pathnames": {
// en: "/pathnames",
// ja: "/pfadnamen",
この意味は、パスの名前を多言語かするか?ということ。
EX)/en/about
/de/ueber-uns

今回は必要ないのでしません。
https://next-intl-docs.vercel.app/docs/routing/middleware#localizing-pathnames

oo

Unable to find next-intl locale because the middleware didn't run on this request. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale. The notFound() function will be called as a result.

こんなエラーが出た。

ミドルウェアが間違ったファイルにセットアップされています (例:srcフォルダーを使用していますが、middleware.tsルート フォルダーに追加されているなど)。

ルートにあった。。。

oo

srcに直したら、こんなエラーが出た。

⨯ Internal error: Error: Expected a suspended thenable. This is a bug in React. Please file an issue.
⨯ Error: failed to pipe response

https://github.com/vercel/next.js/issues/51477
ここが参考になりそう。

https://next-intl-docs.vercel.app/docs/environments/server-client-components#async-components
これ見ろって言ってた。

getTranslationsuseTranslations は非同期のサーバー コンポーネントでは機能しません。代わりにfrom を使用する必要があるかもしれませんnext-intl/server。このリンクをご覧くださいhttps://next-intl-docs.vercel.app/docs/environments/server-client-components#async-components

確かに。多言語対応の部分をコメントアウトしたらエラー出なくなった。

該当コード
import ShareButtons from "@/components/elements/ShareButtons/ShareButtons";
import Copy from "@/features/home/components/Copy";
import CTA from "@/features/home/components/CTA";
import FAQComponent from "@/features/home/components/FAQ";
import Feature from "@/features/home/components/Feature";
import Title from "@/features/home/components/Title";
import { FAQData } from "@/features/home/types/faq";
import { getFAQData } from "@/lib/faq";
// import { useTranslations } from "next-intl";

export default async function Home() {
  // const t = useTranslations("Home"); // Home名前空間で翻訳を取得
  const faqData: FAQData = await getFAQData();

  return (
    <main className="flex min-h-screen flex-col items-center justify-between">
      <div className="w-full">
        <section className="hero bg-muted py-16">
          <Title />
          <div className="mt-16">
            <CTA />
          </div>
          <div className="mt-16 flex items-center justify-center flex-col">
            {/* <p className="mr-4 mb-4 text-sm">{t("share")}</p> */}
            <ShareButtons />
          </div>
        </section>
        <section className="feature py-16">
          <Feature />
        </section>
        <div className="py-4">
          <CTA />
        </div>
        <section className="copy py-16">
          <Copy />
        </section>
        <section className="faq">
          <FAQComponent faqs={faqData} />
        </section>
      </div>
    </main>
  );
}

だし、確かに非同期処理してた...!!
const faqData: FAQData = await getFAQData();

oo

というか、このfaqの部分、非同期処理要らなくね?という気づき。

FAQのデータを取得するために非同期処理を使用しているようですが、この場合、非同期処理を使用する必要はありません。

getFAQData関数では、fs.promises.readFileを使用して非同期的にファイルを読み込んでいますが、これをブロッキングなfs.readFileSyncに変更することで、同期的にファイルを読み込むことができます。

以下のようにgetFAQData関数を変更してください:

// src/lib/faq.ts
import fs from "fs";
import path from "path";
import { FAQData } from "@/features/home/types/faq";

export function getFAQData(): FAQData {
  const filePath = path.join(
    process.cwd(),
    "src",
    "features/home/data/faq.json"
  );
  const jsonData = fs.readFileSync(filePath, "utf8");
  const data: FAQData = JSON.parse(jsonData);
  return data;
}

そして、HomeコンポーネントではgetFAQData関数を同期的に呼び出すように変更します:

// ...
export default function Home() {
  const faqData: FAQData = getFAQData();

  return (
    // ...
  );
}

これにより、非同期処理を使用せずにFAQデータを取得することができます。

ただし、注意点として、fs.readFileSyncはブロッキングな操作であるため、大きなファイルを読み込む場合にはパフォーマンスに影響を与える可能性があります。FAQのデータが比較的小さい場合は問題ありませんが、大量のデータを扱う場合は非同期処理を検討する必要があります。

また、Next.jsのgetStaticPropsgetServerSidePropsを使用する場合は、非同期処理を使用することが推奨されています。これらの関数内では、非同期処理を使用してデータを取得し、propsとしてコンポーネントに渡すことができます。

今回のケースでは、FAQのデータが静的なものであれば、getStaticPropsを使用してビルド時にデータを取得するのが良いでしょう。

以上の点を考慮して、FAQデータの取得方法を適切に選択してください。

oo

直したのにまだエラー出る。なんで、と思ってたら。
export default async function Home() {こうなってた。
export default function Home() {こう直したら無事通った。

oo

言語切り替えselect実装

  • navigation.tsの作成
  • スイッチ用のコンポーネント作成
    • src/components/elements/LocaleSwitch/LocaleSwitcher.tsx
    • src/components/elements/LocaleSwitch/LocaleSwitcherSelect.tsx
      エグザンプルはプルダウン式の選択だったが、
      JA / EN のようにしたかったので、修正。
src/components/elements/LocaleSwitch/LocaleSwitcher.tsx
src/components/elements/LocaleSwitch/LocaleSwitcher.tsx
"use client";

import { locales } from "@/config";
import { usePathname, useRouter } from "@/navigation";
import { useLocale } from "next-intl";
import { useTransition } from "react";

export default function LocaleSwitcher() {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const pathname = usePathname();
  const currentLocale = useLocale();

  function onLocaleChange(nextLocale: string) {
    startTransition(() => {
      router.replace({ pathname }, { locale: nextLocale });
    });
  }

  return (
    <div className="flex space-x-4">
      {locales.map((locale) => (
        <button
          key={locale}
          className={`${
            currentLocale === locale
              ? "text-blue-500 font-bold"
              : "text-gray-400"
          } ${isPending ? "opacity-50 cursor-not-allowed" : ""}`}
          onClick={() => onLocaleChange(locale)}
          disabled={isPending}
        >
          {locale === "ja" ? "JA" : "EN"}
        </button>
      ))}
    </div>
  );
}
oo

現在、http://localhost:3000/en/restrictionsこれは表示できるのですが、
http://localhost:3000/ja/restrictionsにアクセスすると、http://localhost:3000/restrictionsにリダイレクトされ404が出ます。
http://localhost:3000/restrictions自体にアクセスしても404になります。
日本語の時にはhttp://localhost:3000/restrictionsこれで表示できるようにしたいのですが、どうしたらいいですか

-> なんかよくわからなかったので、
export const localePrefix = "always";にして、プレフィックスをぜったいつけるようにしちゃう。

oo

サイト内リンクをプレフィックスに対応させる。

i18n.config.ts
+ import { createSharedPathnamesNavigation } from "next-intl/navigation";

  export const locales = ["en-us", "ar-eg"] as const;
  export type Locale = (typeof locales)[number];

+ export const { Link } = createSharedPathnamesNavigation({ locales });
src/app/components/layouts/Header.tsx

  import { useTranslations } from "next-intl";
- import Link from "next/link";
+ import { Link } from "@/i18n.config";

  export default function Header() {
    const t = useTranslations("Header");

    // ...
  }
oo

/config.tsで管理してたけど、よくわからなくなるので、
/i18n.config.tsに変更する。

oo
messages/en.json
{
  "Home": {
    "このページをシェアする": "do share"
  }
}
messages/ja.json
{
  "Home": {
    "このページをシェアする": "このページをシェアする"
  }
}
src/app/[locale]/page.tsx
            <p className="mr-4 mb-4 text-sm">{t("このページをシェアする")}</p>

これでいけた...!!!すげー。わかりやすい〜

oo

マークダウンで管理していた部分も多言語対応させる。

1.言語ごとのマークダウンファイルを用意する
* privacy.en.md: 英語版の規約ページ用のマークダウンファイル
* privacy.ja.md: 日本語版の規約ページ用のマークダウンファイル
2. next-intl を使用して、言語ごとのマークダウンファイルを動的にインポートする

src/app/[locale]/privacy/page.tsx
import { useLocale } from "next-intl";
import MarkdownContent from "@/features/terms/components/MarkdownContent";

export default async function PrivacyPage() {
  const locale = useLocale();
  const privacyContent = await import(`@/features/terms/data/privacy.${locale}.md`).then(
    (module) => module.default
  );

  return <MarkdownContent content={privacyContent} />;
}
oo

辞書の中で改行を扱いたい!

https://next-intl-docs.vercel.app/docs/usage/messages#rich-text
これを参照。

src/features/home/components/CTA.tsx
import PreRegisterButton from "@/components/elements/Button/PreRegisterButton";
import { useTranslations } from "next-intl";

export default function CTA() {
  const t = useTranslations("CTA");
  return (
    <div className="flex flex-col items-center">
      <PreRegisterButton />
      <p className="text-center text-xs text-gray-500 pt-2">
        {t.rich("CTA", { br: () => <br /> })}
      </p>
    </div>
  );
}
messages/en.json
{
  "TOP": {
    "このページをシェアする": "do share"
  },
  "CTA": {
    "CTA": "betadsad<br></br>dasdas<br></br>adassdad."
  }
}

これでうまくいった。
※改行タグを、
の省略形だとうまくいかないので、<br></br>で書く。

ここで何をしているかの考察
brというタグがあったら、改行タグに変換するぞ。をしている。

oo

辞書の中でクラスを当てたい

先ほどのページを参照。
brというタグがあったら、改行タグに変換するぞ。をしている。
ように、
XXXXというタグがあったら、YYYYタグに変換するという書き方をしたらいけるはず。

一つのワードに複数のタグを入れたい

rich関数の中身。

(method) rich<string>(key: string, values?: RichTranslationValues | undefined, formats?: Partial<Formats> | undefined): string | ReactElement<any, string | JSXElementConstructor<...>> | ReactNodeArray

以下のようにしたらいけるらしい →いけた。

example.tsx
{t.rich("特徴の説明文", {
    br: () => <br />,
    strong: (text) => <span className="bg-yellow-200">{text}</span>,
})}

スマホ版で改行させたいとき。これも便利。

example.tsx
{t.rich("特徴の説明文", {
    br: () => <br />,
    brSp: () => <br className="md:hidden" />,
})}
example.json
      "XXXXX": "ssss<brSp></brSP> ssss<br></br>sss",

※<brSp></brSP>の前か後ろにスペース入れないと、PCの時にスペースがつかない

oo

辞書を階層構造にしたい。

en.json
{
  "auth": {
    "SignUp": {
      "title": "Sign up",
      "form": {
        "placeholder": "Please enter your name",
        "submit": "Submit"
      }
    }
  }
}
SignUp.tsx

import {useTranslations} from 'next-intl';
 
function SignUp() {
  // Provide the lowest common denominator that contains
  // all messages this component needs to consume.
  const t = useTranslations('auth.SignUp');
 
  return (
    <>
      <h1>{t('title')}</h1>
      <form>
        <input
          // The remaining hierarchy can be resolved by
          // using `.` to access nested messages.
          placeholder={t('form.placeholder')}
        />
        <button type="submit">{t('form.submit')}</button>
      </form>
    </>
  );
}
oo

言語によって参照するファイルを変える

マークアップファイル

src/app/[locale]/privacy/page.tsx
import MarkdownContent from "@/features/terms/components/MarkdownContent";
import { useLocale } from "next-intl";

export default async function PrivacyPage() {
  const locale = useLocale();
  const privacyContent = await import(
    `@/features/terms/data/privacy.${locale}.md`
  ).then((module) => module.default);

  return <MarkdownContent content={privacyContent} />;
}

jsonファイル

src/lib/faq.ts
import { FAQData } from "@/features/home/types/faq";
import fs from "fs";
import { useLocale } from "next-intl";
import path from "path";

export function getFAQData(): FAQData {
  const locale = useLocale();
  const filePath = path.join(
    process.cwd(),
    "src",
    `features/home/data/faq.${locale}.json`
  );
  const jsonData = fs.readFileSync(filePath, "utf8");
  const data: FAQData = JSON.parse(jsonData);
  return data;
}
oo

言語によって画像を出し分けたい

https://stackoverflow.com/questions/76127955/how-can-i-choose-picture-based-on-the-language-with-nextjs-13-and-next-intl

exampleA
import imgPL from '/public/images/myImage_pl.png';
import imgEN from '/public/images/myImage_en.png';
import { useLocale } from 'next-intl';

export default function Page() {
  const locale = useLocale();

  return (

    <Image src={locale==='pl' ? imgPL:imgEN} alt="" width={200} />
    
  );
}
exampleB
import { useLocale } from 'next-intl';

export default function Page() {
  const locale = useLocale();

  return (
    <Image src={`/images/myImage_${locale}.png` alt="" width={200} />
  );
}