정의
Locale suffix는 마크다운 파일명 끝에 .en.md, .ja.md처럼 언어 코드를 붙여, slug는 공유하고 locale만 다른 문서 판을 구분하는 저장 방식이다. suffix가 없는 파일은 기본 locale(예: ko)로 취급한다.
같은 my-article slug 아래 my-article.md(ko), my-article.en.md, my-article.ja.md가 공존하면, href·canonical·hreflang이 slug 축을 공유해 다국어 사이트맵을 단순하게 유지할 수 있다.
왜 필요한가
언어별로 content/en/, content/ja/처럼 폴더를 나누면 category·내부 링크·빌드 스크립트가 갈라져 유지 비용이 커진다. suffix 방식은 디렉터리 구조는 그대로 두고 locale만 데이터 축으로 추가한다.
다만 locale을 파일명에만 넣고 DB primary key나 검색 인덱스 id는 type:slug로 남기면, 행 충돌·검색 hydration 0건·MiniSearch duplicate id 예외가 조용히 발생한다. locale은 파일명 → 타입 → SQLite id → search-index id → API 쿼리까지 한 번에 꿰어야 한다.
동작 원리
- 로더: glob 시
.{locale}.md를 파싱해ContentDocument.locale을 채운다. 없으면ko. - SQLite id:
type:slug:locale(예:knowledge:webpack-splitchunks:en). list/detail API는 요청 locale로 WHERE. - 검색 인덱스: MiniSearch document id도 DB와 동일 포맷.
SearchScope.locale로 필터. - 미번역 fallback: detail에서 요청 locale 문서가 없으면
/ko로 redirect(404·빈 fallback render 대신 명시적). - 번역 메타: en/ja 기계번역은 frontmatter
translated: machine. status를 draft로 숨기면 해당 locale 리스트가 비어 보인다. - SEO: sitemap hreflang은 파일이 실제 존재하는 locale만 출력. canonical·
og:locale·JSON-LDinLanguage동기화.
실무 적용
build-content-db.ts,schema.ts,loader.ts,queries.ts,search-index.ts를 한 PR에서 locale 인지형으로 확장한다.- PK/id 포맷 변경 전
grep으로 소비처(검색 hydration, list API, relations)를 전수 확인한다. - detail 4곳에 복붙된 redirect를
getLocalizedDocumentOrRedirect같은 헬퍼 하나로 모은다. - relations·load-more·무한 스크롤 API도 활성 locale을 명시 인자로 받는다.
- 대량 번역 위임 시 "Read 압축 시 본문 재구성 금지,
limit로 페이지네이션 재읽기"를 지시한다.
트레이드오프
| 선택 | 장점 | 단점 |
|---|---|---|
| locale suffix | slug·href 공유, hreflang 단순 | id 포맷 변경 시 전 파이프라인 동시 수정 |
locale subtree (/en/...) | 폴더만 보면 언어 구분 쉬움 | slug·링크·빌드 규칙 이중화 |
| 단일 파일 + frontmatter locale | 파일 수 적음 | 동일 slug 다언어 동시 편집·diff 어려움 |
| 미번역 404 | 엄격 | UX 거침 |
| 미번역 → ko redirect | 읽을 수 있는 fallback | URL이 ko로 바뀜(의도적 선택) |
사용하면 안 되는 경우
- 문서마다 locale이 완전히 다른 slug·구조를 가져야 하는 경우(번역이 아니라 별 콘텐츠).
- id에 locale을 넣기 전에 검색·DB 소비처를 맞출 계획이 없는 경우(부분 적용).
- 기계번역본을 draft로 숨겨 해당 언어 리스트를 비우는 경우(의도치 않은 빈 카탈로그).
흔한 실수
- DB id만
type:slug:locale로 바꾸고 search-index id는type:slug로 남김 → duplicate throw 또는 0건 hydration. - list 페이지만 locale 스코프하고 relations/load-more는 ko 고정.
translated: machine인데 status draft로 숨겨 en/ja 네비가 "비어 있음"처럼 보임.- 서브에이전트가 압축된 Read로 본문을 재구성해 번역 파일이 원문과 어긋남.
관련 개념
- json-ld-structured-data —
inLanguage·canonical 메타 - inverted-index-full-text-search — 검색 document id 정합
- single-source-of-truth-content-metadata — frontmatter SSOT