Multilingual ใน Angular

ลองหาวิธีทำ multilingual application ใน angular แล้วเจอตัวที่เป็น translation service ที่มีหน้าตาคล้าย ๆ กับที่เคยทำใน AngularJS ต้องใช้พวก translation provider อะไรพวกนี้ หาไปเรื่อย ๆ ก็เจอ @ngx-translate ที่ดูเจ๋ง แต่ตอนเอาไปใช้นี่น่าจะใช้เวลานานเหมือนกัน เลยลองทำเล่นดูเองดีกว่า

ไอเดียคือ แค่อยากเอาไปใช้งานได้ผ่าน pipe แบบนี้

{{ 'hello' | translate }}

เลยคิด abstraction ไว้ว่ามี pipe ทำหน้าที่เป็น translator interface และมี translator service ทำหน้าที่เป็นตัวแปรภาษาอีกที ประมาณนี้

Translation Table

ตอนแปลภาษา ตัวที่เป็น translation table จะมีหน้าตาเป็น key-value ถูกเก็บไว้ใน app/i18n/langs มีหน้าตาแบบนี้

const translation_table = {
  'translate_me': 'Translate me',
  'hello_world': 'Hello world'
};
export default translation_table;

โดย key จะเป็นคำที่ต้องการให้แปล ส่วน value เป็นคำที่แปลแล้ว เสร็จแล้วก็ไปเพิ่มใน app/i18n/index.ts เพื่อให้รู้จัก translation table ใหม่ แบบนี้

import { default as en_US } from './langs/en';
import { default as th_TH } from './langs/th';
import { default as zh_CN } from './langs/zh';

const TRANSLATIONS = {
  'en': en_US,
  'th': th_TH,
  'zh': zh_CN
};

export default TRANSLATIONS;

ตอนนี้ translation table จะมีภาษาที่เราเพิ่มไปใหม่แล้ว ก็เริ่มเขียน code ได้

ลองทำส่วนที่เป็น service สำหรับการแปลภาษา

ใน service ก็ทำตรงไปตรงมาคือ เลือกชุดคำแปลจากภาษาที่เลือกอยู่ตอนนี้ ถ้าไม่เจอภาษาที่เลือก ก็ให้ return คำที่ส่งเข้ามามันดื้อ ๆ หรือถ้าเจอชุดภาษา แต่ไม่เจอคำใน key ของ translation table ก็ return คำนั้นดื้อ ๆ เหมือนกัน ก็จะได้โค้ดประมาณนี้

import { Injectable } from '@angular/core';
import { default as translation } from '../../i18n';

@Injectable()
export class TranslatorService {
  private lang;
  private translation_table: any;

  private default_lang = 'en';

  constructor() {
    this.setTranslation(translation);
    this.setLanguage(this.default_lang);
  }

  setTranslation(t: any) {
    this.translation_table = t;
  }

  setLanguage(l: string) {
    this.lang = l;
  }

  getLanguage(): string {
    return this.lang;
  }

  translate(t: string): string {
    if (!this.translation_table.hasOwnProperty(this.lang)) {
      return t;
    }
    if (!this.translation_table[this.lang].hasOwnProperty(t)) {
      return t;
    }
    return this.translation_table[this.lang][t];
  }
}

เท่านี้ก็จะได้ service สำหรับแปลภาษาแล้ว ที่เหลือก็เอาไปสร้างเป็น pipe ก็จะได้หน้าตาแบบนี้

import { Pipe, PipeTransform } from '@angular/core';
import { TranslatorService } from '../services/translator/translator.service';

@Pipe({
  name: 'translate',
  pure: false
})

export class TranslatePipe implements PipeTransform {

  constructor(private translator: TranslatorService) {}

  transform(value: string): string {
    return this.translator.translate(value);
  }

}

ข้อสังเกตคือ option pure ที่จะเป็นตัวบอกว่า pipe เป็น pure หรือ impure สถานการณ์นี้เอาตามความรู้ที่มีตอนนี้คือใช้ impure จะช่วยให้ translation pipe ทำงานได้อย่างที่ต้องการ

Pure VS Impure Pipe

ลองไปอ่าน pipe ในเว็บ angular ก็เจอว่า default ของ pipe มันจะเป็น pure แล้วไอ้ pure เนี่ย มันจะทำครั้งเดียวตอน load ครั้งแรก หรือตอนที่ value มันเปลี่ยน นั่นคือถ้าเรามีการเปลี่ยนภาษา ซึ่งเป็นส่วนหนึ่งของ service หลังจากหน้าเว็บโหลดแล้ว จะทำให้ angular ไม่รู้ว่ามีการเปลี่ยนแปลง วิธีที่จะทำให้ angular รู้ว่าต้อง render pipe ใหม่อีกที คือเปลี่ยนแปลงค่าที่ส่งให้ pipe ใหม่ หรือใช้ pipe ที่เป็น impure

impure pipe มันก็ไม่ได้ฉลาดอะไรขนาดนั้นที่จะรู้ว่า data มันเปลี่ยน แต่มันก็หลับหูหลับตาทำทุก ๆ event ที่เกิดขึ้นเลย ไม่ว่าจะเป็นกดปุ่ม focus blur ฯลฯ ดังนั้นถ้าเรากดปุ่มเปลี่ยนภาษา ตัว pipe ที่เป็น impure pipe มันก็จะบังคับ render ใหม่อัตโนมัติ

ทำไมไม่เก็บ translation table เป็น json แล้วค่อยโหลดมาเมื่อใช้

  • แค่อยากลอง
  • translation table ไม่ใหญ่ และมีภาษาไม่เจอ ไม่อยากเปลืองแรงทำท่านั้น
  • ขี้เกียจ

โค้ดตัวอย่าง

อยู่นี่ https://github.com/chonla/ng-multilingual-example

Leave a Reply