DEV Community

Gaëtan Redin
Gaëtan Redin

Posted on • Originally published at Medium on

Angular multi translation files

Done with ngx-translate

I’ve been using ngx-translate for as long as I can remember. I find it easier than the native Angular i18n solution.

Translations is often complicated to maintain because the translation file grows as the application grows.

My use cases:

  • I don’t want to have all my translations in one single file
  • I want to know where to write my translations
  • I want to keep a common file because there are still common wordings
  • Bonus, I want to make that more readable, reviewable

Project Architecture

This is how my assets are architectured to let the solution works. this can be done in an another way but I don’t variabilize the path to the common files (which can be done if you want to handle it).

- assets
  - ...
  - i18n
    - common
      - fr.json
      - en.json
      - ...
    - feature-1
      - fr.json
      - en.json
      - ...
    - feature-2
      - fr.json
      - en.json
      - ...
    - ...
Enter fullscreen mode Exit fullscreen mode

Custom loader

here is the doc about how to create a custom loader.

Let’s read my suggestion for a custom loader:

// model for a resource to load
export type Resource = { prefix: string; suffix: string };


export class MultiTranslateHttpLoader implements TranslateLoader {
  resources: Resources[];
  withCommon: boolean;

  constructor(
    private readonly http: HttpClient,
    { resources, withCommon = true }: { resources: Resource[], withCommon?: boolean }
  ) {
    this.resources = resources;
    this.withCommon = withCommon;
  }

  getTranslation(lang: string): Observable<Record<string, unknown>> {
    let resources: Resource[] = [...this.resources];

    if (this.withCommon) {
      // order matters! like this, all translations from common can be overrode with features' translations
      resources = [
        { prefix: './assets/i18n/common/', suffix: '.json' }, 
        ...resources
      ];
    }

    return forkJoin(resources.map((config: Resource) => {
      return this.http.get<Record<string, unknown>>(`${config.prefix}${lang}${config.suffix}`);
    })).pipe(
      map((response: Record<string, unknown>[]) => 
        mergeObjectsRecursively(response)),
    );
  }
}

export const mergeObjectsRecursively = 
    (objects: Record<string, unknown>[]): Record<string, unknown> {
  const mergedObject: Record<string, unknown> = {};

  for (const obj of objects) {
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        if (typeof obj[key] === 'object' && obj[key] !== null) {
          mergedObject[key] = mergeObjectsRecursively([mergedObject[key], obj[key]]);
        } else {
          mergedObject[key] = obj[key];
        }
      }
    }
  }

  return mergedObject;
}
Enter fullscreen mode Exit fullscreen mode

How to use it

With common translations:

TranslateModule.(forRoot|forChild)({
  loader: {
    provide: TranslateLoader,
    useFactory: (http: HttpClient): MultiTranslateHttpLoader => {
      return new MultiTranslateLoader(http, {
        resources: [
          { prefix: './assets/i18n/feature-1/', suffix: '.json' },
          { prefix: './assets/i18n/feature-2/', suffix: '.json' },
          ...
        ],
      });
    },
    deps: [HttpClient],
  },
})
Enter fullscreen mode Exit fullscreen mode

Without common translations:

TranslateModule.(forRoot|forChild)({
  loader: {
    provide: TranslateLoader,
    useFactory: (http: HttpClient): MultiTranslateHttpLoader => {
      return new MultiTranslateLoader(http, {
        withCommon: false,
        resources: [
          { prefix: './assets/i18n/feature-1/', suffix: '.json' },
          { prefix: './assets/i18n/feature-2/', suffix: '.json' },
          ...
        ],
      });
    },
    deps: [HttpClient],
  },
})
Enter fullscreen mode Exit fullscreen mode

Result

// assets/i18n/feature-1/en.json

{
  "HELLO": "HELLO",
  "CIVILITIES": {
    "MR": "Mister",
    "MS": "Miss"
  }
}

// assets/i18n/feature-2/en.json
{
  "TITLE": "LONG TITLE",
}

// generated translations
{
  "HELLO": "HELLO",
  "CIVILITIES": {
    "MR": "Mister",
    "MS": "Miss"
  },
  "TITLE": "LONG TITLE",
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now you can split your translation files, you will see how it will be easier to read, maintain and review. Don’t forget to use the lazy loading feature (from Angular) and then you will load only translation files required for a specific route.

If you need more details or want to give me your opinion let me a comment, don’t hezitate.

Thanks for reading.

Top comments (0)