Next.js에 i18n 적용하기
사용자의 지역
다국어 지원을 위해 유저가 어떤 locale의 유저인지 알 수 있어야한다.
클라이언트 - window.navigator
유저가 브라우저를 사용하고 있는 환경이라면 Web API 이용 가능.
navigator.language는 유저가 사용중인 브라우저UI 언어를 저장하고 있음. 이는 브라우저가 직접 세팅함.
navigator.language 를 가져오면 ko, en, en-US등 과 같은 UTS Locale Identifiers 형식의 string을 가져올 수 있음.
` const locale = window.navigator.language`
서버 - HTTP Header 의 Accept-Language
Next.js에서 서버 사이드 렌더링 후에 locale을 가져오는 방법은 UX를 고려한다면 썩 좋은 방법은 아니다. => 따라서 서버에서 유저가 사용 중인 언어를 알 수 있어야 한다.
- Next.js는 사용자가 웹 애플리케이션의 루트에 방문했을 때 요청하는 HTTP의 Accept-Language 헤더에 기반하여 사용자의 locale을 감지.
Accept-Language 는 navigator.language 와 마찬가지로 브라우저가 설정.
- Accept-Language는 브라우저의 버전 및 사용자 설정과 함께 핑거프린팅이 될 수 있기 때문에 직접 변경하는 것은 권장되지 않음
Next.js의 Internationalized Routing
서버는 Accept-Language 값을 통해 사용자의 언어를 감지한다.
locale에 맞는 URL로 라우팅 .
html tag에 lang 속성을 추가.
왜 Next.js는 i18n을 위해 라우팅하는 방법을 선택했을까? 값을 메모리에 할당 이후 메모리 값을 통해 언어를 수정하는 방법은 마찬가지로 Next.js가 기본적으로 서버 사이드 렌더링을 하기 때문에 UX를 고려한다면 좋지 않다. HTTP 헤더에서 Accept-Language 를 직접 수정하는 방법도 권장되지 않는다. 따라서 Next.js에서는 정적 페이지에서 i18n을 지원하는 방법(URL을 기반으로 i18n을 지원)을 따라가고 있다고 생각된다.
핵심은 Next.js에서 i18n을 제공하기 위해서는 example.com/ko, example.com/en-US 와 같은 식으로 라우팅이 이루어져야 한다는 것
다행히도 Next.js는 v10.0.0 부터 i18n Routing을 기본적으로 제공한다. 이를 이용하기 위해서 next.config.js 를 설정해야한다.
next.config.js 설정하기
1
2
3
4
5
6
7
8
9
//next.config.js
module.exports = {
...
i18n: {
locales: ['en-US', 'en', 'ko'], //지원한 언어를 설정
defaultLocale: 'en-US',
},
...
}
- 기본적으로 제공할 locale을 defaultLocale 에서 설정
서버가 감지한 locale이 ko일 때, locales 배열에 ko가 있기 때문에 최종적인 locale 은 ko
서버가 감지한 locale이 en-GB일 때, locales 배열에 en-GB가 없는 대신 en가 있기 때문에 최종적인 locale 은 en
서버가 감지한 locale이 fr일 때, locales 배열에 fr가 없기 때문에 최종적인 locale은 defaultLocale 인 en-US
- 한국어와 영어만 제공할 예정이라면? 영어를 defaultLocale로 설정하는 것이 글로벌한 서비스를 위해 적절할 것
Next.js 은 locale strategies 로 두가지 방법을 제공한다. Sub-path Routing: example.com/ko 처럼 서브 경로로 라우팅됩니다. Domain Routing: example.com 처럼 다른 도메인으로 라우팅
Default Locale 도 Sub-path 적용하기
default locale 에도 sub-path를 적용하고 싶다면 Next.js의 미들웨어를 사용해야한다.
next.config.js를 다음과 같이 수정.
1
2
3
4
5
6
7
8
9
10
11
// next.config.js
module.exports = {
i18n: {
// default는 잘못 입력한 것이 아닙니다. default를 제외하고 수정 해주세요.
locales: ['default', 'en', 'ko'],
defaultLocale: 'default',
localeDetection: false,
},
trailingSlash: true,
}
pages 디렉토리가 존재하는 디렉토리 내에 middleware.ts 를 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const PUBLIC_FILE = /\.(.*)$/
export async function middleware(req: NextRequest) {
if (
req.nextUrl.pathname.startsWith('/_next') ||
req.nextUrl.pathname.includes('/api/') ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return
}
if (req.nextUrl.locale === 'default') {
// 만약 default locale을 수정하고 싶다면 여기의 `en`을 변경하세요.
const locale = req.cookies.get('NEXT_LOCALE')?.value || 'en'
return NextResponse.redirect(
new URL(`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url)
)
}
}
i18n 설치 및 코드 세팅하기
애플리케이션 내의 텍스트를 추출하고 locale 에 따라 다른 언어를 제공하기 위한 라이브러리 설치
1
yarn add react-i18next i18next
다국어 json 파일 생성
src/translations 하위에 각 locale에 맞는 json 파일 생성
1
2
3
4
5
6
7
8
9
// en_US.json
{
"hello": "Hello!"
}
// ko_KR.json
{
"hello": "안녕!"
}
next-i18next.config.js
앞서 next.config.js 에 작성하였던 i18n을 next-i18next.config.js로 옮긴다.
굳이 이렇게 하는 이유?? next-i18next에게 defaultLocale이나 기타 locale이 무엇인지 알려주어 서버에서 번역을 preload하기 위함이다..
1
2
3
4
5
6
7
8
9
//next-i18next.config.js
module.exports = {
i18n: {
locales: ['default', 'en', 'ko'],
defaultLocale: 'default',
localeDetection: false,
},
}
1
2
3
4
5
6
7
8
9
//next.config.js
const { i18n } = require('./next-i18next.config')
module.exports = {
...
i18n,
...
}
appWithTranslation
- appWithTranslation는 i18nextProvider 를 추가하기 위한 HOC. _app 을 감싸주어야 한다.
1
2
3
4
5
6
7
8
9
import { appWithTranslation } from 'next-i18next'
const App = ({ Component, pageProps }) => (
<Component {...pageProps} />
)
export default appWithTranslation(App)
i18n.ts 파일 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/src/utils/ 하위에 설정 파일 생성
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import usTranslations from '@/translations/en_US.json';
import krTranslations from '@/translations/ko_KR.json';
const resources = {
en_US: {
translation: usTranslations,
},
ko_KR: {
translation: krTranslations,
},
};
i18n.use(initReactI18next).init({
resources,
lng: 'en_US',
fallbackLng: 'en_US',
debug: true,
interpolation: { escapeValue: true },
returnObjects: true,
returnEmptyString: true,
returnNull: true,
});
export default i18n;
다국어 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// \_app.tsx에 I18nextProvider 태그로 감싸준다.
import { AppProps } from 'next/app';
import 'bootstrap/dist/css/bootstrap.min.css';
import '@/styles/style.scss';
import Layout from '@/components/layout/Layout';
import { RecoilRoot } from 'recoil';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/utils/i18n';
function MyApp({ Component, pageProps }: AppProps) {
return (
<I18nextProvider i18n={i18n}>
<RecoilRoot>
<Layout>
<Component {...pageProps} />
</Layout>
</RecoilRoot>
</I18nextProvider>
);
}
export default MyApp;
index.tsx에 {t(‘hello’)} 와 같이 사용
1
2
3
4
5
6
7
8
import { useTranslation } from 'react-i18next';
const Index = () => {
const { t } = useTranslation();
return <div>{t('hello')}</div>;
};
export default Index;
참고자료
https://nextjs.org/docs/pages/building-your-application/routing/internationalization#locale-strategies
https://react.i18next.com/