[Next.js] 포트폴리오 웹 페이지 제작기 - 9. i18n

olwooz·2023년 2월 22일
0

next-i18next를 사용해 국제화를 구현할 것이다.

세팅

1. 설치

yarn add next-i18next react-i18next i18next

2. 설정

2.1 common.json

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."
  }
}

2.2 next-i18next.config.js

프로젝트의 루트에 next-i18next.config.js 파일을 생성한다.

// next-i18next.config.js

module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'ko', 'zh'],
  },
}

2.3 next.config.js

next.config.js 파일에 i18n 객체를 전달해 로컬 라우팅을 가능하게 한다.

// next.config.js

const { i18n } = require('./next-i18next.config')

module.exports = {
  i18n,
}

적용

1. appWithTranslation

appWithTranslation으로 _app.tsxApp 컴포넌트를 감싼다.

// 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);

2. 페이지

국제화를 사용할 페이지 파일 상단에 아래 내용을 추가해준다.

// 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'])),
    },
  };
}
  
/* ... */

3. 컴포넌트

컴포넌트에서 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>
    )
  );
})}

/* ... */

결과

localhost:3000/en


localhost:3000/ko


localhost:3000/zh

언어별 폰트

중국어 간체 폰트가 깨져서, 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을 바꿔야 한다.

hook

여러 개의 버튼이 동일한 함수를 사용할 것이기 때문에 함수를 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을 드디어 처음 적용해본 프로젝트가 됐다.
글로벌 서비스에게 필수적인 라이브러리니까 꼭 사용해보고 싶었는데 좋은 경험이 됐다.
정말 필요한 기능들은 모두 끝난 것으로 보이는데, 이제 스타일을 다듬을 차례가 된 것 같다.

0개의 댓글