[ ] 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:
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
@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:
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:
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)Please let me know if you need to know anything else.
@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:
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:
@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.
@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
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. 馃檪