angular-translate (i18n)

여기는 안 읽으셔도 됩니다

현재도 영어와 필리핀어(따갈로그어)로 서비스를 하고 있었지만, 핵심 기능에서는 실질적으로 영어만 써왔다. 그러던 중에 점차 한국어의 지원 필요할 것 같다는 문의들과 앞으로 런칭할 베트남/인도네시아 쪽의 지원도 필요하게 된 상황이라 다국어가 빠르게 지원되면 될 수록 좋을 것 같다고 판단, 작업을 시작했다.

반응형 웹을 위해 앵귤러를 레일즈에 섞어쓰고 있었는데 앵귤러 개발자인 나는 다국어를 앵귤러에서 해야한다고 주장했고, 그 이유는 크게 두 가지다. 장기적으로는 프론트가 퓨어 앵귤러로 돌아갔으면 좋겠다. 그리고 앱을 아이오닉으로 짰으니 레일즈에서 다국어 지원을 해봐야 아이오닉에서 다국어 지원하려면 어차피 다시 앵귤러단에서 만져야하지 않겠냐. 이럴거면 앵귤러로 진행해서 하나의 번역모듈을 두 곳에 적용하는 게 더 낫지 않는가.

 


그렇게 찾아보던 중에 angular-translate를 발견했다.

conceptual_overview
그림의 출처. 번역 정보가 있으면 뷰에 관련된 directive, filter에서는 물론이고 비지니스 로직이 돌아가는 service로도 이용이 가능하다는 점, 그리고 de facto에 가까운 지지율과 완성도를 보여준다는 점에서 선택했다. 웹 쪽에 작업이 끝나고 앱을 알아보니 내가 사용했던 yeoman generator에서는 이미 기본으로 들어가있던 라이브러리여서 한 번 더 소름이었다는 건 함정.

기본 틀은 다음과 같다.

$translateProvider에서 번역 정보를 추가, fallback 언어를 설정, 선호언어를 설정하고 앵귤러를 로딩한다. 그 뒤에 앵귤러가 돌아가면서 directive, filter, service에서는 현재 사용 중인 언어를 기준으로 문자열을 출력하는 방식이며, 중간에 $translate.use('ko'); 와 같이 서비스에서 언어를 바꿔주면 모든 출력이 그 언어로 바뀌게 되는 방식이다. 물론, 앵귤러니까 당연히 페이지 로딩 없이 전환된다.

간단한 예제로 들어가기 전에 한 가지 더 장점을 뽑자면, 번역 정보를 위에서 기술한 바와 같이 $translateProvider 에서 하는 것 이외에도 비동기적으로 (그림에서 보이는 파란색 박스) 불러올 수도 있다. 서비스의 성장속도가 워낙 빨라서 rsync와 같은 녀석으로 번역 모듈을 싱크해주는 방법을 임시로 쓸 것 같은데 이후에는 서버에서 번역정보를 받아와서 하나의 번역 정보 관리로 웹과 앱을 동시에 관리하는 것도 ‘가능’해 보인다는 것 또한 매력적인 기능이다. 물론 서비스 자체가 컨텐츠의 양이 무척 적어서 굳이 이렇게까지 해야하나는 것은 또 다른 문제인건 함정.

 


서버에서도 의외로 사용자의 언어를 알고 있어야 하는 부분이 있었다. 이메일을 보낼 때 어떤 언어로 보내야할 지 결정해야하는 것이 바로 그것인데, 레일즈단에서는 이 기능을 위한 용어로 lang 대신에 locale을 쓰고 있었다. 따라서 앞으로 나올 코드의 변수에서 locale이 자주 보일텐데 참고하시길.


'use strict';

angular.module('myTranslate', ['pascalprecht.translate'])
.config(function($translateProvider) {
  var localeList = ['en', 'ph'];
  var locale = window.navigator.languages ? window.navigator.languages[0] : null;
  locale = locale || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage;
  if (s.contains(locale, '-')) {
     locale = locale.split('-')[0];
  }

  if (s.contains(locale, '_')) {
    locale = locale.split('_')[0];
  }

  var isLocaleNull = !locale;
  var notDefinedLocale = locale && (_.indexOf(localeList, locale.toLowerCase()) === -1);
  if (isLocaleNull || notDefinedLocale) {
    locale = 'en';
  }
  else {
    locale = locale.toLowerCase();
  }

  console.log('locale chosen in translate.js', locale);

  $translateProvider
  .preferredLanguage(locale)
  .usePostCompiling(true)
  .useSanitizeValueStrategy('escapeParameters');
});

위와 같은 코드로 맨 처음 번역 모듈을 초기화한다.

코드가 굉장히 지저분하다. ㅇㅈ? ㅇ~ㅇㅈ! 물론 angular-translate 에는 determinePreferredLanguage() 라는 함수로 preferredLanguage를 자동으로 설정할 수 있다. 하지만, 코드의 주석을 살펴보면 모든 브라우저에서 항상 적당한 언어를 찾아내는 것은 아니며, null을 설정할 수도 있다… 는 것을 보고, 물론 fallback 언어를 설정할 수 있긴 하지만, 다른곳에서 null 예외처리 해주느니 그냥 null이 아니더라도 기본 값이 들어갔으면 하는 마음에 이렇게 넣었다.

물론, 라이브러리가 워낙 괜찮아서 fallback  언어로 믿고 그냥 써도 될 것 같다. 위 코드의 console.log 위쪽의 코드가 브라우저 정보로 로케일을 추측해내는 부분이니 이렇게 까지 해야해? 라는 분들은 지워버리고 라이브러리의 메소드를 사용하길.

 


이후에는 다음과 같이 각각 언어파일 en.js, ko.js를 생성하고 번역 정보를 채워넣는다.

..
$translateProvider
.translations('en', {
  NAV: {
    HOME: 'Home',
    LOG_IN: 'Log in'
  }
});
..
..
$translateProvider
.translations('ko', {
  NAV: {
    HOME: '홈',
    LOG_IN: '로그인'
  }
});

 


그 후에는 다음과 같이 적용한다.

<body ng-controller="TranslateCtrl as translate" translate-language="{{ translate.locale }}">
  <a class="btn btn-default" ng-click="translate.toggle()">Toggle</a>
{{ 'NAV.HOME' | translate }} <span translate="NAV.LOG_IN"><span>
</body>

뷰 코드.

app.controller('TranslateCtrl', function($translate) {
  var vm = this;
  vm.toggle = function() {
    if (vm.locale === 'en') vm.locale = 'ko';
    else vm.locale = 'en';

    $translate.use(vm.locale);
  };
  vm.init = function() {
    vm.locale = $translate.use();
  };
  vm.init();
});

컨트롤러 코드.

 


뷰에서는 필터와 디렉티브 방식 모두 적용했다.

translate 디렉티브를 적용하기 위해서는 부모/조상 엘리먼트 중에 translate-language가 있어야한다. 그리고 컨트롤러에는 언어를 바꿔주기 위한 toggle함수를 만들었다.  $translate.use() 로 현재 사용 중인 언어를 알 수 있음에도 불구하고 굳이 따로 컨트롤러에서 변수를 설정한 이유는 각 언어에 따라서 뷰에서 분기해야하는 경우가 조금씩 생기더라고. ng-show 와 같은 디렉티브를 사용하기 위해 모든 컨트롤러에서 각각 변수를 드느니, 차라리 전역변수와 비슷한 느낌으로 쓰자. 어차피 번역은 모든 html element에 영향을 미치는게 맞으니 전역적으로 생각하는 것이 맞다는 느낌적인 느낌!

 


이것으로 기본적인 예제를 마친다.

예제에서 설명하지 않은 부분을 간략하게 키워드만 정리하자면 유저가 마지막으로 어떤 언어를 선택했었는지를 기억하기 위해서 쿠키, 로컬스토리지 등을 직접 바꿔주는 부분에서 구현하기 보다는 라이브러리에서 제공하는 storage, custom storage 기능을 참고해보자.  그 외에는 같은 문장에 언어말고 다른 분기 때문에 그 때 그 때 다른 ‘값’이 들어가야하는 경우도 있는데, 이 기능은 Variable replacement를 참고하자.

 

Leave a Reply