-
useTranslations()で得られるtは React Hook から返される関数 イベントハンドラuseCallbackやuseEffect内で直接tを参照すると、React は その依存関係が変わったときに再計算する必要がある と考えます-
依存配列に入れていないと
react-hooks/exhaustive-deps警告が出ます-
翻訳関数
tは依存配列に入れる -
イベントハンドラや useEffect 内で使うときも同じ
-
もし
tを渡す関数を別で作る場合は、引数として渡す方法 も安全です:
こうすれば依存配列の問題も起きません。
💡 要は 「イベントハンドラやコールバック関数の中で使う変数は、必ず依存配列に入れるか引数で渡す」 というルールです。
### 使ってない変数があると怒られるし、それを消した後に、.nextをyarn devで入れ直すと
npx eslint --fix .で起きるエラーは結構消える。
あとは、高階関数と依存注入使えば大体うまく行く。大体。
-
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
import { Locale, normalizePath, isPublicPlanPageURL, getLoginPagePath, getHomePagePath } from '@/utils/path';
const PUBLIC_FILE = /\.(.*)$/;
// 公開認証ページ
const PUBLIC_ROUTES = ['/auth/login', '/auth/register', '/auth/forgot-password', '/auth/reset-password'];
// next-intlのミドルウェアを初期化
const intlMiddleware = createIntlMiddleware({
locales: routing.locales,
defaultLocale: routing.defaultLocale,
localePrefix: 'always',
});
export async function middleware(req: NextRequest) {
// パスの末尾スラッシュを削除して正規化
const pathname = normalizePath(req.nextUrl.pathname);
// URLからロケールプレフィックスを抽出
const locale = pathname.split('/')[1] as Locale;
// --- ルート / の場合、ロケール判定 & リダイレクト ---
if (pathname === '/') {
const supportedLocales = routing.locales;
const cookieLocale = req.cookies.get('locale')?.value as Locale | undefined;
// Cookie > Accept-Language > デフォルトの順で最適なロケールを決定
const matchedLocale: Locale =
cookieLocale && supportedLocales.includes(cookieLocale)
? cookieLocale
: (((req.headers.get('accept-language') || '')
.split(',')
.map((l) => l.split(';')[0].trim())
.find((l) => supportedLocales.includes(l as Locale)) as Locale) ?? routing.defaultLocale);
// 決定したロケールでリダイレクトし、Cookieに保存
const url = req.nextUrl.clone();
url.pathname = `/${matchedLocale}`;
const res = NextResponse.redirect(url);
res.cookies.set('locale', matchedLocale, { path: '/' });
return res;
}
// --- 公開ファイルはスキップ ---
if (PUBLIC_FILE.test(pathname)) return;
// --- パブリックリンクの場合、ロケールプレフィックスがなければ追加 ---
if (isPublicPlanPageURL(pathname) && !routing.locales.includes(locale)) {
const cookieLocale = req.cookies.get('locale')?.value as Locale | undefined;
const matchedLocale: Locale =
cookieLocale && routing.locales.includes(cookieLocale) ? cookieLocale : routing.defaultLocale;
const url = req.nextUrl.clone();
url.pathname = `/${matchedLocale}${pathname}`;
return NextResponse.redirect(url);
}
const session = await auth();
const isPublicRoute = PUBLIC_ROUTES.some((route) => pathname.includes(route));
// --- 認証済みが認証ページにアクセス → トップにリダイレクト ---
if (session && isPublicRoute) {
return NextResponse.redirect(new URL(getHomePagePath(locale), req.url));
}
// --- 未認証が公開ページ以外にアクセス → ログインページにリダイレクト ---
if (!session && !isPublicRoute && !isPublicPlanPageURL(pathname)) {
return NextResponse.redirect(new URL(getLoginPagePath(locale), req.url));
}
// --- next-intl ミドルウェア適用 ---
const intlResponse = intlMiddleware(req);
intlResponse.headers.set('x-url', pathname);
intlResponse.headers.set('x-locale', locale);
// 認証済みなら Cookie にパスを残す
if (session) {
intlResponse.cookies.set('X_URL', pathname, { path: '/' });
}
return intlResponse;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
};/*
、正規表現でUUIDを抽出するところだけ、汎用性を上げるためにLOCALE TYPEを書いておく。
*/
export type Locale = 'ja' | 'en'; // 必要に応じて他言語追加
const LOCALES: Locale[] = ['ja', 'en'];
const localePattern = LOCALES.join('|');
export const UUID_PATH_REGEX = new RegExp(
`^/(?:(${localePattern})/)(?:public/)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`,
);
export const PUBLIC_UUID_PATH_REGEX = new RegExp(`^/(?:(${localePattern})/)public/`);
/**
* 現在のページがパブリックリンクのページか判定する
*/
export const isPublicPlanPageURL = (path: string) => {
const localePattern = LOCALES.join('|');
return new RegExp(`^/(?:(${localePattern})/)?plan/public/`).test(path);
};
/**
* 現在のページがパブリックリンクのページか判定する
*/
export const isPublicPlanPageURL = (path: string) => {
return PUBLIC_UUID_PATH_REGEX.test(path);
};
/**
* 未認証ユーザー用のログインページパスを生成
*/
export const getLoginPagePath = (locale: Locale) => `/${locale}/auth/login`;
/**
* 認証済みユーザー用のトップページパス
*/
export const getHomePagePath = (locale: Locale) => `/${locale}/`;
import '../globals.css';
import { ReactNode } from 'react';
import { NextIntlClientProvider } from 'next-intl';
import { headers } from 'next/headers';
import PageTemplate from '@/components/templates/PageTemplate';
import NoExistUserSessionLogout from '@/components/atoms/NoExistUserSessionLogout';
import prisma from '@/utils/prismaClient';
import { auth } from '@/auth';
import { makeImagePath } from '@/utils/image';
import { TRPCProvider } from '@/trpc/client';
import { extractPlanUUIDFromPath, isPublicPlanPageURL } from '@/utils/path';
import { makeGuestUser } from '@/utils/user';
import getRequestConfig from '@/i18n/request';
import { routing } from '@/i18n/routing';
export default async function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
const reqHeaders = await headers();
const pathname = reqHeaders.get('x-url');
// --- next-intl 用 locale / messages を取得 ---
const locale = reqHeaders.get('x-locale') || routing.defaultLocale; // middleware でヘッダーにセットしていればここから取得
const requestConfig = await getRequestConfig({ requestLocale: Promise.resolve(locale) });
// --- loginしなくても見れる情報の取得をここでやる。公開プラン判定 ---
const isPublicPage = pathname ? isPublicPageURL(pathname) : false;
const uuid = pathname ? extractUUIDFromPath(pathname) : null;
const session = await auth();
const user ; ログインユーザーの取得をsession情報から照らし合わせて行う。ログインしていないユーザーの場合は、ここで閲覧者用の使い捨てユーザーを用意する。
// --- レンダリング ---
return (
<html>
<body className="min-h-screen">
<TRPCProvider>
<NextIntlClientProvider locale={requestConfig.locale} messages={requestConfig.messages}>
<NoExistUserSessionLogout hasSession={!!session} hasUser={!!user}>
{user ? (
<PageTemplate>
{children}
</PageTemplate>
) : (
<>{children}</> // ログインページなどの認証が不要なページ
)}
</NoExistUserSessionLogout>
</NextIntlClientProvider>
</TRPCProvider>
</body>
</html>
);
}↑これをsrc/app/[locale]/layout.tsxにおく。
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
// リクエストされた言語の取得と検証
const requested = await requestLocale;
// サポートされている言語かチェックし、未対応の場合はデフォルト言語を使用
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
return {
locale,
messages: (await import(`@/../messages/${locale}.json`)).default,
};
});import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
// 利用可能な言語とデフォルト言語を設定
export const routing = defineRouting({
locales: ['en', 'ja'],
defaultLocale: 'ja',
});
// ナビゲーション用のユーティリティを作成
export const { Link, useRouter } = createNavigation(routing);↑2つをsrc/i18n直下に、それぞれrequest.ts, routing.tsとして置く。
そして、next.config.tsを
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
// 翻訳設定ファイルのパスを指定してプラグイン作成
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [{ hostname: 'lh3.googleusercontent.com', protocol: 'https' }],
},
serverExternalPackages: ['pino', 'pino-pretty'],
output: 'standalone',
experimental: {
authInterrupts: true,
},
};
// Next.js 設定を next-intl でラップして export
export default withNextIntl(nextConfig); { "common": {
"//": "汎用的に使う翻訳 ",
"search": "Search",
"clear": "Clear",
"copy": "Copy",
"editAttribute": "Edit Attribute",
"delete": "Delete",
"toggleArchive": "Toggle Archive",
"archive": "Archive",
"description": {
"//": "説明コンポーネントに使う翻訳 ",
"editLabel": "Edit description",
"viewLabel": "View description",
"placeholder": "Enter a description",
"submit": "Update"
}
},
"header": {
"publicLinkAccess": "Public link access",
"menu": {
"adminPanel": "Admin Panel",
"profileSettings": "Profile Settings",
"logout": "Logout"
}
}
みたいに、root/messages/ja, en.jsonを書く。
