웹 i18n
국제화는 마지막에 하는 번역 작업이 아니라 아키텍처입니다. 목표는 특별한 경우 없이 모든 로케일에 대해 올바르게 렌더링되는 UI입니다. 즉, 연결된 문장, 하드코딩된 형식, 독일어가 35% 더 길거나 아랍어가 방향을 뒤집을 때 깨지는 레이아웃이 없어야 합니다.
이 스킬을 사용해야 할 때
사용자가 다음을 요청할 때 사용합니다:
- 언어, 언어 전환기 또는 로케일 라우팅 추가
- 앱 번역, 문자열 추출 또는 번역 워크플로우 설정
- 복수형, 성별 또는 "1개 항목 / 2개 항목" 버그 수정
- 로케일별 날짜, 숫자, 통화, 상대 시간 또는 목록 형식 지정
- RTL(아랍어, 히브리어, 페르시아어, 우르두어) 지원
- hreflang, 현지화된 메타데이터 또는 다국어 사이트맵 추가
- 기존 i18n 설정, 누락된 키 또는 번역 불일치 검토
- next-intl, react-i18next, FormatJS, Lingui 또는 TMS 중에서 선택
관련 참고 자료는 references/에 있고 스크립트는 scripts/에 있습니다. 작업에 필요한 것만 읽으십시오. 경로는 이 스킬의 디렉토리를 기준으로 합니다(에이전트 런타임은 스킬이 로드될 때 이를 출력합니다).
필수 원칙
- 번역된 조각을 연결하여 문장을 절대 만들지 마십시오. 전체 메시지를 플레이스홀더와 함께 번역하십시오:
"{count}개의 새 메시지"처럼 사용하고,t('new') + ' ' + count + ' ' + t('messages')처럼 사용하지 마십시오. - 키로 영어 텍스트를 절대 사용하지 마십시오. 키는 안정적인 식별자(
inbox.unreadCount)이므로, 복사본 편집이 조회를 깨뜨리거나 모든 번역을 무효화하지 않습니다. - 모든 사용자 대상 문자열은 외부화되어야 합니다. 여기에는
aria-label,alt,title,placeholder, 유효성 검사 오류, 빈 상태 메시지, 토스트, 이메일, 푸시 알림 및 PDF 출력이 포함됩니다. - 형식을 절대 하드코딩하지 마십시오.
toFixed(2) + '$', 수동MM/DD/YYYY, 영어 월 이름,24h가정은 안 됩니다.Intl을 사용하십시오. - 로케일은 언어가 아니라 BCP 47 태그입니다.
pt-BR과pt-PT,zh-Hans와zh-Hant,es-MX와es-ES는 서로 다른 제품입니다. 전체 태그를 저장하십시오. - 서버에서 로케일 상태는 요청 범위여야 합니다. SSR 동안 현재 로케일을 모듈 수준의 가변 변수에 절대 넣지 마십시오. 동시 요청이 서로의 언어를 누출시킵니다.
- TMS, 커뮤니티 또는 CMS에서 가져온 번역은 신뢰할 수 없는 입력입니다. 소독(sanitizing) 없이
innerHTML/dangerouslySetInnerHTML에 절대 전달하지 마십시오.
워크플로우
1. 코드 변경 전 감사
- 아무것도 제안하기 전에 스택, 라우터 및 기존 i18n 라이브러리를 파악하십시오.
- 하드코딩된 문자열을 찾으십시오:
출력을 후보로 취급하고 절대적인 진리로 취급하지 마십시오: 테스트 픽스처, 상수, URL 및 개발자 대상 로그는 건너뛰십시오.node scripts/scan-hardcoded.mjs src - 현재 제공되는 모든 로케일과 카탈로그가 있는 위치를 목록화하십시오.
- 가장 위험도가 높은 영역을 먼저 식별하십시오: 인증, 체크아웃, 오류, 이메일 및 법적 문구.
2. 아키텍처 결정 (기록해 두십시오)
코드를 작성하기 전에 다음 질문에 답하십시오:
| 결정 | 기본값 | 참고 |
|---|---|---|
| 로케일 전략 | 경로 접두사 /es/ | 최고의 SEO와 캐시 가능성. 이유가 있는 경우에만 하위 도메인/ccTLD 사용. |
| 기본 로케일 | 소스 언어 | 재정의 없이 처음 방문자를 추측된 로케일로 자동 리디렉션하지 마십시오. |
| 로케일 감지 | URL → 쿠키 → Accept-Language → 기본값 | URL이 우선 순위이므로 공유 링크가 언어를 변경하지 않습니다. |
| 카탈로그 형식 | 로케일별, 네임스페이스별 JSON | 네임스페이스를 지연 로드하고 모든 로케일을 클라이언트에 절대 제공하지 마십시오. |
| 메시지 구문 | ICU MessageFormat | 라이브러리와 TMS 간에 이식 가능; plural/select/ordinal 지원. |
| 키 스타일 | feature.component.element | 안정적이고 검색 가능하며 영어 텍스트 없음. |
| 번역 출처 | TMS 또는 저장소, 둘 다 아님 | 하나를 선택하고 CI에서 한 방향으로 동기화하십시오. |
| 폴백 | 로케일 체인 es-MX → es → en | 개발 중에 폴백을 기록하여 간격을 포착하십시오. |
3. 메시지 외부화 및 구조화
- 문자열을 의미 있는 키로 바꾸고 복사본을 기본 로케일 카탈로그로 이동하십시오.
- 전체 문장당 하나의 키를 사용하십시오. 문장에 링크나 굵은 글씨가 포함된 경우 분할하는 대신 리치 텍스트 보간을 사용하십시오.
- 플레이스홀더는 이름을 사용하고(
{userName}), 위치 기반({0})을 사용하지 마십시오. - 라이브러리가 명시적으로 유형화된 태그를 지원하지 않는 한 카탈로그에 HTML을 넣지 마십시오.
- 페이지별이 아닌 제품 표면별로 그룹화하십시오:
checkout.json,auth.json,errors.json.
복수형, select, ordinal 및 리치 텍스트 규칙은 references/icu.md를 참조하십시오.
4. 로케일 라우팅 및 렌더링 연결
- 경로에서 로케일을 파생하고 지원 목록에 대해 유효성을 검사한 후, 알 수 없는 로케일은 자동으로 폴백하지 말고 404를 반환하십시오.
- 현재 경로와 쿼리 문자열을 유지하는 언어 전환기를 제공하십시오.
- 서버 렌더링 문서에서
<html lang="...">및<html dir="...">를 설정하십시오. - 활성 로케일의 카탈로그와 기본 폴백 카탈로그만 로드하십시오.
- 동일한 카탈로그를 서버 및 클라이언트 렌더링에서 사용할 수 있게 하십시오. 중복되거나 분기된 번역 로직을 피하십시오.
프레임워크별 설정 및 함정: references/frameworks.md.
라우팅, 감지, hreflang 및 사이트맵: references/routing-seo.md.
5. 로케일 민감 값 처리
수동 형식 지정 없이 Intl(또는 프레임워크 래퍼)을 사용하십시오:
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
- 타임스탬프는 UTC로 저장하고 사용자의 시간대(서버 시간대 아님)로 렌더링하십시오.
- 통화는 로케일과 동일하지 않습니다. 프랑스 사용자가 USD로 지불할 수 있습니다.
- 정렬/검색은
<또는 로케일 없는localeCompare()대신Intl.Collator를 사용하십시오. - 대소문자 변환:
toLocaleUpperCase(locale)을 사용하십시오. 터키어i는İ가 되기 때문입니다.
6. 확장 및 RTL 설계
- 텍스트 길이가 +30–40% 증가(독일어, 핀란드어, 러시아어)하고 고정 너비 버튼이나 한 줄 레이블이 없다고 가정하십시오.
- 컨테이너가 커지도록 하고 줄 바꿈을 허용하며 의사 현지화로 테스트하십시오:
node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json left/right대신 논리 CSS 속성(margin-inline-start,padding-inline,inset-inline)을 사용하십시오.- 방향성 아이콘(화살표, 쉐브론, 진행 표시줄)은 미러링하고 로고, 시계, 미디어 컨트롤 또는 체크 표시는 미러링하지 마십시오.
- 사용자 생성 인라인 콘텐츠는
<bdi>또는dir="auto"로 감싸십시오.
전체 RTL 체크리스트: references/rtl.md.
7. CI에서 번역 QA 설정
최소한 카탈로그가 구조적으로 유효하지 않으면 빌드가 실패하도록 하십시오:
node scripts/check-locales.mjs locales --base en
누락/추가 키, 빈 값, 플레이스홀더 불일치 및 잘못된 ICU 구문을 검사합니다. 가능한 경우 라이브러리의 CLI를 통해 ICU 컴파일을 추가하십시오:
npx formatjs compile-folder --ast locales/en locales/compiled/en
복수형 카테고리, 고정 시간대의 날짜/숫자 출력 및 RTL 렌더링에 대한 테스트를 추가하십시오. references/testing.md를 참조하십시오.
주요 결정 및 함정
라이브러리 선택
| 필요 | 선택 |
|---|---|
| Next.js App Router, 서버 컴포넌트 | next-intl |
| React SPA, 성숙한 생태계, 많은 플러그인 | react-i18next |
| ICU 우선, 프레임워크 비종속, CLI 추출 | FormatJS (react-intl) |
| 컴파일 타임 카탈로그, 작은 런타임, 타입 안전성 | Lingui |
| Vue | vue-i18n |
| Svelte | svelte-i18n 또는 paraglide |
| Astro | 내장 i18n 라우팅 + 메시지 라이브러리 |
| Vanilla / 최소 | Intl + 작은 카탈로그 로더 |
이미 i18n 라이브러리가 있는 앱에 두 번째 i18n 라이브러리를 도입하지 마십시오. 기존 설정을 확장하십시오.
복수형
복수형 카테고리는 언어마다 다릅니다. 영어에는 one/other가 있고, 아랍어에는 zero/one/two/few/many/other가 있으며, 일본어에는 other만 있습니다. count === 1 ? ... : ...를 절대 작성하지 마십시오.
"inbox.unread": "{count, plural, =0 {읽지 않은 메시지 없음} one {#개의 읽지 않은 메시지} other {#개의 읽지 않은 메시지}}"
성별 및 문법적 일치
성별 변수에 select를 사용하고 변수 값은 언어 중립적(female, male, neutral, unknown)으로 유지하고 현지화된 단어를 사용하지 마십시오:
"profile.greeting": "{gender, select, female {환영합니다, {name}} male {환영합니다, {name}} other {환영합니다, {name}}}"
언어가 형용사, 명사 또는 관사에 대한 일치가 필요한 경우 문자열을 해킹하는 대신 문법적 성별/격을 인수로 노출하십시오.
리치 텍스트
"Read our " + link + " terms"를 세 개의 키로 분할하지 마십시오. 태그 보간을 사용하십시오:
"signup.terms": "<link>이용약관</link>에 동의합니다"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })
SEO
- 로케일당 하나의 인덱싱 가능한 URL, 동일한 URL 뒤에 기계 번역된 콘텐츠를 제공하지 마십시오.
- 자기 참조
hreflang과x-default, 절대 URL, 모든 로케일을 포함하십시오. <title>, 메타 설명, OG 태그 및 구조화된 데이터를 현지화하십시오.- 현지화된 페이지에서
hreflang과noindex를 결합하지 마십시오.
자세한 내용은 references/routing-seo.md를 참조하십시오.
캐싱
- 경로의 로케일은 CDN 캐싱을 간단하게 만듭니다. 헤더로 협상하는 경우
Vary: Accept-Language를 보내고 캐시 조각화를 예상하십시오. - 개인화된 언어 기본 설정을 공유 CDN 응답에 캐시하지 마십시오.
Content-Language를 렌더링된 로케일과 일관되게 유지하십시오.
검토 체크리스트
i18n이 완료되었다고 선언하기 전에:
- 컴포넌트, 이메일 또는 오류에 하드코딩된 사용자 대상 문자열이 없음
- 문장 연결 없음, 모든 메시지는 완전한 번역 가능 단위
- 키는 의미 있고 안정적이며 키로 영어 텍스트 없음
- 모든 플레이스홀더가 모든 로케일에 존재하고 ICU 구문이 컴파일됨
- 복수형은
Intl.PluralRules/ICU를 사용하고count === 1을 사용하지 않음 - 성별/일치는
select를 통해 처리되고"/a"접미사를 사용하지 않음 - 날짜, 숫자, 통화, 상대 시간은 명시적인 로케일/시간대와 함께
Intl사용 -
<html lang>및dir이 첫 페인트(SSR)에서 정확하고 효과 내에서 설정되지 않음 - 로케일 전환기는 경로/쿼리를 유지하고 딥 링크와 함께 작동
- 알 수 없는 로케일은 자동 폴백 대신 404 반환
- 레이아웃이 +35% 확장 및 RTL에서 정상 작동
-
hreflang, 표준, 사이트맵 및 현지화된 메타데이터가 정확함 - 카탈로그 유효성 검사가 CI에서 실행됨
- 폴백 동작이 의도적이고 관찰 가능함
안티 패턴
t('hello') + ', ' + name— 분할된 문장key={englishText}— 키로 번역된 텍스트count === 1 ? 'item' : 'items'— 영어 전용 복수형 로직- 로케일 또는 시간대 없는
new Date().toLocaleDateString() - 금액에
value.toFixed(2)사용 - 문서 수준의
dir대신 컴포넌트 전체에 흩어진locale === 'ar' && { direction: 'rtl' } useEffect내부에서<html lang>설정 — 깜빡임을 유발하고 크롤러를 방해함- 모든 로케일을 초기 번들에 로드
- 재정의할 방법 없이 IP로 자동 리디렉션
- 고유 명사, 제품 이름, 코드 식별자 또는 URL 번역
- 문자열을 기계 번역하고 검토 없이 제공
- 하나의 카탈로그 파일이 수백 개의 키 이상으로 확장될 것이라고 가정