transloco: Language fallback doesn't work properly

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request
[ ] Other... Please describe:

Current behavior

Custom TranslocoFallbackStrategy not working properly. It will skip some of the fallback langs. Because the this.failedCounter is misused. the fallbacks variable is re-evaluate on each failure, because the parameter passed to this.fallbackStrategy.getNextLangs(lang) is the current failed language. It’s not the original failed language. So when this.failedCounter’s value increases to 1. It will ignore the first element in new fallbacks.

Given the original lang zh-Hans-CN:

  1. Load zh-Hans-CN failed.
  2. getNextLangs() returns ['zh-Hans', 'zh', 'en']. failedCounter is 0. It will try to load zh-Hans. And increase failedCounter to 1
  3. Load zh-Hans failed
  4. getNextLangs() returns ['zh', 'en'], because handleFailure receives zh-Hans as parameter instead of zh-Hans-CN. failedCounter is 1. It will try to load en. Error Here: ‘zh’ is skipped!

https://github.com/ngneat/transloco/blob/master/projects/ngneat/transloco/src/lib/transloco.service.ts#L558

  private handleFailure(lang: string, mergedOptions) {
    const splitted = lang.split('/');
    const fallbacks = mergedOptions.fallbackLangs || this.fallbackStrategy.getNextLangs(lang);
    const nextLang = fallbacks[this.failedCounter];
    this.failedLangs.add(lang);

Expected behavior

Given the original lang zh-Hans-CN, and getNextLangs() returns ['zh-Hans', 'zh', 'en'].

Transloco should try to load files in the following order:

  1. zh-Hans-CN.json
  2. zh-Hans.json
  3. zh.json
  4. en.json

In above scenario, zh.json will be skipped.

Minimal reproduction of the problem with instructions

For bug reports please provide the STEPS TO REPRODUCE and if possible a MINIMAL DEMO of the problem, for that you could use our stackblitz example

What is the motivation / use case for changing the behavior?

Environment


Angular version: X.Y.Z


Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: XX  
- Platform:  

Others:

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 23 (12 by maintainers)

Most upvoted comments

  • As a fix for the current situation the getNextLangs should be called only once on the first language failure.

    • Totally agree. This is what I’m trying to do in the fix PR. The getNextLangs() is only called at the first time of loading translation failed. And the langs in NextLangs in tried one by one, until loading a translation successfully. In the meanwhile, getNextLangs() must not be evaluated again.
  • In the future I think that getNextLangs should be replaced with getNextLang, so the strategy will decide individually for each failed lang what should be loaded next.

    • I really like this idea. From a API consumer perspective, provide a single fallback language according to the failed one is enough. It’s simpler and easier to understand. From transloco perspective, it’s easier to implement, I think.

@shaharkazaz

@shaharkazaz Thanks very much for replying.

This is my custom TranslocoFallbackStrategy:

export class LanguageFallbackStrategy implements TranslocoFallbackStrategy {
  static readonly defaultLang = 'en';

  /**
   * @example
   * 
   * failedLang: 'zh-Hans-CN'
   * Returns: ['zh-Hans', 'zh', 'en']
   *
   * lang: 'fr-CA'
   * Returns: ['fr', 'en']
   */
  getNextLangs(failedLang: string): string[] {
    const langs = this.getAllLangs(failedLang);
    return failedLang === langs[0] ? langs.slice(1) : langs;
  }

  /**
   * @example
   * 
   * lang: 'zh-Hans-CN'
   * Returns: ['zh-Hans-CN', 'zh-Hans', 'zh', 'en']
   *
   * lang: 'fr-CA'
   * Returns: ['fr-CA', 'fr', 'en']
   */
  getAllLangs(lang: string): string[] {
    if (this.isLangValid(lang)) {
      const langs: string[] = [];

      // For example, `zh-Hans-CN` may next check for
      // `zh-Hans`, then if `zh-Hans` is not found, `zh`.
      const splitted = lang.split('-');
      splitted.reduce((acc, cur) => {
        const lang = acc ? `${acc}-${cur}` : `${cur}`;
        langs.unshift(lang);
        return lang;
      }, '');

      // append the default language to the end
      const lastLang = langs[langs.length - 1];
      if (lastLang !== LanguageFallbackStrategy.defaultLang) {
        langs.push(LanguageFallbackStrategy.defaultLang);
      }

      return langs;
    } else {
      return [LanguageFallbackStrategy.defaultLang];
    }
  }
}

And add it to root providers:

export const translocoFallbackStrategy = {
  provide: TRANSLOCO_FALLBACK_STRATEGY,
  useClass: LanguageFallbackStrategy,
};

@NgModule({
  imports: [TranslocoModule],
  exports: [TranslocoModule],
})
export class I18nModule {
  static forRoot(): ModuleWithProviders<I18nModule> {
    return {
      ngModule: I18nModule,
      providers: [
        acceptLanguageInterceptor,
        selectBrowserLanguageProvider,
        translocoConfig,
        translocoFallbackStrategy,
        translocoLoader,
      ],
    };
  }
}

I thought this is all I need to code to make the language fallback work. However, this is not working as I expected. 😄

@zhongsp hey! Really busy times for me, I’ll try to get to it this week, but no promises 🙏

Once I’ll get some time I’ll address it!