59.1 번역 문구는 어디에 어떻게 담아요?

문구를 코드에서 빼내기로 했으면, 이제 어디에 담을지를 정해야 해요. 정답은 언어마다 하나씩번역 사전이에요. 보통 언어 코드로 이름 붙인 JSON 파일을 써요.


상황: 언어별로 같은 열쇠에 다른 문장을 담아요.
// ko.json
{ "login": "로그인", "logout": "로그아웃", "greeting": "환영합니다" }
// en.json
{ "login": "Log in", "logout": "Log out", "greeting": "Welcome" }


핵심은 양쪽이 똑같은 열쇠(key)를 쓴다는 거예요. 코드는 login이라는 열쇠만 알면 되고, 실제로 "로그인"을 꺼낼지 "Log in"을 꺼낼지는 지금 언어가 뭐냐에 따라 사전이 알아서 정해줘요. 언어를 늘리는 건 ja.json 파일 하나 더 만드는 일이 되죠. 코드는 손도 안 대요.

59.2 키 이름은 어떻게 지어요?

여기서 초보들이 꼭 하는 실수가 있어요. 영어 문장 자체를 열쇠로 쓰는 거예요. 예를 들어 t("Log in")처럼요. 편해 보이지만 함정이에요. 영어 문구가 "Log in"에서 "Sign in"으로 바뀌는 순간, 열쇠가 바뀌어서 모든 언어의 번역이 통째로 미아가 돼요.


그래서 열쇠는 뜻을 담은 짧은 이름으로 지어요. 그리고 화면이 커지면 영역별로 묶어서 관리해요. 이걸 네임스페이스(namespace, 이름 충돌을 막는 구역 나누기)라고 해요.


상황: 열쇠를 영역별로 계층화해요.
{
"auth": { "login": "로그인", "signup": "회원가입" },
"cart": { "empty": "장바구니가 비었어요", "checkout": "결제하기" }
}
// 꺼낼 때: t("auth.login"), t("cart.checkout")


auth.login처럼 점으로 길을 찍으면, 같은 login이라는 이름이 auth에도 있고 다른 곳에도 있어도 안 부딪혀요. 번역가에게 파일을 넘길 때도 어느 화면 문구인지 구역으로 딱 보여서 오역이 줄어요.

59.3 이름이나 개수를 문장에 어떻게 끼워 넣어요?

문제는 문장 안에 값이 끼는 경우예요. "홍길동님, 안녕하세요"에서 이름은 사람마다 다르잖아요. 이때 초보들은 문장을 토막 내서 이어붙여요. t("hi") + name + t("comma")처럼요. 이건 재앙이에요. 언어마다 어순이 다르거든요. 영어는 이름이 앞에 오고 한국어는 뒤에 와요. 토막을 이어붙이면 그 순서를 못 바꿔요.


정답은 보간(interpolation, 문장에 자리표시를 두고 값을 채워 넣기)이에요. 문장은 통째로 두고, 값이 들어갈 자리에 {name} 같은 표시만 남겨요.


상황: 문장은 온전히 두고 자리표시에 값을 채워요.
const dict = {
ko: { greet: "{name}님, 안녕하세요" },
en: { greet: "Hello, {name}" }
};
function t(key, vars = {}, lang) {
let text = dict[lang][key];
for (const k in vars) text = text.replace("{" + k + "}", vars[k]);
return text;
}
t("greet", { name: "지민" }, "ko"); // "지민님, 안녕하세요"
t("greet", { name: "지민" }, "en"); // "Hello, 지민"


보세요. 문장이 통째로 있으니 번역가가 {name}의 위치를 언어에 맞게 옮길 수 있어요. 한국어는 뒤, 영어는 앞에 두면 되죠. 문장을 절대 토막 내지 마세요. 그게 번역 관리의 제1 계명이에요.

59.4 "1개"랑 "3개"는 왜 따로 처리해야 해요?

다음 골칫거리는 개수예요. 영어로 "1 item", "3 items"를 보세요. 하나면 item, 둘 이상이면 s가 붙어 items가 돼요. 이걸 if문으로 "1이면 단수, 아니면 복수"라고 짜고 싶겠지만, 그렇게 하면 다른 언어에서 다 틀려요.


언어마다 복수 규칙이 완전히 달라서, 각 언어가 수를 어떤 범주(one, few, many, other 같은 분류)로 보는지 알아야 해요. 이걸 알려주는 게 Intl.PluralRules예요. 영어로 직접 돌려봤어요.


상황: 영어에서 각 숫자가 어떤 복수 범주인지 물어봐요.
const pr = new Intl.PluralRules("en");
[0, 1, 2, 5].map(n => n + ":" + pr.select(n));


결과:
["0:other", "1:one", "2:other", "5:other"]


영어는 1만 one이고 나머진 전부 other예요. 그래서 사전에 범주별 문장을 두고, PluralRules가 골라준 범주로 문장을 꺼내면 돼요.


상황: 범주별 문장을 두고 골라 써요.
const msg = { one: "{n} item", other: "{n} items" };
const cat = new Intl.PluralRules("en").select(3); // "other"
msg[cat].replace("{n}", 3); // "3 items"

59.5 한국어는 복수형이 없는데 왜 신경 써요?

"한국어는 1개나 3개나 다 개인데, 이게 무슨 상관이에요?" 맞아요. 그래서 더 조심해야 해요. 여러 언어에 걸쳐 PluralRules를 돌려봤어요.


상황: 언어별로 숫자 1, 2, 5의 복수 범주를 비교해요.
for (const loc of ["ko", "en", "ru", "ar"]) {
const pr = new Intl.PluralRules(loc);
console.log(loc, [1, 2, 5].map(n => pr.select(n)));
}


결과:
ko [ "other", "other", "other" ]
en [ "one", "other", "other" ]
ru [ "one", "few", "many" ]
ar [ "one", "two", "few" ]


보세요. 한국어는 죄다 other 하나예요. 문장 한 개만 두면 되죠. 그런데 영어는 , 러시아어는 숫자에 따라 여러 범주, 아랍어는 심지어 two라는 "둘 전용" 범주까지 있어요. 한국어만 생각하고 "단수/복수 두 개면 되겠지" 하고 구조를 짜면, 러시아어를 넣는 순간 다 부서져요. 범주는 언어가 정한다는 걸 처음부터 인정하고, 사전을 범주별로 담을 수 있게 열어둬야 나중에 안 뜯어고쳐요.

59.6 번역이 없으면 화면이 깨지지 않게 하려면요?

실무에선 번역이 빠지는 일이 반드시 생겨요. 새 기능을 한국어로 먼저 내보내면, 영어 번역이 며칠 늦어요. 이때 en.json에 열쇠가 없으면 화면에 undefined가 뜨거나 앱이 터져요. 그래서 안전망이 필요해요.


상황: 번역이 없으면 기본 언어로, 그것도 없으면 열쇠라도 보여줘요.
function t(key, lang) {
return dict[lang]?.[key] // 원하는 언어
?? dict["en"]?.[key] // 없으면 기본 언어
?? key; // 그것도 없으면 열쇠 그대로
}


세 단 후퇴가 핵심이에요. 번역이 늦어도 사용자는 최소한 영어 문장은 보고, 최악의 경우에도 auth.login 같은 열쇠라도 떠서 화면이 안 깨져요. 여기에 개발 중에는 빠진 열쇠를 콘솔에 경고로 찍어두면, 번역 누락을 배포 전에 잡을 수 있어요. 조용히 넘어가면 나중에 사용자가 먼저 발견하거든요.

59.7 오늘 내용을 정리해볼게요

번역 문구를 관리하는 방법을 훑었어요.


문구는 언어별 사전같은 열쇠로 담고, 열쇠는 영어 문장이 아니라 뜻을 담은 이름으로 지어 네임스페이스로 묶어요. 문장에 값이 낄 땐 토막 내지 말고 보간으로 {name} 자리에 채워, 어순이 다른 언어도 자연스럽게 나오게 하세요.


개수는 언어마다 복수 범주가 달라서 Intl.PluralRules가 골라준 범주로 문장을 꺼내야 해요. 한국어는 하나지만 러시아어나 아랍어는 여럿이라, 처음부터 범주별로 담을 수 있게 열어두세요. 마지막으로 번역이 빠져도 기본 언어, 열쇠 순으로 후퇴시켜 화면이 절대 깨지지 않게 안전망을 깔아두면 든든해요.