Javascript API를 이용해 유튜브 영상을 재생, 일시정지 합니다.
React, Next에서 i18next를 사용해서 언어셋 설정하기
2023-10-28
Explanation
시간이 흐르고 흘러 어느덧 10월의 마지막 주 주말입니다. 올해도 얼마 남지 않았네요(두렵다..)
날이 많이 쌀쌀해지고 있어요 다들 건강 조심하세요.
오늘은!! react와 next에서 i18next를 사용해서 언어셋을 설정하는 방법을 간단하게 적어보려 합니다.
작성된 코드는 https://github.com/falsy/blog-post-example/tree/master/i18next-react-next 에서 확인하실 수 있습니다!
우선 필요한 패키지를 설치해 줍니다.
1 |
$ npm i -D i18next react-i18next |
다음으로 간단하게 한국어과 영어로 언어셋을 만들어 줄게요.
1 2 3 4 |
// src/locales/ko.json { "안녕하세요": "안녕하세요" } |
1 2 3 4 |
// src/localse/en.json { "안녕하세요": "hello" } |
다음으로 i18n 설정도 간단하게,
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 |
// 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 |
그럼 이제 설정한걸 적용해 볼까요?
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 |
// 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 /> ) |
끝! 엄청 간단하네요!
역시, 우선 패키지 먼저 설치해 줄게요.
1 |
$ npm i -D i18next react-i18next next-i18next |
react 에서는 src/locales 에 있던 언어셋을 public 에 아래와 같이 넣어줄게요.
1 2 3 4 |
// public/locales/ko/common.json { "안녕하세요": "안녕하세요" } |
1 2 3 4 |
// public/localse/en/common.json { "안녕하세요": "hello" } |
(React에서 했을때랑 디렉토리명, 파일명 다 조금씩 달라졌어요.)
다음으로 next-i18next.config.js를 만들어서 next.config.js에 적용해줍니다.
1 2 3 4 5 6 7 |
// next-i18next.config.js module.exports = { i18n: { locales: ['ko', 'en'], defaultLocale: 'ko' }, } |
1 2 3 4 5 6 |
// next.config.js const { i18n } = require('./next-i18next.config') module.exports = { i18n } |
이제 _app 파일로 가서, appWithTranslation로 감싸주고
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 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) |
그럼 이제 적용해 볼까요?
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 30 31 32 33 |
// 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는 도메인에 첫번째 패스에 언어를 넣는 구성으로 사용이되는 거 같아요.
그래서 언어 변경을 할때, 저는 예시로 간단하게 정규표현식으로 언어 부분의 패스를 바꿔주었어요.
1 |
const { i18n, t } = useTranslation('common') |
여기서 common은 아까 만든 언어셋 파일명이랍니다(common.json). 생략해도 되지만 그러면 사용할때 ‘common.’ 으로 시작해야 해요.
1 2 3 4 5 |
const { i18n, t } = useTranslation() ... <div>{t('common.안녕하세요')}</div> |
작성하는 김에, 마지막으로! Next의 앱 디랙토리 구조에서의 적용도 해볼게요.
우선 i8n 설정을 먼저 해줄게요.
아까와 같이 언어셋을 app 디랙토리 안에 i18n 폴더를 만들고 그안에 위치해줄게요.
1 2 3 4 |
// app/i18n/locales/ko/common.json { "안녕하세요": "안녕하세요" } |
1 2 3 4 |
// app/i18n/localse/en/common.json { "안녕하세요": "hello" } |
그리고 설정파일을 만들어줄게요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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를 만들어 줄게요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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 } } |
이제 페이지 디랙토리를 구성해볼까요?
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 |
// 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> ) } |
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 30 31 32 |
// 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를 만들어 줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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