Steven
Steven6 분 소요

안전망 없이 서른 개 언어를 출시하다

GeekBye v1.8.3은 react-i18next를 배선하고 앱 전체를 30개 언어로 번역했습니다. 라이브러리를 붙이는 일은 판에 박힌 작업이었죠. 흥미로운 부분은 빠져 있던 것입니다: 키 정합성 검사도, 문자열 추출기도 없었기에 — 하드코딩된 영어가 새어 나갔고, 덴마크어 오타가 출시됐으며, 변경 로그는 이 앱이 28개인지 29개인지 30개 언어를 말하는지 자기들끼리도 합의하지 못했습니다.

엔지니어링
i18n
데스크톱
GeekBye 릴리스
안전망 없이 서른 개 언어를 출시하다

어떤 릴리스는 하나의 기능입니다. GeekBye v1.8.3은 그렇게 보입니다 — "이제 앱이 30개 언어를 구사한다" — 하지만 그 기능은 쉬운 부분이었고, 이 글이 다루는 것은 그게 아닙니다. 2026년 2월 17일부터 19일 사이, 두 개의 풀 리퀘스트가 react-i18next를 앱에 배선하고, 하드코딩된 모든 영어 문자열을 번역 호출로 리팩터링하고, 서른 개의 로케일 파일을 묶어 넣었습니다. 진짜 작업이긴 하지만, 라이브러리가 판에 박힌 일로 만들어 주도록 설계된 종류의 작업입니다. 흥미로운 엔지니어링은 이 릴리스가 만들지 않은 모든 것 — 이미 깨진 채로 출시된 것을 잡아냈을 안전망입니다.

배선은 쉬운 80%였다

인프라 커밋은 i18next 위에 react-i18next를 얹고 관습적인 일을 했습니다: initReactI18next를 호출하고, 서른 개의 로케일 JSON을 전부 정적으로 임포트하고, 그것들을 하나의 resources 객체 아래 등록하는 i18n/index.ts. 지연 로딩은 없습니다 — 모든 언어가 번들에 실려 나갑니다. 번역 문자열은 src/i18n/locales/*.json에 평평한, 점으로 네임스페이스가 붙은 맵으로 살아 있습니다: sidebar.meetings, startMeeting.start, 키 하나에 문자열 하나.

하드코딩된 영어에서 t('key')로의 이전은 정직한 방식으로, 컴포넌트 그룹 단위로 진행됐고, 그 훑기의 궤적을 커밋 로그에서 읽어 낼 수 있습니다: 사이드바, 내비게이션, 채팅이 먼저; 그다음 설정, 로그인, 온보딩; 그다음 미팅, 대시보드, 토스트, 내비바 툴팁. 각 커밋은 리터럴 문자열 한 뭉치를 번역 호출로 교체했습니다. 영어가 아닌 파일 스물아홉 개가 단일 커밋으로 착지했습니다 — 영어 원본을 불가리아어에서 베트남어에 이르는 모든 것으로 번역한, 구천 줄짜리 투하였습니다.

그중 어느 것도 제대로 해내기가 어렵지 않은데, 잘못 하면 시끄럽기 때문입니다: 프로바이더가 빠지면 크래시가 나고, 깨진 JSON은 파싱에 실패하며, 잘못된 임포트는 빌드를 부숩니다. 라이브러리는 기계적인 실수에는 안전 난간을 줍니다. 하지만 실제로 일어난 그 실수에 대해서는 난간을 주지 않습니다.

세 개의 언어 설정, 그리고 그것이 옳은 이유

무엇이 깨졌는지로 가기 전에, v1.8.3이 정확히 옳게 해낸 한 가지가 있습니다. 많은 앱이 뭉개 버리지만 그래서는 안 되는 구분이기 때문입니다. 이 릴리스 이후, GeekBye는 서로 독립적인 세 개의 언어 설정을 갖습니다:

  • AI 응답 언어 (output_language) — 어시스턴트가 어떤 언어로 답하는지. 셋 중 가장 오래된 것으로, 2025년 11월부터입니다.
  • 트랜스크립트 언어 (transcriptLanguage) — 음성 엔진이 당신의 마이크를 어떤 언어로 받아쓰는지. 몇 릴리스 전인 v1.7.6에서 배관됐습니다.
  • 앱 언어 (app_language) — 버튼, 메뉴, 온보딩의 언어. v1.8.3에서 새로 생겼습니다.

이것들은 진짜로 서로 다른 축입니다. 베를린의 어떤 개발자는 독일어 인터페이스, 영어 AI 답변(붙여 넣는 문서가 그 언어니까), 그리고 영어 트랜스크립션(미팅이 영어니까)을 원할 수 있습니다. 이것들을 하나의 "언어" 설정으로 뭉개는 것은 그 사람에게는 틀린 일이고, 이 릴리스에서 추가된 온보딩 단계는 정확히 그 이유로 세 가지를 별개의 드롭다운으로 제시합니다.

멋진 손길은 업그레이드 경로입니다. 일회성 마이그레이션이 업데이트 후 첫 실행에서 각 기존 사용자의 output_language를 새 app_language 키로 복사하고, 프로바이더는 폴백 체인을 통해 언어를 해석합니다 — app_language, 그다음 output_language, 그다음 'en'. 그래서 스페인어 AI 답변을 원한다고 이미 앱에게 말해 둔 사용자들은 영어로 리셋되어 설정을 찾아 헤매는 대신, 스페인어 인터페이스로 깨어났습니다. 새 환경설정을 관련된 옛 것으로부터 시드하는 것은 마이그레이션 하나의 비용으로 모든 업그레이드 사용자에게서 자잘한 성가심 하나를 덜어 주는 작은 배려입니다.

다시 그려지려 하지 않던 온보딩

i18n 작업에서 끄집어낼 만한 진짜 버그가 하나 있는데, 런타임 언어 전환을 하는 사람이라면 누구나 걸릴 React 함정이기 때문입니다. 온보딩 마법사는 옵션 목록 일부를 useMemo 안에서 만듭니다. 언어 단계 그 자체에서 언어를 바꾸면, UI는 즉시 새 언어로 다시 그려져야 하는데 — 그러지 않았습니다. t가 메모의 의존성 배열에 없었기 때문입니다. 번역 함수는 언어가 바뀔 때 정체성이 바뀝니다; 메모이즈된 값이 t로부터 만들어지지만 그것에 의존하지 않으면, React는 아무렇지 않게 오래된, 이전 언어 버전을 당신에게 건넵니다. 이 수정은 로그에 두 번 나타납니다 — 한 번은 마법사에, 한 번은 설정 드롭다운 목록에 — 이는 곧, 번역된 콘텐츠를 메모이즈하는 모든 곳에서 항상 t를 나열하는 법을 배우기 전까지 저지르게 되는 종류의 실수라는 뜻입니다.

온보딩에는 변경 로그가 언급하지 않는 주름도 하나 있었습니다: 이것이 화이트라벨 코드베이스이기 때문에, 번역해야 할 온보딩 트리가 있습니다 — GeekBye 마법사 하나와 Pavleur 마법사 하나 — 나란한 onboarding.*onboarding.pavleur.* 키 네임스페이스 아래 살고 있습니다. 모든 온보딩 문자열이 두 번, 두 개의 브랜드 목소리로, 스물아홉 개 언어 전부에 걸쳐 번역됐습니다.

빌드가 빠졌는지도 몰랐던 것

이것을 체크리스트가 아니라 교훈으로 만드는 부분이 여기 있습니다. v1.8.3에는 하드코딩된 문자열을 추출하는 자동 방법도, 로케일 간 키 정합성을 검사하는 CI 검사도 없었습니다. 그래서 "완료"의 정의는 "누군가가 기억하는 컴포넌트를 방문해서 눈에 보이는 문자열을 감쌌다"였습니다. 그 정의는 새어 나갑니다.

릴리스 2주 뒤, _"translate all hardcoded English strings across 10 components into 30 languages"_라는 메시지의 정리 커밋이 착지했습니다. 열 개 컴포넌트가 — 이슈 신고 폼, 요약 및 지표 탭, 리포트 탭, 트랜스크립트와 채팅 콘텐츠, 사용량 한도 프롬프트, 채팅 메시지, 코드 복사 버튼, 플로팅 액션 버튼 — v1.8.3에 실려 나가면서 모두에게, 모든 언어에서 여전히 영어로 말하고 있었습니다. 아무의 빌드도 실패하지 않았습니다. 어떤 테스트도 빨갛게 되지 않았습니다. 독일어 사용자는 그저 리포트 탭에서 영어를 봤고, 유일한 탐지기는 결국 그것을 들여다본 사람 한 명이었습니다.

같은 부재가 더 작은 창피들을 낳았습니다. 어떤 덴마크어 번역이 잘못된 단어와 함께 나갔고 — Luk applikationen helt 대신 Luk application helt — 이후 릴리스에서 패치됐는데, 원어민 리뷰어 관문도 없었고 파이프라인이 알 방법도 없었기 때문입니다. "앱 종료" 키들은 초기 묶음에서 빠져 있다가 며칠 뒤 채워졌습니다. 그리고 그 후에 출시된 모든 기능이 — 수식 키 프리셋, 마이크 음소거, 리캡 칩, 오버레이 불투명도 — 자기 몫의 "새 키를 29개 로케일로 번역" 커밋을 뒤에 영원히 끌고 다녔습니다. 그 불변식을 자동으로 강제하는 것이 아무것도 없었기 때문입니다.

커밋 메시지에서 그 무질서를 측정할 수도 있습니다. 똑같은 서른 개 로케일 파일이, 릴리스마다 "30개 언어", "29개 언어", 그리고 — 한 커밋에서는 — "28개 로케일"로 묘사됩니다. 추가되거나 제거된 것은 없습니다; v1.8.3부터 v2까지 모든 태그에서 파일은 서른 개입니다. 숫자가 흔들리는 것은 "우리는 몇 개 언어를 출시하는가"에 대한 단일 진실 원천이 없었기 때문이고, 그래서 각 커밋 작성자가 손으로 셌으며, 어떨 때는 영어를 하나의 언어로 포함하고 어떨 때는 원본으로 취급했습니다. 자기 개수조차 일정하게 유지하지 못하는 변경 로그는, 그것이 묘사하는 대상이 기계에 의해 강제되고 있지 않다는 작고 정직한 신호입니다.

복수 처리의 지뢰

짚어 둘 만한 것이 하나 더 있는데, 시끄럽게 깨져서가 아니라 이 설정이 불러들이면서 경고해 주지도 않을 종류의 버그이기 때문입니다. 이 로케일 파일들에서 복수 처리는 수동으로, 키 쌍으로 이뤄집니다: meetings.meetingCountmeetings.meetingCountPlural, profiles.filesAttachedfilesAttachedPlural. 이것은 깔끔한 영어 단수/복수 분리입니다. 그리고 이것은 이제 앱이 지원한다고 주장하는 언어 상당수에 대해서는 틀린 것이기도 합니다. 러시아어, 폴란드어, 체코어는 숫자에 따라 복수형이 세 개 혹은 네 개입니다 — "파일 1개", "파일 2개", "파일 5개", "파일 21개"가 서로 다른 어미를 취할 수 있습니다. 두 키짜리 단수/복수 체계는 그것을 표현할 수 없어서, 그 개수들은 문법적으로 어긋나게 렌더링됩니다. i18next는 바로 이런 경우를 위한 진짜 복수 처리 기계를 갖고 있습니다; 수동 쌍은 그것을 우회하며, 어떤 커밋도 그 틈을 인정하지 않습니다. 하드코딩 문자열 누수의 복수 처리판입니다: 빌드에는 보이지 않고, 오직 원어민에게만 보입니다.

이번 릴리스가 우리에게 가르쳐 준 세 가지

  1. 라이브러리는 쉬운 80%; 도구가 어려운 20%다. react-i18next를 배선하고 문자열을 대량 번역하는 것은 풀린 문제입니다. 서른 개 파일을 영원히 정합 상태로 유지하는 것은 아닙니다 — 그리고 그것에는 하드코딩 문자열 추출기와 누락 키를 잡는 CI 검사가 필요한데, 라이브러리는 그 어느 것도 대신 출시해 주지 않습니다. "완전한 i18n"을 주장하기 전에 그것들을 만드세요. 그러지 않으면 당신은 지킬 방법이 없는 불변식을 주장한 셈입니다.
  2. 실제로 다른 언어 설정은 분리하라. UI 언어, 트랜스크립트 언어, AI 응답 언어는 세 개의 축이고, v1.8.3이 각각에 자기 키를 준 것은 옳았습니다 — 그리고 새 것을 관련된 옛 것으로부터 마이그레이션으로 시드해서 업그레이드가 사용자에게 아무 비용도 들지 않게 한 것은 더욱 옳았습니다.
  3. 변경 로그의 개수는 규율 지표다. 똑같은 파일 세트가 세 개의 커밋에 걸쳐 28개, 29개, 30개 언어일 때, 문제는 숫자가 아닙니다 — 빠진 진실 원천이 문제입니다. 기계가 당신의 언어를 세고 있지 않다면, 아무도 두 번 같은 방식으로 세지 않습니다.

v1 이야기의 이전 장은 대시보드를 하나의 웨이브 차트를 중심으로 다시 설계하다(v1.8.2)를 보시고; 전체 궤적은 소프트웨어를 완벽하게 출시하는 것의 해부학을 보세요.