robisim74 / angular-l10n

Angular library to translate texts, dates and numbers
MIT License
380 stars 59 forks source link

Allow for template elements inside of params #347

Closed duncte123 closed 3 months ago

duncte123 commented 3 months ago

Is your feature request related to a problem? Please describe. Inside of my app I have components that deal with the localisation of time, for these components I want to use the time element. But that is not all. Some if my strings need links in them and some of these links are required to be routerLinks.

I am currently migrating away from ngx-translate as it is in maintenance mode and does not support this feature.

A great example of what I would like already exists in the real world as vue-i18n has this feature. (see additional context)

Describe the solution you'd like An awesome way to go about this is to just allow a template to be inserted as a parameter like this:

<ng-template #baseLinkTemplate>
  <a routerLink="/about">{{ 'something.linkText' | translate:locale.language }}</a>
</ng-template>

<p [params]="{ linkText: baseLinkTemplate}" l10nTranslate>something.base<p>

Outputting:

<p>click this cool <a href="/about">Hello world</a></p>

Describe alternatives you've considered I have tried to use arrays for specific keys, but that solution does not work as some languages may have a different sentence structure source.

Comming form ngx-translate I have looked at this proposal, it works, but isn't perfect and has a lot of issues when switching languages causing duplicated elements.

I have looked for a lot of libs but none seem to offer this feature.

Additional context With the following translation keys

{
 "something": {
    "base" : "click this cool {{linkText}}",
    "linkText": "Hello world",
  },
  "schedule": {
    "startsIn": "Begins {{start-time}}"
  },
}

Vue allows us to do the following ($formatTime is a custom function defined by me)

<i18n path="schedule.startsIn" tag="span">
    <template #start-time>
        <time :datetime="startTime">{{ $formatTime(startTime) }}</time>
    </template>
</i18n>

This will render as the following

With the HTML rendering as

<span>Begins <time datetime="2023-05-12T13:00:00Z">15:00</time></span>

Another example of this would be

<i18n path="something.base" tag="p">
    <template #linkText>
        <a routerLink="/about">{{ $t('something.linkText')  }}</a>
    </template>
</i18n>

With the HTML rendering as:

<p>click this cool <a href="/about">Hello world</a></p>
robisim74 commented 3 months ago

@duncte123 If I understand correctly there are two issues here.

About the time element, with this library you can simply do this (since the time will be formatted as a string):

<span [params]="{ startTime: today | l10nDate:locale.language:{ timeStyle: 'short' }}" l10nTranslate>startsIn</span>

where today is a date, and json is: { "startsIn": "Begins {{startTime}}" }

About the routerLink, as part of a translated text, it's an old problem in Angular, because it's not simply a string. You can find other issues in this repository where the problem is addressed, such as these: https://github.com/robisim74/angular-l10n/issues/128, https://github.com/robisim74/angular-l10n/issues/206

This scenario is not supported by this library, and I don't think it will be: hydration enabled during SSR does not support DOM manipulation.

Personally, I remain of this opinion: https://github.com/robisim74/angular-l10n/issues/206#issuecomment-424981972

Greetings

duncte123 commented 3 months ago

Thank you for your reply, unfortunately this library does not render the time using the semantic element as is preferred in my application hence me bringing it up.

The stackblitz example shows me this

robisim74 commented 3 months ago

@duncte123 I get it.

But if you have this need (for links, time element or other html tags) why don't you develop a custom slot component like the one in vue-i18n?

For example something like this:

@Component({
    selector: 'l10n',
    template: `
    <span #contentWrapper>
        <ng-content></ng-content>
    </span>
    `,
    standalone: true,
    imports: [
        CommonModule,
        L10nTranslationModule
    ],
    // host: { ngSkipHydration: 'true' } if you hydration enabled
})
export class L10nComponent {
    @ViewChild('contentWrapper') contentWrapper: ElementRef;

    @Input() key: string;

    private translation = inject(L10nTranslationService);
    private renderer = inject(Renderer2);

    private destroy = new Subject<boolean>();

    private elementWrapper: HTMLElement;
    private element: HTMLElement;

    ngAfterViewInit() {
        if (!this.element && !this.elementWrapper) {
            this.elementWrapper = this.contentWrapper.nativeElement;
            this.element = this.contentWrapper.nativeElement.childNodes[0];

            this.translation.onChange().pipe(takeUntil(this.destroy)).subscribe({
                next: () => {
                    this.replace();
                }
            });
        }
    }

    replace() {
        const val = this.translation.translate(this.key, { inner: this.element.outerHTML });
        this.renderer.setProperty(this.elementWrapper, 'innerHTML', val);
    }

    ngOnDestroy(): void {
        this.destroy.next(true);
    }
} 

Usage:

<l10n key="startsIn">
    <time [dateTime]="today">{{ today | l10nDate:locale.language:{ timeStyle: 'short' } }}</time>
</l10n>
<l10n key="something.base">
  <a routerLink="/about" l10nTranslate>something.linkText</a>
</l10n>

and the json with a fixed inner param:

{
  "startsIn": "Begins {{inner}}",
  "something": {
    "base" : "click this cool {{inner}}",
    "linkText": "Hello world"
  }
}

Just keep in mind that you are manipulating the DOM to do this, and you may have problems if you use SSR with hydration enabled.

duncte123 commented 3 months ago

I hadn't actually thought about slot components, thanks for the recommendation :)