1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Re-work I18nComponent to use Signals

This commit is contained in:
Shane
2025-07-25 09:13:44 -07:00
parent e153b56ca2
commit e30b39d807
3 changed files with 38 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
import { Directive, TemplateRef } from "@angular/core";
import { Directive } from "@angular/core";
/**
* Structural directive that can be used to mark a template reference inside an I18nComponent.
@@ -8,8 +8,5 @@ import { Directive, TemplateRef } from "@angular/core";
*/
@Directive({
selector: "[bit-i18n-part]",
standalone: true,
})
export class I18nPartDirective {
constructor(public templateRef: TemplateRef<any>) {}
}
export class I18nPartDirective {}

View File

@@ -1,11 +1,4 @@
import {
AfterContentInit,
Component,
ContentChildren,
Input,
QueryList,
TemplateRef,
} from "@angular/core";
import { Component, TemplateRef, input, computed, contentChildren } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -48,7 +41,7 @@ interface I18nStringPart {
selector: "[bit-i18n]",
imports: [SharedModule],
template: `
<ng-container *ngFor="let part of translationParts">
<ng-container *ngFor="let part of translationParts()">
<ng-container *ngIf="part.templateRef != undefined; else text">
<ng-container
*ngTemplateOutlet="part.templateRef; context: { $implicit: part.text }"
@@ -57,54 +50,47 @@ interface I18nStringPart {
<ng-template #text>{{ part.text }}</ng-template>
</ng-container>
`,
standalone: true,
})
export class I18nComponent implements AfterContentInit {
@Input("bit-i18n")
translationKey: string;
export class I18nComponent {
translationKey = input.required<string>({ alias: "bit-i18n" });
/**
* Optional arguments to pass to the translation function.
*/
@Input()
args: (string | number)[] = [];
args = input<(string | number)[]>([]);
@ContentChildren(I18nPartDirective)
templateTags: QueryList<I18nPartDirective>;
private tagTemplates = contentChildren(I18nPartDirective, { read: TemplateRef });
protected translationParts: I18nStringPart[] = [];
private translatedText = computed(() => {
const translatedText = this.i18nService.t(this.translationKey(), ...this.args());
return this.parseTranslatedString(translatedText);
});
protected translationParts = computed<I18nStringPart[]>(() => {
const [translationParts, tagCount] = this.translatedText();
const tagTemplates = this.tagTemplates();
const tagTemplateCount = tagTemplates.length;
if (tagCount !== tagTemplateCount) {
this.logService.warning(
`The translation for "${this.translationKey()}" has ${tagCount} template tag(s), but ${tagTemplateCount} bit-i18n-part directive(s) were found.`,
);
}
translationParts
.filter((part) => part.tagId !== undefined)
.forEach((part) => {
part.templateRef = tagTemplates[part.tagId!];
});
return translationParts;
});
constructor(
private i18nService: I18nService,
private logService: LogService,
) {}
ngAfterContentInit() {
const translatedText = this.i18nService.t(
this.translationKey,
this.args[0],
this.args[1],
this.args[2],
);
const [translationParts, tagCount] = this.parseTranslatedString(translatedText);
this.translationParts = translationParts;
if (tagCount !== this.templateTags.length) {
this.logService.warning(
`The translation for "${this.translationKey}" has ${tagCount} template tag(s), but ${this.templateTags.length} bit-i18n-part directive(s) were found.`,
);
}
// Assign any templateRefs to the translation parts
this.templateTags.forEach((tag, index) => {
this.translationParts.forEach((part) => {
if (part.tagId === index) {
part.templateRef = tag.templateRef;
}
});
});
}
/**
* Parses a translated string into an array of parts separated by tag identifiers.
* Tag identifiers must be numbers surrounded by angle brackets.
@@ -118,7 +104,7 @@ export class I18nComponent implements AfterContentInit {
private parseTranslatedString(inputString: string): [I18nStringPart[], number] {
const regex = /<(\d+)>(.*?)<\/\1>|([^<]+)/g;
const parts: I18nStringPart[] = [];
let match: RegExpMatchArray;
let match: RegExpMatchArray | null;
let tagCount = 0;
while ((match = regex.exec(inputString)) !== null) {

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Canvas, Primary, Controls, Source } from "@storybook/addon-docs";
import { Meta, Story } from "@storybook/addon-docs";
import * as stories from "./i18n.stories";
@@ -8,11 +8,10 @@ import * as stories from "./i18n.stories";
The `[bit-i18n]` component is an alternative to the `i18n` pipe that supports template wrapping. It
supports wrapping developer defined tags around parts the translated text. It allows translators to
translate the original text more accurately and so that it still follows the grammar rules of the
target language.
translate the original text more accurately to follow the grammar rules of the target language.
The templating syntax uses numeric marker tags `<0></0>` around text that will be wrapped. The
marker tags should numbered sequentially starting from 0. The marker tags are then matched and
marker tags should be numbered sequentially starting from 0. The marker tags are then matched and
replaced by the `*bit-i18n-part` directives in the order they appear in the template.
If a corresponding `*bit-i18n-part` directive is not found for a marker tag, the marker tag's
@@ -68,7 +67,7 @@ be rendered as is.
</div>
```
## Missing Template Example
## I18n Arguments Example
You can also pass arguments to the `i18nService.t()` method via the `[args]` input attribute;
@@ -80,7 +79,7 @@ You can also pass arguments to the `i18nService.t()` method via the `[args]` inp
```html
<!-- argExample in messages.json
`This is an example with <0>link</0> tags and $SOME_ARG$.`
"This is an example with <0>link</0> tags and $SOME_ARG$."
-->
<div bit-i18n="argExample" [args]="['passed args']">
<!-- <0></0> -->