iT邦幫忙

2022 iThome 鐵人賽

DAY 20
0
Modern Web

angular專案開發指南系列 第 20

i18n 多國語系 - Ngx-Translate

  • 分享至 

  • xImage
  •  

前言

國際化 (i18n) 是一個過程,用於對你的應用進行設計和準備以便在全球不同地區使用。本地化是指為不同的本地環境建構應用版本的過程,包括提取用於翻譯成不同語言的文字,以及格式化特定本地環境的資料。

本地環境(locale)用於標識某個區域(例如某個國家/地區),人們會在該區域內使用特定的語言或語言變體。本地環境決定了日期、時間、數字和貨幣的格式和解析方式,以及各種測量單位和時區、語言、國家/地區的翻譯名稱。


i18n 多國語系檔案架構

直接以 navigator.language 為檔案命名,方便語系選擇如下,

p110


Ngx-Translate 基本使用方式

[動態] 翻譯字段到 typescript

getLangMenuItems(data) {
    this.menuItemsLang = data;

    NbMenuItems.forEach((item) => {
        // home.menuitem recursive of menu structure
        item.title = this.languageService.recursiveLangAppend(data, item);
    });

    this.items = NbMenuItems;
}

[靜態] 翻譯字段到 HTML

<nb-card-header> {{ "ROBOT.SETTING" | translate }} </nb-card-header>

Ngx-Translate 安裝方式

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

根模組設定

src\app\app.module.ts

// Ngx-Translate
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

...

// 在根模組中 **(預設會使用「/assets/i18n/」及「.json」)**
export `function createTranslateLoader`(http: HttpClient) {
    return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

@NgModule({
    ...

    imports: [
        ...

        // 引入 TranslateModule
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: createTranslateLoader,
                deps: [HttpClient],
            },
        })
    ]
})
export class AppModule {}

my-app 側邊菜單的多國語系實作

安裝 @ngx-translate 並設定完成後,我們來實際應用多國語系模組到 my-app 中。

STEP 1. 以側邊巢狀菜單結構作為 json翻譯檔結構,內容範例如下,

src\assets\i18n\zh-TW.json

{
    ...

    "MENU_ITEMS": {

        ...

        "ABOUT": {
            "TITLE": "公司簡介"
        },
        "TENANT": {
            "TITLE": "租戶管理"
        },
        "LAND": {
            "TITLE": "土地管理"
        }
    }
}

src\assets\i18n\zh-US.json

{
    ...

    "MENU_ITEMS": {

        ...

        "ABOUT": {
            "TITLE": "ABOUT"
        },
        "TENANT": {
            "TITLE": "TENANT"
        },
        "LAND": {
            "TITLE": "LAND"
        }
    }
}

STEP 2. 新增監聽語系改變或解析需要翻譯的目錄結構服務

src\app\core\utils\language.service.ts

import { Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
import { take } from 'rxjs/operators';

/**
 * ### Language Service - 監聽語系改變或解析需要翻譯的目錄結構服務
 *
 * @export
 * @class LanguageService
 */
@Injectable({
    providedIn: 'root',
})
export class LanguageService {
    constructor(private translateService: TranslateService) {}

    /**
     * ### ReplaySubject - language$
     *
     * @memberof LanguageService
     */
    language$ = new ReplaySubject<LangChangeEvent>(1);

    /**
     * ### langService
     *
     * @type {*}
     * @memberof LanguageService
     */
    langService: any = null;

    /**
     * set i18n language
     *
     * @param {string} [language='']
     * @memberof LanguageService
     */
    setLang(language: string = '') {
        let lang = language || localStorage.getItem('lang') || navigator.language;
        localStorage.setItem('lang', lang);

        // 觸發 Rxjs
        this.setLangRxjs(lang);
    }

    /**
     * set language RxJS
     *
     * @param {string} [lang='']
     * @memberof LanguageService
     */
    setLangRxjs(lang: string = '') {
        this.langService = this.translateService.onLangChange.pipe(take(1)).subscribe((result) => {
            this.language$.next(result);
        });

        // 改變使用的語系
        this.translateService.use(lang);
    }

    /**
     * ### 解析巢狀結構目錄
     *
     * i18n recursive of menu structure
     *
     * @param {object} langData
     * @param {object} item
     * @return {string} item.title
     * @memberof LanguageService
     */
    recursiveLangAppend(langData: any, item: any) {
        if (!langData || !item) return '';

        if (!!item.children) {
            item.children.forEach((element: any) => {
                return this.recursiveLangAppend(langData, element);
            });
        }

        if (item.key.split('.').length > 1) {
            let childTitleObj: any = {};
            let childTitle = '';
            item.key.split('.').forEach((val: any) => {
                if (!!Object.keys(childTitleObj).length) {
                    childTitle = childTitleObj[val];
                    childTitleObj = childTitleObj[val];
                } else childTitleObj = langData[val];
            });
            item.title = childTitle;
        } else item.title = langData[item.key];

        return item.title;
    }
}

STEP 3. 在要翻譯的組件 HomeComponent 中

src\app\home\home.component.ts

// @ngx-translate
import { TranslateService } from '@ngx-translate/core';

// Language Service - 監聽語系改變或解析需要翻譯的目錄結構
import { LanguageService } from '@core/utils/language.service';

/**
 * ## 首頁元件
 *
 * @export
 * @class HomeComponent
 * @implements {OnInit}
 */
@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
    // 注入服務
    constructor(
        private translateService: TranslateService,
        private languageService: LanguageService
    ) {
        // 取得瀏覽器目前語系
        this.translateService.use(localStorage.getItem('lang') || navigator.language);

        // 將對應語系寫入語系菜單中
        this.translateService.get('HEADER.LANG').subscribe((resp) => this.getLang(resp));
    }

    /**
     * ### 側邊巢狀菜單
     *
     * @type {NbMenuItem[]}
     * @memberof HomeComponent
     */
    items: NbMenuItem[] = NbMenuItems;

    /**
     * ### 語系項目
     *
     * @memberof HomeComponent
     */
    languageMenu = [{ title: '', tag: '' }];

    /**
     * ### 可供切換的語系菜單的 i18n
     *
     * @param {*} data
     * @memberof HomeComponent
     */
    getLang(data: any) {
        this.languageMenu = [
            { title: data.TW, tag: 'zh-TW' },
            { title: data.US, tag: 'en-US' },
        ];
    }

    /**
     * ### 縮放側邊巢狀菜單
     *
     * @return {*}
     * @memberof HomeComponent
     */
    toggle() {
        this.sidebarService.toggle(true);
        return false;
    }

    /**
     * ### i18n 翻譯側邊巢狀菜單
     *
     * @param {*} data
     * @memberof HomeComponent
     */
    getNbMenuItems(data: any) {
        NbMenuItems.forEach((item: any) => {
            item.title = this.languageService.recursiveLangAppend(data, item);
        });
    }

    ngOnInit(): void {
        this.nbMenuService.onItemClick().subscribe((title: { item: any; tag: any }) => {
            // 監聽到有切換語系的動作且為合法語系則進入更換語言的方法
            if (title.item.tag === 'zh-TW' || title.item.tag === 'en-US') {
                this.languageService.setLang(title.item.tag);
                this.translateService.get('HEADER.LANG').subscribe((resp) => this.getLang(resp));
                this.translateService.get('MENU_ITEMS').subscribe((resp) => this.getNbMenuItems(resp));
            }
        });
    }
}

STEP 4. 側邊菜單列表 - 增加 Key 欄位

src\app\home\home.menuitem.ts

export const NbMenuItems = [
    {
        key: 'ABOUT.TITLE', // 對應 i18n [json檔] 的 key
        title: '公司簡介',
        icon: { icon: 'at-outline', pack: 'eva' },
        link: '/home/about',
    },
    {
        key: 'TENANT.TITLE', // 對應 i18n [json檔] 的 key
        title: '租戶管理',
        icon: { icon: 'briefcase-outline', pack: 'eva' },
        link: '/home/tenant',
    },
    {
        key: 'LAND.TITLE', // 對應 i18n [json檔] 的 key
        title: '土地管理',
        icon: { icon: 'bar-chart-2-outline', pack: 'eva' },
        link: '/home/land',
    },
];

STEP 5. HomeComponent 元件增加 i18n 語系切換按鈕

src\app\home\home.component.html

<nb-layout class="home">
    <!-- header -->
    <nb-layout-header fixed>
        <div class="header">
            <a (click)="toggle()">
                <!-- 縮放側邊巢狀菜單 -->
                <nb-action icon="menu-outline"></nb-action>
            </a>
            <a>
                <!-- 可供切換的語言項目 -->
                <nb-action icon="globe-outline" [nbContextMenu]="languageMenu"></nb-action>
            </a>
        </div>
    </nb-layout-header>

    <!-- 側邊巢狀菜單 -->
    <nb-sidebar>
        <nb-menu [items]="items"></nb-menu>
    </nb-sidebar>

    <!-- 頁面內容 -->
    <nb-layout-column class="colored-column-info">
        <router-outlet></router-outlet>
    </nb-layout-column>
</nb-layout>

STEP 6. 美化一下頁面

src\app\home\home.component.scss

.home {
    .header {
        width: 100%;
        display: flex;
        justify-content: space-between;
    }
}

成果畫面

g11


Ngx-Translate 延遲加載作法

i18n 獨立加載示意圖,左邊是原本作法,右邊是延遲加載,

p111

黃色區塊延遲加載的 i18n檔案,藍色為預設檔案,
此作法可將各模組的 i18n分開管理,與延遲載入模組的檔案結構相同並獲得延遲載入 i18n 的效果。

p112

在根模組中 (使用「./assets/i18n/dashboard/」及「.json」)

p113

在功能模組中的根模組需要改2個紅框處的設定

p114

配合 Rxjs設計 language.service以在各模組間溝通訊息

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

i18n 延遲加載檔案格式

{
    "DASHBOARD": {
        "PIE": "圓餅圖",
        "BAR": "長條圖",
        "HEAT": "熱力圖",
        "CHART_QUARY": {
            "LONGBAR": "很長的",
            "HOT": "很熱的"
        }
    }
}

i18n 延遲加載使用方式

import { TranslateService } from '@ngx-translate/core';
import { LanguageService } from '@core/utils/language.service';

constructor(private translateService: TranslateService, private languageService: LanguageService) {
    this.languageService.language$.subscribe((resp) => {
        this.translateService.use(resp.lang);
        this.translateService.get('DASHBOARD').subscribe((lang) => (this.DASHBOARD = lang));
    });
}

i18n 變數傳遞式翻譯

i18n 變數傳遞式翻譯檔案格式

{
    "DASHBOARD": {
        "PIE": "{{itemNo}} 圓餅圖",
        "BAR": "{{itemStr}} 長條圖",
        "HEAT": "{{itemStr}} 熱力圖",
        "CHART_QUARY": {
            "LONGBAR": "很長的",
            "HOT": "很熱的"
        }
    }
}

i18n 變數傳遞式翻譯使用方式

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

{{ "DASHBOARD.PIE" | translate: { itemNo: "1號" } }}

p115

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

{{ "DASHBOARD.BAR" | translate: { itemStr: "DASHBOARD.CHART_QUARY.LONGBAR" | translate } }}

p116


結論

Angular 本身有提供多國語系 Angular 國際化 的功能,但是不同語系需要編譯不同版本,使用上較為複雜,因此較推薦使用 ngx-translate,即使在開發模式下也能方便看到即時切換語系的效果。

又有新的需求了,為了加強租賃物件本身的宣傳,需要新的元件讓使用者維護物件資訊。


參考

多語系實務應用

[Angular] ngx-translate

ngx-translate

多國語系 NGX-Translate

Angular 國際化


上一篇
製作共用指令與管道
下一篇
共用元件的動態載入(1)
系列文
angular專案開發指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言