이 레포는 “3개 언어(ko/en/ja) 글로벌 서비스”를 가정하고, 비개발자도 직접 i18n 사양을 만져보며 검증할 수 있도록 만든 i18n UX/워크플로우 샌드박스입니다.
핵심 컨셉은 다음 두 가지입니다.
- SSOT 관점에서의 ‘문구’ 실험장: JSON 메시지(번역 리소스) 편집 → UI 즉시 반영
- 문구 ↔ 코드 연결: 특정 문구(키)를 클릭하면, 실제
t("...")호출 위치가 코드 에디터에서 하이라이트됨
npm install
npm run dev- 브라우저:
http://localhost:3000 - **루트(
/)**는 자동으로/{defaultLocale}로 리다이렉트됩니다 (defaultLocale은i18n/locales.ts)
이 앱은 한 화면에서 i18n을 “한 번에” 확인하기 위해 3개의 pane으로 구성됩니다.
- Preview (UI pane): 실제 UI(문구 길이/서식/로케일 민감 동작)를 렌더링
- JSON Editor pane: 메시지 JSON 편집 + 키 클릭(=포커스)으로 하이라이트 트리거
- Code Editor pane:
t("...")호출 위치 하이라이트 + (필요 시) 해당 라인이 화면 중앙에 오도록 스크롤
핵심 페이지는 /{locale} (예: /ko, /en, /ja) 입니다.
- 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.ListFormat의style/type를 직접 바꿔 “and/or/unit” 차이를 확인Intl.DateTimeFormat.formatRange로 날짜 범위 표기(중복 연/월 생략 등) 확인- BiDi(혼합 방향 텍스트): LTR UI에 히브리어 토큰을 섞어 정렬/줄바꿈 깨짐 재현
- 문구 ↔ UI ↔ 코드 연결
- JSON에서 키 선택 → Preview와 Code Editor에 동일 키가 하이라이트
- 키를 바꾸기 전까지 하이라이트가 유지됨(타임아웃 없음)
- 상태 유지(Persistence)
- locale 전환 후에도 JSON/Code editor 스크롤 위치 유지
- locale 전환 후에도 마지막으로 선택한 키 하이라이트 유지
큰 흐름은 아래와 같습니다.
- JSON Editor에서 keyPath 선택
JsonEditor가 선택된 키의 keyPath를onActiveKeyPathChange로 전달합니다.
- 선택된 keyPath → “코드 검색용 query”로 변환
- JSON에서의 keyPath는
sandbox.xxx.yyy같은 형태이고, - 코드에서는
useTranslations("sandbox")이후t("xxx.yyy")로 호출하므로, SandboxClient에서sandbox.prefix를 제거해 query로 만듭니다.
- 현재 보고 있는 코드 파일(client/server)에서 호출 위치 찾기
SandboxClient는 선택된 파일(client또는server)에 query가 존재하는지 먼저 확인하고,- 없으면 다른 파일로 자동 전환합니다.
- 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
- 스크롤 정책
- “사용자 클릭”으로 발생한 하이라이트 요청일 때만 그 라인을 중앙에 오도록 스크롤합니다.
- “locale 전환으로 복원”된 경우에는 기본적으로 스크롤을 건드리지 않되,
- 하이라이트 라인이 화면 밖이면 그때만 화면에 보이도록 중앙 스크롤로 보정합니다.
- 전역(
window)에서[/]keydown을 받아 locale을 변경합니다 (components/LocaleNav.tsx) - 동시에 에디터 내부에서만 키 이벤트가 소비되는 경우를 대비해,
JsonEditor/CodeEditor도 동일 단축키를 잡아app:locale-switch커스텀 이벤트를 디스패치합니다.
messages/ko.jsonmessages/en.jsonmessages/ja.json
sandbox.identity.*: 이름/경칭/주소/전화(일본어 후리가나 포함)sandbox.grammar.*: 서수(selectordinal) / 한국어 조사(받침 기반)sandbox.layoutStress.*: word-break/line-break/hyphens/writing-mode + 폰트 fallbacksandbox.complexFormatting.*: 리스트/날짜 범위(formatRange)/BiDi
- 세 언어 JSON에 동일한 keyPath로 값을 추가합니다.
- UI 코드에서
useTranslations("sandbox")후t("...")로 호출합니다.
예) messages/ko.json
{
"sandbox": {
"example": {
"hello": "안녕하세요 {name}"
}
}
}예) 호출
const t = useTranslations("sandbox");
return <p>{t("example.hello", { name: "Alex" })}</p>;샌드박스에서 어디에서도 참조되지 않는 키는 제거하는 것을 권장합니다(세 언어 파일에서 동일하게 정리).
- Locale 정의:
i18n/locales.ts next-intlrequest config:i18n/request.ts- 요청 locale을 검증하고(
isAppLocale),messages/{locale}.json을 로드합니다.
- 요청 locale을 검증하고(
- 라우팅 미들웨어:
middleware.tslocalePrefix: "always"를 사용합니다.
- 라우팅 구조:
app/[locale]/*app/[locale]/page.tsx는 서버에서 source code를 읽어SandboxClient에 전달합니다(코드 에디터용).
현재는
ko/en/ja3개 언어를 기준으로 만들어져 있습니다.
i18n/locales.ts에 locale 추가messages/{newLocale}.json추가middleware.ts의 matcher 업데이트SandboxClient.tsx의messagesByLocale매핑(샌드박스 에디터 초기값) 업데이트
- 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)이 예상치 못한 강조를 만들 수 있으니 비활성화/오버라이드 정책을 확인합니다.
- CodeMirror 테마/기본 기능(
Intl.DateTimeFormat.formatRange런타임 에러가 날 때formatRange는this가Intl.DateTimeFormat인스턴스여야 합니다.- 아래처럼 메서드를 분리해서 호출하면(Detached call)
incompatible receiver에러가 날 수 있어요:const fn = dtf.formatRange; fn(a, b)(금지)
- 항상
dtf.formatRange(a, b)형태로 호출하세요.