Angular uses pipelines and directives for multi-language switching without third-party library references

        I often encounter projects that require switching between multiple languages ​​at work. This article documents a language switching solution implemented in Angular pages by using pipes and custom directives.

1. Realize the effect

        The text displayed on the page is automatically translated and switched according to the selected language, as shown in the figure below:

        At this point, the strings of the page template are all written in the pipeline format: 

<h1>{
   
   { "app.title" | translate}}</h1>
<div>
  <span>{
   
   { "app.demo.text" | translate}}</span>
  <input type="text" placeholder="{
   
   { 'app.demo.placeholder' | translate}}">
  <button (click)="showMessage()">{
   
   {"app.demo.btn" | translate}}</button>
</div>
<button *ngFor="let lan of languages" (click)="setLanguage(lan.code)">
  {
   
   {lan.icon + " " + lan.text}}
</button>

        Among them, "app.title", "app.demo.text", 'app.demo.placeholder', "app.demo.btn" are the keys of each string, and translate is the pipeline selector. The translate pipeline uses the key to find the corresponding translation for translation. The dictionary used to look up translations is loaded into the page memory by loading files. The translation file format is as follows:

        The pipeline finds the corresponding translation by calling the translation method in the language service class (LanService).

2. Language service

        The language service class (LanService) mainly includes methods for loading translation files, synchronous translation, asynchronous translation, and language setting.

2.1. Method of loading translation files

        Load translation files via HttpClient.get():

  /**
   * 加载翻译文件
   * @returns 
   */
  loadTranslation(languageCode: string = this.curLanguage) {
    const jsonFile = `assets/language/${languageCode}.json`;
    this.isLoading = true;
    return new Promise<void>((resolve, reject) => {
      this.http.get<any>(jsonFile).subscribe(data => {
        this.translation = data;
        this.isLoading = false;
        this.lanChange.next(languageCode);
        resolve();
      }, err => {
        reject(`load translation json error: ${err}`);
      })
    });
  }

        After the file is loaded, a language switching notification (this.lanChange.next(languageCode);) will be issued, and the method that subscribes to the notification will execute the corresponding subscription logic.

2.2. Synchronous translation method

        Obtain the corresponding translation in the current dictionary through the key. If no translation corresponding to the key is found, the key will be returned directly. If the service is loading the translation file, the key will also be returned directly:

  /**
   * 获取单个文本的翻译 同步方法
   * @param key 文本资源key
   * @returns 对应翻译结果字符串,未查找到返回key
   */
  translate(key: string) {
    if (this.isLoading) return key;
    else return this.translation[key] || key;
  }

        For some scenarios where it is determined that the language pack json has been loaded, you can use the synchronous translation method to obtain translations. For example, the text prompted by clicking the button on the page can be translated synchronously:

  /**
   * 弹出提示文字
   */
  showMessage() {
    alert(this.lanService.translate("app.welcome"));
  }

2.3. Asynchronous translation method

        Obtain the corresponding translation in the current dictionary by key, and the returned data is encapsulated in Observable format. If the service is loading translation files, subscribe to the notification of language switching, and then search for the corresponding translation after the file is loaded and send a notification; otherwise, directly search for the translation:

  /**
   *  获取单个文本的翻译 异步方法
   * @param key 文本资源key
   * @returns 对应翻译结果字符串,未查找到返回key
   */
  get(key: string) {
    if (this.isLoading) {
      return from(new Promise<string>(resolve => {
        this.lanChange.subscribe(lan => {
          resolve(this.translation[key] || key);
        })
      }));
    } else {
      return of(this.translation[key] || key);
    }
  }

2.4, set the language method

        When the method is called, the translation file is reloaded according to the incoming language, and the current language is updated after loading:

  /**
   * 设置页面语言
   * @param languageCode 语言代码 zh-CN | en-US
   */
  setLanguage(languageCode: string) {
    if (this.curLanguage != languageCode) {
      this.loadTranslation(languageCode).then(() => {
        this.curLanguage = languageCode;
      });
    }
  }

3. The page starts to load the translation file

        The translation file needs to be loaded when the page starts, and the loading method of page startup can be registered in AppModule:

export function StartUpServiceFactory(startUpService: StartUpService) {
  return () => startUpService.load();
}
const APP_INIT_PROVIDERS = [
  StartUpService,
  {
    provide: APP_INITIALIZER,
    useFactory: StartUpServiceFactory,
    deps: [StartUpService],
    multi: true
  }
];
@NgModule({
    //...
    providers: [
        ...APP_INIT_PROVIDERS
    ],
    // ...
})
export class AppModule { }

        Call the method of loading translation files of the language service class (LanService) in the startup service class (StartUpService):

  /**
   * 启动加载项
   */
  load() {
    this.lan.loadTranslation();
  }

4. Pipeline method

        In the pipeline method, the text conversion is performed by asynchronously obtaining the translation, and the incoming key and its translation will be cached, and each query will first query the cached value. Translations are fetched asynchronously without caching. The pipeline will also subscribe to the language switching notification of the language service class (LanService), and reacquire the translation after receiving the notification, and update the translation result:

  transform(key: string) {
    if (this.cacheKey == key) return this.translation;
    this.cacheKey = key;
    this.updateTranslation(key);
    this.unsubscribe();
    if (!this.lanChange$) {
      this.lanChange$ = this.lanService.lanChange.subscribe(lan => {
        if (this.cacheKey) {
          this.cacheKey = "";
          this.updateTranslation(key);
        }
      });
    }
    return this.translation;
  }

        Among them, the updateTranslation() method completes the asynchronous acquisition of translation:

  /**
   * 异步更新翻译
   * @param key 字符串key
   */
  updateTranslation(key: string) {
    let callback = (tran: string) => {
      this.translation = tran || key;
      this.cacheKey = key;
      this._ref.markForCheck();
    }
    this.lanService.get(key).subscribe(callback);
  }

        In order for Angular state checks to detect changes in the pipeline value, the pure field of the pipeline decorator must be set to false, otherwise the page will not render the text changes caused by updating the language:

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements PipeTransform{}

5. Implementation of custom instructions

        Using the above pipeline can fully meet the needs of language switching. However, when using the pipeline, only the key corresponding to each string can be seen in the template HTML page code. If it is a code written for teamwork or a long time ago, it cannot Immediately understand the translated text corresponding to the key. At this time, if it is implemented in the form of an instruction, the key is passed to the instruction through parameters, and the element innerText is modified through the instruction. At this time, the translation corresponding to any language can be reserved in the template HTML page, of course, Chinese is the most familiar:

<h1 [lan]="'app.title'">多语言切换演示</h1>
<span [lan]="'app.demo.text'">文本内容演示</span>
<button [lan]="'app.demo.btn'" (click)="showMessage()">点击提示文字</button>

        In this way, it can not only meet the needs of switching languages, but also can see the Chinese information at a glance, and improve the readability of the code. Command code:

  /**
   * 传入的字符串key
   */
  @Input('lan')
  set lan(value: string) {
    this.key = value;
    this.setText(value);
  }
  
    /**
   * 异步获取翻译设置元素内容文本
   * @param key 
   */
  setText(key: string) {
    key = key || '';
    if (key.length > 0) {
      const el = this.el.nativeElement;
      this.lanService.get(key).subscribe(tran => {
        el.innerText = tran;
        this.changeDetectorRef.markForCheck();
      });
    }
  }
  
  //订阅服务类通知更新翻译
  this.lanChange$ = this.lanService.lanChange.subscribe((lan) => {
      if (this.key.length > 0) {
        this.setText(this.key);
      }
   });

        But like placeholder, you can only use pipelines.

        Complete project code download: Angular uses pipelines and instructions to implement multi-language switching examples, no third-party library references-Typescript Documentation Class Resources-CSDN Download

Guess you like

Origin blog.csdn.net/evanyanglibo/article/details/128084926