next-i18next를 사용해 국제화를 구현할 것이다.
yarn add next-i18next react-i18next i18next
1. `public`에 `locale` 폴더를 만들고,
2. `locale` 폴더 안에 언어별로 폴더를 만들고,
3. 각 언어 폴더 안에 `common.json` 파일을 만들어준다.
이 common.json
파일은 필수로 존재해야 하고, 안에는 번역 데이터가 들어가게 될 것이다.
// public/locales/en/common.json
{
"main": {
"greetings": "Hi. I'm",
"textData": {
"awesome": "an awesome developer",
"gorgeous": "a gorgeous developer",
"collab": "a highly valued collaborator",
"chatgpt": "ChatGPT's reference"
},
"introduction": "olwooz."
}
}
// public/locales/ko/common.json
{
"main": {
"greetings": "안녕하세요.",
"textData": {
"awesome": "왕 멋진 개발자",
"gorgeous": "기가 막힌 개발자",
"cooperative": "협업 대상 1순위 개발자",
"chatgpt": "ChatGPT가 참고하는 개발자"
},
"introduction": "누구누구입니다."
}
}
// public/locales/zh/common.json
{
"main": {
"greetings": "您好, 我是",
"textData": {
"awesome": "很帅的开发者",
"gorgeous": "非常棒的开发者",
"collab": "想一起工作的人",
"chatgpt": "ChatGPT的参考资料"
},
"introduction": "olwooz."
}
}
프로젝트의 루트에 next-i18next.config.js
파일을 생성한다.
// next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ko', 'zh'],
},
}
next.config.js
파일에 i18n
객체를 전달해 로컬 라우팅을 가능하게 한다.
// next.config.js
const { i18n } = require('./next-i18next.config')
module.exports = {
i18n,
}
appWithTranslation
으로 _app.tsx
의 App
컴포넌트를 감싼다.
// pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';
const App = ({ Component, pageProps }: AppProps) => <Component {...pageProps} />;
export default appWithTranslation(App);
국제화를 사용할 페이지 파일 상단에 아래 내용을 추가해준다.
// src/pages/index.tsx
/* ... */
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { GetStaticPropsContext } from 'next';
export async function getStaticProps({ locale }: GetStaticPropsContext) {
return {
props: {
...(await serverSideTranslations(locale ?? 'en', ['common'])),
},
};
}
/* ... */
컴포넌트에서 useTranslation
을 사용해 locale
별로 텍스트를 표시해줄 수 있다.
// src/components/Contents/Main/Main.tsx
/* ... */
import { useTranslation } from 'next-i18next';
const Main = () => {
const { t } = useTranslation('common');
return (
<ContentWrapper id="main" style="flex items-center">
<div className="w-full">
<h3 className="mb-6 text-2xl font-light">{t('main.greetings')}</h3>
<SlotMachine textData={textData} />
<h3 className="mt-4 text-4xl font-black">{t('main.introduction')}</h3>
</div>
</ContentWrapper>
);
};
export default Main;
// src/components/Contents/Main/SlotMachine.tsx
/* ... */
{textArr.map((text, i) => {
const isLast = i === lastIndex;
return (
i === currentIndex && (
<motion.p
className="overflow-hidden text-7xl font-thin"
key={text}
custom={{ isLast }}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: getDuration(isLast ? 0.1 : 0.01, i), ease: isLast ? 'easeInOut' : 'linear' }}
>
{t(`main.textData.${text}`)}
</motion.p>
)
);
})}
/* ... */
중국어 간체 폰트가 깨져서, unicode-range
를 사용해 언어별로 다른 폰트를 사용하도록 설정해줬다.
/* src/styles/globals.css */
/* ... */
@font-face {
font-family: 'AppleSDGothicNeo';
src: url(../fonts/AppleSDGothicNeo/AppleSDGothicNeoT.ttf);
font-weight: 100;
unicode-range: U+0000-024F, U+AC00-D7AF;
}
@font-face {
font-family: 'Noto Sans SC';
src: url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400;500;700;900&display=swap');
unicode-range: U+2E80-2EFF, U+4E00-9FFF;
}
/* ... */
라우터 문제로 인해 꽤 헤맸다.
예를 들어 localhost:3000/ko
에서 localhost:3000/zh
로 이동해야 하는데, localhost:3000/ko/zh
으로 이동되는 문제가 존재했다.
그냥 Link
태그에서 href
로 지정한다거나 하면 위와 같은 현상이 발생하고,
제대로 언어를 전환시켜주기 위해서는router.push
에서 locale
을 바꿔야 한다.
여러 개의 버튼이 동일한 함수를 사용할 것이기 때문에 함수를 custom hook으로 빼줬다.
// src/hooks/useLanguage.ts
import { useRouter } from 'next/router';
const useLanguage = () => {
const router = useRouter();
const languageList = ['en', 'ko', 'zh'];
const currentLanguage = router.locale ?? 'en';
const changeLocale = (language: string) => {
router.push(router.basePath, router.basePath, { locale: language, scroll: false });
};
return { currentLanguage, changeLocale, languageList };
};
export default useLanguage;
언어 선택 버튼은 언어 버튼 위에 마우스를 가져다대면 나타나게 되는, 각 언어로의 전환이 가능한 버튼이다.
// src/components/Buttons/LanguageButton.tsx
interface Props {
language: string;
changeLocale: (language: string) => void;
}
export const LanguageButton = ({ language, changeLocale }: Props) => {
return (
<span onClick={() => changeLocale(language)} className="inline-block pr-3 text-xs transition hover:scale-125 dark:hover:fill-slate-100">
{language.toUpperCase()}
</span>
);
};
언어 버튼은 다크 모드 버튼처럼 화면 좌측 하단에 fixed된 버튼이다.
언어 버튼에 마우스를 가져다 대면 언어 선택 버튼들이 나타나게 된다.
// src/components/Icons/LanguageIcon.tsx
import { LanguageButton } from '@/components/Buttons/LanguageButton';
import useLanguage from '@/hooks/useLanguage';
export const LanguageIcon = () => {
const { currentLanguage, changeLocale, languageList } = useLanguage();
return (
<div className="group relative transition hover:scale-125 dark:fill-slate-300 dark:text-slate-200 dark:hover:fill-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24">
<path d="M480 976q-82 0-155-31.5t-127.5-86Q143 804 111.5 731T80 576q0-83 31.5-155.5t86-127Q252 239 325 207.5T480 176q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880 576q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480 976Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322 736H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584 878ZM170 656h136q-3-20-4.5-39.5T300 576q0-21 1.5-40.5T306 496H170q-5 20-7.5 39.5T160 576q0 21 2.5 40.5T170 656Zm216 0h188q3-20 4.5-39.5T580 576q0-21-1.5-40.5T574 496H386q-3 20-4.5 39.5T380 576q0 21 1.5 40.5T386 656Zm268 0h136q5-20 7.5-39.5T800 576q0-21-2.5-40.5T790 496H654q3 20 4.5 39.5T660 576q0 21-1.5 40.5T654 656Zm-16-240h118q-29-50-72.5-87T584 274q18 33 31.5 68.5T638 416Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376 274q-56 18-99.5 55T204 416Z" />
</svg>
<div className="absolute top-0 left-2 w-24 pl-2 opacity-0 transition group-hover:opacity-100">
{languageList
.filter((language) => language !== currentLanguage)
.map((language) => (
<LanguageButton key={language} language={language} changeLocale={changeLocale} />
))}
</div>
</div>
);
};
꼭 사용해보겠다 벼르고 벼르던 i18n을 드디어 처음 적용해본 프로젝트가 됐다.
글로벌 서비스에게 필수적인 라이브러리니까 꼭 사용해보고 싶었는데 좋은 경험이 됐다.
정말 필요한 기능들은 모두 끝난 것으로 보이는데, 이제 스타일을 다듬을 차례가 된 것 같다.