[Next.js] next-intl 다국어 지원
Next.js에서 다국어 지원을 만들면서 겪은 문제점과 구축된 내용. next-intl을 선택한 이유
코딩목차
- 1. 시작하면서
- 2. next-intl
- 2.1. 작동원리 (개인적 이해)
- 2.2. 기능
- 2.3. 설치
- 3. 다국어 구축. next-intl 세팅
- 3.1. 구조 세팅
- 3.2. next.config.js 세팅
- 3.3. routing.js 세팅
- 3.4. navigation.js 세팅
- 3.5. middleware.js 세팅
- 3.6. request.js 세팅
- 4. 다국어 구축. 페이지에서 사용
- 4.1. messages 번역 파일들
- 4.2. layout.js 세팅
- 4.3. page.js 클라이언트 사용
- 4.4. page.js 서버 사용
- 5. 번외 내용
- 5.1. 언어 변경 스위치
- 5.2. 번역된 내용이 없을 때
- 5.3. Meta 데이터
시작하면서
오랜만에 코딩 기록.
사이트의 기능추가로 다국어를 넣기로 했다.
처음 검색해보니
next-i18next를 사용하라고 해서,
next-i18next, react-i18next, i18next을 설치해봤지만
npm install next-i18next react-i18next i18next
하지만 이 방식은
작동하지 않았다.
계속 404를 반환했다.
이유를 찾아보니,
해당 방식은 page/ 라우팅 전용으로
현재 프로젝트 app/ 디렉토리와 호환되지 않는다고 한다.
그래서 검색해보니 글 중에서
next-intl을 추천하는 내용이 있어서
사용해보게 되었다.
next-intl
공식링크 : next-intl 사이트
아래 내용은 개인적 이해이다.
정확한 내용은 공식 사이트를 참고바란다.
작동원리 (개인적 이해)
개인적으로 이해한 작동원리는...
4개의 설정을 만들어 작동한다.
routing에서 제공 언어와 디폴트 언어를 설정하고,
navigation에서 언어별 페이지 전환 기능,
middleware에서 navigatin을 호출해 자동 전환.
request에서 언어별 json을 반환한다.
기능
클라이언트의 언어에 따라서
해당 언어의 페이지로 리다이렉트를 하고,
해당 언어의 언어 json을 로딩한다.
만약 해당 언어가 없으면
디폴트 언어로 리다이렉트한다.
설치
npm install next-intl
당연히 우선
node.js를 먼저 설치해야 한다.
(참고: 《코딩공부-Node.js》)
다국어 구축. next-intl 세팅
구조 세팅
우선 프로젝트 구조를 수정해야 한다.
프로젝트 폴더에서 아래처럼 만든다.
├── messages
│ ├── ko.json
│ └── zh-CN.json
├── src
│ ├── i18n
│ │ ├── routing.js
│ │ ├── navigation.js
│ │ └── request.js
│ ├── middleware.js
│ └── app
│ └── [locale]
│ ├── layout.js
│ └── page.js
└── next.config.js
messages
폴더는
번역하는 언어를 보관하는 곳이다.
그 안으로 language 코드명으로 json을 만든다.
src
안에는 원래 app
가 있을 것이다.app
아래에 동적라우팅 [locale]
를 만들고,
(참고: 《동적 라우팅 (Dynamic Routes)》)
원래 있던 페이지 파일들을 [locale]
안으로 이동한다.
src
아래에i18n
폴더와 middleware.js
를 만든다.
src/i18n
아래에routing.js
, navigation.js
, request.js
3개의 파일을 만들면 세팅을 완료된다.
next.config.js 세팅
/** @type {import('next').NextConfig} */
const createNextIntlPlugin = require('next-intl/plugin');
const withNextIntl = createNextIntlPlugin();
const nextConfig = {
// 각 설정들
};
module.exports = withNextIntl(nextConfig);
next-intl 플러그인을 호출하고,
nextConfig를 감쌓 모듈로 내보내면 된다.
routing.js 세팅
// src/i18n/routing.js
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['ko', 'zh-CN'],
defaultLocale: 'ko',
});
routing.js에서는
제공 언어와 디폴트 언어를 설정한다.
navigation.js 세팅
// src/i18n/navigation.js
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
현재 페이지를 확인하고
리다이렉트하는 기능으로 보인다.
아마 제작자분이 더 유연한 제동을 위해 구분한 것 같다.
middleware.js 세팅
// src/middleware.js
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};
자동 리다이렉트 제어하는 것 같다.
api 등, 페이지가 아닌 부분을 호출할 때,
locale을 변경하는 것을 예외 시켰다.
request.js 세팅
// src/i18n/request.js
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,
};
});
언어팩을 반환하는 부분이다.getRequestConfig
을 호출해서 설정을 한다.
요청된 언어를 확인하고,
있으면 해당언어, 없으면 디폴트 언어를 반환한다.
다국어 구축. 페이지에서 사용
이제 페이지를 세팅해야 한다.
위의 next-intl을 기본 세팅이고,
밑은 실제 페이지에 적용하는 부분이다.
messages 번역 파일들
우선 번역 파일들을 만들어 둔다.
// messages/ko.json
{
"HomePage": {
"title": "안녕하세요",
"contents": "임시로 작성한 환영문구"
}
}
// messages/zh-CN.json
{
"HomePage": {
"title": "你好",
"contents": "临时欢迎文"
}
}
위처럼 사용 페이지를 구분하면 좋다.
구분하지 않아도 되긴 하지만,
차후 관리가 힘들어질 수 있다.
layout.js 세팅
최상위 layout에 세팅을 해야한다.
// app/[locale]/layout.js
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
export default async function LocaleLayout({ children, params }) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale}>
<head>...</head>
<body>
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
</html>
);
}
위처럼,
만약 강제로 없는 언어의 페이지로가면
notFound()를 호출한다.
html의 lang
을 locale
로 설정해서,
언어와 함께 lang도 변화하게 한다.
body 하위의 모든 자녀를NextIntlClientProvider
를 사용해서 감쌓준다.
이름에서 알 수 있듯
CSR, 클라이언트 측 제어 컴포넌트로 보인다.
page.js 클라이언트 사용
'use client';
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('contents')}</p>
</div>
);
}
messages에 번역 파일이 있기 때문에,
해당 파일의
HomePage.title
HomePage.contents
이 출력된다.
CSR로 'use client'에서 작동한다.
page.js 서버 사용
import { getTranslations } from 'next-intl/server';
export default async function HomePage() {
const t = await getTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('contents')}</p>
</div>
);
}
동일하게
HomePage.title
HomePage.contents
이 출력된다.
SSR로 json을 호출 및 읽기를 await 해야 한다.
즉 페이지 또한 async 이다.
기능 자체는 여기까지이다.
이렇게 하면 선택한 언어별로
json에서 번역문을 읽어서 반환한다.
번외 내용
언어 변경 스위치
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';
const locales = [
['ko', '한국어'],
['zh-CN', '简体字'],
];
export default function LanguageSwitcher() {
const currentLocale = useLocale();
const pathname = usePathname();
const router = useRouter();
const handleLocaleChange = (newLocale) => {
if (!pathname) return;
const segments = pathname.split('/');
segments[1] = newLocale;
const newPath = segments.join('/');
router.push(newPath);
};
return (
<select value={currentLocale} onChange={(e) => handleLocaleChange(e.target.value)}>
{locales.map(([locale, title]) => (
<option key={locale} value={locale}>
{title}
</option>
))}
</select>
);
}
반환의 select
부분부터 보면,
현재 locale을 값으로 넣는다.
option은 map으로 간단하게 만들었다.
(이러면 locale에 의해 title도 변한다)
onChange로 handler를 호출하고 value를 전달한다.
handleLocaleChange
에서는
path를 다시 조합하고router.push
로 새 페이지를 호출한다.
번역된 내용이 없을 때
ko.json 혹 zh-CN.json에
해당 번역이 없는 경우가 있다.
next-intl에서는 에러 메세지를 던지고 원래 값을 반환한다.
예시로 만약 위의 예시에서
zh-CN.json 에 contents 내용이 없으면HomePage.contents
라는 문구(string)을 반환한다.
즉 최소한 에러로 멈추는 일은 없다.
하지만 에러 메세지가 있는 건 동일하다.
이 때는 next-intl has
를 사용할 수 있다.
import { getTranslations } from 'next-intl/server';
export default async function HomePage() {
const t = await getTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
{t.has('contents') && <p> {t.('contents')}</p>}
</div>
);
}
위의 코드에서는contents
번역문이 있을 때만 <p>를 반환했다.
Meta 데이터
next-intl의 내용은 아니다.
next.js에서 generateMetadata
으로
동적으로 meta 데이터를 제어할 수 있다.
다른 동적 메타와 동일하게generateMetadata
안에서getTranslations
을 호출하면 된다.
(참고: 《Next.js 메타데이터》)