diff --git a/libs/components/src/i18n/i18n-part.directive.ts b/libs/components/src/i18n/i18n-part.directive.ts index 9d1b544ef43..bad77e30505 100644 --- a/libs/components/src/i18n/i18n-part.directive.ts +++ b/libs/components/src/i18n/i18n-part.directive.ts @@ -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) {} -} +export class I18nPartDirective {} diff --git a/libs/components/src/i18n/i18n.component.ts b/libs/components/src/i18n/i18n.component.ts index 6d9aaa322e5..11540f686ca 100644 --- a/libs/components/src/i18n/i18n.component.ts +++ b/libs/components/src/i18n/i18n.component.ts @@ -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: ` - + {{ part.text }} `, - standalone: true, }) -export class I18nComponent implements AfterContentInit { - @Input("bit-i18n") - translationKey: string; +export class I18nComponent { + translationKey = input.required({ alias: "bit-i18n" }); /** * Optional arguments to pass to the translation function. */ - @Input() - args: (string | number)[] = []; + args = input<(string | number)[]>([]); - @ContentChildren(I18nPartDirective) - templateTags: QueryList; + 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(() => { + 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) { diff --git a/libs/components/src/i18n/i18n.mdx b/libs/components/src/i18n/i18n.mdx index b1695a942d8..7b266e82850 100644 --- a/libs/components/src/i18n/i18n.mdx +++ b/libs/components/src/i18n/i18n.mdx @@ -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>` 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. ``` -## 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