diff --git a/libs/components/src/i18n/i18n.component.ts b/libs/components/src/i18n/i18n.component.ts
index aa69696af33..6d9aaa322e5 100644
--- a/libs/components/src/i18n/i18n.component.ts
+++ b/libs/components/src/i18n/i18n.component.ts
@@ -91,7 +91,7 @@ export class I18nComponent implements AfterContentInit {
if (tagCount !== this.templateTags.length) {
this.logService.warning(
- `The translation for "${this.translationKey}" has ${tagCount} template tags(s), but ${this.templateTags.length} bit-i18n-part directive(s) were found.`,
+ `The translation for "${this.translationKey}" has ${tagCount} template tag(s), but ${this.templateTags.length} bit-i18n-part directive(s) were found.`,
);
}
diff --git a/libs/components/src/i18n/i18n.spec.ts b/libs/components/src/i18n/i18n.spec.ts
new file mode 100644
index 00000000000..4bd067bc910
--- /dev/null
+++ b/libs/components/src/i18n/i18n.spec.ts
@@ -0,0 +1,120 @@
+import { Component } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { mock } from "jest-mock-extended";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+
+import { I18nMockService } from "../utils/i18n-mock.service";
+
+import { I18nPartDirective } from "./i18n-part.directive";
+import { I18nComponent } from "./i18n.component";
+
+@Component({
+ selector: "test",
+ template: `
+
+ {{ text }}
+ {{ text }}
+
+ {{ text }}
+
+
+ `,
+})
+class TestComponent {
+ translationKey: string;
+ args: any[] = [];
+}
+
+describe("I18nComponent", () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ const mockLogService = mock();
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ imports: [I18nComponent, I18nPartDirective],
+ providers: [
+ {
+ provide: I18nService,
+ useFactory: () => {
+ return new I18nMockService({
+ basicExample: () =>
+ `This is an example with <0>link0> tags and <1>bold1> tags. The entire sentence can be <2>translated as a whole2> and re-arranged according to each language's grammar rules.`,
+ outOfOrder: () =>
+ `This is another example with <1>bold1> tags to show that tag order does not matter and the <0>link0> tags are after.`,
+ tooManyTags: () => `<0>First0>, <1>Second1>, <2>Third2>, <3>Missing Fourth3>`,
+ argsExample: (arg1: string) =>
+ `This is an example with <0>link0> tags and ${arg1}.`,
+ });
+ },
+ },
+ {
+ provide: LogService,
+ useValue: mockLogService,
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ });
+
+ it("should render the basicExample translation with all templates rendered", () => {
+ component.translationKey = "basicExample";
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector("p").textContent).toContain(
+ "This is an example with link tags and bold tags. The entire sentence can be translated as a whole and re-arranged according to each language's grammar rules",
+ );
+ expect(compiled.querySelector("a").textContent).toContain("link");
+ expect(compiled.querySelector("strong").textContent).toContain("bold");
+ expect(compiled.querySelector("a > strong").textContent).toContain("translated as a whole");
+ });
+
+ it("should render the outOfOrder translation with all templates rendered", () => {
+ component.translationKey = "outOfOrder";
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector("p").textContent).toContain(
+ "This is another example with bold tags to show that tag order does not matter and the link tags are after",
+ );
+ expect(compiled.querySelector("a").textContent).toContain("link");
+ expect(compiled.querySelector("strong").textContent).toContain("bold");
+ expect(compiled.querySelector("a > strong")).toBeNull(); // 3rd tag is not present
+ expect(compiled.querySelector("strong + a")).not.toBeNull(); // strong should come before a
+ });
+
+ it("should render the tooManyTags translation without extra identifiers", () => {
+ component.translationKey = "tooManyTags";
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector("p").textContent).toContain(
+ "First, Second, Third, Missing Fourth",
+ );
+
+ expect(mockLogService.warning).toHaveBeenCalledWith(
+ `The translation for "tooManyTags" has 4 template tag(s), but 3 bit-i18n-part directive(s) were found.`,
+ );
+
+ expect(compiled.querySelector("a").textContent).toContain("First");
+ expect(compiled.querySelector("strong").textContent).toContain("Second");
+ expect(compiled.querySelector("a > strong").textContent).toContain("Third");
+ });
+
+ it("should render the argsExample translation with the passed args", () => {
+ component.translationKey = "argsExample";
+ component.args = ["passed args"];
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector("p").textContent).toContain(
+ "This is an example with link tags and passed args.",
+ );
+ expect(compiled.querySelector("a").textContent).toContain("link");
+ });
+});