서론
이 문서는 next-i18next을 기반으로 next.js page router에 다국어 기능을 적용하는 과정을 다룹니다.
세팅하기
Nextjs에서는 i18n 라우팅을 제공하고 있습니다. 이 기능을 도메인 혹은 pathname을 통해 설정 언어를 url로 표현할 수 있습니다.
url을 통한 다국어 기능에는 몇 가지 이점이 있습니다.
1. SEO 최적화
URL에 매칭되는 페이지의 언어가 결정되기 때문에 메타 테그(<html lang="en">)를 통해 해당 페이지 언어를 명시할 수 있어 해당 언어의 검색결과에 노출될 수 있습니다.
2. 링크 및 북마크 생성 가능
설정된 언어가 URL에 담겨있기 때문에 해당 언어로 된 링크를 생성할 수 있습니다.
3. 번역 결과를 캐싱할 수 있음
해당 페이지 결과를 캐싱할 수 있다는 이점은 여러 가지면에서 도움이 됩니다.
- 캐싱을 통해 검색 엔진에 빠른 응답을 제공할 수 있어 검색 순위를 높이는데 도움이 됩니다.
- 서버 비용을 절감할 수 있다.
이러한 이유로 SSR을 제공하는 next.js에서 URL기반 다국어 기능을 제공한다고 볼 수 있습니다.
하지만 next.js에서 제공하는 기능은 제한적인데 Accept-Language를 통해 획득한 언어를 url에 매칭시켜 주는 역할로 제한됩니다. 즉 실제 번역을 하는 작업은 별도의 도구를 사용해야 합니다.
nextjs의 i18n routing과 완벽하게 결합하여 다국어 기능을 제공하는 패키지가 next-i18next 입니다.
이 패키지는 react-i18next를 기반으로 동작하며 SSR, SSG을 지원하고 파일 컨벤션으로 동작하기 때문에 설정이 비교적 간소화됐습니다.
next-i18next 세팅
이 부분에 대해서는 [next-i18next]project setup 문서를 참고해주시길 바랍니다.
한 가지 주의할 점은 useTransition을 next-i18next로 부터 사용해야 한다는 점인데 인텔리센스에서 react-i18next와 중복될 수 있습니다. 이 점이 상당히 귀찮은데 저는 아래와 같은 방법으로 이름을 명명하는 방식으로 우회했습니다.
export { useTranslation as useLocalization } from 'next-i18next';
또 한가지 아쉬운 점은 타입 추론의 부분인데 네임스페이스를 명시했음에도 해당 네임스페이스에 속해 있는 요소들이 추론되지 않아서 계속 키를 확인해야 하는 불편함이 있습니다. 이 부분은 타입을 오버라이드해서 해결할 계획입니다.
const { t } = useLocalization('common');
// greeting이 추론되지 않는다.
t('greeting')
번역하기
Page Component
페이지 컴포넌트에서 getStaticProps또는 getServerSideProps에서 serverSideTranslations를 호출해야 번역 파일이 로드됩니다.
기본적으로 모든 namespace가 호출되지만 페이지에 따라 serverSideTranslations 에 필요한 namespace만 명시하는 방식으로 호출되는 번역 파일을 제한할 수 있습니다. 일부 상황에서 Client 사이드에서 번역 파일을 요청해야 할 경우가 있을 수 있습니다.
이 경우 해당 링크 의 가이드를 확인하시길 바랍니다.
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale)),
},
}
}
Component
컴포넌트에서는 앞서 설명한 useLocalization 훅을 이용해 번역하면 됩니다.
serverSideTranslations 함수를 통해 번역파일을 로드해야 번역이 정상적으로 동작합니다.
export const Footer = () => {
const { t } = useLocalization('common')
return (
<footer>
<p>{t('description')}</p>
</footer>
)
}
언어 변경하기
nextjs에서는 transition-between-locales 섹션에서 useRouter를 이용한 방법과 Link를 이용한 방법 두가지를 제공하고 있습니다.
저는 아래와 같은 훅을 만들어 언어를 변경했습니다.
코드중 쿠키에 "NEXT_LOCALE"을 키로 locale을 설정하는 부분이 있는데 이 쿠키를 통해 next.js에서 설정된 언어를 기록합니다.
(참고: leveraging-the-next_locale-cookie)
/**
* 사용 언어 전환
* 전환된 언어를 쿠키에 저장한다.
*
* @example
* ```js
* setLanguage({locale:'en'}); // 영어로 전환
* setLanguage({locale:'kr'}); // 한글로 전환
* ```
*/
export const useLanguage = () => {
const router = useRouter();
const setLanguage = useCallback(
({ locale = "ko", persist = true }: SetLanguageParams) => {
const { pathname, asPath, query } = router;
Cookies.set("NEXT_LOCALE", locale, { expires: 365 });
router.push({ pathname, query }, asPath, { locale });
},
[router, company],
);
return { setLanguage };
};
번역 파일 관리하기
번역을 하는 작업은 일반적으로 협업을 통해 이뤄집니다. 그렇다면 비 개발자가 번역하고 이를 동기화하는 과정이 필요하다는 뜻이 됩니다.
저는 이 문서를 참고하여 구글 스프레드 시트를 조회하여 번역 정보가 담긴 JSON을 생성하는 방식으로 번역 파일을 관리했습니다.
구글 스프레드시트를 조회하기 위해서는 google-spreadsheet를 사용했습니다.
아래와 같은 구조로 작성된 스프레드시트를 조회하여 언어별 JSON파일을 생성했습니다.
sheet를 구분하여 번역파일의 namespace로 활용했습니다.
const Scope = {
Spreadsheets: 'https://www.googleapis.com/auth/spreadsheets',
};
/**
* @see https://theoephraim.github.io/node-google-spreadsheet/#/guides/authentication
*/
const serviceAccountAuth = new JWT({
email: env.email,
key: env.private_key,
scopes: [Scope.Spreadsheets],
});
/**
* 스프레드시트 헤더
*/
const Header = {
Key: 'key',
};
const convertSingleSheetToJson = async (sheet) => {
try {
await sheet.loadCells();
const rows = await sheet.getRows();
return sheet.headerValues.reduce((prev, header, index) => {
if (header === Header.Key) return prev;
const jsonData = {};
for (let j = 1; j < rows.length + 1; j++) {
const key = sheet.getCell(j, 0).value;
const value = sheet.getCell(j, index).value;
jsonData[key] = value;
}
const translationFileInfo = {
language: header,
namesapce: sheet.title,
data: jsonData,
};
prev.push(translationFileInfo);
return prev;
}, []);
} catch (error) {
console.error(`${sheet.title} 읽기에 실패했습니다. \n`, error);
return [];
}
};
/**
* @description 아래의 셀 구조를 가정한다
* ```js
* [
* ["key", "ko", "en"],
* ["greeting", "안녕하세요", "hello"],
* ]
* ```
*/
const convertAllSheetToJson = async () => {
const doc = new GoogleSpreadsheet(env.sheetId, serviceAccountAuth);
await doc.loadInfo();
const translationFileInfoList = [];
// namespace로 시트를 분리함
const sheets = doc.sheetCount;
for (let i = 0; i < sheets; i++) {
const sheet = doc.sheetsByIndex[i];
const fileInfoList = await convertSingleSheetToJson(sheet);
translationFileInfoList.push(fileInfoList);
}
return translationFileInfoList.flat();
};
/**
* @param language
* @param namesapce
* @param data key-value형식의 번역정보
*/
function writeFile({ namesapce, data, language }) {
const jsonString = JSON.stringify(data, null, 2);
const fileName = `public/locales/${language}/${namesapce}.json`;
fs.writeFileSync(fileName, jsonString);
}
async function main() {
const translationFileInfoList = await convertAllSheetToJson();
translationFileInfoList.forEach(writeFile);
}
main();
참고
https://nextjs.org/docs/pages/building-your-application/routing/internationalization
Routing: Internationalization | Next.js
Next.js has built-in support for internationalized routing and language detection. Learn more here.
nextjs.org
https://github.com/i18next/next-i18next
GitHub - i18next/next-i18next: The easiest way to translate your NextJs apps.
The easiest way to translate your NextJs apps. Contribute to i18next/next-i18next development by creating an account on GitHub.
github.com
https://ui.toast.com/weekly-pick/ko_20210303
국제화(i18n) 자동화 가이드
국제화 과정에서 "복붙"이나 반복적인 수작업으로 인해 고통받는 모든 프런트엔드 개발자를 위하여 작성하였다. 이 자동화 가이드를 따른다면 여러분은 단 한 줄의 스크립트 실행만으로 고통에
ui.toast.com
https://hotsunchip.tistory.com/13
[Next.js 14] App router 기반 Localization / Internationalization [i18next]
다국어를 지원하는 웹에 들어가보면 드롭다운으로 언어를 바꾸고, 그에 맞게 텍스트가 휙휙 바뀌는 것을 본 적이 있을 것이다. 이번 포스팅에서는 i18next 모듈을 이용해서 앱 라우터 기반의 Next.j
hotsunchip.tistory.com
'react' 카테고리의 다른 글
[설계] 격리된 컴포넌트간의 통신 설계 (feat. Eventbus) (0) | 2024.06.28 |
---|---|
[Nextjs] next13에서 react-query의 필요성 (0) | 2024.06.27 |
[설계] 의존성 역전으로 변경에 유연한 설계하기 (0) | 2024.06.26 |
[설계] Compound Pattern으로 변경에 유연한 컴포넌트 설계하기 (0) | 2024.06.24 |
실무에서 가장 많이 실수하는 useEffect 사례 (0) | 2024.06.23 |