반응형

배경


사실 다국어 언어 메뉴를 만들면서 초기구상은 nexti18n 을 사용하기로 했지만, 배움의 부담에 못이겨 하드코딩으로 진행했다. 그러다보니 유지보수는 물론,, 가독성이 완전 제로제로제로였다. 그래서 이를 잘 관리하기 위해 기존 기획했던 nexti18n 을 제대로 적용해보려고 한다.

 

처음부터 잘못됐던


나는 초반에 nexti18n 라이브러리를 적용하기 위해 설치를 했으나 구글링을 통해 제대로 확인해보니 Next13/14 App 기반 환경은 기존 라이브러리를 지원하지 못한다고 한다!?!

 

 

대충 app 에서는 지원하지 않는다는 의미

 

대신 i18next, react-i18next, i18next-resources-to-backend 라이브러리를 사용해 구성하라고 한다.

npm install i18next react-i18next i18next-resources-to-backend

https://locize.com/blog/next-app-dir-i18n/

 

i18n with Next.js 13/14 and app directory / App Router (an i18next guide)

Looking for a way to internationalize your Next.js 13/14 project with the new app directory / App Router paradigm? Then this guide is for you!

locize.com

 

해당 블로그를 따라하라고 하길래 보면서 열심히 따라쳤다. 하지만,, 2년전 자료를 그 때 깨닫고 하지 말았어야 했는데..

 

결론은 언어를 동적 링크를 생성해 주소를 호출하는 과정속에서 알 수 없는 404 에러가 계속 나와 다시 next i18n 에 대해 검색을 진행했다.

돌아도 한참 돌아갔던


조금더 검색을 해보니 next-intl 이라는 라이브러리가 존재한다는것을 알게되었다..! 아뿔싸 나의 내다버린 3시간.. 하지만 개발이란 원래 이런것..

 

설치과정을 잠깐 보자면

설치과정

0. 상황

├── messages (1)
│   ├── en.json
│   └── ...
├── next.config.mjs (2)
└── src
    ├── i18n.ts (3)
    ├── middleware.ts (4)
    └── app
        └── [locale]
            ├── layout.tsx (5)
            └── page.tsx (6)

샘플 폴더 구조는 다음과 같다.

 

1. 라이브러리를 설치한다.

npm install next-intl

 

2. 파일 셋업하기

1) messages/en.json

{ "Index": { "title": "Hello world!" }}
  • 각 언어마다 json 파일 형식으로 글에 들어갈 내용을 작성한다.

2) next.config.mjs

import createNextIntlPlugin from 'next-intl/plugin'; 
const withNextIntl = createNextIntlPlugin(); 
/** @type {import('next').NextConfig} */
const nextConfig = {}; 
export default withNextIntl(nextConfig);
  • next-intl 을 사용하기위한 플러그인을 설정하는 코드다.

3) i18n.ts

import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';

// Can be imported from a shared config
const locales = ['en', 'ko'];

export default getRequestConfig(async ({locale}) => {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();

  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});
  • next-intl은 요청 범위의 구성 객체를 생성하여 사용자의 언어에 따라 메시지 및 기타 옵션을 제공할 수 있으며, 이를 서버 컴포넌트에서 사용할 수 있습니다.

4) middleware.ts

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // A list of all locales that are supported
  locales: ['en', 'ko'],

  // Used when no locale matches
  defaultLocale: 'en'
});

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(ko|en)/:path*']
};
  • middleware 에서는 요청에 따른 언어가 매칭되는지 확인하고 리다이렉트하거나 리라이트 하는 역할을 한다.

5) app/[locale]/layout.tsx

import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';

export default async function LocaleLayout({
  children,
  params: {locale}
}: {
  children: React.ReactNode;
  params: {locale: string};
}) {
  // Providing all messages to the client
  // side is the easiest way to get started
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

 

6) app/[locale]/page.tsx

import {useTranslations} from 'next-intl';

export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}
  • [locale] 이라는 파일 명을 통해 미들웨어에서 매칭된 언어인 경우 주소값을 가질 수 있다.

결과


테스트 성공!

기존 웹사이트에 적용하기


이제 next-intl 을 적용되었으니 기존 사이트에 추가해야한다. locale (언어 변수) 를 전역으로 관리해야한다는 아이디어가 생겨 상태를 관리할 수 있는 LanguageContext.tsx 를 만들었다.

// context/LanguageContext.tsx

"use client"
import { createContext, useContext, useState, ReactNode } from 'react';
import { Language } from '../types';

interface LanguageContextProps {
  selectedLanguage: Language;
  setSelectedLanguage: (language: Language) => void;
}

const LanguageContext = createContext<LanguageContextProps | undefined>(undefined);

interface LanguageProviderProps {
  children: ReactNode;
  initialLocale: Language;
}



export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {
  const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);

  return (
    <LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};



export const useLanguage = (): LanguageContextProps => {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
};

 

로직

 

1. layout.tsx 에서 locale 을 다룰 수 있는 Provider를 만들어 감싼다.

<html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          <LanguageProvider initialLocale={locale}>
          {children}
          </LanguageProvider>
          <div id="global-modal"></div>
          <div id="howtoeat-modal"></div>
        </NextIntlClientProvider>
      </body>
    </html>

 

이렇게 감싸게 되면 LanguageProvider 에 저장된 selectedLanguage 변수로 locale 을 children 에 전달하는 방법을 통해 언어 변수를 관리할 수 있다.

 

2. LanguageProvider selectedLanguage

export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {

  const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);

  return (
    <LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};

 

여기서 저장된 selectedLanguage 는 다음 함수를 호출함으로 외부에서 사용할 수 있다.

 

3. useLanguage

export const useLanguage = (): LanguageContextProps => {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
};
const {selectedLanguage, setSelectedLanguage} = useLanguage();
const router = useRouter();
const pathname = usePathname();

  useEffect(() => {
    const currentLocale = pathname.split('/')[1] as Language;
    if (currentLocale && ["ko", "en", "ja", "th", "ch"].includes(currentLocale)) {
      setSelectedLanguage(currentLocale);
    } else {
      console.error(`Invalid language in path: ${currentLocale}`);
    }

  }, [pathname, setSelectedLanguage]);

 

위와 같이 page.tsx 에서 selectedLanguage 변수에 useLanguage() 로 context 를 전달한다. 이렇게 만든다면 드롭다운을 통해 언어를 선택하고 , 선택된 언어는 Context 로 관리를 할 수 있다.

다음 할 일


이제 복잡한 건 끝났고, 언어별 json 파일을 만들어서 좀 더 보기좋은 코드로 만드는 일만 남았다. 상당히 귀찮고 복잡한 일일 것으로 예상되지만 이것 역시 내가 극복 해야할일...

 

또 생각이 든건데 LanguageContext가 아닌 redux 를 사용해도 뭔가 될 것 같은 기분이다. redux에 대해 제대로 배우질 않아서 적용하지 않았는데 나중에 적용해봐도 될 것같다.

반응형

+ Recent posts