transloco icon indicating copy to clipboard operation
transloco copied to clipboard

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

Open sawa-ko opened this issue 3 years ago • 17 comments

Is there an existing issue for this?

  • [x] 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

sawa-ko avatar Jan 13 '22 20:01 sawa-ko

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

noobyogi0010 avatar Jan 30 '22 14:01 noobyogi0010

@noobyogi0010 please.

sawa-ko avatar Jan 30 '22 16:01 sawa-ko

Can you take a look at the problem? @shaharkazaz

sawa-ko avatar Mar 17 '22 00:03 sawa-ko

@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.

shaharkazaz avatar Mar 17 '22 05:03 shaharkazaz

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.

sawa-ko avatar Mar 18 '22 02:03 sawa-ko

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

mehrad-rafigh avatar Oct 06 '22 09:10 mehrad-rafigh

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

shaharkazaz avatar Oct 11 '22 08:10 shaharkazaz

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

mehrad-rafigh avatar Oct 11 '22 08:10 mehrad-rafigh

@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.

shaharkazaz avatar Oct 12 '22 08:10 shaharkazaz

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.

gmiklich avatar Jan 18 '23 20:01 gmiklich

Also facing the same issue here :)

wall-street-dev avatar Jan 26 '23 22:01 wall-street-dev

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

ratatoeskr666 avatar Apr 26 '23 10:04 ratatoeskr666

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.

henryruhs avatar May 27 '23 22:05 henryruhs

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?

ratatoeskr666 avatar Jun 24 '23 11:06 ratatoeskr666