[글또] 5. [Next.js + React] - i18n으로 다국어 지원하기

jinvicky·2024년 12월 11일
0
post-thumbnail

Intro


모동숲은 글로벌하게 유명한 게임이다. 나는 그 모동숲으로 공지표를 비롯한 커미션을 모집하고 있다. 한국에서는 이제 일정하게 일이 들어오니까 일본어나 영어로도 홍보해서 브라우저에 노출도를 올려서 알리고 싶다! 마음으로 다국어 기능을 구현해보았다.

추후 되는 대로 배포 링크를 올릴 예정이다.

코드는 간단해서 쉽게 할 수 있다.
다만 인터넷 검색했을 때 상위 블로그들로 따라했을 때 is not a function 에러가 골치아팠다.

🔥과정을 살펴보자🔥

🤔 is not a function ?

원인 추정은 대략 3-4가지였다.
1. npm 패키지 잘못 설치했거나 import를 잘못 했다.
2. i18n.ts를 만들고 루트 tsx 파일에서 import해줘야 하는데 에러났다.
3. t() 사용법이 틀렸다.

저 에러는 배열이 아닌 객체타입인데 .map() 함수를 쓴다든지 쓰임에 맞지 않을 때 나는 에러로 알고 있었다.

일단 개발하기 앞서서 구글링을 엄청 했다. 진짜 많이 따라치고 내용도 사실 별거 없는데 왜 안되지? 의 반복이었다가

🔗 https://stackoverflow.com/questions/79002730/cant-use-i18n-with-next-js

원인은 2번이었다. 내용인 즉슨, i18n.ts를 초기화할때 서버단에서 클라이언트 컴포넌트를 호출해서 그렇습니다. (next.js 원칙에 위배됨)
이를 해결하려면 laytout.tsx"use client" 키워드를 붙이거나 Provider 컴포넌트를 만들어야 합니다.

난 후자를 선택했다.

⌨️ Demo Code

테스트용으로 코드를 간단하게 작성해본다.
1. 먼저 json 파일을 3개 정의하고 각각 로케일을 정의한다.

// translations/locale/~.json
// ja.json
{
    "HELLO": "Hello"
}

// en.json
{
    "HELLO": "こんにちは"
}

//ko.json
{
    "HELLO": "안녕하세요"
}
  1. 코딩 전에 필요한 npm 라이브러리를 설치한다.

yarn add i18next react-i18next i18next-browser-languagedetector

  1. 가장 중요한 i18n.ts 파일을 만든다. (translations/i18n.ts)
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import enTranslations from '@/translations/json/en.json';
import koTranslations from '@/translations/json/ko.json';
import jpTranslations from '@/translations/json/ja.json';

const languageDetectorOptions = {
    order: ['navigator', 'querystring', 'cookie', 'localStorage', 'subdomain', 'header'],
};

i18next
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
        // lng: 'ko',
        detection: languageDetectorOptions,
        fallbackLng: 'ko',
        resources: {
            en: {
                translation: enTranslations,
            },
            ko: {
                translation: koTranslations,
            },
            ja: {
                translation: jpTranslations,
            }
        },
        interpolation: {
            escapeValue: false, // xss 보호를 위해서 escape 설정을 한다.
        },
    });

export default i18next;
  1. TranalationProvider 컴포넌트를 만든다.
"use client";

import { I18nextProvider } from "react-i18next";
import i18next from "@/translations/i18n";

export function TranslationProviders({ children }: { children: React.ReactNode }) {
  return <I18nextProvider i18n={i18next}>{children}</I18nextProvider>;
}

앞서 말한 stackoverflow의 2번째 방법이다. layout.tsx"use client" 를 적용할 수 없어서 선택했다.

  1. layout.tsx에서 Provider 컴포넌트로 전체를 감싼다.
<TranslationProviders>
  	<html lang="en">
    	<body>{children}</body>
	</html>
</TranslationProviders>

구현할 기능은 2개였다.
🔸1. 언어 변경 버튼을 눌렀을 때 해당 언어로 로케일이 변경되어야 한다.
🔸2. 브라우저 언어가 영어라면 화면 최초 진입 시 영어 로케일이 보여야 한다.

6-1. 1번 기능에 관한 코드

"use client";
import { useTranslation } from "react-i18next";

const PromotionPage = () => {
  const { t, i18n } = useTranslation();
  return (
    <>
      <div>{t("HELLO")}</div>
      {navigator.language}
      <div>현재 브라우저 언어 : {i18n.language}</div>
      <button className="bg-red-200" onClick={() => i18n.changeLanguage("ja")}>
        일본어로 바꾸기
      </button>
      <button className="bg-blue-200" onClick={() => i18n.changeLanguage("en")}>
        영어로 바꾸기
      </button>
    </>
  );
};

export default PromotionPage;

6-2. 2번 기능 관한 설명
본래 자바스크립트로도 navigator.language를 콘솔에 치면 해당 브라우저의 언어를 조회할 수 있다.
i18next-browser-languagedetector를 사용하면 쉽게 적용할 수 있다.
다만 lng 속성을 주석처리해야 한다. (=> 감지 이전에 여기서 적용된 언어를 먼저 적용한다.)
자동 언어 감지를 할 것이 아니라면 lng 를 살려야 한다.

  • 참고로 일본어는 jp가 아니라 ja였다;;
 .init({
        // lng: 'ko', 
        detection: languageDetectorOptions,
        fallbackLng: 'ko', // 오류 시 대체 언어

Test

크롬은 한국어, 엣지는 영어로 설정을 해놓고 비교한 결과 브라우저 언어를 인식해서 번역이 바뀐 것을 확인할 수 있다.

⌨️ 다중 언어 Navbar 만들기

navbar를 만들어서 적용해보았다. tailwindcss는 자체적으로는 조건부 스타일 적용이 안된다. 이때 tailwind-merge 라이브러리를 쓰는 것이 도움이 된다.

"use client";
import { useTranslation } from "react-i18next";
import { twMerge } from "tailwind-merge";

const NavBar = () => {
  const { i18n } = useTranslation();

  const updateStyle = (lng: "en" | "ja" | "ko") => {
    return twMerge(
      "text-white hover:bg-blue-400 px-3 py-1 rounded",
      i18n.language === lng && "bg-blue-600"
    );
  };

  return (
    <nav className="bg-gray-800 p-4">
      <div className="container mx-auto flex justify-between items-center">
        <div className="text-white text-lg font-bold">Jinvicky's Blog</div>
        <div className="flex space-x-4">
          <button
            className={updateStyle("en")}
            onClick={() => i18n.changeLanguage("en")}
          >
            EN
          </button>
          <button
            className={updateStyle("ja")}
            onClick={() => i18n.changeLanguage("ja")}
          >
            JA
          </button>
          <button
            className={updateStyle("ko")}
            onClick={() => i18n.changeLanguage("ko")}
          >
            KO
          </button>
        </div>
      </div>
    </nav>
  );
};

export default NavBar;
  • twMerge : 마지막 클래스의 우선순위가 가장 높다. (충돌 시 병합)
  • twJoin : 조건부로 스타일을 렌더링 (충돌 시 해결)

🔗 https://xionwcfm.tistory.com/322#😙이거 왜 사용하면 좋은 것일까요%3F-1

💭 추가

useTranslation()의 타입은 TFunction이다.
복잡한 html, 컴포넌트를 번역해야 할 때는 <Trans /> 컴포넌트를 사용한다.

라이브러리를 사용하면 로컬스토리지에 자동으로 i18nextLng라는 키를 가진 아이템이 생성되고 변경된다. 다만 마지막 선택한 언어가 다음에도 유지되지 않는다. (테스트했을 때 detector 유무랑 관계없었음)

이번에 포스팅한 방법은 번역이 적용되는 모든 페이지가 클라이언트 컴포넌트여야 한다는 단점이 있다. 동료는 이 점이 싫어서 다른 방식으로 구현했다는 데 그 방법 또한 공부중이다

Reference


🔗 https://velog.io/@favorcho/i18next-다국어-지원-기능-구현하기

💕 241221 - 홍보 페이지 1차 구현

구글 검색창 자동완성을 열심히 관찰해가면서 자동완성 상단에 뜨는 단어들을 파악했고,
검색창에서 내 스타일과 관련된 검색결과가 많이 나오는 것들로 해시태그를 구성했다.

Tailwindcss arbitrary variants

tailwindcss의 단점은 클래스명이 정말 무한대로 길어진다는 것이다. 또한 모든 클래스에 텍스트 중앙정렬을 하고 싶으면 기존의 나는 아래같이 했다.

<div className="text-center">test</div>
<div className="text-center">test</div>
<div className="text-center">test</div>

Tailwind CSS의 "arbitrary variants"를 사용하여 특정 선택자에 스타일을 적용하는 방식을 쓰면 더 간결한 처리를 할 수 있다.

.desc 클래스를 가진 모든 자식 div에게 sm 반응형 사이즈부터 text-center 스타일을 일괄 적용한다.

<div className="sm:[&>.desc]:text-center">
        <div className="desc">{t("LOST_ARK.PRODUCT.DESC_1")}</div>
        <div className="desc">{t("LOST_ARK.PRODUCT.DESC_2")}</div>
        <div className="p-5 text-md font-semibold text-gray-400 text-center">
          🌟 {t("APPLY_CLICK_HERE")} 🌟
        </div>
 </div>

🤔 동료의 코드 돕다가 알아낸 점인데 저 arbitrary variants가 depth가 깊어지면 className 안에서의 순서가 지켜져야 한다. vscode에서 경고 또는 에러로 보여준다.

또한 line-height를 40px 적용하는 코드도 있다.

<div className="leading-[40px]">{content}</div>

대부분 폰으로 보기 때문에 모바일형도 개발했다. tailwindcss는 sm:, md:, lg: 를 사용해서 내가 원하는 반응형을 구현할 수 있다.

키워드로 검색했을 때 잘 뜨려나..

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

1개의 댓글

comment-user-thumbnail
2024년 12월 21일

241221

i18n.ts에서 init() 내부에서 lng 설정을 주석처리했더니 아래 에러가 발생
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

next.js 공식문서에서 말한 에러 종류에 해당하지는 않았지만, 내 추측으로는 기본 언어를 설정하지 않으면 서버에서 만들어진 html과 detector로 감지한 후 만들어진 클라이언트의 html이 언어 설정이 달라서 에러가 가는 듯 하다.

결과적으로 lng: 'ko'로 설정값을 주었더니 에러 사라짐

🤔 궁금한 점은 그러면 language detector는 언제 작동하는 거지..?

답글 달기