Explanation
시간이 흐르고 흘러 어느덧 10월의 마지막 주 주말입니다. 올해도 얼마 남지 않았네요(두렵다..)
날이 많이 쌀쌀해지고 있어요 다들 건강 조심하세요.
오늘은!! react와 next에서 i18next를 사용해서 언어셋을 설정하는 방법을 간단하게 적어보려 합니다.
작성된 코드는 https://github.com/falsy/blog-post-example/tree/master/i18next-react-next 에서 확인하실 수 있습니다!
1. React
우선 필요한 패키지를 설치해 줍니다.
$ npm i -D i18next react-i18next
다음으로 간단하게 한국어과 영어로 언어셋을 만들어 줄게요.
// src/locales/ko.json
{
"안녕하세요": "안녕하세요"
}
// src/localse/en.json
{
"안녕하세요": "hello"
}
다음으로 i18n 설정도 간단하게,
// src/localse/i18n.ts
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import en from './en.json'
import ko from './ko.json'
const resources = {
en: {
translation: en
},
ko: {
translation: ko
}
}
i18n
.use(initReactI18next)
.init({
resources,
lng: "ko",
fallbackLng: "ko"
})
export default i18n
그럼 이제 설정한걸 적용해 볼까요?
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
const container = document.getElementById('wrap')
const root = ReactDOM.createRoot(container as HTMLElement)
import './locales/i18n'
import { useTranslation } from 'react-i18next'
const App = () => {
const { i18n, t } = useTranslation()
const handleClickChangeLng = () => {
i18n.changeLanguage(i18n.language === 'ko' ? 'en' : 'ko')
}
return (
<>
<div>{t('안녕하세요')}</div>
<p onClick={handleClickChangeLng}>언어변경</p>
</>
)
}
root.render(
<App />
)
끝! 엄청 간단하네요!
2. Next
역시, 우선 패키지 먼저 설치해 줄게요.
$ npm i -D i18next react-i18next next-i18next
react 에서는 src/locales 에 있던 언어셋을 public 에 아래와 같이 넣어줄게요.
// public/locales/ko/common.json
{
"안녕하세요": "안녕하세요"
}
// public/localse/en/common.json
{
"안녕하세요": "hello"
}
(React에서 했을때랑 디렉토리명, 파일명 다 조금씩 달라졌어요.)
다음으로 next-i18next.config.js를 만들어서 next.config.js에 적용해줍니다.
// next-i18next.config.js
module.exports = {
i18n: {
locales: ['ko', 'en'],
defaultLocale: 'ko'
},
}
// next.config.js
const { i18n } = require('./next-i18next.config')
module.exports = {
i18n
}
이제 _app 파일로 가서, appWithTranslation로 감싸주고
// src/_app.tsx
import { AppProps } from 'next/app'
import Head from 'next/head'
import { appWithTranslation } from 'next-i18next'
function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>next-i18next</title>
</Head>
<Component {...pageProps} />
</>
)
}
export default appWithTranslation(App)
그럼 이제 적용해 볼까요?
// src/index.tsx
import { GetServerSideProps } from "next"
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
import { useTranslation } from 'next-i18next'
export default function Home() {
const { i18n, t } = useTranslation('common')
const handleClickChangeLng = () => {
const newlng = i18n.language === 'ko' ? '/en' : ''
const { protocol, host, pathname } = window.location
const newPath = pathname.replace(/^\/en(\/|$)/g, '/')
window.location.href = protocol + '//' + host + newlng + newPath
}
return (
<>
<div>{t('안녕하세요')}</div>
<p onClick={handleClickChangeLng}>언어변경2</p>
</>
)
}
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(
locale as string,
['common']
))
}
}
}
next-i18next는 도메인에 첫번째 패스에 언어를 넣는 구성으로 사용이되는 거 같아요.
그래서 언어 변경을 할때, 저는 예시로 간단하게 정규표현식으로 언어 부분의 패스를 바꿔주었어요.
const { i18n, t } = useTranslation('common')
여기서 common은 아까 만든 언어셋 파일명이랍니다(common.json). 생략해도 되지만 그러면 사용할때 'common.' 으로 시작해야 해요.
const { i18n, t } = useTranslation()
...
<div>{t('common.안녕하세요')}</div>
3. Next (App Directory)
작성하는 김에, 마지막으로! Next의 앱 디랙토리 구조에서의 적용도 해볼게요.
우선 i8n 설정을 먼저 해줄게요.
아까와 같이 언어셋을 app 디랙토리 안에 i18n 폴더를 만들고 그안에 위치해줄게요.
// app/i18n/locales/ko/common.json
{
"안녕하세요": "안녕하세요"
}
// app/i18n/localse/en/common.json
{
"안녕하세요": "hello"
}
그리고 설정파일을 만들어줄게요.
// app/i18n/settings.ts
export const fallbackLng = 'ko'
export const languages = [fallbackLng, 'en']
export const defaultNS = 'common'
export function getOptions (lng = fallbackLng, ns = defaultNS) {
return {
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
}
}
다음 useTranslation를 만들어 줄게요.
// app/i18n/index.ts
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { getOptions } from './settings'
const initI18next = async (lng: string, ns?: string) => {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)))
.init(getOptions(lng, ns))
return i18nInstance
}
export async function useTranslation(lng: string, ns?: string, options: any = {}) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix),
i18n: i18nextInstance
}
}
이제 페이지 디랙토리를 구성해볼까요?
// app/[lng]/layout.ts
import { dir } from 'i18next'
import { languages } from '../i18n/settings'
import { ReactNode } from 'react'
export async function generateStaticParams() {
return languages.map((lng) => ({ lng }))
}
export default function RootLayout({
children,
params: {
lng
}
}: {
children: ReactNode
params: {
lng: string
}
}) {
return (
<html lang={lng} dir={dir(lng)}>
<head />
<body>
{children}
</body>
</html>
)
}
// app/[lng]/page.ts
'use client'
import { fallbackLng, languages } from '../i18n/settings'
import { useTranslation } from '../i18n'
export default async function Page({
params: {
lng
}
}: {
params: {
lng: string
}
}) {
if (languages.indexOf(lng) < 0) lng = fallbackLng
const { t } = await useTranslation(lng)
const handleClickChangeLng = () => {
const newlng = lng === 'ko' ? '/en' : '/ko'
const { protocol, host, pathname } = window.location
const newPath = pathname.replace(/^\/en|ko(\/|$)/g, '/')
window.location.href = protocol + '//' + host + newlng + newPath
}
return (
<>
<p>{t('안녕하세요')}</p>
<p onClick={handleClickChangeLng}>언어변경</p>
</>
)
}
그런데 이렇게 되면, / 패스로 접근하게 되면 404가 뜨기 때문에 middleware를 만들어 줍니다.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { fallbackLng, languages } from './app/i18n/settings'
export const config = {
matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)']
}
export function middleware(req: NextRequest) {
if (!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`))) {
return NextResponse.redirect(new URL(`/${fallbackLng}${req.nextUrl.pathname}`, req.url))
}
return NextResponse.next()
}
조건문 그대로 URI의 패스가 제가 등록한 언어 이름으로 시작하지 않으면 fallbackLng으로 리다이렉트 시키도록 합니다. 위에 config는, 미들웨어가 실행되는 매칭 기준?? 을 설정하는 거에요.
(api, static, image, assets, favicon, sw이 호출 될때는 아래 미들웨어가 실행되지 않도록)
끝
참고.
* https://react.i18next.com/getting-started
* https://locize.com/blog/next-i18n-static/
* https://locize.com/blog/next-13-app-dir-i18n/
* https://github.com/i18next/next-13-app-dir-i18next-example/tree/main



