Localization 언어 키 텍스트 누락 자동 탐지 파이프라인 구축법

Localization 언어 키 텍스트 누락 자동 탐지 파이프라인 구축법

Unity 빌드 전 원본·번역 테이블·코드 참조를 교차 검증해 Localization 언어 키와 텍스트 누락을 자동 탐지하는 CI 파이프라인을 설계하고 구현하는 방법을 정리합니다.

TL;DR

Localization 텍스트 누락은 런타임에 빈 문장, 키 문자열 노출, 예외로 이어질 수 있지만 빌드 전에 정적으로 찾을 수 있다. 기준 언어 테이블의 키 집합, 각 대상 언어의 번역 값, 프로젝트 코드와 에셋의 키 참조를 교차 검증하고 결과를 CI 실패 조건으로 연결하면 배포 전 누락을 차단할 수 있다.

이 글은 Unity 2022.3 LTS, C# 9, Unity Localization 패키지 1.5 계열을 예시로 든다. 다만 CSV, JSON, Google Sheets, 자체 번역 관리 시스템(TMS)을 사용하는 프로젝트에도 같은 검증 모델을 적용할 수 있다.

Localization 언어 키 텍스트 누락은 왜 발생하는가?

언어 키 누락은 단순히 번역팀이 한 문장을 비워 둔 경우에만 생기지 않는다. 기능 브랜치에서 새 키를 추가한 뒤 번역 요청을 누락하거나 키 이름을 바꾸고 참조를 갱신하지 않거나 주소 지정 에셋과 번역 테이블의 반영 순서가 어긋나도 발생한다.

실무에서는 누락을 아래 세 종류로 나누면 원인과 담당자를 빠르게 구분할 수 있다.

분류검출 조건대표 증상우선 처리 담당
키 정의 누락코드 또는 에셋이 참조한 키가 기준 테이블에 없음키 문자열 노출, 조회 실패클라이언트 개발
대상 언어 항목 누락기준 언어에는 키가 있으나 대상 언어 테이블에 키가 없음Fallback 언어 출력 또는 빈 문장번역·콘텐츠 운영
번역 값 누락대상 언어 키는 존재하지만 값이 공백 또는 허용되지 않은 플레이스홀더빈 UI, 잘린 대화문번역·QA
고립된 키(Orphan Key)테이블에는 있지만 코드와 에셋에서 더 이상 참조하지 않음번역 비용 증가, 정리 대상 누적개발·로컬라이제이션

특히 Fallback Locale을 켜 둔 프로젝트는 대상 언어의 누락이 기준 언어 텍스트로 조용히 대체된다. 게임이 멈추지 않기 때문에 QA에서 발견이 늦어지고 실제 사용자 제보나 스토어 리뷰에서 처음 드러나기 쉽다.

기준 언어 테이블과 대상 언어 테이블을 비교해 누락 키를 표시하는 로컬라이제이션 검증 대시보드

누락 자동 탐지 파이프라인은 어떻게 구성할까?

핵심은 기준 키 집합을 하나로 정하고 세 번 비교하는 것이다. 기준 언어를 en으로 둔다면 K_base는 영어 테이블의 모든 키, K_locale은 대상 언어 테이블의 키, K_ref는 코드와 에셋에서 실제 참조한 키가 된다.

KmissingDefinition=KrefKbaseK_{missingDefinition}=K_{ref}-K_{base} KmissingLocale=KbaseKlocaleK_{missingLocale}=K_{base}-K_{locale} Korphan=KbaseKrefK_{orphan}=K_{base}-K_{ref}

번역 값은 키 존재 여부와 별도로 검사한다. 공백을 제거한 값의 길이가 0이면 빈 번역으로 판단한다. 단, 의도적으로 빈 문장을 허용해야 하는 키는 예외 목록으로 분리해야 한다.

flowchart LR
    A[코드·프리팹·씬 스캔] --> D[키 참조 목록 K_ref]
    B[기준 언어 테이블 en] --> E[기준 키 목록 K_base]
    C[대상 언어 테이블 ko ja fr] --> F[언어별 키·값 목록]
    D --> G[검증 스크립트]
    E --> G
    F --> G
    G --> H{오류 등급 판정}
    H -->|오류| I[CI 실패 및 리포트 생성]
    H -->|경고| J[아티팩트 업로드]
    H -->|통과| K[빌드·배포 진행]

1. 키 식별자 규칙을 먼저 고정한다

자동화는 키가 안정적일 때 신뢰할 수 있다. 문장 자체를 키로 쓰기보다 기능과 화면을 드러내는 식별자를 사용한다.

# 권장
ui.login.start_button
quest.main_001.title
error.network.timeout

# 피해야 할 형태
Start Game
게임 시작
button_2024_new

키 이름 변경은 새 키 추가와 기존 키 제거가 동시에 일어나는 작업이다. 따라서 변경 PR에는 번역 영향 목록을 포함하고 CI에서는 기존 키 제거를 경고로 표시하는 편이 안전하다.

2. 기준 언어와 대상 언어 테이블을 검증한다

예시는 테이블을 UTF-8 JSON으로 내보냈다고 가정한다. 실제 Unity Localization 프로젝트라면 StringTableCollection에서 에디터 스크립트로 같은 구조의 JSON을 생성할 수 있다.

{
  "ui.login.start_button": "Start Game",
  "error.network.timeout": "Connection timed out"
}

아래 Python 3.11 스크립트는 기준 언어와 대상 언어의 키·빈 값을 검사한다. 번역 텍스트로 공백만 들어간 경우도 누락으로 처리한다.

from __future__ import annotations

import json
import sys
from pathlib import Path

BASE = Path("Localization/export/en.json")
LOCALES = ["ko", "ja", "fr"]
ALLOW_EMPTY = {"ui.spacer"}


def load_table(path: Path) -> dict[str, str]:
    with path.open(encoding="utf-8") as file:
        return json.load(file)


base = load_table(BASE)
base_keys = set(base)
failed = False

for locale in LOCALES:
    table = load_table(Path(f"Localization/export/{locale}.json"))
    locale_keys = set(table)
    missing_keys = sorted(base_keys - locale_keys)
    empty_values = sorted(
        key for key in base_keys & locale_keys
        if key not in ALLOW_EMPTY and not str(table[key]).strip()
    )

    if missing_keys:
        failed = True
        print(f"::error title=Missing localization keys::{locale}: {', '.join(missing_keys)}")

    if empty_values:
        failed = True
        print(f"::error title=Empty localization values::{locale}: {', '.join(empty_values)}")

sys.exit(1 if failed else 0)

ALLOW_EMPTY를 코드에 계속 쌓아 두기보다는 localization-exceptions.json처럼 리뷰 가능한 파일로 분리하는 것이 좋다. 예외에는 키, 사유, 만료일을 기록해 영구적인 회피 수단이 되는 것을 막는다.

코드에서 참조한 Localization 키는 어떻게 찾을까?

테이블만 비교하면 코드에 존재하지 않는 키를 넣었을 때는 알 수 있지만 개발자가 존재하지 않는 키를 호출한 경우는 놓친다. 그래서 LocalizedString 사용 위치, 래퍼 함수, UI 설정 에셋을 함께 수집해야 한다.

가장 견고한 방식은 문자열 리터럴을 직접 흩뿌리지 않고 단일 래퍼를 두는 것이다.

public static class Loc
{
    public static LocalizedString Text(string key) =>
        new("UI", key);
}

var title = Loc.Text("ui.login.start_button");

정규식 기반 스캐너는 도입하기 쉽지만 C# 문법과 런타임 조합 키를 완전하게 해석하지 못한다. 규모가 커지면 Roslyn 분석기 또는 소스 생성기를 사용해 Loc.Text("...") 호출만 정확히 수집하는 편이 낫다.

다음은 초기 도입에 적합한 Python 정규식 예시다.

import re
from pathlib import Path

pattern = re.compile(r'Loc\.Text\(\s*"([a-z0-9._-]+)"\s*\)')
references: set[str] = set()

for source in Path("Assets").rglob("*.cs"):
    references.update(pattern.findall(source.read_text(encoding="utf-8")))

print("\n".join(sorted(references)))

동적으로 조합하는 키는 정적 분석의 사각지대다. 예를 들어 quest.main_{id}.title은 런타임 값에 따라 달라진다. 이 경우에는 키 패턴을 명시적으로 등록하거나 콘텐츠 빌드 단계에서 생성된 실제 키 목록을 검증 입력으로 사용해야 한다.

CI에서 Localization 누락을 빌드 실패로 만들려면?

다음 순서로 구현하면 기존 프로젝트에도 부담 없이 붙일 수 있다.

  1. 기준 언어를 지정하고 번역 테이블을 CI가 읽을 수 있는 JSON 또는 CSV로 내보낸다.
  2. 언어별 키 누락과 빈 값을 검사한 뒤 코드·에셋 참조 목록과 기준 키를 비교한다.
  3. 오류 리포트를 CI 아티팩트와 PR 체크에 남기고 배포 브랜치에서는 오류 발생 시 빌드를 실패시킨다.

GitHub Actions를 사용한다면 Unity 빌드 직전에 검증 단계를 둔다.

name: Validate Localization

on:
  pull_request:
  push:
    branches: [main]

jobs:
  localization:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Validate tables
        run: python Tools/validate_localization.py
      - name: Validate code references
        run: python Tools/scan_localization_references.py

권장 정책은 환경별로 다르다. 개발 브랜치에서 모든 대상 언어 누락을 즉시 실패시키면 기능 개발이 막힐 수 있다. 반면 출시 브랜치에서는 모든 출시 대상 언어의 누락을 오류로 승격해야 한다.

검사 항목기능 PR릴리스 브랜치야간 전체 검사
기준 테이블에 없는 참조 키오류오류오류
출시 대상 언어의 키 누락경고 또는 오류오류오류
공백 번역 값경고오류오류
고립된 키(Orphan Key)정보경고경고
미사용 예외 항목경고경고오류

누락 판정의 오탐을 줄이는 운영 규칙

자동화의 신뢰도는 실패 횟수보다 오탐 비율에 좌우된다. 팀이 무시하는 경고가 쌓이면 결국 검사 자체가 형식만 남는다.

  • TODO, TBD, TRANSLATE_ME 같은 플레이스홀더는 빈 값과 동일한 오류로 취급한다.
  • 숫자, 통화, 날짜, 사용자 이름만 출력하는 키는 별도 메타데이터로 표시한다. 번역 누락으로 오판하지 않는다.
  • ICU MessageFormat 또는 Smart String을 쓴다면 {count}, {playerName} 같은 변수 이름의 집합도 기준 언어와 비교한다.
  • 번역 메모리나 TMS 동기화가 비동기라면 동기화 완료 커밋을 기준으로 검증을 실행한다.
  • 고립된 키(Orphan Key)는 즉시 삭제하지 않는다. 최근 릴리스에서 사용됐을 가능성이 있으므로 한두 번의 릴리스 주기를 유예한 뒤 제거한다.

변수 토큰 검증은 단순하지만 효과가 크다. 기준 문장과 번역 문장에서 중괄호 토큰을 추출해 집합 차이를 비교하면 서식 오류를 빠르게 찾을 수 있다.

Tmissing=TbaseTlocalizedT_{missing}=T_{base}-T_{localized}

T_missing이 비어 있지 않으면 번역 문장에 필요한 변수 토큰이 사라진 것이므로 오류로 처리한다.

CI 검사 결과에서 언어별 누락 키와 빈 번역 값, 변수 토큰 불일치를 분류한 예시

자주 묻는 질문 (FAQ)

번역이 아직 시작되지 않은 기능도 CI를 실패시켜야 하나요?

기능 브랜치에서는 경고로 남기고 출시 대상 브랜치와 스토어 제출 빌드에서만 오류로 승격하는 방식이 일반적이다. 단, 기준 테이블에 없는 참조 키는 환경과 무관하게 오류로 처리해야 한다.

Fallback Locale이 있는데도 누락 검사가 필요한가요?

필요하다. Fallback은 게임 중단을 줄이는 안전망일 뿐이며 대상 언어 사용자에게 다른 언어가 노출되는 품질 문제를 숨길 수 있다.

정규식으로 코드 키를 찾는 것으로 충분한가요?

키가 문자열 리터럴로 고정된 작은 프로젝트에서는 충분할 수 있다. 키를 동적으로 만들거나 여러 래퍼를 사용하는 프로젝트라면 Roslyn 분석기, 에디터 메타데이터 수집, 콘텐츠 빌드 산출물 검증을 추가해야 한다.

정리

Localization 누락 자동 탐지는 번역 테이블만 검사하는 일이 아니다. 기준 언어 키, 대상 언어 키와 값, 실제 코드·에셋 참조를 함께 비교해야 배포 전 결함을 차단할 수 있다. 먼저 키·빈 값 검증을 CI에 붙이고 이후 변수 토큰과 고립된 키(Orphan Key) 검증을 더하면 운영 비용을 크게 늘리지 않고 다국어 품질을 안정화할 수 있다.

#Localization#다국어 QA#Unity#CI/CD#자동화 테스트#번역 누락#빌드 검증

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs