Skip to content

PLUTO-NIX/i18n-sandbox

Repository files navigation

Next.js i18n Sandbox (App Router + next-intl)

이 레포는 “3개 언어(ko/en/ja) 글로벌 서비스”를 가정하고, 비개발자도 직접 i18n 사양을 만져보며 검증할 수 있도록 만든 i18n UX/워크플로우 샌드박스입니다.

핵심 컨셉은 다음 두 가지입니다.

  • SSOT 관점에서의 ‘문구’ 실험장: JSON 메시지(번역 리소스) 편집 → UI 즉시 반영
  • 문구 ↔ 코드 연결: 특정 문구(키)를 클릭하면, 실제 t("...") 호출 위치가 코드 에디터에서 하이라이트됨

실행 방법

npm install
npm run dev
  • 브라우저: http://localhost:3000
  • **루트(/)**는 자동으로 /{defaultLocale}로 리다이렉트됩니다 (defaultLocalei18n/locales.ts)

화면 구성(3-pane)

이 앱은 한 화면에서 i18n을 “한 번에” 확인하기 위해 3개의 pane으로 구성됩니다.

  • Preview (UI pane): 실제 UI(문구 길이/서식/로케일 민감 동작)를 렌더링
  • JSON Editor pane: 메시지 JSON 편집 + 키 클릭(=포커스)으로 하이라이트 트리거
  • Code Editor pane: t("...") 호출 위치 하이라이트 + (필요 시) 해당 라인이 화면 중앙에 오도록 스크롤

핵심 페이지는 /{locale} (예: /ko, /en, /ja) 입니다.


이 샌드박스가 검증하는 i18n 사양(기능)

  • Locale Routing (URL prefix): /ko/..., /en/..., /ja/...
  • 언어 전환
    • GNB 버튼으로 전환
    • 단축키: [ / ] (입력 중이 아닌 경우, 화면 어디서든)
  • 문장 길이 스트레스 테스트: 짧은/긴 문장에 따른 레이아웃 변형 확인
  • Interpolation: {name}, {project} 같은 변수를 포함한 문구
  • Pluralization / count: 0/1/2/...에 따른 문장 변화
  • 날짜/시간/숫자/통화/퍼센트: 로케일별 포맷 차이
  • Rich text: 문자열 내부에 링크/강조 같은 마크업을 포함하는 패턴(t.rich)
  • Missing key 처리: 번역 누락 시 어떻게 표시/에러가 나는지
  • 문화적 맥락(Cultural Context) 시나리오
    • 이름/주소/전화(Personal identity): 로케일별 이름 순서/경칭 위치/주소 순서 차이(일본어 후리가나 입력 포함)
    • 문법(서수/조사): selectordinal(영어 서수) + 한국어 조사(받침 기반 로직 필요) 데모
  • UI 파괴(Visual Stress Test)
    • word-break/line-break/hyphens 토글 + 폭(px) 시뮬레이션으로 줄바꿈 정책에 따른 UI 깨짐 확인
    • 세로쓰기(writing-mode: vertical-rl) 토글(일본어 중심) + 폰트 fallback(ToFu) 문자열 테스트
  • 복합 포맷팅(Lists & Ranges & BiDi)
    • Intl.ListFormatstyle/type를 직접 바꿔 “and/or/unit” 차이를 확인
    • Intl.DateTimeFormat.formatRange로 날짜 범위 표기(중복 연/월 생략 등) 확인
    • BiDi(혼합 방향 텍스트): LTR UI에 히브리어 토큰을 섞어 정렬/줄바꿈 깨짐 재현
  • 문구 ↔ UI ↔ 코드 연결
    • JSON에서 키 선택 → Preview와 Code Editor에 동일 키가 하이라이트
    • 키를 바꾸기 전까지 하이라이트가 유지됨(타임아웃 없음)
  • 상태 유지(Persistence)
    • locale 전환 후에도 JSON/Code editor 스크롤 위치 유지
    • locale 전환 후에도 마지막으로 선택한 키 하이라이트 유지

“키를 클릭하면 코드가 하이라이트되는” 동작 원리

큰 흐름은 아래와 같습니다.

  1. JSON Editor에서 keyPath 선택
  • JsonEditor가 선택된 키의 keyPathonActiveKeyPathChange로 전달합니다.
  1. 선택된 keyPath → “코드 검색용 query”로 변환
  • JSON에서의 keyPath는 sandbox.xxx.yyy 같은 형태이고,
  • 코드에서는 useTranslations("sandbox") 이후 t("xxx.yyy")로 호출하므로,
  • SandboxClient에서 sandbox. prefix를 제거해 query로 만듭니다.
  1. 현재 보고 있는 코드 파일(client/server)에서 호출 위치 찾기
  • SandboxClient는 선택된 파일(client 또는 server)에 query가 존재하는지 먼저 확인하고,
  • 없으면 다른 파일로 자동 전환합니다.
  1. CodeEditor가 t("...") 호출 위치를 찾아 하이라이트
  • CodeEditor는 현재 소스 텍스트에서 정규식으로 t("query") 또는 t.rich("query")를 찾고,
  • 해당 라인 + 토큰을 강조(amber scheme)합니다.

하이라이트 단일 매칭(중요)

CodeEditor의 매칭은 소스에서 t("...") 호출을 찾는 방식이기 때문에, 같은 키에 대해 t("...")를 여러 번 호출하면(예: label + aria-label) 원하지 않는 라인이 하이라이트될 수 있습니다.\n 이 샌드박스는 이를 방지하기 위해, 신규 섹션의 주요 라벨들은 아래처럼 t("...")를 한 번만 호출해서 변수로 재사용합니다.\n 예: const identityLabelGivenName = t("identity.labelGivenName");\n

  1. 스크롤 정책
  • “사용자 클릭”으로 발생한 하이라이트 요청일 때만 그 라인을 중앙에 오도록 스크롤합니다.
  • “locale 전환으로 복원”된 경우에는 기본적으로 스크롤을 건드리지 않되,
    • 하이라이트 라인이 화면 밖이면 그때만 화면에 보이도록 중앙 스크롤로 보정합니다.

단축키([ / ])가 어디서든 동작하는 이유

  • 전역(window)에서 [ / ] keydown을 받아 locale을 변경합니다 (components/LocaleNav.tsx)
  • 동시에 에디터 내부에서만 키 이벤트가 소비되는 경우를 대비해,
    • JsonEditor/CodeEditor도 동일 단축키를 잡아 app:locale-switch 커스텀 이벤트를 디스패치합니다.

번역 리소스(문자열) 위치

  • messages/ko.json
  • messages/en.json
  • messages/ja.json

새로 추가된 “문화/파괴” 예제 네임스페이스

  • sandbox.identity.*: 이름/경칭/주소/전화(일본어 후리가나 포함)
  • sandbox.grammar.*: 서수(selectordinal) / 한국어 조사(받침 기반)
  • sandbox.layoutStress.*: word-break/line-break/hyphens/writing-mode + 폰트 fallback
  • sandbox.complexFormatting.*: 리스트/날짜 범위(formatRange)/BiDi

키 추가/사용(가장 쉬운 방식)

  1. 세 언어 JSON에 동일한 keyPath로 값을 추가합니다.
  2. UI 코드에서 useTranslations("sandbox")t("...")로 호출합니다.

예) messages/ko.json

{
  "sandbox": {
    "example": {
      "hello": "안녕하세요 {name}"
    }
  }
}

예) 호출

const t = useTranslations("sandbox");
return <p>{t("example.hello", { name: "Alex" })}</p>;

사용하지 않는 키 정리

샌드박스에서 어디에서도 참조되지 않는 키는 제거하는 것을 권장합니다(세 언어 파일에서 동일하게 정리).


i18n 설정(코드 구조)

  • Locale 정의: i18n/locales.ts
  • next-intl request config: i18n/request.ts
    • 요청 locale을 검증하고(isAppLocale), messages/{locale}.json을 로드합니다.
  • 라우팅 미들웨어: middleware.ts
    • localePrefix: "always"를 사용합니다.
  • 라우팅 구조: app/[locale]/*
    • app/[locale]/page.tsx는 서버에서 source code를 읽어 SandboxClient에 전달합니다(코드 에디터용).

로케일/언어를 추가하려면

현재는 ko/en/ja 3개 언어를 기준으로 만들어져 있습니다.

  1. i18n/locales.ts에 locale 추가
  2. messages/{newLocale}.json 추가
  3. middleware.ts의 matcher 업데이트
  4. SandboxClient.tsxmessagesByLocale 매핑(샌드박스 에디터 초기값) 업데이트

기술 스택

  • Next.js 16.1.1 (App Router)
  • React 19
  • next-intl ^4.7.0
  • CodeMirror (@uiw/react-codemirror)
  • Tailwind CSS v4

트러블슈팅(개발자 참고)

  • Hydration mismatch를 피하려면
    • sessionStorage 같은 브라우저 전용 API는 초기 render 시점에 읽지 말고, useEffect에서 복원합니다.
  • 에디터 하이라이트가 안 보일 때
    • CodeMirror 테마/기본 기능(highlightSelectionMatches)이 예상치 못한 강조를 만들 수 있으니 비활성화/오버라이드 정책을 확인합니다.
  • Intl.DateTimeFormat.formatRange 런타임 에러가 날 때
    • formatRangethisIntl.DateTimeFormat 인스턴스여야 합니다.
    • 아래처럼 메서드를 분리해서 호출하면(Detached call) incompatible receiver 에러가 날 수 있어요:
      • const fn = dtf.formatRange; fn(a, b) (금지)
    • 항상 dtf.formatRange(a, b) 형태로 호출하세요.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages