Rails의 i18n 라이브러리는 겉보기보다 훨씬 강력한 도구입니다. 단순히 다국어 번역용으로만 쓰라고 만들어진 것이 아닙니다. 화면에 보여지는 텍스트와 그 텍스트를 출력하는 위치를 분리하고 싶은 모든 상황에서 i18n 라이브러리는 유용하게 활용될 수 있습니다.
저는 Avvo에서 근무하면서 i18n을 활용해 정말 흥미로운 작업들을 해왔습니다. 이번 글에서는 실무에서 큰 도움이 되었던 몇 가지 노하우와, 다소 무모해 보이지만 결과적으로 놀라운 성과를 거둔 라이브러리 '남용' 사례까지 함께 소개하려고 합니다.
자동 HTML 이스케이핑
혹시 뷰 파일에 이런 코드가 있으신가요?
<%= raw(t('form.required_field_header')) %>
사실 이 raw는 필요 없습니다. 번역 키 이름 끝에 _html을 붙이기만 하면 자동으로 이스케이프 처리 없이 HTML을 출력해 줍니다.
<%= t('form.required_field_header_html') %>
아주 간단할 뿐 아니라, 다른 t() 호출과의 일관성도 유지할 수 있어 코드가 훨씬 깔끔해집니다.
로케일 키 더 편리하게 참조하기
로케일 파일을 작성하다 보면 여러 키를 하나의 부모 아래 묶어 관리하는 경우가 많습니다.
en:
bugs:
index:
new_label: "File a new bug"
edit_label: "edit"
delete_label: "delete"
이 키들을 함께 참조하려면 전체 키 경로를 매번 입력하는 방법도 있습니다.
<%= t('bugs.index.edit_label') %> | <%= t('bugs.index.delete_label') %>
하지만 이건 상당히 번거롭고 반복적입니다. t()는 scope 옵션을 지원하기 때문에 훨씬 간결하게 참조할 수 있습니다.
<% bugs_scope = 'bugs.index' -%>
<%= t('edit_label', scope: bugs_scope) %> | <%= t('delete_label', scope: bugs_scope) %>
더 나아가, 부분 템플릿(partial)의 이름을 잘 지었다면 scope조차 생략할 수 있습니다.
<%= t('.edit_label') %> | <%= t('.delete_label') %>
즉, 현재 파일이 bugs/index.html.erb이기 때문에 .edit_label이 자동으로 bugs.index.edit_label로 해석되는 것입니다. 뷰 파일 경로 기반의 암묵적 스코프 덕분에 코드가 눈에 띄게 짧아집니다.
ActiveRecord 백엔드 활용하기
YAML 파일에 정적으로 박아 넣은 번역 데이터가 프로젝트에 맞지 않는 경우도 있습니다. 이럴 때 ActiveRecord i18n 백엔드를 사용하면 어떨까요?
require 'i18n/backend/active_record'
I18n.backend = I18n::Backend::ActiveRecord.new
이렇게 설정하면 데이터베이스의 translations 테이블에서 번역을 조회할 수 있습니다. 덕분에 모든 번역을 미리 정의해 둘 필요가 없고, 필요할 때 즉석에서 새로운 번역을 추가할 수 있습니다.
translations 테이블 생성에는 특정 마이그레이션이 필요합니다. i18n-active_record 저장소의 README에 설정에 필요한 모든 내용이 잘 정리되어 있으니 참고하시기 바랍니다.
서로 다른 객체 타입 간 부분 템플릿 공유하기
Avvo는 변호사 디렉터리, 변호사 리뷰, 법률 상담 서비스를 운영하는 회사입니다. 그런데 몇 년 전에는 사이트에 변호사만 있었던 게 아닙니다. 의사와 치과의사도 함께 있었죠!
직업군 간 UI 상당 부분은 동일했습니다. 하지만 차이도 충분히 많았습니다. 예를 들어 변호사에게 'practice area(업무 분야)'라 불리는 것이 의사에게는 'specialty(진료 과목)'인 식이죠. 이런 차이 때문에 별도 처리 없이 같은 뷰나 부분 템플릿을 공유하기란 지저분한 코드를 양산하기 쉬웠습니다.
그러다 우리는 i18n 시스템으로 이 문제를 풀어보기로 했습니다. 영어권 변호사의 언어는 일종의 영어 '방언'이고, 의사의 언어 역시 마찬가지라고 생각했기 때문입니다. 나중에 다른 언어를 지원할 가능성까지 열어두고 싶었기에, 새로운 로케일 두 개를 만들기로 했습니다. 바로 en_jd(변호사 영어)와 en_md(의사 영어)입니다.
커스텀 i18n 백엔드로 구현하는 것은 생각보다 간단했습니다.
class AvvoI18nStore < I18n::Backend::Simple
def translate(locale, key, options = {})
begin
default = options.delete(:default)
super(locale, key, options)
rescue I18n::MissingTranslationData => e
# "en_jd" 등에서 찾지 못하면 "en"으로 폴백(fallback)
fallback_locale = locale.to_s.split("_").first
super(fallback_locale, key, options.merge(:default => default))
end
end
end
I18n.backend = AvvoI18nStore.new
번역 정의 역시 간단했습니다.
en_jd:
practice_area: "practice area"
en_md:
practice_area: "specialty"
심지어 부분 템플릿 통째로도 손쉽게 번역할 수 있었습니다(파일명에 주목하세요).
<!-- 변호사 배지 HTML -->
<!-- 의사 배지 HTML -->
공통으로 쓰이는 번역은 en 로케일로 자동 폴백되기까지 했습니다.
en:
leaderboard:
title: "Leaderboard"
사이트의 'Doctors' 섹션에서는 기본 로케일을 :en_md로 바꾸고, 그 반대의 경우도 마찬가지로 처리했습니다.
I18n.locale = :en_md
그러니까, 모든 게 그냥 잘 동작했습니다!
솔직히 말하면 이 방식을 추천하고 싶지는 않습니다. 직업마다 서로 다른 '언어'를 사용한다고 생각하는 것 자체가 좀 미친 짓이니까요. 하지만 실제로 사용하는 동안에는 놀랄 만큼 훌륭하게 작동했습니다. 그리고 이 사례는 i18n 같은 단순한 도구가 얼마나 큰 잠재력을 지녔는지 잘 보여줍니다.
이런 노하우는 어디서 배울 수 있을까?
지난주 글에서 초보자에서 전문가로 성장하는 방법 중 하나로 특정 기술을 깊이 파고드는 '딥 다이브'를 이야기한 적 있습니다. 이번 i18n 이야기도 좋은 예다. 우리가 i18n에 대해 학습한 내용 대부분은 Rails 공식 가이드와 API 문서를 읽으면서 얻은 것입니다.
그러니 한 걸음 더 나아가 보세요. 여러분이 의존하는 도구를 제대로 익히세요. 생산성을 높여주는 편의 기능과 트릭들을 발견하게 될 뿐만 아니라, 평생 떠올리지 못했을 완전히 새로운 종류의 해결책까지 열어줄 수 있습니다.