-
-
Notifications
You must be signed in to change notification settings - Fork 578
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
Translating strings with links #223
Comments
I think something like this would be better:
<span [merge]="{{ 'ALREADY_SIGNED_UP' | translate }}">
<a routerLink="/login"></a>
</span> A |
This is a must feature as often in real world apps we would have to embed a routerLink or a click handle to the strings being translated |
@DethAriel did you find any solution to this? |
@deepu105 for now I went with a workaround solution: <span>
{{ 'ALREADY_SIGNED_UP_PREFIX' | translate }}
<a routerLink="/login">{{ 'ALREADY_SIGNED_UP_LINK' | translate }}</a>
{{ 'ALREADY_SIGNED_UP_SUFFIX' | translate }}
</span> And made it really obvious for localizators that these are part of one sentence via the supporting docs. |
Sorry guys, there's no way to create html content with angular components like this based on a string. Angular was written in a way that you could abstract all the dynamic logic from the templates because the idea is that everyone should use precompilation (AoT) and remove the compiler from the code. |
Hi folks! I wish to propose our workaround too:
SOLUTION: we use single variable with text piped | --> example "Hello, please click |here| to register". We implemented custom angular pipe
Then we use it just like that:
We are good, no xss, we can use angular components in the middle, we provide single variable to translators. Thx |
I know this issue is closed, but as it was the hit I got searching for the same problem I would like to present my solution to the problem, implementing a generic solution for ngx-translate. It consist of two directives and a service shared between them. template-translate.directive.ts
template-translation.directive.ts
template-translate.service.ts
templateRefs.model.ts
The solution works by using the Renderer two moving the right HTML/Angular element in place of the replacement string. The interface for this is quite simple, given resource file: "SomeScreen", and resource string:"someResourceString", looking like this: "Replace {{this}} in this sentence":
The string will then be replaced, where the {{this}} will be replaced with the given HTML/Angular element with the *templateTranslation directive. I know that the directive only handles double curly braces atm. And if it is used, every replacement in the resource string should be replaced with an HTML/Angular element. So the solution could probably be fine tuned a bit. But I would say that this is a start for a generic solution for ngx-translate library. Currently the solution uses three private properties/functions from the ngx-translate library which is:
This makes the solution incomplete. It would probably be better if it was implemented natively in ngx-translate, or if the methods was made public in the ngx-translate library. For the developers, would you consider implementing this native in the ngx-translate library? Or maybe open for a potential PR me or another dev could make? And feel free to address if this solution contains any problems which isn't just fine tuning :) |
Thanks @yuristsepaniuk for your pipe solution!
|
@kasperlauge it looks like rocketscience but from the usage its perfect and super flexible! thx for your solution kasper. |
@kasperlauge thanks so much for this - I was planning on writing something with the same approach, and then I found that you'd already done it! Once small issue is that it doesn't take into account waiting for remote translation files to load - |
@DanielSchaffer thank you for the feedback! I actually also discovered that, but solved it by having a global behavior subject translationReady (which is triggered when the translations have been loaded) which is being subscribed to in ngOnInit in the directive before doing anything else :) I hope your solution or this one can solve the problems for others :) |
@kasperlauge - upon further fiddling, you can simplify it a bit further by using Here's my adapted solution (also, please forgive the liberties I took with the name changes): // translated-content.directive.ts
import {
AfterContentInit,
ChangeDetectorRef,
ContentChildren,
Directive,
Input,
OnInit,
OnDestroy,
QueryList,
Renderer2,
ViewContainerRef,
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, merge, Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { TranslatedElementDirective } from './translated-element.directive';
interface TranslationData {
elements: TranslatedElementDirective[];
rawTranslation: string;
}
const TOKEN_START_DEMARC = '{{';
const TOKEN_END_DEMARC = '}}';
// adapted from @kasperlauge's solution in https://github.com/ngx-translate/core/issues/223
@Directive({
selector: '[translatedContent]',
})
export class TranslatedContentDirective implements OnInit, OnDestroy, AfterContentInit {
@Input('translatedContent') translationKey: string;
@ContentChildren(TranslatedElementDirective)
private elements: QueryList<TranslatedElementDirective>;
private subs: Subscription[] = [];
private rawTranslation: Observable<string>;
private translationData: Observable<TranslationData>;
constructor(
private viewRef: ViewContainerRef,
private renderer: Renderer2,
private translateService: TranslateService,
private changeDetectorRef: ChangeDetectorRef,
) {}
public ngOnInit(): void {
this.rawTranslation = this.translateService.get(this.translationKey);
}
public ngAfterContentInit(): void {
// QueryList.changes doesn't re-emit after its initial value, which we have by now
// BehaviorSubjects re-emit their initial value on subscription, so we get what we need by merging
// the BehaviorSubject and the QueryList.changes observable
const elementsSubject = new BehaviorSubject(this.elements.toArray());
const elementsChanges = merge(elementsSubject, this.elements.changes);
this.translationData = combineLatest(this.rawTranslation, elementsChanges)
.pipe(map(([rawTranslation]) => ({
elements: this.elements.toArray(),
rawTranslation,
})));
this.subs.push(this.translationData.subscribe(this.render.bind(this)));
}
private render(translationData: TranslationData): void {
if (!translationData.rawTranslation || translationData.rawTranslation === this.translationKey) {
throw new Error(`No resource matching the key '${this.translationKey}'`);
}
this.viewRef.clear();
let lastTokenEnd = 0;
while (lastTokenEnd < translationData.rawTranslation.length) {
const tokenStartDemarc = translationData.rawTranslation.indexOf(TOKEN_START_DEMARC, lastTokenEnd);
if (tokenStartDemarc < 0) {
break;
}
const tokenStart = tokenStartDemarc + TOKEN_START_DEMARC.length;
const tokenEnd = translationData.rawTranslation.indexOf(TOKEN_END_DEMARC, tokenStart);
if (tokenEnd < 0) {
throw new Error(`Encountered unterminated token in translation string '${this.translationKey}'`);
}
const tokenEndDemarc = tokenEnd + TOKEN_END_DEMARC.length;
const precedingText = translationData.rawTranslation.substring(lastTokenEnd, tokenStartDemarc);
const precedingTextElement = this.renderer.createText(precedingText);
this.renderer.appendChild(this.viewRef.element.nativeElement, precedingTextElement);
const elementKey = translationData.rawTranslation.substring(tokenStart, tokenEnd);
const embeddedElementTemplate = translationData.elements.find(element => element.elementKey === elementKey);
if (embeddedElementTemplate) {
const embeddedElementView = embeddedElementTemplate.viewRef.createEmbeddedView(embeddedElementTemplate.templateRef);
this.renderer.appendChild(this.viewRef.element.nativeElement, embeddedElementView.rootNodes[0]);
} else {
const missingTokenText = translationData.rawTranslation.substring(tokenStartDemarc, tokenEndDemarc);
const missingTokenElement = this.renderer.createText(missingTokenText);
this.renderer.appendChild(this.viewRef.element.nativeElement, missingTokenElement);
}
lastTokenEnd = tokenEndDemarc;
}
const trailingText = translationData.rawTranslation.substring(lastTokenEnd);
const trailingTextElement = this.renderer.createText(trailingText);
this.renderer.appendChild(this.viewRef.element.nativeElement, trailingTextElement);
// in case the rendering happens outside of a change detection event, this ensures that any translations in the
// embedded elements are rendered
this.changeDetectorRef.detectChanges();
}
public ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
} // translated-element.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[translatedElement]',
})
export class TranslatedElementDirective {
@Input('translatedElement')
public elementKey: string;
constructor(
public readonly viewRef: ViewContainerRef,
public readonly templateRef: TemplateRef<any>,
) {}
} And it's used like so:
|
@DanielSchaffer Awesome solution! I might look into it and adapt mine to use that in the future :) |
@DanielSchaffer I tried your solution. When I use |
@alexander-myltsev so the clarify - you're seeing the content that replaces |
@DanielSchaffer exactly the opposite. In your example, I see |
Hi, @alexander-myltsev, I also tried @DanielSchaffer solution and I got same problem as you had. How can I fix that? The problem is |
Hi @Bat-Orshikh , I didn't use the code. I need 3 places to do it, and did it manually. When I need it regularly, I'm going to figure out how it works. |
Hi again @alexander-myltsev , I resolved the problem that |
@Bat-Orshikh can you please post your solution. Not sure what to do |
Thanks @kasperlauge and @DanielSchaffer for your solutions. I've tried it and it worked great! As opposed to this, the solution of @yuristsepaniuk (upon which the whole ngx-translate-cut plugin is built) should be used with caution, since it relies on the order of translatable parts, which may not always be the same between different languages. |
@kasperlauge @DanielSchaffer thank you for the great solution
|
We use arrays, I think it is a pretty generalized and straight-forward approach, hope it helps someone :) JSON:
HTML:
|
This seems to be broken in angular 13. The snippet below causes the application to hang, trying to find a solution while (this.viewRef.element.nativeElement.firstChild) {
this.renderer.removeChild(this.viewRef.element.nativeElement, this.viewRef.element.nativeElement.firstChild);
} Edit: working in angular 13 https://gist.github.com/duncte123/e80f5cadbe08f24c31a83893353391fd |
The problem with this approach is that not all languages maintain the same word order, so it only works in specific cases. |
Does anyone have a working solution for Angular 16? |
@RikudouSage This is what I use on angular 13, have not net had the time to upgrade to 16. Really wishing this feature will be included in the lib in the future. I made this issue a while back in the hope that they will implement it #1417 https://gist.github.com/duncte123/e80f5cadbe08f24c31a83893353391fd |
@duncte123 Thanks! Though I've moved to transloco with the ngx-transloco-markup plugin which offers such functionality. |
Thanks for the tip, I'll check it out |
I know it's an old topic, but this approach works for me. ` translate = inject(TranslateService);
"message": "Brugerkontoen er nu oprettet. Du kan {{link}} med det samme." ` |
this approach works for me in the template: ` onClick(element: HTMLDivElement) { "emailAlreadyRegistered": { |
I'm submitting a ... (check one with "x")
This is somewhere in between "how to" and "what if".
Use case
Embedding angular links into translation resources, something like:
The strings are self-explanatory.
Work-around
One could do something along those lines:
which would be much harder to track for the localization team.
Difficulties
Security. Such template layout would require to call
DomSanitizer.bypassSecurityTrustHtml
at the very least, which in turn requires extracting the localizable string into a variable (see this plunker):If this is skipped, the following string will be output to browser console:
"WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss)."
. The resulting HTML will not contain therouterLink
attribute on an<a>
element.Router hooks. Even if we bypass the HTML sanitization, it's still unclear how Angular is supposed to tie such a link into the routing stuff. In the above plunker the thing is not hooked, and I have yet to figure this out (any help?)
Now what?
That's the thing - I don't know, and I'm looking for suggestions on how to solve this problem in a non-workaroundish way. AFAIK it's not possible to do something like
bypassSecurityTrustHtml
from within a custom pipe (though it would probably be a nice, but heavily misused feature).On the other hand, if we could make the plunker work in expected way, this could potentially be extracted into a reusable
TranslateInsecureContentService
utility.Please tell us about your environment:
The text was updated successfully, but these errors were encountered: