
Rails에는 국제화 프레임워크가 기본으로 포함되어 있습니다. 시작하기 위해 별다른 준비가 필요하지 않습니다. I18n API는 스택의 일부이며, 정적 문자열 번역, 날짜 및 숫자 서식 지정, 로케일 간 복수형 처리 등 기본적인 기능을 훌륭하게 처리해 줍니다.
하지만 ‘내장’이라고 해서 ‘완벽’하다는 뜻은 아닙니다. 로컬라이제이션된 URL, 자바스크립트 번역, 다국어 SEO와 같은 요구 사항이 생기면, Rails가 기본적으로 제공하는 범위를 벗어나 작업하게 됩니다. 이러한 공백을 메우려면 신중한 아키텍처 설계가 필요합니다.
두 가지 측면, 즉 Rails I18n API가 처리하는 부분과 직접 구현하거나 위임해야 하는 부분을 함께 살펴보겠습니다.
I18n API는 문자열을 번역하는 I18n.t 메서드와 날짜 및 숫자를 지역화하는 I18n.l 메서드라는 두 가지 핵심 메서드를 제공합니다. 두 메서드 모두 config/locales/ 디렉터리에 있는 YAML 또는 Ruby 파일에서 데이터를 읽어오며, Rails는 시작 시 이를 자동으로 불러옵니다.
기본적으로 정적 UI 문자열, 날짜 및 숫자 서식, ActiveRecord 유효성 검사 메시지가 포함됩니다. 대부분의 단일 언어 프로젝트의 경우, 이 정도면 출시하기에 충분합니다.
조금만 더 들어가면 금방 빈틈이 드러납니다.
Rails I18n은 URL 구조에 대해 별도의 처리를 제공하지 않습니다. 즉, 현지화된 라우트, 서브도메인 감지, 경로 접두사 등의 기능이 없습니다. 또한 제품명이나 블로그 게시물처럼 데이터베이스에 저장된 콘텐츠를 번역하는 메커니즘도 없습니다. 자바스크립트 파일은 파이프라인 외부에 완전히 분리되어 있습니다. 그리고 hreflang 태그, 번역된 슬러그, 언어별 사이트맵과 같은 다국어 SEO 관련 작업은 프레임워크가 관여하지 않는 별도의 작업이 필요합니다.
건축을 시작하기 전에 경계가 어디에 있는지 파악해야 합니다.
rails-i18n 젬으로 시작해 보세요.
Rails는 영어 로케일 데이터만 제공하므로, 이 데이터가 없으면 비영어권 로케일에서 I18n.l(Date.today)가 오류가 발생하며, Active Record 유효성 검사 메시지와 같은 프레임워크 수준의 문자열은 현재 로케일과 관계없이 영어로 유지됩니다.
# Gemfile
gem "rails-i18n"그런 다음 애플리케이션 기본 설정을 구성하세요:
# config/application.rb
config.i18n.default_locale = :en
config.i18n.사용가능한_로케일 = [:en, :fr]
config.i18n.enforce_available_locales = true
enforce_available_locales = true로 설정하면 앱이 해당 목록에 없는 로케일을 설정하려고 할 때 오류가 발생합니다. 이 설정이 없으면, 프로덕션 환경에서 오타나 형식이 잘못된 URL 매개변수로 인해 지원되지 않는 로케일이 설정되어도 아무런 경고 없이 처리됩니다.
최신 버전의 Rails는 한 단계 깊이의 디렉토리만 스캔하기 때문에, 깊게 중첩된 폴더(예: config/locales/views/products/)는 종종 누락됩니다. 다음 줄을 추가하면 앱이 확장됨에 따라 모든 하위 디렉토리가 포함되도록 보장할 수 있습니다:
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]이를 통해 번역 파일을 도메인별로 분리할 수 있습니다:
config/locales/
en/
models.yml
views.yml
mailers.yml
fr/
models.yml
views.yml
mailers.yml⚠️ YAML은 true나 false와 같은 값을 부울 값으로 해석합니다. 이를 리터럴 텍스트로 사용하려면 반드시 따옴표로 묶어야 합니다. 최신 버전의 Ruby/YAML에서는 yes와 no를 문자열로 처리하지만, 호환성을 위해 따옴표로 묶는 것은 여전히 안전한 습관입니다:
ko:
답변:
'예': "예"
부정: "아니요"이는 번역 결과가 “yes” 대신 “true”로 반환되어 뷰에 아무것도 표시되지 않거나, 더 나쁜 경우 “true”라는 문자열이 그대로 표시되는 경우에만 발생하는 유형의 버그를 포착합니다.
뷰에서 t와 l은 여러분이 끊임없이 사용하게 될 두 가지 헬퍼입니다.
l은 현재 로케일에 따라 날짜, 시간 및 숫자를 현지화합니다. rails-i18n이 설치되어 있으면 100개 이상의 로케일에 대한 형식 정의가 포함되어 제공됩니다. 이 모듈이 설치되어 있지 않은 상태에서 영어가 아닌 로케일에서 l(Date.today)를 호출하면, Rails에 필요한 현지화된 형식 패턴이 부족하기 때문에 대개 '번역이 없습니다'라는 문자열이나 형식이 지정되지 않은 날짜가 반환됩니다.
<%= l(Date.today) %>
<%= l(Date.today, format: :long) %>t는 키를 기준으로 문자열을 변환합니다. 완전한 형식은 t("products.index.title")이지만, 뷰에서는 지연 조회 약어를 사용할 수 있습니다:
<%= t(".title") %>앞에 붙은 점은 Rails가 현재 뷰 경로를 기준으로 키를 해석하도록 지시합니다. app/views/products/index.html.erb 파일에서 t(".title")은 자동으로 t("products.index.title")로 확장됩니다. 이를 통해 별도의 노력 없이도 키를 간결하게 유지하고 일관된 명명 규칙을 준수할 수 있습니다.
For interpolation, use %{variable} in your YAML and pass the value as a keyword argument:
en:
welcome: "Hello, %{name}"<%= t(".welcome", name: current_user.name) %>번역문에 신뢰할 수 있는 HTML이 포함되어 있다면, 키 이름 뒤에 _html을 추가하세요. 그러면 Rails가 html_safe 호출 없이도 자동으로 해당 키를 안전한 것으로 표시합니다.
en:
notice_html: "Please <strong>confirm</strong> your email."지연 조회(Lazy lookup)는 Rails가 뷰 경로를 추론할 수 있는 경우에만 작동하므로, 백그라운드 작업이나 API 컨트롤러에서는 작동하지 않습니다. 이러한 환경에서는 매번 전체 키를 사용해야 합니다.
로케일 감지 코드를 작성하기 전에 먼저 전략을 정하세요. 크게 다섯 가지 접근 방식이 있으며, 어떤 방식을 선택할지는 앱의 구조와 SEO 요구 사항에 따라 달라집니다.
/fr/products와 같은 경로 접두사는 대외용 Rails 애플리케이션에서 가장 흔히 사용되는 방식입니다. 이 방식은 모든 URL에 언어 설정을 명시적으로 포함하므로, 검색 엔진이 언어별로 독립적으로 색인을 생성할 수 있습니다.
서브도메인과 최상위 도메인(TLD) 전략은 지역적 정체성을 더욱 확고히 하고자 할 때 효과적이지만, DNS 설정 및 SSL 인증서 관리에 따른 부담이 따릅니다.
/products?locale=fr와 같은 URL 매개변수는 SEO가 중요하지 않은 내부 도구에서는 문제없습니다.
데이터베이스에 저장된 기본 설정은 URL이 아닌 사용자의 로케일을 따르는 인증된 앱에서 작동합니다.
어떤 전략을 선택하든, 미묘한 운영 환경 버그를 유발하는 한 가지 구현상의 실수가 있습니다. 바로 I18n.locale =을 직접 사용하는 것입니다.
# Don't do this
before_action { I18n.locale = params[:locale] }I18n.locale = Thread.current에 기록합니다.
Puma 웹 서버를 사용하는 경우, 요청 간에 스레드를 재사용합니다. 요청에서 로케일을 명시적으로 설정하지 않으면, 이전 요청에서 남겨진 로케일을 그대로 상속받습니다.
단일 스레드로 로컬 환경에서 개발할 때는 이런 문제가 전혀 나타나지 않습니다. 하지만 실제 운영 환경에서는 재현하기 어렵고 원인을 추적하기 더욱 힘든 간헐적인 언어 오류 응답을 유발합니다.
대신 around_action 내부에서 I18n.with_locale을 사용하세요:
around_action :switch_localed
ef switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
endwith_locale는 요청이 어떻게 종료되든 상관없이 블록이 종료된 후 이전 로케일을 복원합니다.
/:locale 아래에 경로를 정의하고 default_url_options를 재정의하여, Rails가 모든 URL 헬퍼 앞에 현재 로케일을 자동으로 추가하도록 하세요:
# config/routes.rb
scope "/:locale" do
resources :products
root "home#index"
end
# app/controllers/application_controller.rb
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale]
# Fallback to default if the param is missing or unsupported
valid_locale = I18n.available_locales.map(&:to_s).include?(locale) ? locale : I18n.default_locale
I18n.with_locale(valid_locale, &action)
end
def default_url_options
{ locale: I18n.locale }
end
default_url_options가 설정되어 있으면, 현재 로케일이 :fr일 때 products_path는 자동으로 /fr/products로 렌더링됩니다.
기본 로케일에서 접두사를 생략하려면, 범위 "(:locale)"을 사용하여 해당 세그먼트를 선택 사항으로 설정하세요. 이렇게 하면 영어의 경우 /products, 프랑스어의 경우 /fr/products가 제공되지만, 모호성이 발생할 수 있습니다. /about과 같은 경로는 라우트 순서에 따라 로케일이 지정되지 않은 경우이거나 about이라는 이름의 컨트롤러일 수 있습니다.
서브도메인 감지를 위해, 어떤 설정도 적용하기 전에 request.subdomains.first에서 값을 읽어와 available_locales와 대조하여 유효성을 확인하십시오:
def 서브도메인에서_로케일_추출
subdomain = request.subdomains.first
return nil if subdomain.blank? || subdomain == "www"
subdomain if I18n.available_locales.map(&:to_s).include?(subdomain)
end
www 확인 과정 때문에 로케일로 인식되지 않습니다. localhost에서는 request.subdomains가 빈 배열을 반환하므로, Pow 서버를 사용하거나 /etc/hosts 파일에 항목을 추가하지 않는 한 개발 환경에서 서브도메인 감지가 실패합니다.
별도로 설정해야 할 사항이 하나 있습니다. ApplicationController에 정의된 default_url_options는 메일러나 백그라운드 작업으로 전달되지 않습니다. 해당 컨텍스트에서는 이를 명시적으로 설정해야 합니다:
Rails.application.routes.default_url_options = { host: "example.com", locale: :en }로컬라이제이션된 경로를 설정하면 URL 구조는 자동으로 구성되지만, hreflang 태그, 번역된 슬러그, 다국어 사이트맵은 이 설정 이후에도 전적으로 수동으로 관리해야 합니다.
Weglot 리버스 프록시 통합 기능은 해당 단계를 자동으로 처리하여, 별도의 설정 없이도 hreflang 태그와 언어별 URL을 생성합니다.
영어의 복수형 만들기는 간단합니다. 단수형 하나, 그 외 모든 경우에 쓰는 형태 하나뿐입니다.
en:
messages:
one: "%{count} message"
other: "%{count} messages"
대부분의 언어는 그렇게 간단하지 않습니다.
예를 들어, 불가리아어는 남성형 단어 중 일부에 대해 일반적인 복수형과 특별한 “수량 복수형”을 구분합니다. 따라서 ‘ден’(날)은 보통 ‘дни’(여러 날)가 되지만, 수를 셀 때는 ‘два дена’(이틀)가 됩니다.
bg:
cities:
one: "%{count} ден"
few: "%{count} дена"
many: "%{count} дни"
rails-i18n이 없으면 Rails는 이러한 규칙을 인식하지 못합니다. 따라서 불가리아어 번역은 오류가 발생하거나, 카운트 값이 있을 때마다 다른 형태로 대체될 것입니다.
이 젬은 100개 이상의 로케일에 대한 복수형 처리 로직을 포함하고 있어, 불가리아어, 러시아어, 아랍어, 폴란드어 및 영어와 다른 복수형 규칙을 따르는 기타 모든 언어에서도 올바르게 처리됩니다.
폴백(fallback)은 별개의 문제이며 기본적으로 비활성화되어 있습니다. 폴백이 없으면 번역 키가 누락된 경우 프로덕션 환경에서 번역되지 않은 문자열이 표시됩니다. 이런 문제는 배포된 후 스크린샷에 그대로 드러나게 됩니다.
config/application.rb 파일에서 대체 경로를 활성화하고 기본 대상 경로를 정의합니다:
config.i18n.fallbacks = [I18n.default_locale]fallbacks = true로 설정하면, 현재 로케일에서 키가 누락된 경우 default_locale로 대체됩니다. 지역별 변형의 경우 명시적인 체인을 정의할 수 있습니다:
config.i18n.fallbacks = { "fr-CA": :fr, "en-GB": :en }이 설정은 미리 해 두세요. 이미 일부 번역이 적용된 상태로 운영 중인 앱에 대체 동작을 나중에 추가하는 것은 처음부터 설정해 두는 것보다 훨씬 어렵습니다.
자바스크립트는 Rails I18n 파이프라인에 접근할 수 없습니다.
번역 파일과 Stimulus 컨트롤러 사이를 연결해 주는 t 헬퍼, YAML 로더 또는 내장 브릿지가 없습니다. 서버 측에서 번역 내용을 명시적으로 전달해야 합니다.
Stimulus에서 가장 간단한 방법은 값 API를 사용하는 것입니다. 컨트롤러에서 변환을 값으로 정의하고, HTML에서 이를 설정한 다음, 자바스크립트에서 읽어오면 됩니다:
# app/views/products/index.html.erb
<div data-controller="notification"
data-notification-message-value="<%= t('.success_message') %>">
이 값은 표준 t 헬퍼를 사용하여 서버 측에서 렌더링되므로, 현재 로케일을 자동으로 반영합니다. 자바스크립트 측에서는 이 값을 정적 값으로 선언하고 values API를 통해 읽어오십시오:
// app/javascript/controllers/notification_controller.js
export default class extends Controller {
static values = { message: String }
show() {
alert(this.messageValue)
}
}
각 번역은 명확하게 정의되며, 이를 필요로 하는 컨트롤러의 범위 내에서만 적용됩니다. 전역 범위로 유출되는 것은 없습니다.
컨트롤러에서 여러 개의 번역이 한 번에 필요한 경우, 각 문자열에 대해 개별 값 정의를 추가하는 것보다 이를 JSON 데이터 속성으로 묶어 처리하는 것이 더 깔끔합니다:
<div data-controller="cart"
data-cart-i18n-value="<%= { add: t('.add'), remove: t('.remove') }.to_json %>">
자바스크립트에서는 i18n을 객체 값으로 선언하고 개별 키에 직접 접근합니다:
static values = { i18n: Object }
add() {
console.log(this.i18nValue.add)
}
자바스크립트가 여러 컨트롤러에 걸쳐 광범위한 번역 정보에 접근해야 하는 앱의 경우, i18n-js 젬은 YAML 파일을 JavaScript 객체로 변환하여 I18n.t("key") 형식으로 쿼리할 수 있게 해줍니다.
작동은 하지만 빌드 단계가 하나 추가되고 전체 번역 세트가 클라이언트로 전송됩니다. 이 방법은 처음 두 가지 방식이 반복적으로 사용될 때만 활용하고, 기본 설정으로 삼지는 마십시오.
Turbo Frames를 사용할 때, Turbo Frame의 src URL에서 로케일 접두사가 생략되면, 해당 요청은 "/:locale" 패턴과 일치하지 않아 일반적으로 라우팅 오류(404)가 발생합니다. 프레임 소스에는 항상 로케일을 인식하는 경로 헬퍼를 사용하십시오.
이는 Turbo의 버그가 아닙니다. 오히려 라우팅 설정상의 실수이며, 프레임 내의 모든 URL이 로케일 인식 경로 헬퍼를 사용하는 한, 로케일 감지 섹션의 `around_action`이 이를 방지합니다.
Weglot 리버스 프록시는 완전히 다른 방식을 취합니다. 페이지가 로드된 후 렌더링된 DOM을 변환하므로, Stimulus로 렌더링된 콘텐츠와 동적으로 삽입된 문자열도 별도의 설정 없이도 처리됩니다.
다음 세 가지 범주는 Rails I18n이 처리하는 범위를 완전히 벗어납니다:
Weglot Weglot은 코드 레이어가 아닌 출력 레이어에서 이러한 모든 문제를 해결합니다:
그렇긴 하지만, 이를 평가하기 전에 알아두어야 할 몇 가지 제한 사항이 있습니다:
모든 Rails i18n 구현은 대략적으로 동일한 순서로 이루어지는 동일한 결정들에 귀결됩니다.
먼저, 지역 감지 코드를 작성하기 전에 URL 전략을 정하십시오. 대부분의 대외용 앱에서는 경로 세그먼트 방식이 효과적입니다. 지역적 정체성이 중요한 경우에는 서브도메인을 사용하는 것이 합리적입니다. 데이터베이스에 저장된 설정은 URL이 아닌 사용자에게 따라 지역 설정이 적용되는 인증 기반 앱에 적합합니다.
둘째, 자바스크립트가 번역 내용을 어떻게 가져올지 결정합니다. Stimulus 값 API가 대부분의 경우를 처리합니다. 컨트롤러에서 여러 문자열을 한 번에 필요로 할 때는 JSON 속성을 사용할 수 있습니다.
셋째, 수동으로 구현할 부분을 결정하세요. Rails는 정적 문자열을 잘 처리합니다. SEO 레이어, 번역 워크플로, 데이터베이스 콘텐츠는 모두 별도로 결정해야 할 사항입니다. 이 모든 것을 직접 구현할 수도 있고, 출력 레이어는 Weglot 같은 도구에 맡기고 개발 시간은 애플리케이션 자체에 집중할 Weglot .
출력 레이어를 아예 건너뛰세요. Weglot 14일 동안 Weglot 체험해 보시고, i18n 코드를 건드리지 않고도 다국어 SEO/GEO, 번역된 URL, 자동 콘텐츠 감지 기능을 이용해 보세요.
Weglot 힘을 이해하는 가장 좋은 방법은 직접 확인해 보는 Weglot . 무료로, 아무런 의무 없이 테스트해 보세요.
아직 웹사이트를 연결할 준비가 되지 않았다면 대시보드에서 데모 웹사이트를 이용할 수 있습니다.