Transloco: Custom component's dynamic translation loading

Created on 8 Oct 2019  路  30Comments  路  Source: ngneat/transloco

I'm submitting a...


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

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

Current lazy loading behavior supports only application-related translations that are placed at one global folder. Even this case requires additional time to maintain component's translation files that are separated with component's folder.

i18n/
  en.json
  es.json
  todos/
    en.json
    es.json
src/
  some/
    module/
      component/
        todos/
          images/
          todos.ts
          todos.html
          todos.scss

Also there is no way to seamlessly provide lazy loaded shared lib components translations. Our case is a monorepo with a lot of libs that are used in different applications (at the same repository).

It would be cool to have an ability to provide custom dynamic imports for TRANSLOCO_SCOPE, like this

todos/
  images/
  translation/
    en.json
    es.json
  todos.ts
  todos.html
  todos.scss
export function en() { return import('./translation/en.json'); }
export function es() { return import('./translation/es.json'); }

@Component({
  providers: [
    {
      provide: TRANSLOCO_SCOPE,
      useFactory: () => ({
        scope: 'todos',
        translations: { en, es }
      }),
    },
  ]
})
export class ToDosComponent {}

Currently we found one way to achieve this with some hacks, but it works only at a module level and requires more code

Stackblitz https://stackblitz.com/github/artaommahe/transloco-dynamic-translations/tree/module
Repo https://github.com/artaommahe/transloco-dynamic-translations/tree/module

There are an eagerly loaded component, dynamically loaded via routing and a component from lib. All have dyncamically loading translations that are placed next to the components files. E.x.

And the translations loading code on lang changes
https://github.com/artaommahe/transloco-dynamic-translations/blob/module/libs/translate/src/module.ts#L18

updated solution below: https://github.com/ngneat/transloco/issues/121#issuecomment-540535199

PRs welcome enhancement

Most helpful comment

Inline loaders are now avialable in v2.2.0.

We also released a schematics command to build a unified translation file from all the sub-translation files.
Currently, it supports JSON format, but other formats support is in progress. 馃檪

All 30 comments

@artaommahe It sounds like a helpful feature. Do you want to create a PR?

@artaommahe: We also put the translations in our components, and we solve it by using Webpack, but it would be much nicer if it was a "native" feature.

I would reconsider the implementation, though. Providing the TRANSLOCO_SCOPE provider with the scope (path to the translation folder for the component) should be enough, so you don't have to load the files yourself in all components.

@andreaslarssen can you give an example of how you do it with Webpack? I'm curious. Thanks.

// extra-webpack.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin');
const jsonminify = require('jsonminify');

module.exports = {
  plugins: [
    new CopyWebpackPlugin([
      {
        context: './src/app/',
        from: '**/translations/*.json',
        to: 'assets/i18n/',
        transform (content) {
          // https://github.com/fkei/JSON.minify
          return jsonminify(content.toString());
        }
      }
    ])
  ]
};

Nice! thanks. But I think it will be nice to get support out of the box as @artaommahe suggested.

While implementing this approach in our codebase, found more simple way at the component level using existing TRANSLOCO_SCOPE token.
The main idea is to have a way to register translation at the component first creation moment. And here TRANSLOCO_SCOPE plays his role cause it's injected in template's pipe/directive. Using factory for providing this token we get a factory function that is called once when token is injected for one component instance.

Stackblitz https://stackblitz.com/github/artaommahe/transloco-dynamic-translations/tree/master
Repo https://github.com/artaommahe/transloco-dynamic-translations

Also it works at the module level with same code - provideTranslations at module's providers.

@andreaslarssen we cant use webpack configuration cause it requires manual setup for each of our dozens libs to copy their translations + additional storybook configuration and so on :(

@artaommahe It sounds like a helpful feature. Do you want to create a PR?

nope :( im not sure that my solution is good enough, hope there is something better

In this repo, we have implemented inline loaders. Please check it out and give us your feedback so we can move forward to a release.

Installation:

  1. Clone the repo.
  2. Run npm install.
  3. Replace the Transloco lib in the node modules with the one provided repo's root. ( this is a local version with this feature )
  4. Run npm start

It's really nice that you've implemented inline loaders, but are there really no other ways of doing this then:

export function en() { return import('./translation/en.json'); }
export function es() { return import('./translation/es.json'); }

If you want to load files in each component, all components needs this kind of imports/exports. And imagine if you support 10-15 languages!

We used a few hours yesterday trying to come up with something, and we where thinking it could make sense have a @Transloco() decorator that could take a scope and / or alias?

@Transloco({
  scope: 'translations', // Optional. Defaults to i18n
  alias: 'MY_ALIAS' // Optional?
})

What do you think?

Also, in the new implentation, the "scope" name is used, but it's taken the job of the alias. It's no longer the path (as I think scope should be renamed to), but a root key (alias) if I'm not mistaken?

providers: [
    {
      provide: TRANSLOCO_SCOPE,
      useFactory: () => ({
        scope: 'compA',
        loader: { en, es }
      })
    }
  ]

@andreaslarssen
Can you please explain why decorators? And what is he supposed to do? can you share the implementation?

You won't have to import these inline loaders in every component.
You will import them just in the main lazy-loaded module where you need them (it could be a component if you provide it there).

The implementation presented in the repo is just a POC.
Here is a suggestion of handling multiple languages inline loaders.
Since all the languages will are stated in the availableLangs property in the transloco config, we will export them:

export const availableLangs = ['en', 'es' .... 13 more ];

Then when creating the inline loaders, we will use this export:

import { availableLangs } from "...";

export const loader = availableLangs.reduce((acc, lang) => {
    acc[lang] = () => import(`../i18n/${lang}.json`);
    return acc;
  }, {});

And in our providers:

import {loader} from "..."

providers: [
    {
      provide: TRANSLOCO_SCOPE,
      useFactory: () => ({
        scope: 'compA',
        loader
      })
    }
  ]

Also, in the new implentation, the "scope" name is used, but it's taken the job of the alias. It's no longer the path (as I think scope should be renamed to), but a root key (alias) if I'm not mistaken?

This is a good point and we will figure it out 馃檪

@shaharkazaz i've tried this version with my repo's code and it works fine. Just mapped lib to local folder and changed my provideTranslation helper to

export function provideTranslation(scope: string, translations: IComponentTranslations): Provider {
  return {
    provide: TRANSLOCO_SCOPE,
    useValue: { scope, loader: translations },
  };
}

waiting for new version, thx )

Awesome! That is exactly what I need.

Can you provide a release date?

Thanks!

@NachoVazquez @artaommahe
I'm curious about 2 things in your workflow:

  1. How do you send the splitted files to translation? we are currently working on a merge plugin.
  2. Don't have any issues with the fact that the translations are loaded asynchronously with the module? are you showing some sort of loading?
  1. I'm not sure if I correctly understood your first question. But what I'm doing at the moment for a given lib is:
  2. Dynamically loading the translations of the lib, injecting the TranslationService in the main module of the lib. (any module will do, but it needs to be loaded before any other module on that lib)
  3. Encapsulate the translations in their scope to avoid key collisions.
  4. Use the structural directive with the read attribute in templates.

Please let me know if you need to know anything else.

  1. I didn't see any problem with translations loading time. At this moment I'm not showing a loading, primarily, because I have to check with the rest of the team what would be a consistent visual approach. But I'll get you posted about my results.

@shaharkazaz havent understood first question ) our workflow mainly is in provided repo-examples.
We're ignoring translation loading issue, 95% of our app routes requires authorization. Bundling one default translation statically by import('./translation/en.json'); in app module file (where this file contains just empty string). For other translations we just load them right after authorization and during route guards resolving so no strings flashing for 99% cases.
Also we follow translations folder and files naming agreements to bundle translations to one bundle for each language: always ./translation/{lang}.json files + namedChunks: true or using import(/* webpackChunkName: translation-{lang}-json */ './other/path.json')

The idea behind using decorators came because I was searching for two things:

  1. A way of configuring file loading in the component using the translation file(s)
  2. Make sure the file was loaded before the component was initialized

As for no. 1, my suggested implementation looks something like this:

export function Transloco({scope, alias}) {
  return ((target: Function) => {
    const request = async () => {
      // TODO: Fetch all languages from defined folder
    };

    request().catch();
  });
}

And then:

@Transloco({scope: 'MY_SCOPE', alias: 'MY_ALIAS'})
export class SomeClass(){}

As for no. 2, I now realize that it's (probably) not possible, as it seems like the Angular team doesn't want you to block app bootstrapping in any way. That means I'm not able to do what I want to do, so I'm left with two choices:

  1. Preloading all language (ugh)
  2. Load languages in components and hope that they will resolve in time (ugh)

@andreaslarssen You can listen to the language loaded event and bootstrap your component when it fires.
This way you are sure the language is loaded, and it's not left for luck 馃檪

@artaommahe @NachoVazquez Thanks for the answers! I'll try to reexplain the first question:
You are developing the app in one primary language (correct me if I'm wrong) let's say it's English for the question's sake.
Then you send you JSON file to be translated into the other languages you are supporting, how does the process of sending those files to translation is working? Since your entire translation is split into many JSON files?

@shaharkazaz currently we dont have enough files to have a problems with this. Further were thinking about script that parses our provideTranslation helper usage and outputs scope + translated langs + fields list in default language, this output will go to translators.

I see what you mean. At this point, we are only supporting English. The first step was to detach the hard-coded strings from the template.

In the future the app will support multiple languages, but not all libs and apps may need to be translated. For example, admin apps may not have the requirement of supporting i18n. Another case is when a lib is used both in an admin app and a public one. In that case, the lib needs to support multiple languages. The process in our case will be a matter of need. We will send the libs *.json that need to be translated.

I might have to think of a process of flagging the libs that support i81n if I want a script like the one described by @artaommahe .

For avoiding key conflicts we are following the next best practices.

  • Scope apps and libs into its project name
  • Sub-scope components using component name
  • Add shared scope if needed, only accessible to the project scope. Used to handle common keys like forms errors and others

@shaharkazaz can you show me an example of bootstrapping the component on the language loaded event?

@artaommahe @NachoVazquez Thanks for the input you guys, as I said we are working on something that might help you with this. stay tuned 馃憤

@andreaslarssen well, it's not actually delaying the bootstrap, but more of delaying the content.
The way I see it, you wrap your actual content inside a ngIf which is an indication for a loading state, and once the event fires you change the flag and load the actual content.

<ng-container *ngIf="!langLoaded; else content">
  some loader
</ng-container>
<ng-template #content>...</ng-template>

I think we are getting off-topic here though if you want we can continue this on gitter 馃檪

@shaharkazaz I was just curious as you said (in another issue) that you don't handle string glitching. It's related in the way that dynamic loading (as I see it) needs to handle string glitching (making sure the file is loaded before view renders). Delaying content like that would make the app look funky. But no need to take it further, just wasn't sure what you meant.

Inline loaders are now avialable in v2.2.0.

We also released a schematics command to build a unified translation file from all the sub-translation files.
Currently, it supports JSON format, but other formats support is in progress. 馃檪

@shaharkazaz checked 2.2 with some our cases (that works with our hacky solution)

module-level loaders does not work for non-lazy modules due to singleton-token
https://stackblitz.com/edit/ngneat-transloco-loaders-in-modules?file=src%2Fapp%2Fapp.module.ts

<!-- actual output -->
module1.caption
Module 2
<!-- expected output -->
Module 1
Module 2

also there is an error in console for langChanges$ listener, looks like no CD tick for some cases

@artaommahe

module-level loaders does not work for non-lazy modules due to singleton-token

As you said, following the Angular rules, this only works with lazy-loaded modules or component level providers. (I'll check with @NetanelBasal to see if we can resolve this somehow)

also there is an error in console for langChanges$ listener, looks like no CD tick for some cases

Just run this.cdr.detectChanges();

Just run this.cdr.detectChanges();

looks like there already should be CD tick on translation changes. Manual CD usage usually is a mark of some CD problems in other code..

Amazing!! I'll test it tonight.

@artaommahe It is not related to Transloco. That is how Angular works. The first and the second change are at the same tick.

@shaharkazaz can you clarify why there are two changes at the same tick? looks strange to me

Was this page helpful?
0 / 5 - 0 ratings

Related issues

FallenRiteMonk picture FallenRiteMonk  路  8Comments

SparkMonkey picture SparkMonkey  路  7Comments

gernsdorfer picture gernsdorfer  路  4Comments

zufarzhan picture zufarzhan  路  3Comments

flgraveline picture flgraveline  路  7Comments