【Angular】ngx-translate 多語系實務應用

在Angular上使用ngx-translate 多語系的實務應用
本篇的UI 元件使用的是Angular Material,如果對外觀不太在意或者自己有用別的UI framework(如PrimeNG),則可以忽略Angular Material相關的元件使用及屬性。

安裝
首先最重要的當然就是安裝了,可以參考一下npm上的安裝說明,在終端機下這個指令來安裝:


npm install @ngx-translate/core --save
npm install @ngx-translate/http-loader --save


ngx-translate/http-loader 說明
ngx-translate/core 說明

根模組(AppModule)設定及建立json檔
首先在根模組(AppModule)上設定好TranslateModule,使用forRoot()的方法來設定,
並在上方先宣告function作為TranslateModule 的語系檔讀取器。

app.module.ts:

// AoT requires an exported function for factories 
// 建立TranslateHttpLoader作為語系檔的讀取器
export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}

// 將 TranslateHttpLoader作為 TranslateModule 的語系檔讀取器(loader)
@NgModule({
    imports: [
        AppRoutingModule,
        BrowserModule,
        HttpClientModule,
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient]
            }
        }),
        BrowserAnimationsModule,
        LayoutModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

這個TranslateHttpLoader有兩個參數可以使用,分別是多語系的檔案路徑及json檔,
如果都沒有設定的話預設會使用「/assets/i18n/.json」,
也就是說只要在assets目錄下建立一個叫i18n的資料夾,之後設定語系時和json檔的檔名一樣他就會自己找到相對應的語系了,十分方便!

我們先建立語系分別為英文(en)及中文(zh-tw)兩種json檔。

▼json檔裡面放的就是key、value對應的資料格式

en.json:

{
    "menu": {
        "home": "Home",
        "feature": {
            "name": "Feature module",
            "form": "Form"
        },
        "language": "Language",
        "languageList": {
            "taiwanese": "繁體中文",
            "english": "English"
        }
    }
}

zh-tw.json:

{
    "menu": {
        "home": "首頁",
        "feature": {
            "name": "功能模組",
            "form": "表單"
        },
        "language": "語言",
        "languageList": {
            "taiwanese": "繁體中文",
            "english": "英文"
        }
    }
}

▼現在你的assets目錄下會類似藍色區塊這樣,也就是預設的語系檔,黃色區塊是等等後面會介紹使用延遲載入(Lazy loaded modules)方式的語系檔
好,接下來開始設定根頁面!


根頁面(AppComponent)設定
你其實可以馬上就在根頁面(app.component)上使用pipe來翻譯了,
可是實務上我們並不會在根頁面上寫太多程式,而且通常會在專案中使用共用模組(SharedModule)、功能模組(FeatureModule)、延遲載入功能模組(Lazy Loading Feature Modules),
所以針對上述的各種架構,我們在使用ngx-translate時也要做一些調整。

▼舉例來說,我們最終使用多語系的方式可能是像這樣,在某個元件上(通常是navbar或footer)有語言切換的功能,而選擇後所有其他模組的元件都要一起即時的更改語系。

▼而我們在開發Angular專案時常常會切出一個layout的模組來管理畫面上的基本元件(navbar、footer、menu..等)

所以我們的根頁面(app.component)可能根本沒有其他的程式,類似以下這樣:
app.component.html:

<router-outlet></router-outlet>

但是我們剛剛根模組已經設定好TranslateModule啦! 
那語系預設的選擇和呈現要寫在哪呢? 


我們可以這樣來思考,很明顯的我們語系的轉換是會跨元件溝通的,而ngx-translate對於語系的切換其實是使用一個自己寫好的TranslateService來處理要顯示的語系,
所以我們可以另外再寫一個LanguageService來集中管理這件事情,如此一來即使在navbar切換了語系,其他模組的內容也能跟著切換,在延遲載入模組時也能有一樣的效果。

所以,我們在根頁面(app.component.ts)只需要注入languageService,並執行初始化語系的動作即可。

app.component.ts:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent {
  constructor(private languageService: LanguageService) {
    this.languageService.setInitState();
  }
}


後面會在提到languageService的實作,這裡先了解根頁面要做的事情就好。


LayoutModule設定
剛剛提到一些畫面上共用元件會使用LayoutModule來統一控管,而LayoutModule本身在Angular裡我們並沒有設定成延遲載入的方式,所以這裡只要簡單import TranslateModule 即可。

layout.module.ts

@NgModule({
  imports: [
    SharedModule,
    TranslateModule,
    RouterModule, // 為了使用routerLink
  ],
  declarations: [
    LayoutComponent,
    NavbarComponent,
    FooterComponent,
    MenuComponent
  ],
  entryComponents: [
  ]
})
export class LayoutModule { }


那如果你程式裡有許多模組都不是用延遲載入的時候,每個模組都要import TranslateModule會顯得有點麻煩,如下圖這樣:

那這個時候你就可以考慮對於這些非延遲載入的模組,製作一個共用模組(SharedModule),並把TranslateModule放進去,當然這個共用模組也可以放其他這些模組都會使用到的元件,這樣就可以少寫import TranslateModule這個動作了。


LanguageService
先來實作剛剛在根頁面設定的LanguageService。

language.service.ts

@Injectable({
  providedIn: 'root'
})
export class LanguageService {
  language$ = new ReplaySubject<LangChangeEvent>(1);
  translate = this.translateService;
  // 國旗對照
  countryMap = new Map().set('en', 'flag-us').set('zh-tw', 'flag-tw');

  constructor(private translateService: TranslateService) {}

  setInitState() {
    this.translateService.addLangs(['en', 'zh-tw']);
    // 根據使用者的瀏覽器語言設定,如果是中文就顯示中文,否則都顯示英文
    // 繁體/簡體中文代碼都是zh
    const browserLang = (this.translate.getBrowserLang().includes('zh')) ? 'zh-tw' : 'en'  ;
    this.setLang(browserLang);
  }

  setLang(lang: string) {
    this.translateService.onLangChange.pipe(take(1)).subscribe(result => {
      this.language$.next(result);
    });
    this.translateService.use(lang);
  }
}

setInitState()是在根頁面初始化執行的function,首先告訴translateService我們有的語系,之後設定初始化語系時使用偵測瀏覽器語言的方式來設定,這裡因為只有中英文兩種,所以只簡單設定非中文語系即顯示英文。
如果你的語系有多種,可以自行調整要顯示的時機。

這裡額外提一下關於瀏覽器語言的測試方法,在Chrome瀏覽器的進階設定裡,有語言的項目可以新增,不過值得注意一點的是,所謂「瀏覽器語言」是指你語言順序第一位的那個! 
和你現在瀏覽器顯示什麼語言無關哦!

例如像下圖,我的Chrome瀏覽器雖然介面設定成中文,但是我第一順位是英文,所以在程式裡的getBrowserLang()拿到的會是en而不是zh。

在說明setLang() 設定語系的功能前,先來看一下layout架構。
在RWD響應式網站的設計下,你的選單可能會分成電腦版使用的導覽列(navbar)和手機版用的側欄列(sidenav)兩種元件,
上面甚至可以放不同的功能與連結,在比較小的螢幕解析度時使用側欄列,比較大的解析度使用導覽列。

而我們的多語系自然是兩種元件上都要出現的功能,而且不管哪種選單選擇語系時,在另一種選單上應該即時呈現剛剛選擇的語系,所以這兩種選單上的顯示是會互相關聯的。


如下圖:


也因此,我們在設定語系setLang()時,要監聽onLangChange()事件,並在語系被變動時推播給訂閱language$的人。

這裡使用ReplaySubject而不是BehaviorSubject,因為我們不需要初始值,不過如果還是要用BehaviorSubject老實說程式也是可以運作啦XD

好,這樣一來等等不論是在navbar還是sidenav上呼叫切換語系時,就都能即時變動了


切換語系時更改國旗的方法
有了LanguageService,要更換語系就變得簡單多了,你可以使用ngClass來依據現在的語系切換要顯示的國旗圖。
目前顯示的語系就從translateService的currentLang取得即可。

navbar.component.ts

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit {
  @Input() value: Observable<Menu[]>;
  @Input() menu;
  @Input() isMobile;
  get currentLanguage() {
    return this.languageService.translate.currentLang;
  }

  constructor( private languageService: LanguageService ) {
  }

  getCountryMap(currentLanguage: string) {
    return this.languageService.countryMap.get(currentLanguage);
  }
  useLanguage(language: string) {
    this.languageService.setLang(language);
  }

  ngOnInit() {

  }
}


navbar.component.html:

<mat-toolbar color="primary" class="toolbar">
  <mat-toolbar-row>

    <!-- 手機版-->
    <ng-container *ngIf="isMobile; else desktop">
      <button class="hamburger mat-button" mat-button="" (click)="menu.toggle()">
        <mat-icon class="mat-icon" aria-hidden="true">menu</mat-icon>
        <div class="mat-button-ripple mat-ripple" matripple=""></div>
        <div class="mat-button-focus-overlay"></div>
      </button>
      <h1 style="outline: 0px;" pointer dark-grey-text margin-left [routerLink]="['/']">Angular NgxTranslate Demo</h1>
    </ng-container>

    <!-- 電腦版-->
    <ng-template #desktop>
      <div class="container">

        <div class="row justify-content-center">
          <a style="padding: 14px 12px 14px 12px;">
            <h1 style="outline: 0px;" pointer dark-grey-text margin-left [routerLink]="['/']">Angular NgxTranslate Demo</h1>
          </a>

          <ng-container *ngFor="let item of value">
            <button *ngIf="item.subMenus.length == 0; else hasSubMenus" [routerLink]="item.actionUrl" class="link-button"
              mat-button>
              {{ item.display | translate }}
            </button>
            <ng-template #hasSubMenus>
              <button class="link-button" [matMenuTriggerFor]="sub_menu" mat-button>
                {{ item.display | translate }}
                <mat-icon>keyboard_arrow_down</mat-icon>
              </button>
              <mat-menu #sub_menu="matMenu" [overlapTrigger]="false">
                <button *ngFor="let submenu of item.subMenus;" [routerLink]="submenu.actionUrl" mat-menu-item>
                  {{ submenu.display | translate }}
                </button>
              </mat-menu>
            </ng-template>
          </ng-container>

          <button class="link-button" [matMenuTriggerFor]="language" mat-button>
            <figure class="flag" [ngClass]="getCountryMap(currentLanguage)"></figure>
          </button>


          <mat-menu #language="matMenu" [overlapTrigger]="false">
            <button mat-menu-item (click)="useLanguage('en')">
              <figure class="flag flag-us"></figure>
              <span class="countrylist-caption">{{ 'menu.languageList.english' | translate }}</span>
            </button>
            <button mat-menu-item (click)="useLanguage('zh-tw')">
              <figure class="flag flag-tw"></figure>
              <span class="countrylist-caption">{{ 'menu.languageList.taiwanese' | translate }}</span>
            </button>
          </mat-menu>

        </div>
      </div>
    </ng-template>

  </mat-toolbar-row>
</mat-toolbar>

另一個手機板menu寫法和navbar的一樣,
這樣一來你的程式應該能達到切換語系即時更換國旗圖、且在navbar或是sidenav上切換彼此都能正確顯示,那接下來就終於要說明使用pipe翻譯囉!



使用translate pipe來翻譯
我們拿一個模組來當範例,我們有一個login的模組,而這個模組在app-routing.module.ts本身用延遲載入的方式使用,那這個時候多語系的載入方式要用TranslateModule.forChild()。

如果你的模組本身不是用延遲載入,那就和LayoutModule設定一樣直接import TranslateModule就好。

如果你的多語系檔沒有要用延遲載入的方式下載(網頁打開時就都先把json檔載到用戶端),就不用多設定。

login.module.ts

@NgModule({
  declarations: [
    LoginComponent
  ],
  imports: [
    SharedModule,
    LoginRoutingModule,
    TranslateModule.forChild()
  ]
})
export class LoginModule {}
 
那我們現在來開始在login.component.html上使用翻譯了。

最基本的使用方式就是在要顯示文字的地方使用translate  pipe,比如說在最上面的範例json裡,我們定義了menu的中英文對照組,在原本顯示「首頁」兩個字的地方改成用translate  pipe顯示,
如果你的json是巢狀結構,只要下去就能拿到下一階層的key值。


<!-- 原本的網頁文字 -->
<div>首頁</div>

<div>
    <!-- 改成用 translate  pipe來顯示,最常使用的方式 -->
    <h1>{{ 'menu.home' | translate }}</h1>

    <!-- 也可以用 directive的方式 (key as attribute)-->
    <p [translate]="'menu.home'"></p>

    <!-- directive 另一種方式 (key as content of element) -->
    <p translate>menu.home</p>
</div>

此時你json裡的命名原則就很重要了,畢竟在程式上沒辦法在直接用中文,最好訂出一套規則,以免頁面上有很多相似變數時找錯變數。

你也可以定義出共用的變數來顯示,比如說錯誤訊息或是一些儲存等動作,或者是下拉選單用的固定變數。

例如你的網頁上有很多地方都會用到儲存和取消的按鈕,可以通通都使用同一個變數。


{
    "common": {
        "save": "儲存",
        "cancel": "取消"
    }
}

{
    "common": {
        "save": "Save",
        "cancel": "Cancel"
    }
}



使用 translate with parameters 傳遞變數來翻譯
最基本的翻譯完成了,你可以即時的用剛剛設計好的國旗切換來看看成果。

有些重複性質的東西,比如說「姓名為必填欄位」、「帳號為必填欄位」,這種XXX為必填欄位」的多語系翻譯,如果每一個都要寫各自的對照組也太麻煩了,可以使用傳遞變數的方式來統一使用。

{
    "error": {
        "require": "{{ itemName }}為必填欄位"
    }
}

{
    "error": {
        "require": "{{ itemName }} is required."
    }
}


那在html上要怎麼使用呢? 在json檔裡大括弧內的就是要傳遞的變數。

▼如果要傳進去的變數沒有要跟著一起翻譯就直接寫

<div>{{ 'error.require' | translate: {'itemName': '092884521' } }}</div>


如果那個要傳進去的變數剛好也是要翻譯的文字,那就再使用translate 即可。

<div>{{ 'error.require' | translate: {'itemName': 'enum.gender' | translate } }}</div>



延遲載入多語系檔
有些時候功能多了,那麼json的檔案大小也就會變得肥大,
或是有些比較敏感的對照字,你希望在使用者登入後才下載,亦或是點到某個模組才下載該模組的json檔,那麼這個時候你就可以使用延遲載入多語系檔。

不過剛剛最一開始在根模組我們已經用預設的方式設定好要讀取的json檔路徑和檔名了,那要怎麼樣才能讀取自己設定的路徑和檔案呢?

可以在TranslateModule.forChild()時設定屬性isolate為true,這樣子一來就會建立另一個TranslateService的實體,就可以在另一個TranslateService上設定檔案要載入的路徑及檔名

▼isolate true和false的差異:


再看一次剛剛的assets目錄,假設有一個home模組,在i18n下只放和home相關的翻譯內容。

home.module.ts:

export function createTranslateLoader(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/home/', '.json');
}

@NgModule({
  declarations: [
    HomeComponent
  ],
  imports: [
    SharedModule,
    HomeRoutingModule,
    TranslateModule.forChild({
      loader: {
          provide: TranslateLoader,
          useFactory: createTranslateLoader,
          deps: [HttpClient]
      },
      isolate: true
  })
  ]
})
export class HomeModule {
  language$ = this.languageService.language$;
    constructor(
        private translateService: TranslateService,
        private languageService: LanguageService,
    ) {
        this.language$.pipe(map(language => language.lang)).subscribe(lang => this.translateService.use(lang));
    }
}


和上面的login模組不同的是在home模組有另外設定了TranslateHttpLoader,
因為是不同個TranslateService了,所以要訂閱language$,這樣子切換語系時才能跟著一起換。

整體語系切換效果如下:


github完整程式碼:https://github.com/t5957810/ngx-translate

相關連結:
@ngx-translate/core

How to split your i18n file per lazy loaded module with ngx-translate?

How to translate your Angular 7 app with ngx-translate



結論:
有了ngx-translate,在Angular要使用多語系就變得相當簡單且方便了!



其他Angular相關:

【Angular】如何在token失效並重新登入後,重發前次Request?

Next
Previous
Click here for Comments

4 意見:

avatar

您好
我想請問
可是當我瀏覽器重新整理後
他不會抓取我選取的那個語言
他會直接抓取一開始所設定的那個語言~“~

如果是要三個語言需要怎麼去改寫??

avatar

Hi VIVIAN,
因為瀏覽器重新整理後相當於整個app重啟,所以程式又去執行this.translate.getBrowserLang()
而你的chrome語系如果是中文的就還是會去顯示中文,如果你希望重整時抓取你之前選過的語系,比如你之前選擇的是日文,那可以在選擇語系完後把這個日文數值存在localstorage,
然後在程式執行getBrowserLang()前先判斷,如果localstorage有值則以他為優先,沒有才用瀏覽器預設語系,這樣下次重新整理時就可以顯示日文而不是中文了。

如果要增加語言,在this.translateService.addLangs() 新增即可,比如要多日文jp,
就是this.translateService.addLangs(['en', 'zh-tw','jp']);
然後顯示語言的部分就要再改寫,因為我範例是非中文即顯示英文,如果你要多別的語言顯示就要再判斷顯示時機。

avatar

你好,想請問一下setLang的部分 監聽onLangChange的時候為什麼要使用take(1)呢?

avatar

版主好
抓本機的語言
用this.translate.getBrowserCultureLang() 比較好哦
中文才不會只是zh
而是會有 zh-tw zh-cn之分

留言經過版主審核後才會出現哦!
因為不這樣做會有很多垃圾留言,不好意思 > <