本篇的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)兩種元件,
上面甚至可以放不同的功能與連結,在比較小的螢幕解析度時使用側欄列,比較大的解析度使用導覽列。
而我們的多語系自然是兩種元件上都要出現的功能,而且不管哪種選單選擇語系時,在另一種選單上應該即時呈現剛剛選擇的語系,所以這兩種選單上的顯示是會互相關聯的。
如下圖:
這裡使用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?
4 意見:
您好
我想請問
可是當我瀏覽器重新整理後
他不會抓取我選取的那個語言
他會直接抓取一開始所設定的那個語言~“~
如果是要三個語言需要怎麼去改寫??
Hi VIVIAN,
因為瀏覽器重新整理後相當於整個app重啟,所以程式又去執行this.translate.getBrowserLang()
而你的chrome語系如果是中文的就還是會去顯示中文,如果你希望重整時抓取你之前選過的語系,比如你之前選擇的是日文,那可以在選擇語系完後把這個日文數值存在localstorage,
然後在程式執行getBrowserLang()前先判斷,如果localstorage有值則以他為優先,沒有才用瀏覽器預設語系,這樣下次重新整理時就可以顯示日文而不是中文了。
如果要增加語言,在this.translateService.addLangs() 新增即可,比如要多日文jp,
就是this.translateService.addLangs(['en', 'zh-tw','jp']);
然後顯示語言的部分就要再改寫,因為我範例是非中文即顯示英文,如果你要多別的語言顯示就要再判斷顯示時機。
你好,想請問一下setLang的部分 監聽onLangChange的時候為什麼要使用take(1)呢?
版主好
抓本機的語言
用this.translate.getBrowserCultureLang() 比較好哦
中文才不會只是zh
而是會有 zh-tw zh-cn之分
留言經過版主審核後才會出現哦!
因為不這樣做會有很多垃圾留言,不好意思 > <