Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug(inline-loader): Translations with the inline loader strategy only work at the template level #539

Open
1 task done
sawa-ko opened this issue Jan 13, 2022 · 16 comments

Comments

@sawa-ko
Copy link

sawa-ko commented Jan 13, 2022

Is there an existing issue for this?

  • I have searched the existing issues

Which Transloco package(s) are the source of the bug?

Transloco, Locale

Is this a regression?

Yes

Current behavior

I try to get translations through my component's service, but it always throws error that the translation has not been found, but in the component's template it works perfectly.

I have tried the .translate() and .selectTranslate() methods and nothing works.

Code

@Component({
  templateUrl: './commands.component.html',
  styleUrls: ['./commands.component.less'],
  providers: [
    {
      provide: TRANSLOCO_SCOPE,
      multi: true,
      useValue: {
        scope: 'commands/page',
        alias: 'commands',
        loader: i18nLoader((lang: string) => import(`./i18n/${lang}.json`)),
      },
    },
  ],
})
export class CommandsComponent {
  constructor(private i18nService: TranslocoService) {
    this.i18nService.selectTranslate('commands.service.meta.title').subscribe((x) => console.log(x));
  }
}

Results

image

image

image

Expected behavior

Correctly obtain the translations at the service and template level of the component.

Please provide a link to a minimal reproduction of the bug

N/A

Transloco Config

No response

Please provide the environment you discovered this bug in

Transloco: 3.1.1
Angular: 13.1.1
Node: 16.11.0
Package Manager: yarn
OS: Windows 11

Browser

Microsoft Edge Version 97.0.1072.55

Additional context

The translation keys are correct and I have also tried to use the same translation keys that are in the template and they work but it does not work at the service level either.

I would like to make a pull request for this bug

No

@noobyogi0010
Copy link

Hi, @kaname-png
I would like to work upon this issue if no one else has picked it up already.

@sawa-ko
Copy link
Author

sawa-ko commented Jan 30, 2022

@noobyogi0010 please.

@sawa-ko
Copy link
Author

sawa-ko commented Mar 17, 2022

Can you take a look at the problem? @shaharkazaz

@shaharkazaz
Copy link
Collaborator

@noobyogi0010 do you still want this issue? :)
@kaname-png I'm swamped I couldn't reach the issue or any other in matter of fact that are pending in Transloco.
I'll try to get to it sometime soon.

@sawa-ko
Copy link
Author

sawa-ko commented Mar 18, 2022

Thanks for answering @shaharkazaz, I hope you can or someone can solve this problem, this prevents me from being able to continue with my project since I need it.

And thank you very much in advance.

@mehrad-rafigh
Copy link

mehrad-rafigh commented Oct 6, 2022

Hey @shaharkazaz any update on this issue? I am facing the same issue.

@shaharkazaz
Copy link
Collaborator

@mehrad-rafigh you are welcome to pick this up and open a PR :)
I have no estimate on this from my end.

@mehrad-rafigh
Copy link

@shaharkazaz I would like to do that. Can you please point me to the direction? I haven't contributed to transloco before

@shaharkazaz
Copy link
Collaborator

@mehrad-rafigh I'm currently on vacation so I don't have my laptop and I didn't get the chance to investigate the issue myself.

I suggest you start by creating a small dedicated repo that reproduces the issue and try to work your way from there by using a local version of the library so you can debug the issue easily.

@gmiklich
Copy link

I encountered the same issue, but I found that if you pass a scope as the third argument to the selectTranslate method, it will wait for the scope to load. You can't pass in a string like scopeName, but if you pass in a ProviderScope (easiest thing to do is probably just to inject the TRANSLOCO_SCOPE object), it works.

To cater to your specific example, I think the following will work:
this.i18nService.selectTranslate('service.meta.title', {}, this.translocoScope[0]).subscribe((x) => console.log(x));

I used an array accessor there since you have the scope provider marked as multi: true, so to be fair, it might not be at the start of the array, but I think you get the point. this.translcocoScope would be the injected token btw.

@wall-street-dev
Copy link

Also facing the same issue here :)

@ratatoeskr666
Copy link

The issue arises when scopes are provided via mutli: true.
It seems like the scopes are not yet necessarily fully loaded when using translocoService.translate(...). translate only looks the key up in the existing translations. When this method is being called before the scope was loaded before, it returns undefined.

@gmiklich approach works fine and it does make sense that a asynchronous method is needed when retrieving a lazy module translation.

I thought about a way by initialize lazy loaded modules during the dynamic import, so that the localization download is being included in the lazy load but I didn't come up this a good solution. Maybe some of you guys has an idea

@henryruhs
Copy link

henryruhs commented May 27, 2023

I wrote this provider factory, that might be useful for one or another person.

import { Provider } from '@angular/core';
import { TRANSLOCO_SCOPE } from '@ngneat/transloco';

export function translateProvider(scope : string) : Provider
{
	const provider : Provider =
	{
		provide: TRANSLOCO_SCOPE,
		useValue:
		{
			scope,
			loader:
			{
				en: () => import('libs/frontend/' + scope + '/src/assets/i18n/en.json'),
				de: () => import('libs/frontend/' + scope + '/src/assets/i18n/de.json')
			}
		},
		multi: true
	};

	return provider;
}

It can either be used in modules (feature lib) or directly in components:

providers:
[
	translateProvider('shared')
]

Unfortunately I face the same issue of unloaded scopes which are causing flickers and repaints on the app. From my observation using translate() in pipes is totally broken - only using pure: false works as a workaround otherwise the untranslated text stays on first load, on second load it appears instantly. My app is using a typical app-shell - feature-one - shared approach, where loading the shared scope and therefore the second is causing issues.

@ratatoeskr666
Copy link

I don't know if it helps, but I found kind of a hackaround.

First of all, my setup:
Host app module defines a http loader

@NgModule({
  exports: [ TranslocoModule ],
  providers: [
    {
      provide: TRANSLOCO_CONFIG,
      useValue: translocoConfig({
        availableLangs: ['en', 'de'],
        defaultLang: 'en',
        // Remove this option if your application
        // doesn't support changing language in runtime.
        reRenderOnLangChange: true,
        prodMode: !isDevMode(),
      })
    },
    { provide: TRANSLOCO_LOADER, useClass: TranslocoHttpLoader }
  ]
})
export class TranslocoRootModule {}
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {

  private readonly http = inject(HttpClient)

  getTranslation(lang: string) {
    return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
  }
}

The feature module with the scope is imported in the root app module and provides the scope like so:

{
      provide: TRANSLOCO_SCOPE,
      multi: true,
      useValue: {
        scope: 'feature-scope-name',
        loader: scopeLoader(
          (lang: string, root: string) => import(`./${root}/${lang}.json`)
        ),
      },
},

Using translations from the scope feature-scope-name sometimes works, sometimes not. I didn't find out, why sometimes the scope fails and why sometimes it doesn't.

Solution

I now decided not to use a scopeLoader for this use case and instead, trying to utilize the httpLoader from the root module for this job.
I removed the loader from feature.module.ts:

{
      provide: TRANSLOCO_SCOPE,
      multi: true,
      useValue: {
        scope: 'feature-scope-name',
      },
},

Additional I wrote an APP_INITIALIZER factory
app.module.ts

{
      provide: APP_INITIALIZER,
      useFactory: initialize,
      multi: true,
      deps: [TranslocoService],
    },
export function initialize( translateService: TranslocoService ): () => Promise<void> {
  return () =>
    new Promise<void>(async (resolve) => {

      // Setting default language to enforce loading translation before app starts 
      translateService.setActiveLang(translateService.getDefaultLang());

      // Scope hack... 
      const scopeLang = translateService._completeScopeWithLang('inspector');
      await firstValueFrom(translateService.selectTranslation(scopeLang));

      resolve();
    });
}

Additionally you have to output the library i18n files into the app assets in angular.json(or project.json in NX):

 "assets": [
          {
            "input": "libs/feature/src/lib/i18n",
            "glob": "**/*",
            "output": "assets/i18n/feature-scope-name"
          }
        ],

Conclusion

I think, it's not a good solution because you need extra steps like defining the assets in the angular.json. Additionally the languages initialize on app start which also is not good from performance side.
But since scopes are somehow broken, this is my way to go for now.

Lazy Loading

For lazy loaded modules, this approach also should work. But yeah, it's a fail to load the child modules translations before the actual child module has been loaded. Maybe the scope hack can be executed right before the lazy module is being imported. Maybe in a guard?

@dhufnagel
Copy link

Are there any plans to fix this issue? It is more than 2 years old and I now ran into the same issues.

@dhufnagel
Copy link

dhufnagel commented Aug 22, 2024

So am I right, that the scoped tranlation keys are always prefixed with the scope name? Wouldn't it be possible to inject all scope providers in selectTranslate, use the first part of the translation key to find the scope and the corresponding provider and then to wait until the inlineloader is loaded?

I don't think it is a great developer experience to always have to provide the ProviderScope from the injected array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants