[ ] 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:
I have a list of objects used for a dropdown containing a key from which a localized label is created. As the list is to be sorted by its localized label alphabetically the translation has to be done in code instead of within the template.
The user is able to switch the language during app usage. The documentation mentions the langChanges$ Observable to get informed on these language changes so we can update the list:
transloco.langChanges$.pipe(
takeUntil(this.unsubscribe$)
).subscribe(() => this.updateList());
If the language is switched to for the first time the langChange event is emitted before the corresponding language file has been loaded and thus errors are written to the console as well as leaving the labels untranslated.
First workaround is to wait for the load event of the translation file too:
transloco.langChanges$.pipe(
takeUntil(this.unsubscribe$),
switchMap(() => transloco.events$)
).subscribe(() => this.updateList())
But then of course this won't work if the file has already been fetched, getting us to
transloco.langChanges$.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
const testKey: string = 'existing-key';
if (testKey !== transloco.translate('existing-key')) {
this.determineFilterKeys();
} else {
transloco.events$.pipe(take(1)).subscribe(() => this.updateList()));
}
);
But you still get errors logged on the console and this does feel way too complicated.
The langChanges event should be emitted if the language is ready in that either the file with translations is already present or successfully loaded. Alternatively there should be a languageReady event or similar.
Loading of translation should be part of the language switch.
Angular version: 8.2.14
transloco version: 2.12.1
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: 12.2.0
- Platform: Mac/Windows
Others:
Hi @darkv,
I think what you are looking for is translationLoadSuccess event.
from docs:
translocoService.events$.pipe(
filter(e => e.type === 'translationLoadSuccess')
).subscribe(({ langName, scope }) => {
...
});
Thanks for that tip @itayod. But this unfortunately does only work partly.
Let's say we have an app and the available languages are _en_ and _de_ with default _en_. The user loads the app and automatically _en.json_ will be loaded and the translationLoadSuccess event fired. Now the user switches to _de_. Then the German translation file _de.json_ will be loaded and again translationLoadSuccess event is emitted. So far so good.
Next the user decides to switch back to English but the translation file has already been fetched before so no loading event will be emitted and no code will be triggered.
I have a list of objects used for a dropdown containing a key from which a localized label is created. As the list is to be sorted by its localized label alphabetically the translation has to be done in code instead of within the template.
ts file, I suggest you use selectTranslationObject method.@darkv Did you ever find a solution to this? I'm also in a situation where a Scoped Component relies on "in code" translations to pass to a graph service.
I tried using ...
this.translocoService.events$.pipe(
filter(
e =>
e.type === 'translationLoadSuccess' &&
e.payload.scope === this.scope.scope
)
).subscribe(() => {});
... but this only updates on the initial load, and previously said, won't fire again when the language is changed for the 2nd time. Whilst this.translocoService.langChanges$.subscribe(lang => ...); will fire, but then you won't be able to translate until the file is loaded.
I'm not an RxJS wizard, so not sure there is a use case which would wait for Sub A to emit a value (translationLoadSuccess) before emitting the lang change event, but if already loaded emit lang change on it's own.
@ChazUK translationLoadSuccess is fired when the translation loads, when you switch back and forward the translation is already loaded, and therefore this event isn't fired again, unlike the langChanges$ which fires on every lang change.
Why not listen to the value changes?
this.translocoService.selectTranslate('some.translate').subscribe((value) => {
this.updateGraphTranslations(value)
});
This is just an example based on what you wrote.
@ChazUK Like @shaharkazaz suggests use a specific translation key for probing to detect a language change. For better code readability I have created a small helper function that plugs everything together and will trigger a passed callback function.
@darkv are you able to share that helper? A lot of the translations in code are powered by specific keys passed into the component, or grabbed from API data. I'm guessing you have a "fake" translation which you listen for changes?
That is what I currently have:
export function onLangChange(
transloco: TranslocoService,
testKey: string,
callbackFn?: (this: void) => any,
): Observable<void> {
return transloco.langChanges$.pipe(
switchMap(() => transloco.selectTranslate(testKey)),
tap(() => callbackFn ? callbackFn() : null));
}
Then you can use this somewhere in your code:
onLangChange(this.transloco, 'some.translation.key', () => this.callWhenLanguageChanges())
You could even get rid of the second _testKey_ parameter if you can rely on some key that will always be present in your projects.
@darkv I'm not suggesting to listen to a language change via some key from the translation. it was an example since it seems like he is looking for a translation value to pass into his graph service.
If you just want an indication that both the language has changed but the new translation is loaded (from either cache or server) why not do:
this.translocoService.langChanges$.pipe(switchMap(() => this.selectTranslation())).subscribe(() => {
...do some translations
});
And if you want to wait for a specific scope you can pass the value into the selectTranslation with the lang given in the langChanges$
Let me think about it, maybe we need to expose something from the service that replaces that code.
I've tried converting it to this, but all I'm getting is Missing translation for 'testScope.loaded'.
ngOnInit() {
this.subscriptions$.add(
combineLatest([
this.translocoService.selectTranslate(`${this.scope.scope}.loaded`),
this.chartData$,
]).subscribe(([loaded, data]) => {
console.log(loaded);
if (this.chart) {
this.updateChart();
} else {
this.renderChart();
}
})
);
}
The template has <ng-container *transloco="let t; read: 'annualReturnsGraph'">{{ t('loaded') }}</ng-container> and is displaying the string correctly.
@ChazUK have you tried my suggestion?
@shaharkazaz I have.
In Component
this.translocoService.langChanges$
.pipe(switchMap(() => this.translocoService.selectTranslation()))
.subscribe(translation => console.log(translation));
Initially it displays my global translations which I have loaded at the module level with a custom loader.
Module
@Injectable({ providedIn: 'root' })
export class CustomLoader implements TranslocoLoader {
getTranslation(lang: string) {
return import(`../i18n/${lang}.json`);
}
}
It doesn't fire again when component scope is loaded. Same when I change language for the first time, the new translations from the global JSON for the new language is loaded, but not the scoped.
@ChazUK I'm confused, do you need an indication that the translations were loaded, or do you need the values? according to your example seems like you don't care about the translation values you just want to trigger a chart reflow.
Also, as I stated:
And if you want to wait for a specific scope you can pass the value into the
selectTranslationwith the lang given in thelangChanges$
The example you wrote will only fire at the main translation file loading.
... Same when I change language for the first time
Do you mean the language that the app is initiated with? or when you actually (calling the setActiveLang) change the language it doesn't fire?
I need the value, but I don't know specifically what value I need, which is why I used a dummy key ${this.scope.scope}.loaded in the first example.
I want to update the graph when A) the data is updated via onChanges, B) when the language changes. And because of B I need to know when the translation files are loaded because I'm using Scoped Loaders for my components.
@ChazUK ok, have you tried my example with specifying the scope to the selectTranslation function?
Also I'm confused at how I can wait for a specific scope with selectTranslation? I can only set language as a parameter for that method. If you mean using selectTranslate I still get the same issue of the translation not being loaded of first firing, which would mean my graph labels would be undefined, or display the transloco key.
this.translocoService.langChanges$
.pipe(
switchMap(lang =>
this.translocoService.selectTranslate(
`${this.scope.scope}.loaded`,
{},
lang
)
)
)
.subscribe(translation => console.log(translation));
@ChazUK No, I meant selectTranslation. See this stackblitz example and tell me if that's what you need.
@shaharkazaz I have tried, with that implementation I am getting the initial console log Active lang is, but then the selectTranslation isn't completing.
I'm not sure why my forked stackblitz isn't working https://stackblitz.com/edit/transloco-waiting-for-scoped-translations-cicniq?file=src/app/lazy/lazy/lazy.component.ts, but here's the code below.
_demo/i18n/en.json_
{
"title": "Scoped Inline Component Demo English"
}
_demo/i18n/es.json_
{
"title": "Scoped Inline Component Demo Spanish"
}
_demo/demo.component.ts_
import { Component, Inject, OnInit } from "@angular/core";
import { TranslocoService, TRANSLOCO_SCOPE } from "@ngneat/transloco";
import { switchMap } from "rxjs/operators";
import { scopeLoader } from 'scoped-translations';
@Component({
selector: "app-demo",
template: `
<ng-container *transloco="let t">
<h1>{{ t("demo.title") }}</h1>
</ng-container>
`,
providers: [
{
provide: TRANSLOCO_SCOPE,
useValue: {
scope: "demo",
loader: scopeLoader((lang, root) => import(`./${root}/${lang}.json`)),
},
},
],
})
export class DemoComponent implements OnInit {
constructor(
protected translocoService: TranslocoService,
@Inject(TRANSLOCO_SCOPE) protected scope
) {}
ngOnInit() {
this.translocoService.langChanges$
.pipe(
switchMap((lang) => {
console.log("Active lang is:", `${this.scope.scope}/${lang}`);
return this.translocoService.selectTranslation(
`${this.scope.scope}/${lang}`
);
})
)
.subscribe((v) => {
console.log("Lang changed and scope loaded!");
console.log({
title: this.translocoService.translate(`${this.scope.scope}.title`),
});
});
}
}
_scoped-translations.ts_
export const availableLangs = ["en", "es"];
export const scopeLoader = (importer, root = "i18n") => {
return availableLangs.reduce((acc, lang) => {
acc[lang] = () => importer(lang, root);
return acc;
}, {});
};
@ChazUK Seems like Stackblitz doesn't support Webpack's dynamic imports and that's why it's not working for you.
I have created an implementation on code sandbox, please try it there.
We are thinking about exposing a new function from the service that will take most of the boilerplate code in my example into a single function.
@shaharkazaz Thanks for the demo. I had a few problems with the loader as I want to reuse it across many components, so updated that to
export const availableLangs = ['en', 'es'];
export const scopeLoader = (importer, scope = null, root = 'i18n') => {
return availableLangs.reduce((acc, lang) => {
acc[scope ? `${scope}/${lang}` : lang] = () => importer(lang, root);
return acc;
}, {});
};
This can then be included on general components which don't need "in code" translations, omitting the scope in the loader:
@Component({
selector: 'app-general',
template: `
<ng-container *transloco="let t; read: 'generalComponent'">{{
t('title')
}}</ng-container>
`,
providers: [
{
provide: TRANSLOCO_SCOPE,
useValue: {
scope: 'generalComponent',
loader: scopeLoader((lang, root) => import(`./${root}/${lang}.json`)),
},
},
],
})
export class GeneralComponent
And now can also be used in components which require "in code" translations
const loader = scopeLoader(
(lang, root) => import(`./${root}/${lang}.json`),
'inCode'
);
@Component({
selector: 'app-in-code',
template: `
<div #view></div>
`,
providers: [
{
provide: TRANSLOCO_SCOPE,
useValue: {
scope: 'inCode',
loader
},
},
],
})
export class InCodeComponent implements OnInit {
constructor(
protected translocoService: TranslocoService,
@Inject(TRANSLOCO_SCOPE) protected scope,
) {}
ngOnInit() {
this.translocoService.langChanges$
.pipe(
switchMap(lang => {
console.log('Active lang is:', lang);
return this.translocoService.load(`${this.scope.scope}/${lang}`, {
inlineLoader: loader,
});
})
)
.subscribe(v => {
console.log('Lang changed and scope loaded!', v);
});
}
}
Tested and working! Will look forward to the new function.
This still isn't merged yet - where is the problem to add a cleanway to allow this... for example only fire the language switch event after the required lang is available... i have the problem that i have to translate the labels on an object in code... and in the view i can't bind to an observable because it comes from a lib. So when i switch from initial lang to alternating lang and trigger my translation onChange - i get the keys back because the language isn't there yet... all order ways are somehow hacky... This should just work imho
Most helpful comment
@ChazUK Seems like Stackblitz doesn't support Webpack's dynamic imports and that's why it's not working for you.
I have created an implementation on code sandbox, please try it there.
We are thinking about exposing a new function from the service that will take most of the boilerplate code in my example into a single function.