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.

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay