다국어 텍스트
03 만드는 법
이 문서 안에서
사람이 읽는 글자는 거의 전부 i18n/common.<언어>.json 세 파일에 들어 있습니다. 평평한 문자열 사전(1386열쇠 × 3언어)과 108줄짜리 런타임이 전부입니다.
한 줄 요약 — 테스트가 막는 것은 값이 아니라 열쇠가 빠지는 것입니다.
1. 표는 하나, 언어는 셋
단일 출처: src/shared/types/i18n.ts의 LOCALES = ['ko', 'en', 'ja'].
| 파일 | 열쇠 | 뜻 |
|---|---|---|
i18n/ | 1386 | 한국어(저작 원본) |
i18n/ | 1386 | 영어 |
i18n/ | 1386 | 일본어 |
i18n/ | 1 | 언어 이름 라벨 |
중첩 없는 평평한 사전. 복수형·성별 변형 없음. 파일명 규칙 i18n/<네임스페이스>.<언어>.json. 현재 네임스페이스는 common 하나. 열쇠는 정렬 안 됨(저작 순서).
2. 열쇠 이름 규칙
점으로 계층을 나눕니다: <도메인>.<항목> 또는 <도메인>.<하위>.<항목>.
| 패턴 | 예 |
|---|---|
| 이름·설명 짝 | item. / item. |
| 미션(예외) | mission. / . |
| 주민(예외) | villager. / villager. |
| 주민 개성 | villager. / villager. / villager. / villager. |
식별자 표기:
| 종류 | 표기 | 예 |
|---|---|---|
| 아이템·전리품 | snake_ | item. |
| 광산·던전 | kebab- | mine. |
| 주민 후보 | <직업>- | villager. |
| 미션 | <주기><지표>_ | mission. |
| 상품 | snake_ | shop. |
| 기금 지표 | camelCase | shop. |
미션 주기 접두어: d_ 일일, w_ 주간, m_ 월간, a_ 업적.
코드가 조립하는 동적 열쇠(전체 검색 불가):
| 패턴 |
|---|
item. / . |
inv. |
intro. |
villager. / . |
villager. · villager. · villager. · villager. |
role. |
mission. / . |
shop. / shop. |
mine. / battle. |
equip. |
effect. |
3. 값에 숫자를 꽂는 문법
{이름} 하나뿐. tf(key, params)가 /\{(\w+)\}/g로 치환합니다. params에 없는 토큰은 그대로 남음. 복수형·순서 재배치 없음. 세 언어의 자리표시자 집합은 127개 열쇠 전부 완전 일치.
주요 토큰: {n}(83) {name}(27) {m}(10) {g}/{s}(각 6) {gold}(5) {lv}(4) {role}/{h}(각 3).
4. 로케일 감지 순서
detectLocale():
<html lang>속성 — 정확히 일치할 때.navigator.languages— 전체 일치, 그다음-앞 기본 언어 일치.- 둘 다 실패 →
en.
game.html에 lang= 없음(테스트가 강제). setLocale()이 <html lang>을 씀 → 전환 후 detectLocale()은 전환값 반환. 세이브에 locale 저장, 신규 세이브 기본값 = detectLocale().
t(key) 폴백: 현재 로케일 → en → 열쇠 문자열 자체. 빈 문자열도 "있음" 취급.
폴백이 다른 표면:
| 표면 | 폴백 |
|---|---|
게임 UI (t/tf) | 로케일 → en → 열쇠 문자열 |
패치 노트 (pickLocalized) | 로케일 → en → ko → ja → "" |
스토어 캡션 (captionFor) | 로케일 → ko → "" |
스토어 캡션 (messageFor) | 로케일 → ko → 예외 |
| 스토어 등록정보 | 폴백 없음 |
5. 테스트가 강제하는 계약
tests/i18n-coverage.test.ts:
| 검사 | 막는 것 |
|---|---|
| 세 표의 열쇠 집합 동일 | 한 언어에만 열쇠 추가 |
| 빈 값 금지 | 빈 문자열이 폴백을 건너뛰는 것 |
| en·ja 표에 한글 금지 | 번역 누락 |
| 인트로가 열쇠를 담음 | 컷씬만 한국어로 남는 것 |
| 모든 인트로 beat 3언어 해석 | 값 없는 열쇠 |
game.에 lang= 없음 | 전 세계를 한 언어에 고정 |
신규 세이브 로케일 = detectLocale() | 감지를 기본값이 덮는 것 |
다른 도메인 테스트 계약:
| 강제하는 것 |
|---|
| 레시피 이름·설명 존재, 필수 UI 열쇠 30종, 자리표시자 보존 |
폐기 직업 role.*·villager.*·equip.* 잔존 금지 |
| 후보 66명 개성 보유, 개성·자기소개·대사 3언어 존재, 자기소개·대사 중복 금지 |
| 폐기 퀘스트 열쇠 잔존 금지 |
| 폐기 접속 알림 4종 잔존 금지 |
모든 상품에 shop./shop. 존재 |
| 스토어 이름 == 게임 내 이름(3언어), ≤30자·≤45자 |
| 광산·후보 이름·안내 열쇠 존재 |
6. 금칙어 필터
src/shared/services/name-filter.ts 554줄. 게임군 전체 공통 복사본, 순수 함수.
스코프 둘: name(이름 입력, PROFANITY+NAME_ONLY) · text(채팅, PROFANITY만).
| 목록 | 수 |
|---|---|
PROFANITY | 108 |
NAME_ | 25 |
SAFE_ | 87 (오탐 방지 어간) |
RESERVED_ | 11 (부분 일치) |
RESERVED_ | 13 (전체 일치만) |
정규화 파이프라인: 가나 합성 → 한글 외 NFKD+결합문자 제거+NFC → 소문자 → 리트 치환 13개 → 허용 문자만 남기되 원문 위치 기록. 반복 축약 2패스(2회·1회). SAFE_WORDS 어간이 히트를 통째로 덮으면 무시.
공개 API:
| 함수 | 동작 |
|---|---|
containsProfanity(raw, scope) | 첫 히트에서 중단 |
maskProfanity(raw, maskChar) | text 스코프, 히트 구간 마스크 |
isReservedName(raw) | EXACT 전체 일치 + ANYWHERE 부분 일치 |
sanitizeVisibleText(raw) | 보이지 않는 문자 제거 → 공백 축약 → 양끝 다듬기 |
checkDisplayName(raw, {min,max}) | 다듬기→길이→금칙어(name)→예약어 |
safeDisplayName(raw, seed, limits) | 통과 시 그대로, 아니면 Player#### |
거절 사유(intro.name.error.${reason}): empty · too-short · too-long · profanity · reserved.
길이 상한:
| 상수 | 값 |
|---|---|
| 인트로 이름 최대 | 8 |
checkDisplayName 기본 max | 16 |
| 서버 이름/채팅/아바타 | 24 / 280 / 32 |
| 피드백 min/max | 3 / 1000 |
7. 용어 원본·흔들림
용어의 3개 국어 정본은 용어집에 있습니다.
8. 톤
| 표면 | 화자 | 한국어 어투 | 예 |
|---|---|---|---|
| 인트로 나레이션 | 주인공 독백 | 해라체·과거형 | 성문은 부서져 있었다. |
| 튜토리얼 | 집사 바르톨 | 하십시오체(폐하) | 준비는 끝났습니다. 폐하. |
| 일반 UI | 시스템 | 합쇼체 -습니다 | 아직 주민이 없습니다. |
| 확인 질문 | 시스템 | - | {name}(으)로 이동할까요? |
| 버튼·라벨 | — | 명사/명령 어간 | 수거·제작·영입 |
| 아이템 설명 | 시스템 | -입니다 | 진한 육수입니다. |
| 주민 소개 | 시스템 | 명사구/-합니다 | 손이 야무진 초보 농부 |
| 주민 자기소개·대사 | 주민 본인 | 1인칭 합쇼체(폐하께) | 미나입니다. 한 줄이라도 제대로 매겠습니다. |
| 상품 설명 | 개발자 해설 | 합쇼체 | 교환비는 낮게 잡았습니다. |
9. 문구 추가·수정·삭제
추가: 세 파일 같은 자리에 열쇠 추가 → 같은 토큰 집합 → t()/tf() 호출 → pnpm test. 수정: 세 파일 함께. 상품 이름은 fastlane/lib/seal_iap.rb도 함께. 삭제: 세 파일 함께 + 잔존 방지 테스트 추가가 관행.
이어서 읽기
- 용어집 — 3개 국어 정본과 흔들림 판단
- UI 선언 시스템 — 자리표시자가 언제 최종 값인지
- 실시간 방과 채팅 — 필터 통과 후 방 전파 경로
- 인장소와 상품 — 상품 이름이 스토어와 묶이는 이유
- 빌드와 테스트 — 계약 테스트가 속한 명령