@forgeroute/ui 0.4.78 → 0.4.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import * as i1 from "@angular/forms";
4
4
  import { FormsModule } from "@angular/forms";
5
5
  import { HttpClient, HttpErrorResponse } from "@angular/common/http";
6
6
  import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
7
- import { AlertComponent, BadgeComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UiFieldValidity, createFieldReveal, getErrorMessage, requiredIssue } from "@forgeroute/ui";
7
+ import { AlertComponent, BadgeComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UI_DEFAULT_LABELS, UiFieldValidity, UiLabelsService, createFieldReveal, formatAttachmentSize, getErrorMessage, requiredIssue, safeLocale, uiLocale, uiText } from "@forgeroute/ui";
8
8
  import { Subject, catchError, debounceTime, defer, distinctUntilChanged, map, of, switchMap, take, throwError } from "rxjs";
9
9
  let nextVideoPickerId = 0;
10
10
  var VideoPickerComponent = class VideoPickerComponent {
@@ -12,6 +12,7 @@ var VideoPickerComponent = class VideoPickerComponent {
12
12
  return `${this.uid}-opt-${i}`;
13
13
  }
14
14
  constructor() {
15
+ this.labels = inject(UiLabelsService);
15
16
  this.videoSelected = output();
16
17
  this.label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
17
18
  this.required = input(false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
@@ -36,7 +37,7 @@ var VideoPickerComponent = class VideoPickerComponent {
36
37
  this.activeIndex = signal(-1, ...ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []);
37
38
  this.reveal = createFieldReveal(this.showErrors);
38
39
  this.invalid = computed(() => this.required() && !this.selected(), ...ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []);
39
- this.errorMessage = computed(() => this.invalid() ? requiredIssue(this.label() || "Video").message : "", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
40
+ this.errorMessage = computed(() => this.invalid() ? requiredIssue(this.label() || this.labels.t("common.video")).message : "", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
40
41
  this.visibleError = computed(() => this.reveal.revealed() ? this.errorMessage() : "", ...ngDevMode ? [{ debugName: "visibleError" }] : /* istanbul ignore next */ []);
41
42
  this.activeDescendant = computed(() => {
42
43
  const i = this.activeIndex();
@@ -44,9 +45,9 @@ var VideoPickerComponent = class VideoPickerComponent {
44
45
  }, ...ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []);
45
46
  this.liveText = computed(() => {
46
47
  if (!this.showResults()) return "";
47
- if (this.loading() || this.pending()) return "Searching";
48
+ if (this.loading() || this.pending()) return this.labels.t("search.searching");
48
49
  const n = this.results().length;
49
- return n === 0 ? "No results" : n === 1 ? "1 result" : `${n} results`;
50
+ return this.labels.t(n === 0 ? "messagingSendMessagePanel.resultsCount_zero" : n === 1 ? "messagingSendMessagePanel.resultsCount_one" : "messagingSendMessagePanel.resultsCount_other", { count: n });
50
51
  }, ...ngDevMode ? [{ debugName: "liveText" }] : /* istanbul ignore next */ []);
51
52
  this.search$ = new Subject();
52
53
  this.blurTimer = null;
@@ -231,12 +232,12 @@ var VideoPickerComponent = class VideoPickerComponent {
231
232
  }
232
233
  @if (selected()) {
233
234
  <div class="selected-input" role="group" tabindex="-1"
234
- [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : 'Selected video'">
235
+ [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.selectedVideo')">
235
236
  <div class="selected-video">
236
237
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
237
238
  <span>{{ selected()!.title }}</span>
238
239
  </div>
239
- <button type="button" class="clear-btn" (click)="clear()" aria-label="Remove video">&times;</button>
240
+ <button type="button" class="clear-btn" (click)="clear()" [attr.aria-label]="labels.t('messagingVideoPicker.removeVideo')">&times;</button>
240
241
  </div>
241
242
  } @else {
242
243
  <input
@@ -245,9 +246,9 @@ var VideoPickerComponent = class VideoPickerComponent {
245
246
  role="combobox"
246
247
  aria-autocomplete="list"
247
248
  aria-haspopup="listbox"
248
- placeholder="Search videos..."
249
+ [placeholder]="labels.t('messagingVideoPicker.searchVideos')"
249
250
  [id]="inputId"
250
- [attr.aria-label]="label() ? null : 'Search videos'"
251
+ [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.searchVideosAria')"
251
252
  [attr.aria-required]="required() || null"
252
253
  [attr.aria-invalid]="visibleError() ? 'true' : null"
253
254
  [attr.aria-describedby]="visibleError() && !showResults() ? errorId : null"
@@ -263,12 +264,12 @@ var VideoPickerComponent = class VideoPickerComponent {
263
264
  />
264
265
  @if (showResults()) {
265
266
  <div class="picker-dropdown" role="listbox" [id]="listboxId"
266
- [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : 'Videos'"
267
+ [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : labels.t('common.videos')"
267
268
  (mousedown)="$event.preventDefault()">
268
269
  @if (loading() || (pending() && results().length === 0)) {
269
- <div class="picker-loading">Searching...</div>
270
+ <div class="picker-loading">{{ labels.t('messagingVideoPicker.searching') }}</div>
270
271
  } @else if (results().length === 0 && query.length >= 2) {
271
- <div class="picker-empty">No videos found</div>
272
+ <div class="picker-empty">{{ labels.t('messagingVideoPicker.noVideosFound') }}</div>
272
273
  } @else {
273
274
  @for (v of results(); track v.id; let i = $index) {
274
275
  <button type="button" class="picker-option" role="option" tabindex="-1"
@@ -356,12 +357,12 @@ i0.ɵɵngDeclareClassMetadata({
356
357
  }
357
358
  @if (selected()) {
358
359
  <div class="selected-input" role="group" tabindex="-1"
359
- [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : 'Selected video'">
360
+ [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.selectedVideo')">
360
361
  <div class="selected-video">
361
362
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
362
363
  <span>{{ selected()!.title }}</span>
363
364
  </div>
364
- <button type="button" class="clear-btn" (click)="clear()" aria-label="Remove video">&times;</button>
365
+ <button type="button" class="clear-btn" (click)="clear()" [attr.aria-label]="labels.t('messagingVideoPicker.removeVideo')">&times;</button>
365
366
  </div>
366
367
  } @else {
367
368
  <input
@@ -370,9 +371,9 @@ i0.ɵɵngDeclareClassMetadata({
370
371
  role="combobox"
371
372
  aria-autocomplete="list"
372
373
  aria-haspopup="listbox"
373
- placeholder="Search videos..."
374
+ [placeholder]="labels.t('messagingVideoPicker.searchVideos')"
374
375
  [id]="inputId"
375
- [attr.aria-label]="label() ? null : 'Search videos'"
376
+ [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.searchVideosAria')"
376
377
  [attr.aria-required]="required() || null"
377
378
  [attr.aria-invalid]="visibleError() ? 'true' : null"
378
379
  [attr.aria-describedby]="visibleError() && !showResults() ? errorId : null"
@@ -388,12 +389,12 @@ i0.ɵɵngDeclareClassMetadata({
388
389
  />
389
390
  @if (showResults()) {
390
391
  <div class="picker-dropdown" role="listbox" [id]="listboxId"
391
- [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : 'Videos'"
392
+ [attr.aria-labelledby]="label() ? labelId : null" [attr.aria-label]="label() ? null : labels.t('common.videos')"
392
393
  (mousedown)="$event.preventDefault()">
393
394
  @if (loading() || (pending() && results().length === 0)) {
394
- <div class="picker-loading">Searching...</div>
395
+ <div class="picker-loading">{{ labels.t('messagingVideoPicker.searching') }}</div>
395
396
  } @else if (results().length === 0 && query.length >= 2) {
396
- <div class="picker-empty">No videos found</div>
397
+ <div class="picker-empty">{{ labels.t('messagingVideoPicker.noVideosFound') }}</div>
397
398
  } @else {
398
399
  @for (v of results(); track v.id; let i = $index) {
399
400
  <button type="button" class="picker-option" role="option" tabindex="-1"
@@ -464,6 +465,7 @@ var TemplatePickerComponent = class TemplatePickerComponent {
464
465
  return `${this.uid}-opt-${i}`;
465
466
  }
466
467
  constructor() {
468
+ this.labels = inject(UiLabelsService);
467
469
  this.http = inject(HttpClient);
468
470
  this.apiBase = inject(FORGEROUTE_CONFIG, { optional: true })?.baseUrl ?? "/api";
469
471
  this.destroyRef = inject(DestroyRef);
@@ -472,7 +474,11 @@ var TemplatePickerComponent = class TemplatePickerComponent {
472
474
  this.search$ = new Subject();
473
475
  this.channel = input("", ...ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []);
474
476
  this.selectedId = input(null, ...ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []);
475
- this.label = input("Template", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
477
+ this.labelInput = input(null, {
478
+ ...ngDevMode ? { debugName: "labelInput" } : /* istanbul ignore next */ {},
479
+ alias: "label"
480
+ });
481
+ this.label = computed(() => this.labelInput() ?? this.labels.t("messagingTemplatePicker.label"), ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
476
482
  this.required = input(false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
477
483
  this.showErrors = input(false, ...ngDevMode ? [{ debugName: "showErrors" }] : /* istanbul ignore next */ []);
478
484
  this.templateSelected = output();
@@ -490,9 +496,9 @@ var TemplatePickerComponent = class TemplatePickerComponent {
490
496
  this.visibleError = computed(() => this.reveal.revealed() ? this.errorMessage() : "", ...ngDevMode ? [{ debugName: "visibleError" }] : /* istanbul ignore next */ []);
491
497
  this.liveText = computed(() => {
492
498
  if (!this.showResults()) return "";
493
- if (this.loading()) return "Searching";
499
+ if (this.loading()) return this.labels.t("search.searching");
494
500
  const n = this.results().length;
495
- return n === 0 ? "No results" : n === 1 ? "1 result" : `${n} results`;
501
+ return this.labels.t(n === 0 ? "messagingSendMessagePanel.resultsCount_zero" : n === 1 ? "messagingSendMessagePanel.resultsCount_one" : "messagingSendMessagePanel.resultsCount_other", { count: n });
496
502
  }, ...ngDevMode ? [{ debugName: "liveText" }] : /* istanbul ignore next */ []);
497
503
  this.uid = `ui-tpl-${TemplatePickerComponent.nextId++}`;
498
504
  this.inputId = `${this.uid}-input`;
@@ -501,20 +507,6 @@ var TemplatePickerComponent = class TemplatePickerComponent {
501
507
  this.blurTimer = null;
502
508
  this.lastOptionMouse = 0;
503
509
  this.lastPreviewMouse = 0;
504
- this.sampleVars = {
505
- first_name: "Sarah",
506
- last_name: "Johnson",
507
- full_name: "Sarah Johnson",
508
- email: "sarah@example.com",
509
- phone: "(555) 867-5309",
510
- property_address: "1842 Maple Grove Lane",
511
- address: "1842 Maple Grove Lane",
512
- price: "$485,000",
513
- list_price: "$485,000",
514
- agent_name: "Alex Rivera",
515
- company_name: "Real Estate",
516
- closing_date: "Sept 15, 2026"
517
- };
518
510
  effect(() => {
519
511
  this.invalidChange.emit(this.invalid());
520
512
  });
@@ -637,6 +629,22 @@ var TemplatePickerComponent = class TemplatePickerComponent {
637
629
  body: tpl.body
638
630
  });
639
631
  }
632
+ get sampleVars() {
633
+ return {
634
+ first_name: "Sarah",
635
+ last_name: "Johnson",
636
+ full_name: "Sarah Johnson",
637
+ email: "sarah@example.com",
638
+ phone: "(555) 867-5309",
639
+ property_address: this.labels.t("messagingTemplatePicker.sampleAddress"),
640
+ address: this.labels.t("messagingTemplatePicker.sampleAddress"),
641
+ price: "$485,000",
642
+ list_price: "$485,000",
643
+ agent_name: "Alex Rivera",
644
+ company_name: this.labels.t("messagingTemplatePicker.sampleCompany"),
645
+ closing_date: this.labels.t("messagingTemplatePicker.sampleClosingDate")
646
+ };
647
+ }
640
648
  previewText(raw) {
641
649
  const resolved = raw.replace(/\{\{(\w+)\}\}/g, (_, k) => this.sampleVars[k] ?? "…");
642
650
  return resolved.length > 80 ? resolved.slice(0, 80) + "…" : resolved;
@@ -652,15 +660,15 @@ var TemplatePickerComponent = class TemplatePickerComponent {
652
660
  return true;
653
661
  }
654
662
  activeDateLabel(tpl) {
655
- if (tpl.useAllYear !== false) return "Active all year";
656
- const fmt = (d) => new Date(d).toLocaleDateString("en-US", {
663
+ if (tpl.useAllYear !== false) return this.labels.t("messagingSendMessagePanel.activeAllYear");
664
+ const fmt = (d) => new Date(d).toLocaleDateString(safeLocale(this.labels.locale()), {
657
665
  month: "short",
658
666
  day: "numeric"
659
667
  });
660
668
  if (tpl.activeFrom && tpl.activeTo) return `${fmt(tpl.activeFrom)} – ${fmt(tpl.activeTo)}`;
661
- if (tpl.activeFrom) return `From ${fmt(tpl.activeFrom)}`;
662
- if (tpl.activeTo) return `Until ${fmt(tpl.activeTo)}`;
663
- return "Active all year";
669
+ if (tpl.activeFrom) return this.labels.t("messagingSendMessagePanel.activeFrom", { date: fmt(tpl.activeFrom) });
670
+ if (tpl.activeTo) return this.labels.t("messagingSendMessagePanel.activeUntil", { date: fmt(tpl.activeTo) });
671
+ return this.labels.t("messagingSendMessagePanel.activeAllYear");
664
672
  }
665
673
  static {
666
674
  this.ɵfac = i0.ɵɵngDeclareFactory({
@@ -694,8 +702,8 @@ var TemplatePickerComponent = class TemplatePickerComponent {
694
702
  isRequired: false,
695
703
  transformFunction: null
696
704
  },
697
- label: {
698
- classPropertyName: "label",
705
+ labelInput: {
706
+ classPropertyName: "labelInput",
699
707
  publicName: "label",
700
708
  isSignal: true,
701
709
  isRequired: false,
@@ -737,11 +745,11 @@ var TemplatePickerComponent = class TemplatePickerComponent {
737
745
  type="text"
738
746
  role="combobox"
739
747
  [id]="inputId"
740
- placeholder="Search templates..."
748
+ [placeholder]="labels.t('messagingTemplatePicker.searchTemplates')"
741
749
  autocomplete="off"
742
750
  aria-autocomplete="list"
743
751
  aria-haspopup="listbox"
744
- [attr.aria-label]="label() ? null : 'Search templates'"
752
+ [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.searchTemplatesAria')"
745
753
  [attr.aria-invalid]="visibleError() ? 'true' : null"
746
754
  [attr.aria-describedby]="visibleError() && !showResults() ? errorId : null"
747
755
  [attr.aria-required]="required() || null"
@@ -755,11 +763,11 @@ var TemplatePickerComponent = class TemplatePickerComponent {
755
763
  (keydown)="onKeydown($event)" />
756
764
 
757
765
  @if (showResults()) {
758
- <div class="picker-dropdown" [id]="listId" role="listbox" [attr.aria-label]="label() || 'Search templates'" (mousedown)="$event.preventDefault()">
766
+ <div class="picker-dropdown" [id]="listId" role="listbox" [attr.aria-label]="label() || labels.t('messagingSendMessagePanel.searchTemplatesAria')" (mousedown)="$event.preventDefault()">
759
767
  @if (loading()) {
760
- <div class="picker-loading">Searching...</div>
768
+ <div class="picker-loading">{{ labels.t('messagingTemplatePicker.searching') }}</div>
761
769
  } @else if (results().length === 0) {
762
- <div class="picker-empty">No templates found</div>
770
+ <div class="picker-empty">{{ labels.t('messagingTemplatePicker.noTemplatesFound') }}</div>
763
771
  } @else {
764
772
  @for (tpl of results(); track tpl.id; let i = $index) {
765
773
  <div
@@ -776,17 +784,17 @@ var TemplatePickerComponent = class TemplatePickerComponent {
776
784
  <span class="option-title">
777
785
  <span class="option-name">{{ tpl.name }}</span>
778
786
  @if (tpl.isSystem !== undefined) {
779
- <span class="option-badge" [class.system]="tpl.isSystem">{{ tpl.isSystem ? 'System' : 'Custom' }}</span>
787
+ <span class="option-badge" [class.system]="tpl.isSystem">{{ tpl.isSystem ? labels.t('messagingTemplatePicker.system') : labels.t('messagingTemplatePicker.custom') }}</span>
780
788
  }
781
789
  </span>
782
790
  <span class="option-detail">
783
- @if (!isCurrentlyActive(tpl)) { <span class="option-inactive">Inactive · </span> }{{ previewText(tpl.subject || tpl.body) }}
791
+ @if (!isCurrentlyActive(tpl)) { <span class="option-inactive">{{ labels.t('messagingTemplatePicker.inactive') }} · </span> }{{ previewText(tpl.subject || tpl.body) }}
784
792
  </span>
785
793
  </span>
786
794
  <button
787
795
  type="button"
788
796
  class="option-preview-btn"
789
- [attr.aria-label]="'Preview ' + tpl.name"
797
+ [attr.aria-label]="labels.t('messagingSendMessagePanel.previewNamed', { name: tpl.name })"
790
798
  (mousedown)="$event.stopPropagation(); $event.preventDefault(); onPreviewMouseDown(tpl)"
791
799
  (click)="$event.stopPropagation(); onPreviewClick(tpl)">
792
800
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
@@ -875,11 +883,11 @@ i0.ɵɵngDeclareClassMetadata({
875
883
  type="text"
876
884
  role="combobox"
877
885
  [id]="inputId"
878
- placeholder="Search templates..."
886
+ [placeholder]="labels.t('messagingTemplatePicker.searchTemplates')"
879
887
  autocomplete="off"
880
888
  aria-autocomplete="list"
881
889
  aria-haspopup="listbox"
882
- [attr.aria-label]="label() ? null : 'Search templates'"
890
+ [attr.aria-label]="label() ? null : labels.t('messagingSendMessagePanel.searchTemplatesAria')"
883
891
  [attr.aria-invalid]="visibleError() ? 'true' : null"
884
892
  [attr.aria-describedby]="visibleError() && !showResults() ? errorId : null"
885
893
  [attr.aria-required]="required() || null"
@@ -893,11 +901,11 @@ i0.ɵɵngDeclareClassMetadata({
893
901
  (keydown)="onKeydown($event)" />
894
902
 
895
903
  @if (showResults()) {
896
- <div class="picker-dropdown" [id]="listId" role="listbox" [attr.aria-label]="label() || 'Search templates'" (mousedown)="$event.preventDefault()">
904
+ <div class="picker-dropdown" [id]="listId" role="listbox" [attr.aria-label]="label() || labels.t('messagingSendMessagePanel.searchTemplatesAria')" (mousedown)="$event.preventDefault()">
897
905
  @if (loading()) {
898
- <div class="picker-loading">Searching...</div>
906
+ <div class="picker-loading">{{ labels.t('messagingTemplatePicker.searching') }}</div>
899
907
  } @else if (results().length === 0) {
900
- <div class="picker-empty">No templates found</div>
908
+ <div class="picker-empty">{{ labels.t('messagingTemplatePicker.noTemplatesFound') }}</div>
901
909
  } @else {
902
910
  @for (tpl of results(); track tpl.id; let i = $index) {
903
911
  <div
@@ -914,17 +922,17 @@ i0.ɵɵngDeclareClassMetadata({
914
922
  <span class="option-title">
915
923
  <span class="option-name">{{ tpl.name }}</span>
916
924
  @if (tpl.isSystem !== undefined) {
917
- <span class="option-badge" [class.system]="tpl.isSystem">{{ tpl.isSystem ? 'System' : 'Custom' }}</span>
925
+ <span class="option-badge" [class.system]="tpl.isSystem">{{ tpl.isSystem ? labels.t('messagingTemplatePicker.system') : labels.t('messagingTemplatePicker.custom') }}</span>
918
926
  }
919
927
  </span>
920
928
  <span class="option-detail">
921
- @if (!isCurrentlyActive(tpl)) { <span class="option-inactive">Inactive · </span> }{{ previewText(tpl.subject || tpl.body) }}
929
+ @if (!isCurrentlyActive(tpl)) { <span class="option-inactive">{{ labels.t('messagingTemplatePicker.inactive') }} · </span> }{{ previewText(tpl.subject || tpl.body) }}
922
930
  </span>
923
931
  </span>
924
932
  <button
925
933
  type="button"
926
934
  class="option-preview-btn"
927
- [attr.aria-label]="'Preview ' + tpl.name"
935
+ [attr.aria-label]="labels.t('messagingSendMessagePanel.previewNamed', { name: tpl.name })"
928
936
  (mousedown)="$event.stopPropagation(); $event.preventDefault(); onPreviewMouseDown(tpl)"
929
937
  (click)="$event.stopPropagation(); onPreviewClick(tpl)">
930
938
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
@@ -966,7 +974,7 @@ i0.ɵɵngDeclareClassMetadata({
966
974
  required: false
967
975
  }]
968
976
  }],
969
- label: [{
977
+ labelInput: [{
970
978
  type: i0.Input,
971
979
  args: [{
972
980
  isSignal: true,
@@ -1015,7 +1023,7 @@ const SIGNER_ROLES = [
1015
1023
  "signer3",
1016
1024
  "signer4"
1017
1025
  ];
1018
- const SIGNER_ROLE_LABEL_DEFAULT = (role) => `Signer ${roleNumber(role)}`;
1026
+ const SIGNER_ROLE_LABEL_DEFAULT = (role) => uiText("messagingDocumentSendOptions.signerRole", { n: roleNumber(role) });
1019
1027
  function roleNumber(role) {
1020
1028
  return Number(role.slice(6));
1021
1029
  }
@@ -1097,15 +1105,15 @@ function describeDocumentSigners(docs, states, cfg) {
1097
1105
  info.roles = roles;
1098
1106
  info.multiSigner = roles.length > 1;
1099
1107
  const d = per[0].doc;
1100
- if (roles.length > 1) info.fieldsLine = `Fields placed for ${roles.length} signers`;
1101
- else if (informational && d.hasMapping !== void 0) info.fieldsLine = "No signature fields placed. Sent for viewing only.";
1108
+ if (roles.length > 1) info.fieldsLine = uiText("messagingDocumentSendOptions.fieldsForSigners", { count: roles.length });
1109
+ else if (informational && d.hasMapping !== void 0) info.fieldsLine = uiText("messagingDocumentSendOptions.noFieldsPlaced");
1102
1110
  } else info.multiDocumentNote = signable.some((p) => effectiveRoles(p.state.roles).length > 1);
1103
1111
  return info;
1104
1112
  }
1105
1113
  function countersignLine(info, cfg, viaDocuSign) {
1106
1114
  if (!info || !cfg || info.informational || !info.countersign) return "";
1107
- if (viaDocuSign) return "You countersign after the client signs.";
1108
- return `Countersigned by ${cfg.countersignerName?.trim() || "you"} after all signers.`;
1115
+ if (viaDocuSign) return uiText("messagingDocumentSendOptions.youCountersign");
1116
+ return uiText("messagingDocumentSendOptions.countersignedBy", { name: cfg.countersignerName?.trim() || uiText("messagingDocumentSendOptions.you") });
1109
1117
  }
1110
1118
  function buildSigners(roles, recipient, picks) {
1111
1119
  if (roles.length < 2) return null;
@@ -1129,12 +1137,15 @@ function signerProblems(roles, recipient, picks, cfg) {
1129
1137
  for (const role of roles) {
1130
1138
  const p = role === "signer1" ? recipient : picks[role];
1131
1139
  if (!p) {
1132
- out[role] = `${signerRoleLabel(role, cfg)} is required.`;
1140
+ out[role] = uiText("validation.required", { label: signerRoleLabel(role, cfg) });
1133
1141
  continue;
1134
1142
  }
1135
- if (cfg?.requireSignerEmail !== false && !(p.email ?? "").trim()) out[role] = `${p.name} has no email address.`;
1143
+ if (cfg?.requireSignerEmail !== false && !(p.email ?? "").trim()) out[role] = uiText("messagingDocumentSendOptions.signerNoEmail", { name: p.name });
1136
1144
  const earlier = seen.get(p.id);
1137
- if (earlier && !cfg?.allowSameContactInRoles && !out[role]) out[role] = `${p.name} is already ${signerRoleLabel(earlier, cfg)}.`;
1145
+ if (earlier && !cfg?.allowSameContactInRoles && !out[role]) out[role] = uiText("messagingDocumentSendOptions.signerAlreadyRole", {
1146
+ name: p.name,
1147
+ role: signerRoleLabel(earlier, cfg)
1148
+ });
1138
1149
  if (!earlier) seen.set(p.id, role);
1139
1150
  }
1140
1151
  return out;
@@ -1157,7 +1168,7 @@ function forwardContext(ctx, types) {
1157
1168
  } : { dropped };
1158
1169
  }
1159
1170
  function mergeFieldText(f) {
1160
- const reason = f.detail?.trim() || (f.reason === "NO_CONTEXT" ? "not available for this send" : f.reason === "NO_DATA" ? "no value on record" : f.reason === "UNKNOWN_PATH" ? "unknown field" : "");
1171
+ const reason = f.detail?.trim() || (f.reason === "NO_CONTEXT" || f.reason === "NO_DATA" || f.reason === "UNKNOWN_PATH" ? uiText(`messagingDocumentSendOptions.mergeReason.${f.reason}`) : "");
1161
1172
  return reason ? `${f.label} (${reason})` : f.label;
1162
1173
  }
1163
1174
  function mergeWarningLines(res, docTitles) {
@@ -1166,11 +1177,11 @@ function mergeWarningLines(res, docTitles) {
1166
1177
  return docs.map((d) => {
1167
1178
  const list = d.empty.map(mergeFieldText).join(", ");
1168
1179
  const title = d.title || docTitles[d.documentId] || "";
1169
- return `${prefixed && title ? `${title}: ` : ""}These fields will be left empty: ${list}.`;
1180
+ return `${prefixed && title ? `${title}: ` : ""}${uiText("messagingDocumentSendOptions.mergeEmpty", { list })}`;
1170
1181
  });
1171
1182
  }
1172
1183
  function countersignPlaceLabel(title) {
1173
- return `Place your countersignature box on ${title}`;
1184
+ return uiText("messagingDocumentSendOptions.placeCountersignOn", { title });
1174
1185
  }
1175
1186
  const DEFAULT_SIGNING_OPTIONS = {
1176
1187
  expiryDays: 7,
@@ -1225,12 +1236,20 @@ const INFORMATIONAL_PURPOSE_LABELS = {
1225
1236
  BOTH: "View link and attachment"
1226
1237
  };
1227
1238
  var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1239
+ get searchFailedMessage() {
1240
+ return this.labels.t("messagingDocumentSendOptions.searchFailedMessage");
1241
+ }
1228
1242
  constructor() {
1243
+ this.labels = inject(UiLabelsService);
1229
1244
  this.http = inject(HttpClient);
1230
1245
  this.destroyRef = inject(DestroyRef);
1231
1246
  this.apiBase = inject(FORGEROUTE_CONFIG, { optional: true })?.baseUrl ?? "/api";
1232
1247
  this.channel = input("email", ...ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []);
1233
- this.label = input("Documents", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
1248
+ this.labelInput = input(null, {
1249
+ ...ngDevMode ? { debugName: "labelInput" } : /* istanbul ignore next */ {},
1250
+ alias: "label"
1251
+ });
1252
+ this.label = computed(() => this.labelInput() ?? this.labels.t("messagingDocumentSendOptions.label"), ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
1234
1253
  this.required = input(false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
1235
1254
  this.showErrors = input(false, ...ngDevMode ? [{ debugName: "showErrors" }] : /* istanbul ignore next */ []);
1236
1255
  this.valueChange = output();
@@ -1252,11 +1271,14 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1252
1271
  this.purposeOptions = computed(() => {
1253
1272
  const allowed = this.purposes();
1254
1273
  const base = DOCUMENT_PURPOSE_OPTIONS.filter((o) => allowed.includes(o.value));
1255
- const opts = base.length ? base : DOCUMENT_PURPOSE_OPTIONS;
1256
- return this.signerInfo()?.informational ? opts.map((o) => ({
1274
+ const opts = (base.length ? base : DOCUMENT_PURPOSE_OPTIONS).map((o) => ({
1257
1275
  ...o,
1258
- label: INFORMATIONAL_PURPOSE_LABELS[o.value] ?? o.label
1259
- })) : opts;
1276
+ label: this.labels.t(`messagingDocumentSendOptions.purpose.${o.value}`)
1277
+ }));
1278
+ return this.signerInfo()?.informational ? opts.map((o) => INFORMATIONAL_PURPOSE_LABELS[o.value] ? {
1279
+ ...o,
1280
+ label: this.labels.t(`messagingDocumentSendOptions.purposeInfo.${o.value}`)
1281
+ } : o) : opts;
1260
1282
  }, ...ngDevMode ? [{ debugName: "purposeOptions" }] : /* istanbul ignore next */ []);
1261
1283
  this.search$ = new Subject();
1262
1284
  this.options = signal([], ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
@@ -1272,10 +1294,15 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1272
1294
  this.expiryDays = signal(String(DEFAULT_SIGNING_OPTIONS.expiryDays), ...ngDevMode ? [{ debugName: "expiryDays" }] : /* istanbul ignore next */ []);
1273
1295
  this.reminderIntervalDays = signal(String(DEFAULT_SIGNING_OPTIONS.reminderIntervalDays), ...ngDevMode ? [{ debugName: "reminderIntervalDays" }] : /* istanbul ignore next */ []);
1274
1296
  this.maxReminders = signal(String(DEFAULT_SIGNING_OPTIONS.maxReminders), ...ngDevMode ? [{ debugName: "maxReminders" }] : /* istanbul ignore next */ []);
1275
- this.providerOptions = DOCUMENT_PROVIDER_OPTIONS;
1297
+ this.providerOptions = computed(() => [{
1298
+ value: "NATIVE",
1299
+ label: this.labels.t("messagingDocumentSendOptions.provider.native")
1300
+ }, {
1301
+ value: "DOCUSIGN",
1302
+ label: this.labels.t("messagingDocumentSendOptions.provider.docusign")
1303
+ }], ...ngDevMode ? [{ debugName: "providerOptions" }] : /* istanbul ignore next */ []);
1276
1304
  this.combo = viewChild(ComboboxComponent, ...ngDevMode ? [{ debugName: "combo" }] : /* istanbul ignore next */ []);
1277
1305
  this.searchFailed = signal(false, ...ngDevMode ? [{ debugName: "searchFailed" }] : /* istanbul ignore next */ []);
1278
- this.searchFailedMessage = "Couldn't search your documents. Check your connection and try again.";
1279
1306
  this.available = computed(() => this.channel() !== "text" || this.purposes().includes("ESIGN"), ...ngDevMode ? [{ debugName: "available" }] : /* istanbul ignore next */ []);
1280
1307
  this.providerChoiceShown = computed(() => {
1281
1308
  if (!(this.docuSignConnected() && this.channel() === "email" && this.selected().length === 1 && this.purposes().includes("ESIGN"))) return false;
@@ -1290,23 +1317,32 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1290
1317
  this.envelopeProblem = computed(() => {
1291
1318
  if (!this.envelope() || this.envelopeStatus() !== "ready") return "";
1292
1319
  const docs = this.selected();
1293
- if (docs.length === 0) return "This envelope has no documents. Add a document first.";
1320
+ if (docs.length === 0) return this.labels.t("messagingDocumentSendOptions.envelopeNoDocs");
1294
1321
  const inactive = docs.find((d) => d.status !== void 0 && d.status !== "ACTIVE");
1295
- return inactive ? `"${inactive.title}" is ${inactive.status.toLowerCase()}, so this envelope can't be sent. Restore it or remove it from the envelope first.` : "";
1322
+ return inactive ? this.labels.t("messagingDocumentSendOptions.envelopeInactive", {
1323
+ title: inactive.title,
1324
+ status: inactive.status.toLowerCase()
1325
+ }) : "";
1296
1326
  }, ...ngDevMode ? [{ debugName: "envelopeProblem" }] : /* istanbul ignore next */ []);
1297
1327
  this.envelopeReadMessage = computed(() => {
1298
1328
  const status = this.envelopeStatus();
1299
- return status === "loading" ? "Reading the envelope's documents…" : status === "error" ? "The documents of this envelope couldn't be read, so it can't be sent yet." : "";
1329
+ return status === "loading" ? this.labels.t("messagingDocumentSendOptions.envelopeReading") : status === "error" ? this.labels.t("messagingDocumentSendOptions.envelopeReadFailed") : "";
1300
1330
  }, ...ngDevMode ? [{ debugName: "envelopeReadMessage" }] : /* istanbul ignore next */ []);
1301
1331
  this.signerReadMessage = computed(() => {
1302
1332
  const status = this.signerInfo()?.status;
1303
- return status === "loading" ? "Reading the document's signers…" : status === "error" ? "The signers of this document couldn't be read, so it can't be sent yet." : "";
1333
+ return status === "loading" ? this.labels.t("messagingDocumentSendOptions.signersReading") : status === "error" ? this.labels.t("messagingDocumentSendOptions.signersReadFailed") : "";
1304
1334
  }, ...ngDevMode ? [{ debugName: "signerReadMessage" }] : /* istanbul ignore next */ []);
1305
1335
  this.signingSummary = computed(() => {
1306
- const day = (raw) => `${raw} ${raw.trim() === "1" ? "day" : "days"}`;
1336
+ const day = (raw) => this.labels.t(raw.trim() === "1" ? "messagingDocumentSendOptions.day_one" : "messagingDocumentSendOptions.day_other", { n: raw });
1307
1337
  const max = this.maxReminders().trim();
1308
- const reminders = max === "0" ? "no reminders" : `reminders every ${day(this.reminderIntervalDays().trim())}, up to ${max}`;
1309
- return `Signing options: expires in ${day(this.expiryDays().trim())}, ${reminders}`;
1338
+ const reminders = max === "0" ? this.labels.t("messagingDocumentSendOptions.noReminders") : this.labels.t("messagingDocumentSendOptions.remindersEvery", {
1339
+ interval: day(this.reminderIntervalDays().trim()),
1340
+ max
1341
+ });
1342
+ return this.labels.t("messagingDocumentSendOptions.signingSummary", {
1343
+ expiry: day(this.expiryDays().trim()),
1344
+ reminders
1345
+ });
1310
1346
  }, ...ngDevMode ? [{ debugName: "signingSummary" }] : /* istanbul ignore next */ []);
1311
1347
  this.signingExpanded = computed(() => this.signingOpen() || this.signingError() !== "", ...ngDevMode ? [{ debugName: "signingExpanded" }] : /* istanbul ignore next */ []);
1312
1348
  this.purpose = computed(() => {
@@ -1343,24 +1379,29 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1343
1379
  ["maxReminders", this.maxReminders()]
1344
1380
  ];
1345
1381
  for (const [key, raw] of fields) {
1346
- const { min, max, label } = SIGNING_LIMITS[key];
1382
+ const { min, max } = SIGNING_LIMITS[key];
1383
+ const label = this.labels.t(`messagingDocumentSendOptions.${key}`);
1347
1384
  const n = wholeNumber(raw);
1348
- if ((raw ?? "").trim() === "") return `${label} is required.`;
1349
- if (n === null || n < min || n > max) return `${label} must be a whole number from ${min} to ${max}.`;
1385
+ if ((raw ?? "").trim() === "") return this.labels.t("validation.required", { label });
1386
+ if (n === null || n < min || n > max) return this.labels.t("messagingDocumentSendOptions.mustBeWhole", {
1387
+ label,
1388
+ min,
1389
+ max
1390
+ });
1350
1391
  }
1351
1392
  return "";
1352
1393
  }, ...ngDevMode ? [{ debugName: "signingError" }] : /* istanbul ignore next */ []);
1353
1394
  this.documentError = computed(() => {
1354
1395
  const docs = this.selected();
1355
- if (this.required() && docs.length === 0) return "Select at least one document.";
1396
+ if (this.required() && docs.length === 0) return this.labels.t("messagingDocumentSendOptions.selectAtLeastOne");
1356
1397
  if (this.purpose() === "ATTACH") {
1357
1398
  const noFile = docs.find((d) => !d.hasFile);
1358
- if (noFile) return `"${noFile.title}" has no file to attach — choose e-signature or remove it.`;
1399
+ if (noFile) return this.labels.t("messagingDocumentSendOptions.noFileToAttach", { title: noFile.title });
1359
1400
  }
1360
1401
  const env = this.envelopeProblem();
1361
1402
  if (env) return env;
1362
1403
  const info = this.signerInfo();
1363
- if (info?.multiSigner && this.channel() === "text") return `This document has ${info.roles.length} signers. Send it by email.`;
1404
+ if (info?.multiSigner && this.channel() === "text") return this.labels.t("messagingDocumentSendOptions.multiSignerSendByEmail", { count: info.roles.length });
1364
1405
  return "";
1365
1406
  }, ...ngDevMode ? [{ debugName: "documentError" }] : /* istanbul ignore next */ []);
1366
1407
  this.errorMessage = computed(() => this.documentError() || this.envelopeReadMessage() || this.signerReadMessage() || this.signingError(), ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
@@ -1393,7 +1434,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1393
1434
  return {
1394
1435
  value: doc.id,
1395
1436
  label: doc.title,
1396
- description: doc.fileName ?? "No file uploaded",
1437
+ description: doc.fileName ?? this.labels.t("messagingDocumentSendOptions.noFileUploaded"),
1397
1438
  data: doc
1398
1439
  };
1399
1440
  }));
@@ -1511,7 +1552,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1511
1552
  static toOption(d) {
1512
1553
  return {
1513
1554
  id: String(d.id),
1514
- title: String(d.title ?? d.fileName ?? "Untitled document"),
1555
+ title: String(d.title ?? d.fileName ?? uiText("messagingDocumentSendOptions.untitled")),
1515
1556
  fileName: d.fileName ?? null,
1516
1557
  hasFile: Number(d.fileSize ?? 0) > 0 || !!d.fileName,
1517
1558
  ...d.requiresCountersign === true ? { requiresCountersign: true } : {},
@@ -1652,8 +1693,8 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1652
1693
  isRequired: false,
1653
1694
  transformFunction: null
1654
1695
  },
1655
- label: {
1656
- classPropertyName: "label",
1696
+ labelInput: {
1697
+ classPropertyName: "labelInput",
1657
1698
  publicName: "label",
1658
1699
  isSignal: true,
1659
1700
  isRequired: false,
@@ -1757,35 +1798,34 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1757
1798
  <div class="dso" role="group" [attr.aria-label]="label()">
1758
1799
  @if (available()) {
1759
1800
  @if (locked()) {
1760
- <p class="dso-locked-label" data-testid="locked-documents-label">Included documents</p>
1801
+ <p class="dso-locked-label" data-testid="locked-documents-label">{{ labels.t('messagingDocumentSendOptions.includedDocuments') }}</p>
1761
1802
  @if (envelopeStatus() === 'loading') {
1762
1803
  <div role="status"><ui-hint data-testid="envelope-loading">{{ envelopeReadMessage() }}</ui-hint></div>
1763
1804
  } @else if (envelopeStatus() === 'error') {
1764
1805
  <div class="dso-retry">
1765
1806
  <ui-error errorId="dso-envelope-error" [message]="envelopeReadMessage()" />
1766
- <ui-button variant="secondary" size="sm" (click)="retryEnvelope()">Retry</ui-button>
1807
+ <ui-button variant="secondary" size="sm" (click)="retryEnvelope()">{{ labels.t('common.retry') }}</ui-button>
1767
1808
  </div>
1768
1809
  }
1769
1810
  } @else {
1770
1811
  <ui-combobox
1771
1812
  [label]="label()"
1772
1813
  [required]="required()"
1773
- placeholder="Search your documents by name…"
1814
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchYourDocumentsByName')"
1774
1815
  [options]="options()"
1775
1816
  [loading]="loading()"
1776
1817
  [minChars]="2"
1777
- [noResultsText]="searchFailed() ? 'The search failed' : 'No matching documents'"
1778
- [hint]="channel() === 'text' ? 'Documents are texted as a link to the signing page.' : ''"
1818
+ [noResultsText]="searchFailed() ? labels.t('messagingDocumentSendOptions.searchFailed') : labels.t('messagingDocumentSendOptions.noMatchingDocuments')"
1819
+ [hint]="channel() === 'text' ? labels.t('messagingDocumentSendOptions.textedAsLink') : ''"
1779
1820
  [error]="searchFailed() ? searchFailedMessage : ''"
1780
1821
  [showErrors]="searchFailed()"
1781
1822
  (searchChange)="search$.next($event)"
1782
1823
  (valueChange)="onPick($event)" />
1783
- <ui-error errorId="dso-search-error" [message]="searchFailed() ? 'Couldn\\'t search your documents. Check your connection and try again.' : ''" />
1784
1824
  }
1785
1825
 
1786
1826
  @if (selected().length > 0) {
1787
1827
  <!-- One row per document: the chip (or the plain title when the list is fixed) and, beside it, its small Preview button. -->
1788
- <div class="dso-docs" role="group" aria-label="Chosen documents">
1828
+ <div class="dso-docs" role="group" [attr.aria-label]="labels.t('messagingDocumentSendOptions.chosenDocuments')">
1789
1829
  @for (d of selected(); track d.id) {
1790
1830
  <div class="dso-doc">
1791
1831
  @if (locked()) {
@@ -1797,8 +1837,8 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1797
1837
  @if (documentPreview()) {
1798
1838
  <ui-button
1799
1839
  variant="secondary" size="sm"
1800
- [ariaLabel]="selected().length === 1 ? undefined : 'Preview ' + d.title"
1801
- (click)="preview(d)">{{ selected().length === 1 ? 'Preview Document' : 'Preview' }}</ui-button>
1840
+ [ariaLabel]="selected().length === 1 ? undefined : labels.t('messagingSendMessagePanel.previewNamed', { name: d.title })"
1841
+ (click)="preview(d)">{{ selected().length === 1 ? labels.t('messagingDocumentSendOptions.previewDocument') : labels.t('common.preview') }}</ui-button>
1802
1842
  }
1803
1843
  </div>
1804
1844
  }
@@ -1813,30 +1853,30 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1813
1853
  } @else if (signerInfo()?.status === 'error') {
1814
1854
  <div class="dso-retry">
1815
1855
  <ui-error errorId="dso-roles-error" [message]="signerReadMessage()" />
1816
- <ui-button variant="secondary" size="sm" (click)="retryRoles()">Retry</ui-button>
1856
+ <ui-button variant="secondary" size="sm" (click)="retryRoles()">{{ labels.t('common.retry') }}</ui-button>
1817
1857
  </div>
1818
1858
  }
1819
1859
 
1820
1860
  @if (providerChoiceShown()) {
1821
1861
  <ui-radio-group
1822
- label="Signature provider"
1862
+ [label]="labels.t('messagingDocumentSendOptions.signatureProvider')"
1823
1863
  orientation="horizontal"
1824
- [options]="providerOptions"
1864
+ [options]="providerOptions()"
1825
1865
  [ngModel]="provider()"
1826
1866
  (ngModelChange)="setProvider($event)" />
1827
1867
  }
1828
1868
 
1829
1869
  @if (provider() === 'DOCUSIGN') {
1830
- <ui-hint>DocuSign emails the document to the recipient. The Subject and Message above are used for the DocuSign email.</ui-hint>
1870
+ <ui-hint>{{ labels.t('messagingDocumentSendOptions.docusignEmailsTheDocumentTo') }}</ui-hint>
1831
1871
  @if (showCountersign()) {
1832
1872
  <ui-checkbox
1833
- label="Require my countersignature after client signs"
1873
+ [label]="labels.t('messagingDocumentSendOptions.requireMyCountersignatureAfterClient')"
1834
1874
  [ngModel]="countersignChoice()"
1835
1875
  (ngModelChange)="countersignChoice.set(!!$event)" />
1836
1876
  }
1837
1877
  } @else if (channel() === 'email' && selected().length > 0 && purposeOptions().length > 1) {
1838
1878
  <ui-select
1839
- label="What should the recipient do?"
1879
+ [label]="labels.t('messagingDocumentSendOptions.whatShouldTheRecipientDo')"
1840
1880
  [options]="purposeOptions()"
1841
1881
  [ngModel]="purpose()"
1842
1882
  (ngModelChange)="setPurpose($event)" />
@@ -1852,21 +1892,21 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1852
1892
  <ui-button
1853
1893
  variant="ghost" size="xs"
1854
1894
  [ariaExpanded]="signingExpanded()"
1855
- [ariaLabel]="signingExpanded() ? 'Hide signing options' : 'Change signing options'"
1856
- (click)="toggleSigning()">{{ signingExpanded() ? 'Hide' : 'Change' }}</ui-button>
1895
+ [ariaLabel]="signingExpanded() ? labels.t('messagingDocumentSendOptions.hideSigningOptions') : labels.t('messagingDocumentSendOptions.changeSigningOptions')"
1896
+ (click)="toggleSigning()">{{ signingExpanded() ? labels.t('common.hide') : labels.t('common.change') }}</ui-button>
1857
1897
  </div>
1858
1898
  @if (signingExpanded()) {
1859
1899
  <ui-form-section>
1860
1900
  <!-- One column: three labelled number fields do not fit side by side in a 480px panel or on a phone. -->
1861
1901
  <ui-form-row [cols]="1">
1862
1902
  <ui-text-input
1863
- label="Expiry (days)" type="number" [required]="true" [min]="1" [max]="90" [maxDecimals]="0"
1903
+ [label]="labels.t('messagingDocumentSendOptions.expiryDays')" type="number" [required]="true" [min]="1" [max]="90" [maxDecimals]="0"
1864
1904
  [showErrors]="showErrors()" [ngModel]="expiryDays()" (ngModelChange)="expiryDays.set(asText($event))" />
1865
1905
  <ui-text-input
1866
- label="Reminder Interval (days)" type="number" [required]="true" [min]="1" [max]="30" [maxDecimals]="0"
1906
+ [label]="labels.t('messagingDocumentSendOptions.reminderIntervalDays')" type="number" [required]="true" [min]="1" [max]="30" [maxDecimals]="0"
1867
1907
  [showErrors]="showErrors()" [ngModel]="reminderIntervalDays()" (ngModelChange)="reminderIntervalDays.set(asText($event))" />
1868
1908
  <ui-text-input
1869
- label="Max Reminders" type="number" [required]="true" [min]="0" [max]="10" [maxDecimals]="0"
1909
+ [label]="labels.t('messagingDocumentSendOptions.maxReminders')" type="number" [required]="true" [min]="0" [max]="10" [maxDecimals]="0"
1870
1910
  [showErrors]="showErrors()" [ngModel]="maxReminders()" (ngModelChange)="maxReminders.set(asText($event))" />
1871
1911
  </ui-form-row>
1872
1912
  </ui-form-section>
@@ -1874,7 +1914,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1874
1914
  </div>
1875
1915
  }
1876
1916
  } @else {
1877
- <p class="dso-unavailable" role="note">Documents can only be texted as an e-signature link, which isn't available here.</p>
1917
+ <p class="dso-unavailable" role="note">{{ labels.t('messagingDocumentSendOptions.documentsCanOnlyBeTexted') }}</p>
1878
1918
  }
1879
1919
  </div>
1880
1920
  `,
@@ -2157,35 +2197,34 @@ i0.ɵɵngDeclareClassMetadata({
2157
2197
  <div class="dso" role="group" [attr.aria-label]="label()">
2158
2198
  @if (available()) {
2159
2199
  @if (locked()) {
2160
- <p class="dso-locked-label" data-testid="locked-documents-label">Included documents</p>
2200
+ <p class="dso-locked-label" data-testid="locked-documents-label">{{ labels.t('messagingDocumentSendOptions.includedDocuments') }}</p>
2161
2201
  @if (envelopeStatus() === 'loading') {
2162
2202
  <div role="status"><ui-hint data-testid="envelope-loading">{{ envelopeReadMessage() }}</ui-hint></div>
2163
2203
  } @else if (envelopeStatus() === 'error') {
2164
2204
  <div class="dso-retry">
2165
2205
  <ui-error errorId="dso-envelope-error" [message]="envelopeReadMessage()" />
2166
- <ui-button variant="secondary" size="sm" (click)="retryEnvelope()">Retry</ui-button>
2206
+ <ui-button variant="secondary" size="sm" (click)="retryEnvelope()">{{ labels.t('common.retry') }}</ui-button>
2167
2207
  </div>
2168
2208
  }
2169
2209
  } @else {
2170
2210
  <ui-combobox
2171
2211
  [label]="label()"
2172
2212
  [required]="required()"
2173
- placeholder="Search your documents by name…"
2213
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchYourDocumentsByName')"
2174
2214
  [options]="options()"
2175
2215
  [loading]="loading()"
2176
2216
  [minChars]="2"
2177
- [noResultsText]="searchFailed() ? 'The search failed' : 'No matching documents'"
2178
- [hint]="channel() === 'text' ? 'Documents are texted as a link to the signing page.' : ''"
2217
+ [noResultsText]="searchFailed() ? labels.t('messagingDocumentSendOptions.searchFailed') : labels.t('messagingDocumentSendOptions.noMatchingDocuments')"
2218
+ [hint]="channel() === 'text' ? labels.t('messagingDocumentSendOptions.textedAsLink') : ''"
2179
2219
  [error]="searchFailed() ? searchFailedMessage : ''"
2180
2220
  [showErrors]="searchFailed()"
2181
2221
  (searchChange)="search$.next($event)"
2182
2222
  (valueChange)="onPick($event)" />
2183
- <ui-error errorId="dso-search-error" [message]="searchFailed() ? 'Couldn\\'t search your documents. Check your connection and try again.' : ''" />
2184
2223
  }
2185
2224
 
2186
2225
  @if (selected().length > 0) {
2187
2226
  <!-- One row per document: the chip (or the plain title when the list is fixed) and, beside it, its small Preview button. -->
2188
- <div class="dso-docs" role="group" aria-label="Chosen documents">
2227
+ <div class="dso-docs" role="group" [attr.aria-label]="labels.t('messagingDocumentSendOptions.chosenDocuments')">
2189
2228
  @for (d of selected(); track d.id) {
2190
2229
  <div class="dso-doc">
2191
2230
  @if (locked()) {
@@ -2197,8 +2236,8 @@ i0.ɵɵngDeclareClassMetadata({
2197
2236
  @if (documentPreview()) {
2198
2237
  <ui-button
2199
2238
  variant="secondary" size="sm"
2200
- [ariaLabel]="selected().length === 1 ? undefined : 'Preview ' + d.title"
2201
- (click)="preview(d)">{{ selected().length === 1 ? 'Preview Document' : 'Preview' }}</ui-button>
2239
+ [ariaLabel]="selected().length === 1 ? undefined : labels.t('messagingSendMessagePanel.previewNamed', { name: d.title })"
2240
+ (click)="preview(d)">{{ selected().length === 1 ? labels.t('messagingDocumentSendOptions.previewDocument') : labels.t('common.preview') }}</ui-button>
2202
2241
  }
2203
2242
  </div>
2204
2243
  }
@@ -2213,30 +2252,30 @@ i0.ɵɵngDeclareClassMetadata({
2213
2252
  } @else if (signerInfo()?.status === 'error') {
2214
2253
  <div class="dso-retry">
2215
2254
  <ui-error errorId="dso-roles-error" [message]="signerReadMessage()" />
2216
- <ui-button variant="secondary" size="sm" (click)="retryRoles()">Retry</ui-button>
2255
+ <ui-button variant="secondary" size="sm" (click)="retryRoles()">{{ labels.t('common.retry') }}</ui-button>
2217
2256
  </div>
2218
2257
  }
2219
2258
 
2220
2259
  @if (providerChoiceShown()) {
2221
2260
  <ui-radio-group
2222
- label="Signature provider"
2261
+ [label]="labels.t('messagingDocumentSendOptions.signatureProvider')"
2223
2262
  orientation="horizontal"
2224
- [options]="providerOptions"
2263
+ [options]="providerOptions()"
2225
2264
  [ngModel]="provider()"
2226
2265
  (ngModelChange)="setProvider($event)" />
2227
2266
  }
2228
2267
 
2229
2268
  @if (provider() === 'DOCUSIGN') {
2230
- <ui-hint>DocuSign emails the document to the recipient. The Subject and Message above are used for the DocuSign email.</ui-hint>
2269
+ <ui-hint>{{ labels.t('messagingDocumentSendOptions.docusignEmailsTheDocumentTo') }}</ui-hint>
2231
2270
  @if (showCountersign()) {
2232
2271
  <ui-checkbox
2233
- label="Require my countersignature after client signs"
2272
+ [label]="labels.t('messagingDocumentSendOptions.requireMyCountersignatureAfterClient')"
2234
2273
  [ngModel]="countersignChoice()"
2235
2274
  (ngModelChange)="countersignChoice.set(!!$event)" />
2236
2275
  }
2237
2276
  } @else if (channel() === 'email' && selected().length > 0 && purposeOptions().length > 1) {
2238
2277
  <ui-select
2239
- label="What should the recipient do?"
2278
+ [label]="labels.t('messagingDocumentSendOptions.whatShouldTheRecipientDo')"
2240
2279
  [options]="purposeOptions()"
2241
2280
  [ngModel]="purpose()"
2242
2281
  (ngModelChange)="setPurpose($event)" />
@@ -2252,21 +2291,21 @@ i0.ɵɵngDeclareClassMetadata({
2252
2291
  <ui-button
2253
2292
  variant="ghost" size="xs"
2254
2293
  [ariaExpanded]="signingExpanded()"
2255
- [ariaLabel]="signingExpanded() ? 'Hide signing options' : 'Change signing options'"
2256
- (click)="toggleSigning()">{{ signingExpanded() ? 'Hide' : 'Change' }}</ui-button>
2294
+ [ariaLabel]="signingExpanded() ? labels.t('messagingDocumentSendOptions.hideSigningOptions') : labels.t('messagingDocumentSendOptions.changeSigningOptions')"
2295
+ (click)="toggleSigning()">{{ signingExpanded() ? labels.t('common.hide') : labels.t('common.change') }}</ui-button>
2257
2296
  </div>
2258
2297
  @if (signingExpanded()) {
2259
2298
  <ui-form-section>
2260
2299
  <!-- One column: three labelled number fields do not fit side by side in a 480px panel or on a phone. -->
2261
2300
  <ui-form-row [cols]="1">
2262
2301
  <ui-text-input
2263
- label="Expiry (days)" type="number" [required]="true" [min]="1" [max]="90" [maxDecimals]="0"
2302
+ [label]="labels.t('messagingDocumentSendOptions.expiryDays')" type="number" [required]="true" [min]="1" [max]="90" [maxDecimals]="0"
2264
2303
  [showErrors]="showErrors()" [ngModel]="expiryDays()" (ngModelChange)="expiryDays.set(asText($event))" />
2265
2304
  <ui-text-input
2266
- label="Reminder Interval (days)" type="number" [required]="true" [min]="1" [max]="30" [maxDecimals]="0"
2305
+ [label]="labels.t('messagingDocumentSendOptions.reminderIntervalDays')" type="number" [required]="true" [min]="1" [max]="30" [maxDecimals]="0"
2267
2306
  [showErrors]="showErrors()" [ngModel]="reminderIntervalDays()" (ngModelChange)="reminderIntervalDays.set(asText($event))" />
2268
2307
  <ui-text-input
2269
- label="Max Reminders" type="number" [required]="true" [min]="0" [max]="10" [maxDecimals]="0"
2308
+ [label]="labels.t('messagingDocumentSendOptions.maxReminders')" type="number" [required]="true" [min]="0" [max]="10" [maxDecimals]="0"
2270
2309
  [showErrors]="showErrors()" [ngModel]="maxReminders()" (ngModelChange)="maxReminders.set(asText($event))" />
2271
2310
  </ui-form-row>
2272
2311
  </ui-form-section>
@@ -2274,7 +2313,7 @@ i0.ɵɵngDeclareClassMetadata({
2274
2313
  </div>
2275
2314
  }
2276
2315
  } @else {
2277
- <p class="dso-unavailable" role="note">Documents can only be texted as an e-signature link, which isn't available here.</p>
2316
+ <p class="dso-unavailable" role="note">{{ labels.t('messagingDocumentSendOptions.documentsCanOnlyBeTexted') }}</p>
2278
2317
  }
2279
2318
  </div>
2280
2319
  `,
@@ -2291,7 +2330,7 @@ i0.ɵɵngDeclareClassMetadata({
2291
2330
  required: false
2292
2331
  }]
2293
2332
  }],
2294
- label: [{
2333
+ labelInput: [{
2295
2334
  type: i0.Input,
2296
2335
  args: [{
2297
2336
  isSignal: true,
@@ -2403,6 +2442,7 @@ i0.ɵɵngDeclareClassMetadata({
2403
2442
  });
2404
2443
  var DocumentSignersComponent = class DocumentSignersComponent {
2405
2444
  constructor() {
2445
+ this.labels = inject(UiLabelsService);
2406
2446
  this.info = input(null, ...ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []);
2407
2447
  this.config = input(null, ...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
2408
2448
  this.recipient = input({
@@ -2443,7 +2483,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2443
2483
  return countersignPlaceLabel(title);
2444
2484
  }
2445
2485
  placeText(title) {
2446
- return this.placeDocs().length > 1 ? countersignPlaceLabel(title) : "Place your countersignature box";
2486
+ return this.placeDocs().length > 1 ? countersignPlaceLabel(title) : this.labels.t("messagingDocumentSendOptions.placeCountersign");
2447
2487
  }
2448
2488
  label(role) {
2449
2489
  return signerRoleLabel(role, this.config());
@@ -2524,7 +2564,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2524
2564
  ngImport: i0,
2525
2565
  template: `
2526
2566
  @if (rowsShown()) {
2527
- <ui-form-section label="Signers" data-testid="signers-section">
2567
+ <ui-form-section [label]="labels.t('messagingDocumentSendOptions.signers')" data-testid="signers-section">
2528
2568
  @for (role of info()!.roles; track role) {
2529
2569
  @if (role === 'signer1') {
2530
2570
  <div class="ds-row" [attr.data-testid]="'signer-row-' + role">
@@ -2539,7 +2579,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2539
2579
  [required]="true"
2540
2580
  [showErrors]="showErrors()"
2541
2581
  [errorId]="'ds-error-' + role"
2542
- placeholder="Search for a contact…"
2582
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchForAContact')"
2543
2583
  (contactSelected)="pick(role, $event)" />
2544
2584
  <!-- The picker's own "<Role> is required." (shown after blur or with showErrors), else the contact problem (no email / twice). -->
2545
2585
  <div class="ds-error" [attr.id]="'ds-error-' + role">
@@ -2548,7 +2588,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2548
2588
  </div>
2549
2589
  }
2550
2590
  }
2551
- <ui-hint data-testid="signers-order-hint">Signers are emailed one after another, in this order; each receives the link after the previous one signs.</ui-hint>
2591
+ <ui-hint data-testid="signers-order-hint">{{ labels.t('messagingDocumentSendOptions.signersAreEmailedOneAfter') }}</ui-hint>
2552
2592
  </ui-form-section>
2553
2593
  }
2554
2594
  @if (line()) {
@@ -2673,7 +2713,7 @@ i0.ɵɵngDeclareClassMetadata({
2673
2713
  }],
2674
2714
  template: `
2675
2715
  @if (rowsShown()) {
2676
- <ui-form-section label="Signers" data-testid="signers-section">
2716
+ <ui-form-section [label]="labels.t('messagingDocumentSendOptions.signers')" data-testid="signers-section">
2677
2717
  @for (role of info()!.roles; track role) {
2678
2718
  @if (role === 'signer1') {
2679
2719
  <div class="ds-row" [attr.data-testid]="'signer-row-' + role">
@@ -2688,7 +2728,7 @@ i0.ɵɵngDeclareClassMetadata({
2688
2728
  [required]="true"
2689
2729
  [showErrors]="showErrors()"
2690
2730
  [errorId]="'ds-error-' + role"
2691
- placeholder="Search for a contact…"
2731
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchForAContact')"
2692
2732
  (contactSelected)="pick(role, $event)" />
2693
2733
  <!-- The picker's own "<Role> is required." (shown after blur or with showErrors), else the contact problem (no email / twice). -->
2694
2734
  <div class="ds-error" [attr.id]="'ds-error-' + role">
@@ -2697,7 +2737,7 @@ i0.ɵɵngDeclareClassMetadata({
2697
2737
  </div>
2698
2738
  }
2699
2739
  }
2700
- <ui-hint data-testid="signers-order-hint">Signers are emailed one after another, in this order; each receives the link after the previous one signs.</ui-hint>
2740
+ <ui-hint data-testid="signers-order-hint">{{ labels.t('messagingDocumentSendOptions.signersAreEmailedOneAfter') }}</ui-hint>
2701
2741
  </ui-form-section>
2702
2742
  }
2703
2743
  @if (line()) {
@@ -2942,6 +2982,10 @@ i0.ɵɵngDeclareClassMetadata({
2942
2982
  }]
2943
2983
  }
2944
2984
  });
2985
+ const SPANISH_VIDEO_LINK_WORDS = {
2986
+ watchVideo: "Ver video",
2987
+ watchNamed: "Ver \"{{ title }}\": {{ link }}"
2988
+ };
2945
2989
  function buildVideoLink(baseLink, options = {}) {
2946
2990
  const params = [];
2947
2991
  if (options.autoplay) params.push("autoplay=1");
@@ -2962,63 +3006,110 @@ function videoLinkWithDefaults(baseLink, defaults = {}, contactId = "") {
2962
3006
  hideReactions: !!defaults.hideReactions
2963
3007
  });
2964
3008
  }
3009
+ function watchVideoWordList() {
3010
+ return Array.from(/* @__PURE__ */ new Set([
3011
+ uiText("messagingVideoLink.watchVideo"),
3012
+ "Watch video",
3013
+ SPANISH_VIDEO_LINK_WORDS.watchVideo
3014
+ ]));
3015
+ }
3016
+ function watchVideoWords() {
3017
+ return "(?:" + watchVideoWordList().map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")";
3018
+ }
2965
3019
  function stripVideoLink(text) {
2966
- return text.replace(/\n\n🎬 Watch video: [^\n]*/g, "").replace(/\n🎬 Watch video: [^\n]*/g, "").replace(/🎬 Watch video: [^\n]*/g, "").trimEnd();
3020
+ return text.replace(new RegExp(`\\n\\n🎬 ${watchVideoWords()}: [^\\n]*`, "g"), "").replace(new RegExp(`\\n🎬 ${watchVideoWords()}: [^\\n]*`, "g"), "").replace(new RegExp(`🎬 ${watchVideoWords()}: [^\\n]*`, "g"), "").trimEnd();
2967
3021
  }
2968
3022
  function appendVideoLink(text, link) {
2969
3023
  const base = stripVideoLink(text);
2970
- return base ? `${base}\n\n🎬 Watch video: ${link}` : `🎬 Watch video: ${link}`;
3024
+ const line = `🎬 ${uiText("messagingVideoLink.watchVideo")}: ${link}`;
3025
+ return base ? `${base}\n\n${line}` : line;
2971
3026
  }
2972
3027
  function videoMessageLine(title, link) {
2973
- return `Watch "${title}": ${link}`;
3028
+ return uiText("messagingVideoLink.watchNamed", {
3029
+ title,
3030
+ link
3031
+ });
3032
+ }
3033
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3034
+ const fillTemplate = (tpl, title, link) => tpl.replace(/\{\{\s*title\s*\}\}/g, () => title).replace(/\{\{\s*link\s*\}\}/g, () => link);
3035
+ function namedTemplates() {
3036
+ return Array.from(/* @__PURE__ */ new Set([
3037
+ uiText("messagingVideoLink.watchNamed"),
3038
+ UI_DEFAULT_LABELS["messagingVideoLink.watchNamed"],
3039
+ SPANISH_VIDEO_LINK_WORDS.watchNamed
3040
+ ]));
3041
+ }
3042
+ function videoLineForms(line) {
3043
+ const forms = [line];
3044
+ for (const tpl of namedTemplates()) {
3045
+ const parts = tpl.split(/(\{\{\s*(?:title|link)\s*\}\})/);
3046
+ const order = [];
3047
+ const hit = new RegExp("^" + parts.map((part) => {
3048
+ const m = /^\{\{\s*(title|link)\s*\}\}$/.exec(part);
3049
+ if (!m) return escapeRe(part);
3050
+ order.push(m[1]);
3051
+ return m[1] === "title" ? "(.*?)" : "(.+)";
3052
+ }).join("") + "$").exec(line);
3053
+ if (!hit) continue;
3054
+ const found = {};
3055
+ order.forEach((name, i) => {
3056
+ found[name] = hit[i + 1];
3057
+ });
3058
+ for (const t of namedTemplates()) forms.push(fillTemplate(t, found["title"] ?? "", found["link"] ?? ""));
3059
+ forms.push(...watchVideoWordList().map((w) => `🎬 ${w}: ${found["link"] ?? ""}`));
3060
+ break;
3061
+ }
3062
+ const emoji = new RegExp(`^🎬 ${watchVideoWords()}: (.+)$`).exec(line);
3063
+ if (emoji) forms.push(...watchVideoWordList().map((w) => `🎬 ${w}: ${emoji[1]}`));
3064
+ return Array.from(new Set(forms));
2974
3065
  }
2975
3066
  function applyVideoLine(body, prev, next) {
2976
3067
  if (!prev) {
2977
3068
  if (!next) return body;
2978
3069
  return body.trim() ? `${body}\n\n${next}` : next;
2979
3070
  }
2980
- const at = body.indexOf(prev);
2981
- if (at < 0) return body;
2982
- if (next) return body.slice(0, at) + next + body.slice(at + prev.length);
3071
+ const found = videoLineForms(prev).find((form) => body.includes(form));
3072
+ if (found === void 0) return body;
3073
+ const at = body.indexOf(found);
3074
+ if (next) return body.slice(0, at) + next + body.slice(at + found.length);
2983
3075
  const before = body.slice(0, at);
2984
- const after = body.slice(at + prev.length);
3076
+ const after = body.slice(at + found.length);
2985
3077
  const trimmedBefore = before.replace(/\n{1,2}$/, "");
2986
3078
  return (trimmedBefore + after).trim() === "" ? "" : trimmedBefore + after;
2987
3079
  }
2988
- const VIDEO_PLAYBACK_OPTIONS = [{
2989
- value: "click",
2990
- label: "Click to Play"
2991
- }, {
2992
- value: "autoplay",
2993
- label: "Autoplay"
2994
- }];
2995
- const VIDEO_CTA_OPTIONS = [
2996
- {
2997
- value: "",
2998
- label: "None (use video default)"
2999
- },
3000
- {
3001
- value: "schedule",
3002
- label: "Schedule a Call"
3003
- },
3004
- {
3005
- value: "contact",
3006
- label: "Contact Me"
3007
- },
3008
- {
3009
- value: "custom",
3010
- label: "Custom"
3011
- }
3012
- ];
3013
3080
  var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3014
3081
  constructor() {
3082
+ this.labels = inject(UiLabelsService);
3015
3083
  this.video = input(null, ...ngDevMode ? [{ debugName: "video" }] : /* istanbul ignore next */ []);
3016
3084
  this.defaults = input({}, ...ngDevMode ? [{ debugName: "defaults" }] : /* istanbul ignore next */ []);
3017
3085
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
3018
3086
  this.linkChange = output();
3019
3087
  this.removed = output();
3020
- this.playbackOptions = VIDEO_PLAYBACK_OPTIONS;
3021
- this.ctaOptions = VIDEO_CTA_OPTIONS;
3088
+ this.playbackOptions = computed(() => [{
3089
+ value: "click",
3090
+ label: this.labels.t("messagingSendMessagePanel.playback.click")
3091
+ }, {
3092
+ value: "autoplay",
3093
+ label: this.labels.t("messagingSendMessagePanel.playback.autoplay")
3094
+ }], ...ngDevMode ? [{ debugName: "playbackOptions" }] : /* istanbul ignore next */ []);
3095
+ this.ctaOptions = computed(() => [
3096
+ {
3097
+ value: "",
3098
+ label: this.labels.t("messagingSendMessagePanel.cta.none")
3099
+ },
3100
+ {
3101
+ value: "schedule",
3102
+ label: this.labels.t("messagingSendMessagePanel.cta.schedule")
3103
+ },
3104
+ {
3105
+ value: "contact",
3106
+ label: this.labels.t("messagingSendMessagePanel.cta.contact")
3107
+ },
3108
+ {
3109
+ value: "custom",
3110
+ label: this.labels.t("messagingSendMessagePanel.cta.custom")
3111
+ }
3112
+ ], ...ngDevMode ? [{ debugName: "ctaOptions" }] : /* istanbul ignore next */ []);
3022
3113
  this.autoplay = signal("click", ...ngDevMode ? [{ debugName: "autoplay" }] : /* istanbul ignore next */ []);
3023
3114
  this.ctaType = signal("", ...ngDevMode ? [{ debugName: "ctaType" }] : /* istanbul ignore next */ []);
3024
3115
  this.ctaUrl = signal("", ...ngDevMode ? [{ debugName: "ctaUrl" }] : /* istanbul ignore next */ []);
@@ -3127,40 +3218,40 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3127
3218
  <ui-form-section class="vls">
3128
3219
  <div class="vls-bar">
3129
3220
  @if (v.shareLink) {
3130
- <ui-hint><a class="vls-preview" [href]="v.shareLink" target="_blank" rel="noopener" aria-label="Watch Preview (opens in a new tab)">Watch Preview →</a></ui-hint>
3221
+ <ui-hint><a class="vls-preview" [href]="v.shareLink" target="_blank" rel="noopener" [attr.aria-label]="labels.t('messagingVideoLinkSettings.watchPreviewOpensInA')">{{ labels.t('messagingVideoLinkSettings.watchPreview') }}</a></ui-hint>
3131
3222
  }
3132
- <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">Video settings</ui-button>
3133
- <ui-button variant="secondary" size="sm" (click)="removed.emit()">Remove Video</ui-button>
3223
+ <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">{{ labels.t('messagingVideoLinkSettings.videoSettings') }}</ui-button>
3224
+ <ui-button variant="secondary" size="sm" (click)="removed.emit()">{{ labels.t('messagingVideoLinkSettings.removeVideo') }}</ui-button>
3134
3225
  </div>
3135
3226
  <!-- Collapsed by default: the defaults from Communication settings are already applied. -->
3136
3227
  @if (settingsOpen()) {
3137
3228
  <ui-radio-group
3138
- label="Playback"
3229
+ [label]="labels.t('messagingVideoLinkSettings.playback')"
3139
3230
  orientation="horizontal"
3140
- [options]="playbackOptions"
3231
+ [options]="playbackOptions()"
3141
3232
  [ngModel]="autoplay()"
3142
3233
  (ngModelChange)="setAutoplay($event)" />
3143
3234
  <ui-select
3144
- label="Call-to-Action"
3145
- [options]="ctaOptions"
3235
+ [label]="labels.t('messagingVideoLinkSettings.callToAction')"
3236
+ [options]="ctaOptions()"
3146
3237
  [ngModel]="ctaType()"
3147
3238
  (ngModelChange)="setCtaType($event)" />
3148
3239
  @if (ctaType()) {
3149
3240
  <ui-text-input
3150
- label="Call-to-action URL"
3241
+ [label]="labels.t('messagingVideoLinkSettings.callToActionUrl')"
3151
3242
  type="url"
3152
3243
  placeholder="https://..."
3153
3244
  [maxlength]="500"
3154
3245
  [ngModel]="ctaUrl()"
3155
3246
  (ngModelChange)="setCtaUrl($event)" />
3156
3247
  <ui-text-input
3157
- label="Call-to-action label"
3158
- placeholder="Button label (e.g. Book a Call)"
3248
+ [label]="labels.t('messagingVideoLinkSettings.callToActionLabel')"
3249
+ [placeholder]="labels.t('messagingVideoLinkSettings.buttonLabelEGBook')"
3159
3250
  [maxlength]="100"
3160
3251
  [ngModel]="ctaLabel()"
3161
3252
  (ngModelChange)="setCtaLabel($event)" />
3162
3253
  }
3163
- <ui-checkbox label="Hide reactions" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3254
+ <ui-checkbox [label]="labels.t('messagingVideoLinkSettings.hideReactions')" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3164
3255
  }
3165
3256
  </ui-form-section>
3166
3257
  }
@@ -3381,40 +3472,40 @@ i0.ɵɵngDeclareClassMetadata({
3381
3472
  <ui-form-section class="vls">
3382
3473
  <div class="vls-bar">
3383
3474
  @if (v.shareLink) {
3384
- <ui-hint><a class="vls-preview" [href]="v.shareLink" target="_blank" rel="noopener" aria-label="Watch Preview (opens in a new tab)">Watch Preview →</a></ui-hint>
3475
+ <ui-hint><a class="vls-preview" [href]="v.shareLink" target="_blank" rel="noopener" [attr.aria-label]="labels.t('messagingVideoLinkSettings.watchPreviewOpensInA')">{{ labels.t('messagingVideoLinkSettings.watchPreview') }}</a></ui-hint>
3385
3476
  }
3386
- <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">Video settings</ui-button>
3387
- <ui-button variant="secondary" size="sm" (click)="removed.emit()">Remove Video</ui-button>
3477
+ <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">{{ labels.t('messagingVideoLinkSettings.videoSettings') }}</ui-button>
3478
+ <ui-button variant="secondary" size="sm" (click)="removed.emit()">{{ labels.t('messagingVideoLinkSettings.removeVideo') }}</ui-button>
3388
3479
  </div>
3389
3480
  <!-- Collapsed by default: the defaults from Communication settings are already applied. -->
3390
3481
  @if (settingsOpen()) {
3391
3482
  <ui-radio-group
3392
- label="Playback"
3483
+ [label]="labels.t('messagingVideoLinkSettings.playback')"
3393
3484
  orientation="horizontal"
3394
- [options]="playbackOptions"
3485
+ [options]="playbackOptions()"
3395
3486
  [ngModel]="autoplay()"
3396
3487
  (ngModelChange)="setAutoplay($event)" />
3397
3488
  <ui-select
3398
- label="Call-to-Action"
3399
- [options]="ctaOptions"
3489
+ [label]="labels.t('messagingVideoLinkSettings.callToAction')"
3490
+ [options]="ctaOptions()"
3400
3491
  [ngModel]="ctaType()"
3401
3492
  (ngModelChange)="setCtaType($event)" />
3402
3493
  @if (ctaType()) {
3403
3494
  <ui-text-input
3404
- label="Call-to-action URL"
3495
+ [label]="labels.t('messagingVideoLinkSettings.callToActionUrl')"
3405
3496
  type="url"
3406
3497
  placeholder="https://..."
3407
3498
  [maxlength]="500"
3408
3499
  [ngModel]="ctaUrl()"
3409
3500
  (ngModelChange)="setCtaUrl($event)" />
3410
3501
  <ui-text-input
3411
- label="Call-to-action label"
3412
- placeholder="Button label (e.g. Book a Call)"
3502
+ [label]="labels.t('messagingVideoLinkSettings.callToActionLabel')"
3503
+ [placeholder]="labels.t('messagingVideoLinkSettings.buttonLabelEGBook')"
3413
3504
  [maxlength]="100"
3414
3505
  [ngModel]="ctaLabel()"
3415
3506
  (ngModelChange)="setCtaLabel($event)" />
3416
3507
  }
3417
- <ui-checkbox label="Hide reactions" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3508
+ <ui-checkbox [label]="labels.t('messagingVideoLinkSettings.hideReactions')" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3418
3509
  }
3419
3510
  </ui-form-section>
3420
3511
  }
@@ -3478,10 +3569,16 @@ function buildDocumentsPayload(v, delivery, purpose = v.purpose, extras = {}) {
3478
3569
  const DEFAULT_DOCUMENT_SUBJECT = "Document Signature Required";
3479
3570
  function suggestDocumentSubject(titles, purpose, informational = false) {
3480
3571
  if (titles.length === 0) return "";
3481
- if (informational && purpose !== "ATTACH") return titles.length === 1 ? `Document shared: ${titles[0]}` : `${titles.length} documents shared: ${titles[0]}…`;
3482
- const attachOnly = purpose === "ATTACH";
3483
- if (titles.length === 1) return `${attachOnly ? "Document attached" : "Signature required"}: ${titles[0]}`;
3484
- return `${titles.length} documents ${attachOnly ? "attached" : "to sign"}: ${titles[0]}…`;
3572
+ if (informational && purpose !== "ATTACH") return titles.length === 1 ? uiText("messagingSendMessagePanel.subjectShared_one", { title: titles[0] }) : uiText("messagingSendMessagePanel.subjectShared_other", {
3573
+ count: titles.length,
3574
+ title: titles[0]
3575
+ });
3576
+ const base = purpose === "ATTACH" ? "messagingSendMessagePanel.subjectAttached" : "messagingSendMessagePanel.subjectSign";
3577
+ if (titles.length === 1) return uiText(`${base}_one`, { title: titles[0] });
3578
+ return uiText(`${base}_other`, {
3579
+ count: titles.length,
3580
+ title: titles[0]
3581
+ });
3485
3582
  }
3486
3583
  const MAX_CC = 10;
3487
3584
  function parseCcList(raw) {
@@ -3498,9 +3595,9 @@ function parseCcList(raw) {
3498
3595
  const CC_EMAIL_RE = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/;
3499
3596
  function ccError(list) {
3500
3597
  const bad = list.find((a) => a.length > 254 || !CC_EMAIL_RE.test(a));
3501
- if (bad) return `"${bad}" isn't a valid CC email address.`;
3502
- if (list.length > 10) return `You can copy at most 10 people.`;
3503
- if (list.join(",").length > 1e3) return "The CC list is too long.";
3598
+ if (bad) return uiText("messagingSendMessagePanel.ccInvalid", { address: bad });
3599
+ if (list.length > 10) return uiText("messagingSendMessagePanel.ccTooMany", { max: 10 });
3600
+ if (list.join(",").length > 1e3) return uiText("messagingSendMessagePanel.ccTooLong");
3504
3601
  return "";
3505
3602
  }
3506
3603
  function signingBodyFields(d) {
@@ -3555,13 +3652,14 @@ function nonBlankValues(...sources) {
3555
3652
  return out;
3556
3653
  }
3557
3654
  function dateVars(now = /* @__PURE__ */ new Date()) {
3558
- const long = now.toLocaleDateString("en-US", {
3655
+ const locale = uiLocale();
3656
+ const long = now.toLocaleDateString(locale, {
3559
3657
  month: "long",
3560
3658
  day: "numeric",
3561
3659
  year: "numeric"
3562
3660
  });
3563
3661
  return {
3564
- month: now.toLocaleString("en-US", { month: "long" }),
3662
+ month: now.toLocaleString(locale, { month: "long" }),
3565
3663
  year: now.getFullYear().toString(),
3566
3664
  date: long,
3567
3665
  today: long
@@ -3593,6 +3691,9 @@ var SendEmailComponent = class SendEmailComponent {
3593
3691
  get deferredKeys() {
3594
3692
  return this.bulk() ? PERSONAL_TAG_KEYS : [];
3595
3693
  }
3694
+ get EMAIL_DISABLED_MESSAGE() {
3695
+ return this.labels.t("messagingSendMessagePanel.emailDisabled");
3696
+ }
3596
3697
  ngOnInit() {
3597
3698
  this.includeReceipt = this.receiptDefault();
3598
3699
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
@@ -3601,6 +3702,7 @@ var SendEmailComponent = class SendEmailComponent {
3601
3702
  } });
3602
3703
  }
3603
3704
  constructor() {
3705
+ this.labels = inject(UiLabelsService);
3604
3706
  this.destroyRef = inject(DestroyRef);
3605
3707
  this.http = inject(HttpClient);
3606
3708
  this.apiBase = inject(FORGEROUTE_CONFIG, { optional: true })?.baseUrl ?? "/api";
@@ -3617,7 +3719,11 @@ var SendEmailComponent = class SendEmailComponent {
3617
3719
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
3618
3720
  this.sendFn = input(null, ...ngDevMode ? [{ debugName: "sendFn" }] : /* istanbul ignore next */ []);
3619
3721
  this.showReceiptOption = input(false, ...ngDevMode ? [{ debugName: "showReceiptOption" }] : /* istanbul ignore next */ []);
3620
- this.receiptLabel = input("Include payment receipt", ...ngDevMode ? [{ debugName: "receiptLabel" }] : /* istanbul ignore next */ []);
3722
+ this.receiptLabelInput = input(null, {
3723
+ ...ngDevMode ? { debugName: "receiptLabelInput" } : /* istanbul ignore next */ {},
3724
+ alias: "receiptLabel"
3725
+ });
3726
+ this.receiptLabel = computed(() => this.receiptLabelInput() ?? this.labels.t("messagingSendMessagePanel.receiptLabel"), ...ngDevMode ? [{ debugName: "receiptLabel" }] : /* istanbul ignore next */ []);
3621
3727
  this.receiptDefault = input(false, ...ngDevMode ? [{ debugName: "receiptDefault" }] : /* istanbul ignore next */ []);
3622
3728
  this.documents = input(false, ...ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []);
3623
3729
  this.documentPurposes = input([
@@ -3662,7 +3768,6 @@ var SendEmailComponent = class SendEmailComponent {
3662
3768
  this.validChange = output();
3663
3769
  this.rejected = output();
3664
3770
  this.mapFieldsClick = output();
3665
- this.EMAIL_DISABLED_MESSAGE = "Email sending is disabled, so this can't be sent. Enable it in Communication Settings.";
3666
3771
  this.mode = "compose";
3667
3772
  this.subject = "";
3668
3773
  this.body = "";
@@ -3685,13 +3790,13 @@ var SendEmailComponent = class SendEmailComponent {
3685
3790
  this.signersInvalid = false;
3686
3791
  this.mergeWarnings = [];
3687
3792
  this.savingEmail = signal(false, ...ngDevMode ? [{ debugName: "savingEmail" }] : /* istanbul ignore next */ []);
3688
- this.modeTabs = [{
3793
+ this.modeTabs = computed(() => [{
3689
3794
  key: "compose",
3690
- label: "Create Message"
3795
+ label: this.labels.t("messagingSendMessagePanel.tabCompose")
3691
3796
  }, {
3692
3797
  key: "template",
3693
- label: "Use Existing Template"
3694
- }];
3798
+ label: this.labels.t("messagingSendMessagePanel.tabTemplate")
3799
+ }], ...ngDevMode ? [{ debugName: "modeTabs" }] : /* istanbul ignore next */ []);
3695
3800
  this.marketVars = {};
3696
3801
  this._appliedDefaults = {
3697
3802
  subject: "",
@@ -3805,7 +3910,7 @@ var SendEmailComponent = class SendEmailComponent {
3805
3910
  });
3806
3911
  if (this.hasDocuments && this.signerInfo?.multiDocumentNote) out.push({
3807
3912
  key: "multi",
3808
- text: "Documents with several signers are sent one at a time.",
3913
+ text: this.labels.t("messagingSendMessagePanel.multiSignerOneAtATime"),
3809
3914
  testId: "documents-multi-signer-note"
3810
3915
  });
3811
3916
  if (this.hasDocuments) this.mergeWarnings.forEach((text, i) => out.push({
@@ -3829,8 +3934,8 @@ var SendEmailComponent = class SendEmailComponent {
3829
3934
  }
3830
3935
  this.syncSuggestedSubject();
3831
3936
  if (this.hasDocuments) {
3832
- const removed = [this.attachments.length > 0 ? "your attachments" : "", this.showSchedule ? "the schedule" : ""].filter(Boolean);
3833
- if (removed.length > 0) this.docsRemovedNote = `We removed ${removed.join(" and ")} because documents are sent as a request.`;
3937
+ const removed = [this.attachments.length > 0 ? this.labels.t("messagingSendMessagePanel.removedAttachments") : "", this.showSchedule ? this.labels.t("messagingSendMessagePanel.removedSchedule") : ""].filter(Boolean);
3938
+ if (removed.length > 0) this.docsRemovedNote = this.labels.t("messagingSendMessagePanel.removedBecauseRequest", { items: removed.join(` ${this.labels.t("messagingSendMessagePanel.listAnd")} `) });
3834
3939
  this.attachments = [];
3835
3940
  this.showSchedule = false;
3836
3941
  this.scheduledAt = "";
@@ -3842,10 +3947,16 @@ var SendEmailComponent = class SendEmailComponent {
3842
3947
  this.notifyValid();
3843
3948
  }
3844
3949
  get bodyLengthError() {
3845
- return this.resolvedBody.length > this.MAX_BODY ? `Message is too long (${this.resolvedBody.length.toLocaleString("en-US")} of ${this.MAX_BODY.toLocaleString("en-US")} characters).` : "";
3950
+ return this.resolvedBody.length > this.MAX_BODY ? this.labels.t("messagingSendMessagePanel.bodyTooLong", {
3951
+ actual: this.labels.formatNumber(this.resolvedBody.length),
3952
+ max: this.labels.formatNumber(this.MAX_BODY)
3953
+ }) : "";
3846
3954
  }
3847
3955
  get subjectLengthError() {
3848
- return this.resolvedSubject.length > this.MAX_SUBJECT ? `Subject is too long (${this.resolvedSubject.length.toLocaleString("en-US")} of ${this.MAX_SUBJECT} characters).` : "";
3956
+ return this.resolvedSubject.length > this.MAX_SUBJECT ? this.labels.t("messagingSendMessagePanel.subjectTooLong", {
3957
+ actual: this.labels.formatNumber(this.resolvedSubject.length),
3958
+ max: this.MAX_SUBJECT
3959
+ }) : "";
3849
3960
  }
3850
3961
  get sendLengthError() {
3851
3962
  return this.subjectLengthError || (this.mode === "compose" ? "" : this.bodyLengthError);
@@ -3907,7 +4018,7 @@ var SendEmailComponent = class SendEmailComponent {
3907
4018
  }
3908
4019
  get composeTagError() {
3909
4020
  if (this.mode !== "compose") return "";
3910
- return hasLeftoverTags(this.resolvedSubject, this.deferredKeys) || hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
4021
+ return hasLeftoverTags(this.resolvedSubject, this.deferredKeys) || hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? this.labels.t("messagingSendMessagePanel.leftoverTags") : "";
3911
4022
  }
3912
4023
  get unresolvedVars() {
3913
4024
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -4007,7 +4118,7 @@ var SendEmailComponent = class SendEmailComponent {
4007
4118
  const reader = new FileReader();
4008
4119
  reader.onload = () => {
4009
4120
  const base64 = reader.result.split(",")[1];
4010
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
4121
+ const sizeLabel = formatAttachmentSize(file.size);
4011
4122
  this.attachments = [...this.attachments, {
4012
4123
  filename: file.name,
4013
4124
  content: base64,
@@ -4026,7 +4137,7 @@ var SendEmailComponent = class SendEmailComponent {
4026
4137
  insertVideo(v) {
4027
4138
  if (!v.shareLink) return;
4028
4139
  const sep = this.body.trim() ? "\n\n" : "";
4029
- this.body = this.body + sep + `Watch "${v.title}": ${v.shareLink}`;
4140
+ this.body = this.body + sep + videoMessageLine(v.title, v.shareLink);
4030
4141
  this.notifyValid();
4031
4142
  }
4032
4143
  docuSignBody(docs, payload) {
@@ -4071,7 +4182,7 @@ var SendEmailComponent = class SendEmailComponent {
4071
4182
  submit() {
4072
4183
  if (!this.canSend || this.loading || !this.bulk() && !this.contactEmail()) return;
4073
4184
  if (this.bulk() && !this.sendFn()) {
4074
- this.toast.error(BULK_NO_SEND_MESSAGE);
4185
+ this.toast.error(this.labels.t("messagingSendMessagePanel.bulkNoSend"));
4075
4186
  return;
4076
4187
  }
4077
4188
  if (this.mode === "template") this.applyPendingVars();
@@ -4148,7 +4259,7 @@ var SendEmailComponent = class SendEmailComponent {
4148
4259
  });
4149
4260
  const onError = (err) => {
4150
4261
  if (this.bulk() && (destroyed || !this.loading)) return;
4151
- this.toast.error(custom || docs ? getErrorMessage(err, "email", "send") : serverMessage(err) || "Failed to send email");
4262
+ this.toast.error(custom || docs ? getErrorMessage(err, this.labels.t("messagingSendMessagePanel.ctxEmail"), "send") : serverMessage(err) || this.labels.t("messagingSendMessagePanel.failedSendEmail"));
4152
4263
  this.loading = false;
4153
4264
  this.cdr.markForCheck();
4154
4265
  if (this.bulk()) this.bulkSending.emit(null);
@@ -4164,7 +4275,7 @@ var SendEmailComponent = class SendEmailComponent {
4164
4275
  provider: docs.provider,
4165
4276
  informational: docs.signerInfo?.informational === true
4166
4277
  }) : void 0;
4167
- this.toast.show(sentText ?? (docs?.provider === "DOCUSIGN" ? `"${docs.documents[0].title}" sent via DocuSign` : docs && this.documentEnvelope() ? `Envelope "${this.documentEnvelope().name}" sent` : this.showSchedule ? "Email scheduled" : "Email sent"));
4278
+ this.toast.show(sentText ?? (docs?.provider === "DOCUSIGN" ? this.labels.t("messagingSendMessagePanel.sentViaDocusign", { title: docs.documents[0].title }) : docs && this.documentEnvelope() ? this.labels.t("messagingSendMessagePanel.envelopeSent", { name: this.documentEnvelope().name }) : this.showSchedule ? this.labels.t("messagingSendMessagePanel.emailScheduled") : this.labels.t("messagingSendMessagePanel.emailSent")));
4168
4279
  this.loading = false;
4169
4280
  this.attachments = [];
4170
4281
  this.includeReceipt = this.receiptDefault();
@@ -4269,8 +4380,8 @@ var SendEmailComponent = class SendEmailComponent {
4269
4380
  isRequired: false,
4270
4381
  transformFunction: null
4271
4382
  },
4272
- receiptLabel: {
4273
- classPropertyName: "receiptLabel",
4383
+ receiptLabelInput: {
4384
+ classPropertyName: "receiptLabelInput",
4274
4385
  publicName: "receiptLabel",
4275
4386
  isSignal: true,
4276
4387
  isRequired: false,
@@ -4445,25 +4556,21 @@ var SendEmailComponent = class SendEmailComponent {
4445
4556
  <!-- Missing email guard -->
4446
4557
  @if (!bulk() && !contactEmail()) {
4447
4558
  <div class="sm-missing-banner">
4448
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4449
- No email address on file for this contact.
4450
- </div>
4559
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.noEmailAddressOnFile') }}</div>
4451
4560
  <ui-text-input
4452
- label="Add Email Address"
4561
+ [label]="labels.t('messagingSendMessagePanel.addEmailAddress')"
4453
4562
  type="email"
4454
4563
  [required]="true"
4455
4564
  placeholder="email@example.com"
4456
4565
  [(ngModel)]="pendingEmail"
4457
4566
  (ngModelChange)="notifyValid()"
4458
- [error]="pendingEmail && !pendingEmailValid ? 'Please enter a valid email address.' : ''" />
4459
- <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">
4460
- Save &amp; Continue
4461
- </ui-button>
4567
+ [error]="pendingEmail && !pendingEmailValid ? labels.t('validation.email') : ''" />
4568
+ <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">{{ labels.t('messagingSendMessagePanel.saveContinue') }}</ui-button>
4462
4569
  } @else {
4463
4570
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
4464
4571
  @if (!bulk()) {
4465
4572
  <div class="sm-group">
4466
- <span class="sm-group-label">To</span>
4573
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
4467
4574
  <div class="sm-to-card">
4468
4575
  <span class="sm-to-name">{{ contactName() }}</span>
4469
4576
  <span class="sm-to-detail">{{ contactEmail() }}</span>
@@ -4491,7 +4598,7 @@ var SendEmailComponent = class SendEmailComponent {
4491
4598
  <ui-text-input
4492
4599
  label="CC"
4493
4600
  placeholder="name@example.com, other@example.com"
4494
- hint="Separate addresses with commas. Everyone listed receives a copy."
4601
+ [hint]="labels.t('messagingSendMessagePanel.separateAddressesWithCommasEveryone')"
4495
4602
  [(ngModel)]="ccText"
4496
4603
  [error]="ccError"
4497
4604
  (ngModelChange)="notifyValid()" />
@@ -4500,24 +4607,24 @@ var SendEmailComponent = class SendEmailComponent {
4500
4607
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
4501
4608
  @if (showTemplates()) {
4502
4609
  <div class="sm-mode-row">
4503
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
4610
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
4504
4611
  </div>
4505
4612
  }
4506
4613
 
4507
4614
  @if (mode === 'compose') {
4508
4615
  @if (showSubject()) {
4509
4616
  <ui-text-input
4510
- label="Subject"
4617
+ [label]="labels.t('common.subject')"
4511
4618
  [required]="!viaDocuSign"
4512
- placeholder="Email subject..."
4619
+ [placeholder]="labels.t('messagingSendMessagePanel.emailSubject')"
4513
4620
  [maxlength]="MAX_SUBJECT"
4514
4621
  [(ngModel)]="subject"
4515
4622
  (ngModelChange)="notifyValid()" />
4516
4623
  }
4517
4624
  <ui-textarea-input
4518
- label="Message"
4625
+ [label]="labels.t('common.message')"
4519
4626
  [required]="!viaDocuSign"
4520
- placeholder="Write your message..."
4627
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
4521
4628
  [rows]="8"
4522
4629
  [(ngModel)]="body"
4523
4630
  [error]="composeTagError || bodyLengthError"
@@ -4525,7 +4632,7 @@ var SendEmailComponent = class SendEmailComponent {
4525
4632
 
4526
4633
  @if (!hasDocuments && showAttachments()) {
4527
4634
  <div class="sm-field">
4528
- <label class="sm-field-label">Attachments</label>
4635
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</label>
4529
4636
  @if (attachments.length > 0) {
4530
4637
  <div class="attach-chips">
4531
4638
  @for (a of attachments; track a.filename; let i = $index) {
@@ -4533,25 +4640,23 @@ var SendEmailComponent = class SendEmailComponent {
4533
4640
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
4534
4641
  <span class="attach-name">{{ a.filename }}</span>
4535
4642
  <span class="attach-size">{{ a.sizeLabel }}</span>
4536
- <button class="attach-remove" type="button" (click)="removeAttachment(i)" aria-label="Remove attachment">&times;</button>
4643
+ <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="labels.t('messagingSendMessagePanel.removeAttachment')">&times;</button>
4537
4644
  </div>
4538
4645
  }
4539
4646
  </div>
4540
4647
  }
4541
4648
  <input #emailFileInput type="file" multiple hidden (change)="onFilesSelected($event)" />
4542
4649
  <button type="button" class="attach-add-btn" (click)="emailFileInput.click()">
4543
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
4544
- Attach Files
4545
- </button>
4650
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>{{ labels.t('messagingSendMessagePanel.attachFiles') }}</button>
4546
4651
  </div>
4547
4652
 
4548
4653
  }
4549
4654
 
4550
4655
  @if (!hasDocuments && showScheduling()) {
4551
- <ui-checkbox [checked]="showSchedule" label="Schedule for later" (checkedChange)="showSchedule = $event; notifyValid()" />
4656
+ <ui-checkbox [checked]="showSchedule" [label]="labels.t('messagingSendMessagePanel.scheduleForLater')" (checkedChange)="showSchedule = $event; notifyValid()" />
4552
4657
  @if (showSchedule) {
4553
4658
  <ui-datetime-input
4554
- label="Send at"
4659
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
4555
4660
  [min]="minScheduleTime"
4556
4661
  [(ngModel)]="scheduledAt"
4557
4662
  (ngModelChange)="notifyValid()" />
@@ -4561,14 +4666,12 @@ var SendEmailComponent = class SendEmailComponent {
4561
4666
  @if (missingVars.length > 0) {
4562
4667
  <div class="mv-section">
4563
4668
  <p class="mv-hint">
4564
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4565
- This template has fields that need values before sending.
4566
- </p>
4669
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.thisTemplateHasFieldsThat') }}</p>
4567
4670
  @for (key of missingVars; track key) {
4568
4671
  <div class="mv-row">
4569
4672
  <div class="mv-input-wrap">
4570
- <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="'Enter ' + formatVarLabel(key).toLowerCase() + '...'" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
4571
- <ui-button variant="primary" size="sm" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" ariaLabel="Apply value">
4673
+ <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="labels.t('messagingSendMessagePanel.enterValue', { name: formatVarLabel(key).toLowerCase() })" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
4674
+ <ui-button variant="primary" size="sm" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" [ariaLabel]="labels.t('messagingSendMessagePanel.applyValue')">
4572
4675
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
4573
4676
  </ui-button>
4574
4677
  </div>
@@ -4579,22 +4682,18 @@ var SendEmailComponent = class SendEmailComponent {
4579
4682
 
4580
4683
  <div class="tpl-preview-header">
4581
4684
  <ui-button variant="ghost" size="sm" (click)="clearTemplate()">
4582
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
4583
- Back to templates
4584
- </ui-button>
4685
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>{{ labels.t('messagingSendMessagePanel.backToTemplates') }}</ui-button>
4585
4686
  <div class="tpl-view-toggle">
4586
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">Preview</ui-button>
4587
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'raw'" [ariaPressed]="previewFormat === 'raw'" (click)="setPreviewFormat('raw')">Raw</ui-button>
4687
+ <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">{{ labels.t('common.preview') }}</ui-button>
4688
+ <ui-button variant="tab" size="sm" [active]="previewFormat === 'raw'" [ariaPressed]="previewFormat === 'raw'" (click)="setPreviewFormat('raw')">{{ labels.t('messagingSendMessagePanel.raw') }}</ui-button>
4588
4689
  </div>
4589
4690
  </div>
4590
4691
 
4591
4692
  <div class="tpl-subject-card">
4592
4693
  <div class="tpl-subject-card-top">
4593
- <span class="tpl-subject-label">SUBJECT</span>
4694
+ <span class="tpl-subject-label">{{ labels.t('messagingSendMessagePanel.subject') }}</span>
4594
4695
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
4595
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
4596
- View Send Preview
4597
- </ui-button>
4696
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>{{ labels.t('messagingSendMessagePanel.viewSendPreview') }}</ui-button>
4598
4697
  </div>
4599
4698
  <span class="tpl-subject-text">{{ resolvedSubject }}</span>
4600
4699
  </div>
@@ -4602,9 +4701,9 @@ var SendEmailComponent = class SendEmailComponent {
4602
4701
  <div class="tpl-body-preview" [innerHTML]="previewBodyHtml"></div>
4603
4702
  } @else {
4604
4703
  <!-- Raw: the template source exactly as written, {{tags}} included — subject first, then the body. -->
4605
- <div class="tpl-raw-label">Subject</div>
4704
+ <div class="tpl-raw-label">{{ labels.t('common.subject') }}</div>
4606
4705
  <pre class="tpl-subject-raw">{{ subject }}</pre>
4607
- <div class="tpl-raw-label">Body</div>
4706
+ <div class="tpl-raw-label">{{ labels.t('messagingSendMessagePanel.body') }}</div>
4608
4707
  <pre class="tpl-body-raw">{{ body }}</pre>
4609
4708
  }
4610
4709
  } @else {
@@ -4641,10 +4740,10 @@ var SendEmailComponent = class SendEmailComponent {
4641
4740
  <!-- One live region, at most ONE warning banner: every current non-blocking warning is a line in it. Blocking problems are inline on their field. -->
4642
4741
  <div class="sm-docs-live" role="status" aria-live="polite">
4643
4742
  @if (hasDocuments) {
4644
- <p class="sm-docs-note">Your message goes out with the documents. Attachments and scheduling aren't available when sending documents.</p>
4743
+ <p class="sm-docs-note">{{ labels.t('messagingSendMessagePanel.yourMessageGoesOutWith') }}</p>
4645
4744
  }
4646
4745
  @if (warningLines.length > 0) {
4647
- <ui-alert variant="warning" title="Heads up" data-testid="send-warnings">
4746
+ <ui-alert variant="warning" [title]="labels.t('messagingSendMessagePanel.headsUp')" data-testid="send-warnings">
4648
4747
  <ul class="sm-warn-list">
4649
4748
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
4650
4749
  </ul>
@@ -4667,7 +4766,7 @@ var SendEmailComponent = class SendEmailComponent {
4667
4766
 
4668
4767
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
4669
4768
  @if (mode === 'compose' && showVideo()) {
4670
- <ui-form-section label="Video" [divider]="true">
4769
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
4671
4770
  @for (k of [videoPickerKey]; track k) {
4672
4771
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
4673
4772
  }
@@ -4689,20 +4788,18 @@ var SendEmailComponent = class SendEmailComponent {
4689
4788
  <div class="sp-modal" (click)="$event.stopPropagation()">
4690
4789
  <div class="sp-modal-header">
4691
4790
  <div class="sp-modal-title">
4692
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
4693
- Email Preview
4694
- </div>
4695
- <ui-button variant="ghost" size="sm" (click)="closeSendPreview()" ariaLabel="Close preview">
4791
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('messagingSendMessagePanel.emailPreview') }}</div>
4792
+ <ui-button variant="ghost" size="sm" (click)="closeSendPreview()" [ariaLabel]="labels.t('messagingSendMessagePanel.closePreview')">
4696
4793
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
4697
4794
  </ui-button>
4698
4795
  </div>
4699
4796
  <div class="sp-email-meta">
4700
4797
  <div class="sp-meta-row">
4701
- <span class="sp-meta-label">To</span>
4702
- <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
4798
+ <span class="sp-meta-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
4799
+ <span class="sp-meta-value">@if (bulk()) { {{ labels.t(bulkRecipients()!.length === 1 ? 'messagingSendMessagePanel.recipientCount_one' : 'messagingSendMessagePanel.recipientCount_other', { count: bulkRecipients()!.length }) }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
4703
4800
  </div>
4704
4801
  <div class="sp-meta-row">
4705
- <span class="sp-meta-label">Subject</span>
4802
+ <span class="sp-meta-label">{{ labels.t('common.subject') }}</span>
4706
4803
  <span class="sp-meta-value sp-meta-subject">{{ sendPreviewSubject }}</span>
4707
4804
  </div>
4708
4805
  </div>
@@ -5068,25 +5165,21 @@ i0.ɵɵngDeclareClassMetadata({
5068
5165
  <!-- Missing email guard -->
5069
5166
  @if (!bulk() && !contactEmail()) {
5070
5167
  <div class="sm-missing-banner">
5071
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
5072
- No email address on file for this contact.
5073
- </div>
5168
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.noEmailAddressOnFile') }}</div>
5074
5169
  <ui-text-input
5075
- label="Add Email Address"
5170
+ [label]="labels.t('messagingSendMessagePanel.addEmailAddress')"
5076
5171
  type="email"
5077
5172
  [required]="true"
5078
5173
  placeholder="email@example.com"
5079
5174
  [(ngModel)]="pendingEmail"
5080
5175
  (ngModelChange)="notifyValid()"
5081
- [error]="pendingEmail && !pendingEmailValid ? 'Please enter a valid email address.' : ''" />
5082
- <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">
5083
- Save &amp; Continue
5084
- </ui-button>
5176
+ [error]="pendingEmail && !pendingEmailValid ? labels.t('validation.email') : ''" />
5177
+ <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">{{ labels.t('messagingSendMessagePanel.saveContinue') }}</ui-button>
5085
5178
  } @else {
5086
5179
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
5087
5180
  @if (!bulk()) {
5088
5181
  <div class="sm-group">
5089
- <span class="sm-group-label">To</span>
5182
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
5090
5183
  <div class="sm-to-card">
5091
5184
  <span class="sm-to-name">{{ contactName() }}</span>
5092
5185
  <span class="sm-to-detail">{{ contactEmail() }}</span>
@@ -5114,7 +5207,7 @@ i0.ɵɵngDeclareClassMetadata({
5114
5207
  <ui-text-input
5115
5208
  label="CC"
5116
5209
  placeholder="name@example.com, other@example.com"
5117
- hint="Separate addresses with commas. Everyone listed receives a copy."
5210
+ [hint]="labels.t('messagingSendMessagePanel.separateAddressesWithCommasEveryone')"
5118
5211
  [(ngModel)]="ccText"
5119
5212
  [error]="ccError"
5120
5213
  (ngModelChange)="notifyValid()" />
@@ -5123,24 +5216,24 @@ i0.ɵɵngDeclareClassMetadata({
5123
5216
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
5124
5217
  @if (showTemplates()) {
5125
5218
  <div class="sm-mode-row">
5126
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
5219
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
5127
5220
  </div>
5128
5221
  }
5129
5222
 
5130
5223
  @if (mode === 'compose') {
5131
5224
  @if (showSubject()) {
5132
5225
  <ui-text-input
5133
- label="Subject"
5226
+ [label]="labels.t('common.subject')"
5134
5227
  [required]="!viaDocuSign"
5135
- placeholder="Email subject..."
5228
+ [placeholder]="labels.t('messagingSendMessagePanel.emailSubject')"
5136
5229
  [maxlength]="MAX_SUBJECT"
5137
5230
  [(ngModel)]="subject"
5138
5231
  (ngModelChange)="notifyValid()" />
5139
5232
  }
5140
5233
  <ui-textarea-input
5141
- label="Message"
5234
+ [label]="labels.t('common.message')"
5142
5235
  [required]="!viaDocuSign"
5143
- placeholder="Write your message..."
5236
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5144
5237
  [rows]="8"
5145
5238
  [(ngModel)]="body"
5146
5239
  [error]="composeTagError || bodyLengthError"
@@ -5148,7 +5241,7 @@ i0.ɵɵngDeclareClassMetadata({
5148
5241
 
5149
5242
  @if (!hasDocuments && showAttachments()) {
5150
5243
  <div class="sm-field">
5151
- <label class="sm-field-label">Attachments</label>
5244
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</label>
5152
5245
  @if (attachments.length > 0) {
5153
5246
  <div class="attach-chips">
5154
5247
  @for (a of attachments; track a.filename; let i = $index) {
@@ -5156,25 +5249,23 @@ i0.ɵɵngDeclareClassMetadata({
5156
5249
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
5157
5250
  <span class="attach-name">{{ a.filename }}</span>
5158
5251
  <span class="attach-size">{{ a.sizeLabel }}</span>
5159
- <button class="attach-remove" type="button" (click)="removeAttachment(i)" aria-label="Remove attachment">&times;</button>
5252
+ <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="labels.t('messagingSendMessagePanel.removeAttachment')">&times;</button>
5160
5253
  </div>
5161
5254
  }
5162
5255
  </div>
5163
5256
  }
5164
5257
  <input #emailFileInput type="file" multiple hidden (change)="onFilesSelected($event)" />
5165
5258
  <button type="button" class="attach-add-btn" (click)="emailFileInput.click()">
5166
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
5167
- Attach Files
5168
- </button>
5259
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>{{ labels.t('messagingSendMessagePanel.attachFiles') }}</button>
5169
5260
  </div>
5170
5261
 
5171
5262
  }
5172
5263
 
5173
5264
  @if (!hasDocuments && showScheduling()) {
5174
- <ui-checkbox [checked]="showSchedule" label="Schedule for later" (checkedChange)="showSchedule = $event; notifyValid()" />
5265
+ <ui-checkbox [checked]="showSchedule" [label]="labels.t('messagingSendMessagePanel.scheduleForLater')" (checkedChange)="showSchedule = $event; notifyValid()" />
5175
5266
  @if (showSchedule) {
5176
5267
  <ui-datetime-input
5177
- label="Send at"
5268
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
5178
5269
  [min]="minScheduleTime"
5179
5270
  [(ngModel)]="scheduledAt"
5180
5271
  (ngModelChange)="notifyValid()" />
@@ -5184,14 +5275,12 @@ i0.ɵɵngDeclareClassMetadata({
5184
5275
  @if (missingVars.length > 0) {
5185
5276
  <div class="mv-section">
5186
5277
  <p class="mv-hint">
5187
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
5188
- This template has fields that need values before sending.
5189
- </p>
5278
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.thisTemplateHasFieldsThat') }}</p>
5190
5279
  @for (key of missingVars; track key) {
5191
5280
  <div class="mv-row">
5192
5281
  <div class="mv-input-wrap">
5193
- <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="'Enter ' + formatVarLabel(key).toLowerCase() + '...'" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
5194
- <ui-button variant="primary" size="sm" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" ariaLabel="Apply value">
5282
+ <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="labels.t('messagingSendMessagePanel.enterValue', { name: formatVarLabel(key).toLowerCase() })" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
5283
+ <ui-button variant="primary" size="sm" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" [ariaLabel]="labels.t('messagingSendMessagePanel.applyValue')">
5195
5284
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
5196
5285
  </ui-button>
5197
5286
  </div>
@@ -5202,22 +5291,18 @@ i0.ɵɵngDeclareClassMetadata({
5202
5291
 
5203
5292
  <div class="tpl-preview-header">
5204
5293
  <ui-button variant="ghost" size="sm" (click)="clearTemplate()">
5205
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
5206
- Back to templates
5207
- </ui-button>
5294
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>{{ labels.t('messagingSendMessagePanel.backToTemplates') }}</ui-button>
5208
5295
  <div class="tpl-view-toggle">
5209
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">Preview</ui-button>
5210
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'raw'" [ariaPressed]="previewFormat === 'raw'" (click)="setPreviewFormat('raw')">Raw</ui-button>
5296
+ <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">{{ labels.t('common.preview') }}</ui-button>
5297
+ <ui-button variant="tab" size="sm" [active]="previewFormat === 'raw'" [ariaPressed]="previewFormat === 'raw'" (click)="setPreviewFormat('raw')">{{ labels.t('messagingSendMessagePanel.raw') }}</ui-button>
5211
5298
  </div>
5212
5299
  </div>
5213
5300
 
5214
5301
  <div class="tpl-subject-card">
5215
5302
  <div class="tpl-subject-card-top">
5216
- <span class="tpl-subject-label">SUBJECT</span>
5303
+ <span class="tpl-subject-label">{{ labels.t('messagingSendMessagePanel.subject') }}</span>
5217
5304
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
5218
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
5219
- View Send Preview
5220
- </ui-button>
5305
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>{{ labels.t('messagingSendMessagePanel.viewSendPreview') }}</ui-button>
5221
5306
  </div>
5222
5307
  <span class="tpl-subject-text">{{ resolvedSubject }}</span>
5223
5308
  </div>
@@ -5225,9 +5310,9 @@ i0.ɵɵngDeclareClassMetadata({
5225
5310
  <div class="tpl-body-preview" [innerHTML]="previewBodyHtml"></div>
5226
5311
  } @else {
5227
5312
  <!-- Raw: the template source exactly as written, {{tags}} included — subject first, then the body. -->
5228
- <div class="tpl-raw-label">Subject</div>
5313
+ <div class="tpl-raw-label">{{ labels.t('common.subject') }}</div>
5229
5314
  <pre class="tpl-subject-raw">{{ subject }}</pre>
5230
- <div class="tpl-raw-label">Body</div>
5315
+ <div class="tpl-raw-label">{{ labels.t('messagingSendMessagePanel.body') }}</div>
5231
5316
  <pre class="tpl-body-raw">{{ body }}</pre>
5232
5317
  }
5233
5318
  } @else {
@@ -5264,10 +5349,10 @@ i0.ɵɵngDeclareClassMetadata({
5264
5349
  <!-- One live region, at most ONE warning banner: every current non-blocking warning is a line in it. Blocking problems are inline on their field. -->
5265
5350
  <div class="sm-docs-live" role="status" aria-live="polite">
5266
5351
  @if (hasDocuments) {
5267
- <p class="sm-docs-note">Your message goes out with the documents. Attachments and scheduling aren't available when sending documents.</p>
5352
+ <p class="sm-docs-note">{{ labels.t('messagingSendMessagePanel.yourMessageGoesOutWith') }}</p>
5268
5353
  }
5269
5354
  @if (warningLines.length > 0) {
5270
- <ui-alert variant="warning" title="Heads up" data-testid="send-warnings">
5355
+ <ui-alert variant="warning" [title]="labels.t('messagingSendMessagePanel.headsUp')" data-testid="send-warnings">
5271
5356
  <ul class="sm-warn-list">
5272
5357
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
5273
5358
  </ul>
@@ -5290,7 +5375,7 @@ i0.ɵɵngDeclareClassMetadata({
5290
5375
 
5291
5376
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
5292
5377
  @if (mode === 'compose' && showVideo()) {
5293
- <ui-form-section label="Video" [divider]="true">
5378
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
5294
5379
  @for (k of [videoPickerKey]; track k) {
5295
5380
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
5296
5381
  }
@@ -5312,20 +5397,18 @@ i0.ɵɵngDeclareClassMetadata({
5312
5397
  <div class="sp-modal" (click)="$event.stopPropagation()">
5313
5398
  <div class="sp-modal-header">
5314
5399
  <div class="sp-modal-title">
5315
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
5316
- Email Preview
5317
- </div>
5318
- <ui-button variant="ghost" size="sm" (click)="closeSendPreview()" ariaLabel="Close preview">
5400
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('messagingSendMessagePanel.emailPreview') }}</div>
5401
+ <ui-button variant="ghost" size="sm" (click)="closeSendPreview()" [ariaLabel]="labels.t('messagingSendMessagePanel.closePreview')">
5319
5402
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
5320
5403
  </ui-button>
5321
5404
  </div>
5322
5405
  <div class="sp-email-meta">
5323
5406
  <div class="sp-meta-row">
5324
- <span class="sp-meta-label">To</span>
5325
- <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
5407
+ <span class="sp-meta-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
5408
+ <span class="sp-meta-value">@if (bulk()) { {{ labels.t(bulkRecipients()!.length === 1 ? 'messagingSendMessagePanel.recipientCount_one' : 'messagingSendMessagePanel.recipientCount_other', { count: bulkRecipients()!.length }) }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
5326
5409
  </div>
5327
5410
  <div class="sp-meta-row">
5328
- <span class="sp-meta-label">Subject</span>
5411
+ <span class="sp-meta-label">{{ labels.t('common.subject') }}</span>
5329
5412
  <span class="sp-meta-value sp-meta-subject">{{ sendPreviewSubject }}</span>
5330
5413
  </div>
5331
5414
  </div>
@@ -5419,7 +5502,7 @@ i0.ɵɵngDeclareClassMetadata({
5419
5502
  required: false
5420
5503
  }]
5421
5504
  }],
5422
- receiptLabel: [{
5505
+ receiptLabelInput: [{
5423
5506
  type: i0.Input,
5424
5507
  args: [{
5425
5508
  isSignal: true,
@@ -5636,6 +5719,7 @@ i0.ɵɵngDeclareClassMetadata({
5636
5719
  const MAX_SUBJECT = 255;
5637
5720
  var SendInAppComponent = class SendInAppComponent {
5638
5721
  constructor() {
5722
+ this.labels = inject(UiLabelsService);
5639
5723
  this.destroyRef = inject(DestroyRef);
5640
5724
  this.toast = inject(ToastService);
5641
5725
  this.cdr = inject(ChangeDetectorRef);
@@ -5684,7 +5768,7 @@ var SendInAppComponent = class SendInAppComponent {
5684
5768
  const reader = new FileReader();
5685
5769
  reader.onload = () => {
5686
5770
  const base64 = String(reader.result).split(",")[1] ?? "";
5687
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
5771
+ const sizeLabel = formatAttachmentSize(file.size);
5688
5772
  this.attachments = [...this.attachments, {
5689
5773
  filename: file.name,
5690
5774
  content: base64,
@@ -5722,13 +5806,13 @@ var SendInAppComponent = class SendInAppComponent {
5722
5806
  if (this.showSubject()) payload.subject = this.subject.trim();
5723
5807
  send(payload).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
5724
5808
  next: () => {
5725
- this.toast.show("Message sent");
5809
+ this.toast.show(this.labels.t("messagingSendMessagePanel.messageSent"));
5726
5810
  this.sending.set(false);
5727
5811
  this.attachments = [];
5728
5812
  this.sent.emit();
5729
5813
  },
5730
5814
  error: (err) => {
5731
- this.toast.error(serverMessage(err) || "Failed to send message");
5815
+ this.toast.error(serverMessage(err) || this.labels.t("messagingSendMessagePanel.failedSendMessage"));
5732
5816
  this.sending.set(false);
5733
5817
  this.notifyValid();
5734
5818
  }
@@ -5830,7 +5914,7 @@ var SendInAppComponent = class SendInAppComponent {
5830
5914
  ngImport: i0,
5831
5915
  template: `
5832
5916
  <div class="sm-group">
5833
- <span class="sm-group-label">To</span>
5917
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
5834
5918
  <div class="sm-to-card">
5835
5919
  <span class="sm-to-name">{{ contactName() }}</span>
5836
5920
  @if (contactEmail()) { <span class="sm-to-email">{{ contactEmail() }}</span> }
@@ -5838,56 +5922,56 @@ var SendInAppComponent = class SendInAppComponent {
5838
5922
  </div>
5839
5923
  @if (showSubject()) {
5840
5924
  <ui-text-input
5841
- label="Subject"
5925
+ [label]="labels.t('common.subject')"
5842
5926
  [required]="true"
5843
- placeholder="Subject..."
5927
+ [placeholder]="labels.t('messagingSendMessagePanel.subject2')"
5844
5928
  [maxlength]="MAX_SUBJECT"
5845
5929
  [(ngModel)]="subject"
5846
5930
  (ngModelChange)="notifyValid()" />
5847
5931
  }
5848
5932
  @if (richText()) {
5849
5933
  <ui-rich-text-editor
5850
- label="Message"
5934
+ [label]="labels.t('common.message')"
5851
5935
  [required]="true"
5852
- placeholder="Write your message..."
5936
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5853
5937
  [minHeight]="180"
5854
- [error]="richTooLong() ? 'Too long (max ' + limit() + ' characters).' : ''"
5938
+ [error]="richTooLong() ? labels.t('validation.tooLong', { max: limit() }) : ''"
5855
5939
  [(ngModel)]="body"
5856
5940
  (ngModelChange)="notifyValid()" />
5857
5941
  } @else if (limit(); as max) {
5858
5942
  <ui-textarea-input
5859
- label="Message"
5943
+ [label]="labels.t('common.message')"
5860
5944
  [required]="true"
5861
- placeholder="Write your message..."
5945
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5862
5946
  [rows]="8"
5863
5947
  [maxlength]="max"
5864
5948
  [(ngModel)]="body"
5865
5949
  (ngModelChange)="notifyValid()" />
5866
5950
  } @else {
5867
5951
  <ui-textarea-input
5868
- label="Message"
5952
+ [label]="labels.t('common.message')"
5869
5953
  [required]="true"
5870
- placeholder="Write your message..."
5954
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5871
5955
  [rows]="8"
5872
5956
  [(ngModel)]="body"
5873
5957
  (ngModelChange)="notifyValid()" />
5874
5958
  }
5875
5959
  @if (allowAttachments()) {
5876
5960
  <div class="sm-group" data-testid="in-app-attachments">
5877
- <span class="sm-group-label">Attachments</span>
5961
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</span>
5878
5962
  @if (attachments.length > 0) {
5879
5963
  <div class="attach-chips">
5880
5964
  @for (a of attachments; track $index; let i = $index) {
5881
5965
  <div class="attach-chip">
5882
5966
  <span class="attach-name">{{ a.filename }}</span>
5883
5967
  <span class="attach-size">{{ a.sizeLabel }}</span>
5884
- <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="'Remove ' + a.filename">&times;</button>
5968
+ <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="labels.t('common.removeNamed', { name: a.filename })">&times;</button>
5885
5969
  </div>
5886
5970
  }
5887
5971
  </div>
5888
5972
  }
5889
5973
  <input #fileInput type="file" multiple hidden data-testid="in-app-file-input" (change)="onFilesSelected($event)" />
5890
- <button type="button" class="attach-add-btn" (click)="fileInput.click()">Attach Files</button>
5974
+ <button type="button" class="attach-add-btn" (click)="fileInput.click()">{{ labels.t('messagingSendMessagePanel.attachFiles') }}</button>
5891
5975
  </div>
5892
5976
  }
5893
5977
  `,
@@ -6038,7 +6122,7 @@ i0.ɵɵngDeclareClassMetadata({
6038
6122
  ],
6039
6123
  template: `
6040
6124
  <div class="sm-group">
6041
- <span class="sm-group-label">To</span>
6125
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
6042
6126
  <div class="sm-to-card">
6043
6127
  <span class="sm-to-name">{{ contactName() }}</span>
6044
6128
  @if (contactEmail()) { <span class="sm-to-email">{{ contactEmail() }}</span> }
@@ -6046,56 +6130,56 @@ i0.ɵɵngDeclareClassMetadata({
6046
6130
  </div>
6047
6131
  @if (showSubject()) {
6048
6132
  <ui-text-input
6049
- label="Subject"
6133
+ [label]="labels.t('common.subject')"
6050
6134
  [required]="true"
6051
- placeholder="Subject..."
6135
+ [placeholder]="labels.t('messagingSendMessagePanel.subject2')"
6052
6136
  [maxlength]="MAX_SUBJECT"
6053
6137
  [(ngModel)]="subject"
6054
6138
  (ngModelChange)="notifyValid()" />
6055
6139
  }
6056
6140
  @if (richText()) {
6057
6141
  <ui-rich-text-editor
6058
- label="Message"
6142
+ [label]="labels.t('common.message')"
6059
6143
  [required]="true"
6060
- placeholder="Write your message..."
6144
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6061
6145
  [minHeight]="180"
6062
- [error]="richTooLong() ? 'Too long (max ' + limit() + ' characters).' : ''"
6146
+ [error]="richTooLong() ? labels.t('validation.tooLong', { max: limit() }) : ''"
6063
6147
  [(ngModel)]="body"
6064
6148
  (ngModelChange)="notifyValid()" />
6065
6149
  } @else if (limit(); as max) {
6066
6150
  <ui-textarea-input
6067
- label="Message"
6151
+ [label]="labels.t('common.message')"
6068
6152
  [required]="true"
6069
- placeholder="Write your message..."
6153
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6070
6154
  [rows]="8"
6071
6155
  [maxlength]="max"
6072
6156
  [(ngModel)]="body"
6073
6157
  (ngModelChange)="notifyValid()" />
6074
6158
  } @else {
6075
6159
  <ui-textarea-input
6076
- label="Message"
6160
+ [label]="labels.t('common.message')"
6077
6161
  [required]="true"
6078
- placeholder="Write your message..."
6162
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6079
6163
  [rows]="8"
6080
6164
  [(ngModel)]="body"
6081
6165
  (ngModelChange)="notifyValid()" />
6082
6166
  }
6083
6167
  @if (allowAttachments()) {
6084
6168
  <div class="sm-group" data-testid="in-app-attachments">
6085
- <span class="sm-group-label">Attachments</span>
6169
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</span>
6086
6170
  @if (attachments.length > 0) {
6087
6171
  <div class="attach-chips">
6088
6172
  @for (a of attachments; track $index; let i = $index) {
6089
6173
  <div class="attach-chip">
6090
6174
  <span class="attach-name">{{ a.filename }}</span>
6091
6175
  <span class="attach-size">{{ a.sizeLabel }}</span>
6092
- <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="'Remove ' + a.filename">&times;</button>
6176
+ <button class="attach-remove" type="button" (click)="removeAttachment(i)" [attr.aria-label]="labels.t('common.removeNamed', { name: a.filename })">&times;</button>
6093
6177
  </div>
6094
6178
  }
6095
6179
  </div>
6096
6180
  }
6097
6181
  <input #fileInput type="file" multiple hidden data-testid="in-app-file-input" (change)="onFilesSelected($event)" />
6098
- <button type="button" class="attach-add-btn" (click)="fileInput.click()">Attach Files</button>
6182
+ <button type="button" class="attach-add-btn" (click)="fileInput.click()">{{ labels.t('messagingSendMessagePanel.attachFiles') }}</button>
6099
6183
  </div>
6100
6184
  }
6101
6185
  `,
@@ -6217,6 +6301,7 @@ var SendTextComponent = class SendTextComponent {
6217
6301
  } });
6218
6302
  }
6219
6303
  constructor() {
6304
+ this.labels = inject(UiLabelsService);
6220
6305
  this.formatPhone = formatPhone;
6221
6306
  this.destroyRef = inject(DestroyRef);
6222
6307
  this.http = inject(HttpClient);
@@ -6290,13 +6375,13 @@ var SendTextComponent = class SendTextComponent {
6290
6375
  this.mediaAttachment = signal(null, ...ngDevMode ? [{ debugName: "mediaAttachment" }] : /* istanbul ignore next */ []);
6291
6376
  this.mediaUploading = signal(false, ...ngDevMode ? [{ debugName: "mediaUploading" }] : /* istanbul ignore next */ []);
6292
6377
  this.savingPhone = signal(false, ...ngDevMode ? [{ debugName: "savingPhone" }] : /* istanbul ignore next */ []);
6293
- this.modeTabs = [{
6378
+ this.modeTabs = computed(() => [{
6294
6379
  key: "compose",
6295
- label: "Create Message"
6380
+ label: this.labels.t("messagingSendMessagePanel.tabCompose")
6296
6381
  }, {
6297
6382
  key: "template",
6298
- label: "Use Existing Template"
6299
- }];
6383
+ label: this.labels.t("messagingSendMessagePanel.tabTemplate")
6384
+ }], ...ngDevMode ? [{ debugName: "modeTabs" }] : /* istanbul ignore next */ []);
6300
6385
  this.marketVars = {};
6301
6386
  this._appliedDefaultBody = "";
6302
6387
  this.forwardedContext = computed(() => forwardContext(this.sendContext(), this.documentContextTypes()).context, ...ngDevMode ? [{ debugName: "forwardedContext" }] : /* istanbul ignore next */ []);
@@ -6359,8 +6444,8 @@ var SendTextComponent = class SendTextComponent {
6359
6444
  onDocumentsChange(v) {
6360
6445
  this.documentsValue = v;
6361
6446
  if (this.hasDocuments) {
6362
- const removed = [this.mediaAttachment() ? "your image" : "", this.showSchedule ? "the schedule" : ""].filter(Boolean);
6363
- if (removed.length > 0) this.docsRemovedNote = `We removed ${removed.join(" and ")} because documents are sent as a request.`;
6447
+ const removed = [this.mediaAttachment() ? this.labels.t("messagingSendMessagePanel.removedImage") : "", this.showSchedule ? this.labels.t("messagingSendMessagePanel.removedSchedule") : ""].filter(Boolean);
6448
+ if (removed.length > 0) this.docsRemovedNote = this.labels.t("messagingSendMessagePanel.removedBecauseRequest", { items: removed.join(` ${this.labels.t("messagingSendMessagePanel.listAnd")} `) });
6364
6449
  this.showSchedule = false;
6365
6450
  this.scheduledAt = "";
6366
6451
  this.mediaAttachment.set(null);
@@ -6412,10 +6497,10 @@ var SendTextComponent = class SendTextComponent {
6412
6497
  return mergeTagKeys(this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
6413
6498
  }
6414
6499
  get videoLengthError() {
6415
- return this._videoLine && this.body.length > this.MAX_LENGTH ? `The video link makes this text longer than ${this.MAX_LENGTH} characters. Shorten the message or remove the video.` : "";
6500
+ return this._videoLine && this.body.length > this.MAX_LENGTH ? this.labels.t("messagingSendMessagePanel.textTooLongWithVideo", { max: this.MAX_LENGTH }) : "";
6416
6501
  }
6417
6502
  get composeTagError() {
6418
- return this.mode === "compose" && hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
6503
+ return this.mode === "compose" && hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? this.labels.t("messagingSendMessagePanel.leftoverTags") : "";
6419
6504
  }
6420
6505
  get unresolvedVars() {
6421
6506
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -6497,7 +6582,7 @@ var SendTextComponent = class SendTextComponent {
6497
6582
  onFileSelected(event) {
6498
6583
  const file = event.target.files?.[0];
6499
6584
  if (!file) return;
6500
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
6585
+ const sizeLabel = formatAttachmentSize(file.size);
6501
6586
  this.mediaUploading.set(true);
6502
6587
  this.cdr.detectChanges();
6503
6588
  const form = new FormData();
@@ -6514,7 +6599,7 @@ var SendTextComponent = class SendTextComponent {
6514
6599
  this.cdr.detectChanges();
6515
6600
  },
6516
6601
  error: () => {
6517
- this.toast.error("Failed to upload attachment");
6602
+ this.toast.error(this.labels.t("messagingSendMessagePanel.failedUpload"));
6518
6603
  this.mediaUploading.set(false);
6519
6604
  this.cdr.detectChanges();
6520
6605
  }
@@ -6527,7 +6612,7 @@ var SendTextComponent = class SendTextComponent {
6527
6612
  insertVideo(v) {
6528
6613
  if (!v.shareLink) return;
6529
6614
  const sep = this.body.trim() ? "\n\n" : "";
6530
- this.body = (this.body + sep + `Watch "${v.title}": ${v.shareLink}`).slice(0, this.MAX_LENGTH);
6615
+ this.body = (this.body + sep + videoMessageLine(v.title, v.shareLink)).slice(0, this.MAX_LENGTH);
6531
6616
  this.notifyValid();
6532
6617
  }
6533
6618
  onVideoSelected(v) {
@@ -6562,7 +6647,7 @@ var SendTextComponent = class SendTextComponent {
6562
6647
  submit() {
6563
6648
  if (!this.canSend || this.loading || !this.bulk() && !this.contactPhone()) return;
6564
6649
  if (this.bulk() && !this.sendFn()) {
6565
- this.toast.error(BULK_NO_SEND_MESSAGE);
6650
+ this.toast.error(this.labels.t("messagingSendMessagePanel.bulkNoSend"));
6566
6651
  return;
6567
6652
  }
6568
6653
  if (this.mode === "template") this.applyPendingVars();
@@ -6634,7 +6719,7 @@ var SendTextComponent = class SendTextComponent {
6634
6719
  });
6635
6720
  const onError = (err) => {
6636
6721
  if (this.bulk() && (destroyed || !this.loading)) return;
6637
- this.toast.error(custom || docs ? getErrorMessage(err, "text message", "send") : serverMessage(err) || "Failed to send text message");
6722
+ this.toast.error(custom || docs ? getErrorMessage(err, this.labels.t("messagingSendMessagePanel.ctxText"), "send") : serverMessage(err) || this.labels.t("messagingSendMessagePanel.failedSendText"));
6638
6723
  this.loading = false;
6639
6724
  this.cdr.detectChanges();
6640
6725
  if (this.bulk()) this.bulkSending.emit(null);
@@ -6649,7 +6734,7 @@ var SendTextComponent = class SendTextComponent {
6649
6734
  channel: "text",
6650
6735
  informational: docs.signerInfo?.informational === true
6651
6736
  }) : void 0;
6652
- this.toast.show(sentText ?? (docs && this.documentEnvelope() ? `Envelope "${this.documentEnvelope().name}" sent` : this.showSchedule ? "Message scheduled" : "Text message sent"));
6737
+ this.toast.show(sentText ?? (docs && this.documentEnvelope() ? this.labels.t("messagingSendMessagePanel.envelopeSent", { name: this.documentEnvelope().name }) : this.showSchedule ? this.labels.t("messagingSendMessagePanel.messageScheduled") : this.labels.t("messagingSendMessagePanel.textSent")));
6653
6738
  this.loading = false;
6654
6739
  this.mediaAttachment.set(null);
6655
6740
  if (this.bulk()) this.bulkResult.emit({
@@ -6872,25 +6957,21 @@ var SendTextComponent = class SendTextComponent {
6872
6957
  <!-- Missing phone guard -->
6873
6958
  @if (!bulk() && !contactPhone()) {
6874
6959
  <div class="sm-missing-banner">
6875
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
6876
- No phone number on file for this contact.
6877
- </div>
6960
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.noPhoneNumberOnFile') }}</div>
6878
6961
  <ui-text-input
6879
- label="Add Phone Number"
6962
+ [label]="labels.t('messagingSendMessagePanel.addPhoneNumber')"
6880
6963
  type="tel"
6881
6964
  [required]="true"
6882
6965
  placeholder="(555) 000-0000"
6883
6966
  [(ngModel)]="pendingPhone"
6884
6967
  (ngModelChange)="notifyValid()"
6885
- [error]="pendingPhone && !pendingPhoneValid ? 'Please enter a valid phone number.' : ''" />
6886
- <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">
6887
- Save &amp; Continue
6888
- </ui-button>
6968
+ [error]="pendingPhone && !pendingPhoneValid ? labels.t('validation.phone') : ''" />
6969
+ <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">{{ labels.t('messagingSendMessagePanel.saveContinue') }}</ui-button>
6889
6970
  } @else {
6890
6971
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
6891
6972
  @if (!bulk()) {
6892
6973
  <div class="sm-group">
6893
- <span class="sm-group-label">To</span>
6974
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
6894
6975
  <div class="sm-to-card">
6895
6976
  <span class="sm-to-name">{{ contactName() }}</span>
6896
6977
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
@@ -6901,23 +6982,23 @@ var SendTextComponent = class SendTextComponent {
6901
6982
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
6902
6983
  @if (showTemplates()) {
6903
6984
  <div class="sm-mode-row">
6904
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
6985
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
6905
6986
  </div>
6906
6987
  }
6907
6988
 
6908
6989
  @if (mode === 'compose') {
6909
6990
  <div class="sm-field">
6910
6991
  <ui-textarea-input
6911
- label="Message"
6992
+ [label]="labels.t('common.message')"
6912
6993
  [required]="true"
6913
- placeholder="Write your message..."
6994
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6914
6995
  [rows]="8"
6915
6996
  [maxlength]="MAX_LENGTH"
6916
6997
  [(ngModel)]="body"
6917
6998
  [error]="composeTagError || videoLengthError"
6918
6999
  (ngModelChange)="notifyValid()" />
6919
7000
  <div class="sm-char-row">
6920
- <span class="sm-hint">Sent as SMS. Keep messages concise for best delivery.</span>
7001
+ <span class="sm-hint">{{ labels.t('messagingSendMessagePanel.sentAsSmsKeepMessages') }}</span>
6921
7002
  <span class="sm-char-count" [class.sm-char-warn]="body.length > MAX_LENGTH * 0.9">
6922
7003
  {{ body.length }} / {{ MAX_LENGTH }}
6923
7004
  </span>
@@ -6926,14 +7007,14 @@ var SendTextComponent = class SendTextComponent {
6926
7007
 
6927
7008
  @if (!hasDocuments && showAttachments()) {
6928
7009
  <div class="sm-field">
6929
- <label class="sm-field-label">Add Image / Media (MMS)</label>
7010
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.addImageMediaMms') }}</label>
6930
7011
  @if (mediaAttachment()) {
6931
7012
  <div class="attach-chips">
6932
7013
  <div class="attach-chip">
6933
7014
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
6934
7015
  <span class="attach-name">{{ mediaAttachment()!.mediaFileName }}</span>
6935
7016
  <span class="attach-size">{{ mediaAttachment()!.sizeLabel }}</span>
6936
- <button class="attach-remove" type="button" (click)="removeMedia()" aria-label="Remove attachment">&times;</button>
7017
+ <button class="attach-remove" type="button" (click)="removeMedia()" [attr.aria-label]="labels.t('messagingSendMessagePanel.removeAttachment')">&times;</button>
6937
7018
  </div>
6938
7019
  </div>
6939
7020
  }
@@ -6941,10 +7022,10 @@ var SendTextComponent = class SendTextComponent {
6941
7022
  <input #smsFileInput type="file" accept="image/*,video/*,application/pdf" hidden (change)="onFileSelected($event)" />
6942
7023
  <button type="button" class="attach-add-btn" [disabled]="mediaUploading()" (click)="smsFileInput.click()">
6943
7024
  @if (mediaUploading()) {
6944
- <span class="attach-spinner"></span> Uploading...
7025
+ <span class="attach-spinner"></span> {{ labels.t('messagingSendText.uploading') }}
6945
7026
  } @else {
6946
7027
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
6947
- Attach Image
7028
+ {{ labels.t('messagingSendMessagePanel.attachImage') }}
6948
7029
  }
6949
7030
  </button>
6950
7031
  }
@@ -6953,10 +7034,10 @@ var SendTextComponent = class SendTextComponent {
6953
7034
  }
6954
7035
 
6955
7036
  @if (!hasDocuments && showScheduling()) {
6956
- <ui-checkbox [checked]="showSchedule" label="Schedule for later" (checkedChange)="showSchedule = $event; notifyValid()" />
7037
+ <ui-checkbox [checked]="showSchedule" [label]="labels.t('messagingSendMessagePanel.scheduleForLater')" (checkedChange)="showSchedule = $event; notifyValid()" />
6957
7038
  @if (showSchedule) {
6958
7039
  <ui-datetime-input
6959
- label="Send at"
7040
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
6960
7041
  [min]="minScheduleTime"
6961
7042
  [(ngModel)]="scheduledAt"
6962
7043
  (ngModelChange)="notifyValid()" />
@@ -6966,14 +7047,12 @@ var SendTextComponent = class SendTextComponent {
6966
7047
  @if (missingVars.length > 0) {
6967
7048
  <div class="mv-section">
6968
7049
  <p class="mv-hint">
6969
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
6970
- This template has fields that need values before sending.
6971
- </p>
7050
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.thisTemplateHasFieldsThat') }}</p>
6972
7051
  @for (key of missingVars; track key) {
6973
7052
  <div class="mv-row">
6974
7053
  <div class="mv-input-wrap">
6975
- <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="'Enter ' + formatVarLabel(key).toLowerCase() + '...'" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
6976
- <button class="mv-apply-btn" type="button" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" aria-label="Apply value">
7054
+ <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="labels.t('messagingSendMessagePanel.enterValue', { name: formatVarLabel(key).toLowerCase() })" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
7055
+ <button class="mv-apply-btn" type="button" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" [attr.aria-label]="labels.t('messagingSendMessagePanel.applyValue')">
6977
7056
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
6978
7057
  </button>
6979
7058
  </div>
@@ -6984,20 +7063,16 @@ var SendTextComponent = class SendTextComponent {
6984
7063
 
6985
7064
  <div class="tpl-preview-header">
6986
7065
  <button class="tpl-back-btn" type="button" (click)="clearTemplate()">
6987
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
6988
- Back to templates
6989
- </button>
7066
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>{{ labels.t('messagingSendMessagePanel.backToTemplates') }}</button>
6990
7067
  <div class="tpl-view-toggle">
6991
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">Preview</button>
6992
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'raw'" [attr.aria-pressed]="previewFormat === 'raw'" type="button" (click)="setPreviewFormat('raw')">Raw</button>
7068
+ <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">{{ labels.t('common.preview') }}</button>
7069
+ <button class="tpl-toggle-btn" [class.active]="previewFormat === 'raw'" [attr.aria-pressed]="previewFormat === 'raw'" type="button" (click)="setPreviewFormat('raw')">{{ labels.t('messagingSendMessagePanel.raw') }}</button>
6993
7070
  </div>
6994
7071
  </div>
6995
7072
 
6996
7073
  <div class="tpl-send-preview-row">
6997
7074
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
6998
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
6999
- View Send Preview
7000
- </ui-button>
7075
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>{{ labels.t('messagingSendMessagePanel.viewSendPreview') }}</ui-button>
7001
7076
  </div>
7002
7077
 
7003
7078
  @if (previewFormat === 'preview') {
@@ -7037,10 +7112,10 @@ var SendTextComponent = class SendTextComponent {
7037
7112
  <!-- One live region, at most ONE warning banner (every current non-blocking warning is a line in it). -->
7038
7113
  <div class="sm-docs-live" role="status" aria-live="polite">
7039
7114
  @if (hasDocuments) {
7040
- <p class="sm-docs-note">The documents are texted as a link after your message. Images and scheduling aren't available when sending documents.</p>
7115
+ <p class="sm-docs-note">{{ labels.t('messagingSendMessagePanel.theDocumentsAreTextedAs') }}</p>
7041
7116
  }
7042
7117
  @if (warningLines.length > 0) {
7043
- <ui-alert variant="warning" title="Heads up" data-testid="send-warnings">
7118
+ <ui-alert variant="warning" [title]="labels.t('messagingSendMessagePanel.headsUp')" data-testid="send-warnings">
7044
7119
  <ul class="sm-warn-list">
7045
7120
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
7046
7121
  </ul>
@@ -7051,7 +7126,7 @@ var SendTextComponent = class SendTextComponent {
7051
7126
 
7052
7127
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
7053
7128
  @if (mode === 'compose' && showVideo()) {
7054
- <ui-form-section label="Video" [divider]="true">
7129
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
7055
7130
  @for (k of [videoPickerKey]; track k) {
7056
7131
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
7057
7132
  }
@@ -7073,17 +7148,15 @@ var SendTextComponent = class SendTextComponent {
7073
7148
  <div class="sp-modal" (click)="$event.stopPropagation()">
7074
7149
  <div class="sp-modal-header">
7075
7150
  <div class="sp-modal-title">
7076
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
7077
- Text Preview
7078
- </div>
7079
- <button class="sp-close-btn" type="button" (click)="closeSendPreview()" aria-label="Close preview">
7151
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('messagingSendMessagePanel.textPreview') }}</div>
7152
+ <button class="sp-close-btn" type="button" (click)="closeSendPreview()" [attr.aria-label]="labels.t('messagingSendMessagePanel.closePreview')">
7080
7153
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
7081
7154
  </button>
7082
7155
  </div>
7083
7156
  <div class="sp-text-meta">
7084
7157
  <div class="sp-meta-row">
7085
- <span class="sp-meta-label">To</span>
7086
- <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7158
+ <span class="sp-meta-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
7159
+ <span class="sp-meta-value">@if (bulk()) { {{ labels.t(bulkRecipients()!.length === 1 ? 'messagingSendMessagePanel.recipientCount_one' : 'messagingSendMessagePanel.recipientCount_other', { count: bulkRecipients()!.length }) }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7087
7160
  </div>
7088
7161
  </div>
7089
7162
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7097,10 +7170,8 @@ var SendTextComponent = class SendTextComponent {
7097
7170
  <div class="sp-modal" (click)="$event.stopPropagation()">
7098
7171
  <div class="sp-modal-header">
7099
7172
  <div class="sp-modal-title">
7100
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
7101
- Text Preview
7102
- </div>
7103
- <button class="sp-close-btn" type="button" (click)="showQuickPreview = false; previewingTemplate = null" aria-label="Close preview">
7173
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('messagingSendMessagePanel.textPreview') }}</div>
7174
+ <button class="sp-close-btn" type="button" (click)="showQuickPreview = false; previewingTemplate = null" [attr.aria-label]="labels.t('messagingSendMessagePanel.closePreview')">
7104
7175
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
7105
7176
  </button>
7106
7177
  </div>
@@ -7441,25 +7512,21 @@ i0.ɵɵngDeclareClassMetadata({
7441
7512
  <!-- Missing phone guard -->
7442
7513
  @if (!bulk() && !contactPhone()) {
7443
7514
  <div class="sm-missing-banner">
7444
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
7445
- No phone number on file for this contact.
7446
- </div>
7515
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.noPhoneNumberOnFile') }}</div>
7447
7516
  <ui-text-input
7448
- label="Add Phone Number"
7517
+ [label]="labels.t('messagingSendMessagePanel.addPhoneNumber')"
7449
7518
  type="tel"
7450
7519
  [required]="true"
7451
7520
  placeholder="(555) 000-0000"
7452
7521
  [(ngModel)]="pendingPhone"
7453
7522
  (ngModelChange)="notifyValid()"
7454
- [error]="pendingPhone && !pendingPhoneValid ? 'Please enter a valid phone number.' : ''" />
7455
- <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">
7456
- Save &amp; Continue
7457
- </ui-button>
7523
+ [error]="pendingPhone && !pendingPhoneValid ? labels.t('validation.phone') : ''" />
7524
+ <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">{{ labels.t('messagingSendMessagePanel.saveContinue') }}</ui-button>
7458
7525
  } @else {
7459
7526
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
7460
7527
  @if (!bulk()) {
7461
7528
  <div class="sm-group">
7462
- <span class="sm-group-label">To</span>
7529
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
7463
7530
  <div class="sm-to-card">
7464
7531
  <span class="sm-to-name">{{ contactName() }}</span>
7465
7532
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
@@ -7470,23 +7537,23 @@ i0.ɵɵngDeclareClassMetadata({
7470
7537
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
7471
7538
  @if (showTemplates()) {
7472
7539
  <div class="sm-mode-row">
7473
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
7540
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
7474
7541
  </div>
7475
7542
  }
7476
7543
 
7477
7544
  @if (mode === 'compose') {
7478
7545
  <div class="sm-field">
7479
7546
  <ui-textarea-input
7480
- label="Message"
7547
+ [label]="labels.t('common.message')"
7481
7548
  [required]="true"
7482
- placeholder="Write your message..."
7549
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
7483
7550
  [rows]="8"
7484
7551
  [maxlength]="MAX_LENGTH"
7485
7552
  [(ngModel)]="body"
7486
7553
  [error]="composeTagError || videoLengthError"
7487
7554
  (ngModelChange)="notifyValid()" />
7488
7555
  <div class="sm-char-row">
7489
- <span class="sm-hint">Sent as SMS. Keep messages concise for best delivery.</span>
7556
+ <span class="sm-hint">{{ labels.t('messagingSendMessagePanel.sentAsSmsKeepMessages') }}</span>
7490
7557
  <span class="sm-char-count" [class.sm-char-warn]="body.length > MAX_LENGTH * 0.9">
7491
7558
  {{ body.length }} / {{ MAX_LENGTH }}
7492
7559
  </span>
@@ -7495,14 +7562,14 @@ i0.ɵɵngDeclareClassMetadata({
7495
7562
 
7496
7563
  @if (!hasDocuments && showAttachments()) {
7497
7564
  <div class="sm-field">
7498
- <label class="sm-field-label">Add Image / Media (MMS)</label>
7565
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.addImageMediaMms') }}</label>
7499
7566
  @if (mediaAttachment()) {
7500
7567
  <div class="attach-chips">
7501
7568
  <div class="attach-chip">
7502
7569
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
7503
7570
  <span class="attach-name">{{ mediaAttachment()!.mediaFileName }}</span>
7504
7571
  <span class="attach-size">{{ mediaAttachment()!.sizeLabel }}</span>
7505
- <button class="attach-remove" type="button" (click)="removeMedia()" aria-label="Remove attachment">&times;</button>
7572
+ <button class="attach-remove" type="button" (click)="removeMedia()" [attr.aria-label]="labels.t('messagingSendMessagePanel.removeAttachment')">&times;</button>
7506
7573
  </div>
7507
7574
  </div>
7508
7575
  }
@@ -7510,10 +7577,10 @@ i0.ɵɵngDeclareClassMetadata({
7510
7577
  <input #smsFileInput type="file" accept="image/*,video/*,application/pdf" hidden (change)="onFileSelected($event)" />
7511
7578
  <button type="button" class="attach-add-btn" [disabled]="mediaUploading()" (click)="smsFileInput.click()">
7512
7579
  @if (mediaUploading()) {
7513
- <span class="attach-spinner"></span> Uploading...
7580
+ <span class="attach-spinner"></span> {{ labels.t('messagingSendText.uploading') }}
7514
7581
  } @else {
7515
7582
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
7516
- Attach Image
7583
+ {{ labels.t('messagingSendMessagePanel.attachImage') }}
7517
7584
  }
7518
7585
  </button>
7519
7586
  }
@@ -7522,10 +7589,10 @@ i0.ɵɵngDeclareClassMetadata({
7522
7589
  }
7523
7590
 
7524
7591
  @if (!hasDocuments && showScheduling()) {
7525
- <ui-checkbox [checked]="showSchedule" label="Schedule for later" (checkedChange)="showSchedule = $event; notifyValid()" />
7592
+ <ui-checkbox [checked]="showSchedule" [label]="labels.t('messagingSendMessagePanel.scheduleForLater')" (checkedChange)="showSchedule = $event; notifyValid()" />
7526
7593
  @if (showSchedule) {
7527
7594
  <ui-datetime-input
7528
- label="Send at"
7595
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
7529
7596
  [min]="minScheduleTime"
7530
7597
  [(ngModel)]="scheduledAt"
7531
7598
  (ngModelChange)="notifyValid()" />
@@ -7535,14 +7602,12 @@ i0.ɵɵngDeclareClassMetadata({
7535
7602
  @if (missingVars.length > 0) {
7536
7603
  <div class="mv-section">
7537
7604
  <p class="mv-hint">
7538
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
7539
- This template has fields that need values before sending.
7540
- </p>
7605
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>{{ labels.t('messagingSendMessagePanel.thisTemplateHasFieldsThat') }}</p>
7541
7606
  @for (key of missingVars; track key) {
7542
7607
  <div class="mv-row">
7543
7608
  <div class="mv-input-wrap">
7544
- <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="'Enter ' + formatVarLabel(key).toLowerCase() + '...'" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
7545
- <button class="mv-apply-btn" type="button" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" aria-label="Apply value">
7609
+ <ui-text-input class="mv-text-input" [label]="formatVarLabel(key)" [required]="true" [placeholder]="labels.t('messagingSendMessagePanel.enterValue', { name: formatVarLabel(key).toLowerCase() })" [(ngModel)]="pendingInputs[key]" (ngModelChange)="notifyValid()" (keydown.enter)="applyVar(key)" />
7610
+ <button class="mv-apply-btn" type="button" [disabled]="!(pendingInputs[key] || '').trim()" (click)="applyVar(key)" [attr.aria-label]="labels.t('messagingSendMessagePanel.applyValue')">
7546
7611
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
7547
7612
  </button>
7548
7613
  </div>
@@ -7553,20 +7618,16 @@ i0.ɵɵngDeclareClassMetadata({
7553
7618
 
7554
7619
  <div class="tpl-preview-header">
7555
7620
  <button class="tpl-back-btn" type="button" (click)="clearTemplate()">
7556
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
7557
- Back to templates
7558
- </button>
7621
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>{{ labels.t('messagingSendMessagePanel.backToTemplates') }}</button>
7559
7622
  <div class="tpl-view-toggle">
7560
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">Preview</button>
7561
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'raw'" [attr.aria-pressed]="previewFormat === 'raw'" type="button" (click)="setPreviewFormat('raw')">Raw</button>
7623
+ <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">{{ labels.t('common.preview') }}</button>
7624
+ <button class="tpl-toggle-btn" [class.active]="previewFormat === 'raw'" [attr.aria-pressed]="previewFormat === 'raw'" type="button" (click)="setPreviewFormat('raw')">{{ labels.t('messagingSendMessagePanel.raw') }}</button>
7562
7625
  </div>
7563
7626
  </div>
7564
7627
 
7565
7628
  <div class="tpl-send-preview-row">
7566
7629
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
7567
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
7568
- View Send Preview
7569
- </ui-button>
7630
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>{{ labels.t('messagingSendMessagePanel.viewSendPreview') }}</ui-button>
7570
7631
  </div>
7571
7632
 
7572
7633
  @if (previewFormat === 'preview') {
@@ -7606,10 +7667,10 @@ i0.ɵɵngDeclareClassMetadata({
7606
7667
  <!-- One live region, at most ONE warning banner (every current non-blocking warning is a line in it). -->
7607
7668
  <div class="sm-docs-live" role="status" aria-live="polite">
7608
7669
  @if (hasDocuments) {
7609
- <p class="sm-docs-note">The documents are texted as a link after your message. Images and scheduling aren't available when sending documents.</p>
7670
+ <p class="sm-docs-note">{{ labels.t('messagingSendMessagePanel.theDocumentsAreTextedAs') }}</p>
7610
7671
  }
7611
7672
  @if (warningLines.length > 0) {
7612
- <ui-alert variant="warning" title="Heads up" data-testid="send-warnings">
7673
+ <ui-alert variant="warning" [title]="labels.t('messagingSendMessagePanel.headsUp')" data-testid="send-warnings">
7613
7674
  <ul class="sm-warn-list">
7614
7675
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
7615
7676
  </ul>
@@ -7620,7 +7681,7 @@ i0.ɵɵngDeclareClassMetadata({
7620
7681
 
7621
7682
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
7622
7683
  @if (mode === 'compose' && showVideo()) {
7623
- <ui-form-section label="Video" [divider]="true">
7684
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
7624
7685
  @for (k of [videoPickerKey]; track k) {
7625
7686
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
7626
7687
  }
@@ -7642,17 +7703,15 @@ i0.ɵɵngDeclareClassMetadata({
7642
7703
  <div class="sp-modal" (click)="$event.stopPropagation()">
7643
7704
  <div class="sp-modal-header">
7644
7705
  <div class="sp-modal-title">
7645
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
7646
- Text Preview
7647
- </div>
7648
- <button class="sp-close-btn" type="button" (click)="closeSendPreview()" aria-label="Close preview">
7706
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('messagingSendMessagePanel.textPreview') }}</div>
7707
+ <button class="sp-close-btn" type="button" (click)="closeSendPreview()" [attr.aria-label]="labels.t('messagingSendMessagePanel.closePreview')">
7649
7708
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
7650
7709
  </button>
7651
7710
  </div>
7652
7711
  <div class="sp-text-meta">
7653
7712
  <div class="sp-meta-row">
7654
- <span class="sp-meta-label">To</span>
7655
- <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7713
+ <span class="sp-meta-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
7714
+ <span class="sp-meta-value">@if (bulk()) { {{ labels.t(bulkRecipients()!.length === 1 ? 'messagingSendMessagePanel.recipientCount_one' : 'messagingSendMessagePanel.recipientCount_other', { count: bulkRecipients()!.length }) }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7656
7715
  </div>
7657
7716
  </div>
7658
7717
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7666,10 +7725,8 @@ i0.ɵɵngDeclareClassMetadata({
7666
7725
  <div class="sp-modal" (click)="$event.stopPropagation()">
7667
7726
  <div class="sp-modal-header">
7668
7727
  <div class="sp-modal-title">
7669
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
7670
- Text Preview
7671
- </div>
7672
- <button class="sp-close-btn" type="button" (click)="showQuickPreview = false; previewingTemplate = null" aria-label="Close preview">
7728
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('messagingSendMessagePanel.textPreview') }}</div>
7729
+ <button class="sp-close-btn" type="button" (click)="showQuickPreview = false; previewingTemplate = null" [attr.aria-label]="labels.t('messagingSendMessagePanel.closePreview')">
7673
7730
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
7674
7731
  </button>
7675
7732
  </div>
@@ -7912,13 +7969,21 @@ i0.ɵɵngDeclareClassMetadata({
7912
7969
  const BULK_MERGE_TAG_HINT = "Personal details such as {{first_name}} are filled in for each recipient when sent.";
7913
7970
  const SKIPPED_NAMES_SHOWN = 5;
7914
7971
  var SendMessagePanelComponent = class SendMessagePanelComponent {
7915
- skippedReason() {
7916
- return this.channel() === "email" ? "no email address" : "no phone number or no opt-in to texts";
7972
+ get BULK_MERGE_TAG_HINT() {
7973
+ return this.labels.t("messagingSendMessagePanel.bulkMergeTagHint");
7974
+ }
7975
+ skippedReason(count) {
7976
+ const one = count === 1;
7977
+ if (this.channel() === "email") return this.labels.t(one ? "messagingSendMessagePanel.reasonNoEmail_one" : "messagingSendMessagePanel.reasonNoEmail_other");
7978
+ return this.labels.t(one ? "messagingSendMessagePanel.reasonNoPhone_one" : "messagingSendMessagePanel.reasonNoPhone_other");
7917
7979
  }
7918
7980
  skippedNames() {
7919
- const names = this.skippedRecipients().map((r) => r.name || "Unnamed");
7981
+ const names = this.skippedRecipients().map((r) => r.name || this.labels.t("messagingSendMessagePanel.unnamed"));
7920
7982
  const shown = names.slice(0, SKIPPED_NAMES_SHOWN).join(", ");
7921
- return names.length > SKIPPED_NAMES_SHOWN ? `${shown} and ${names.length - SKIPPED_NAMES_SHOWN} more` : shown;
7983
+ return names.length > SKIPPED_NAMES_SHOWN ? this.labels.t("messagingSendMessagePanel.skippedNameMore", {
7984
+ shown,
7985
+ count: names.length - SKIPPED_NAMES_SHOWN
7986
+ }) : shown;
7922
7987
  }
7923
7988
  bulkRecipientsFor(ch) {
7924
7989
  if (!this.bulkMode()) return null;
@@ -7966,15 +8031,20 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7966
8031
  this.jobOptions.set([]);
7967
8032
  }
7968
8033
  constructor() {
8034
+ this.labels = inject(UiLabelsService);
7969
8035
  this.open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
7970
8036
  this.contact = input(null, ...ngDevMode ? [{ debugName: "contact" }] : /* istanbul ignore next */ []);
7971
8037
  this.loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
7972
8038
  this.recipients = input(null, ...ngDevMode ? [{ debugName: "recipients" }] : /* istanbul ignore next */ []);
7973
8039
  this.initialChannel = input(null, ...ngDevMode ? [{ debugName: "initialChannel" }] : /* istanbul ignore next */ []);
7974
- this.recipientNoun = input("recipients", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
8040
+ this.recipientNoun = input("", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
7975
8041
  this.recipientNounSingular = input("", ...ngDevMode ? [{ debugName: "recipientNounSingular" }] : /* istanbul ignore next */ []);
7976
8042
  this.textEnabled = input(false, ...ngDevMode ? [{ debugName: "textEnabled" }] : /* istanbul ignore next */ []);
7977
- this.title = input("Send Message", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
8043
+ this.titleInput = input(null, {
8044
+ ...ngDevMode ? { debugName: "titleInput" } : /* istanbul ignore next */ {},
8045
+ alias: "title"
8046
+ });
8047
+ this.title = computed(() => this.titleInput() ?? this.labels.t("messagingSendMessagePanel.title"), ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
7978
8048
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
7979
8049
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
7980
8050
  this.defaultMessage = input("", ...ngDevMode ? [{ debugName: "defaultMessage" }] : /* istanbul ignore next */ []);
@@ -7982,7 +8052,11 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7982
8052
  this.sendEmail = input(null, ...ngDevMode ? [{ debugName: "sendEmail" }] : /* istanbul ignore next */ []);
7983
8053
  this.sendText = input(null, ...ngDevMode ? [{ debugName: "sendText" }] : /* istanbul ignore next */ []);
7984
8054
  this.showReceiptOption = input(false, ...ngDevMode ? [{ debugName: "showReceiptOption" }] : /* istanbul ignore next */ []);
7985
- this.receiptLabel = input("Include payment receipt", ...ngDevMode ? [{ debugName: "receiptLabel" }] : /* istanbul ignore next */ []);
8055
+ this.receiptLabelInput = input(null, {
8056
+ ...ngDevMode ? { debugName: "receiptLabelInput" } : /* istanbul ignore next */ {},
8057
+ alias: "receiptLabel"
8058
+ });
8059
+ this.receiptLabel = computed(() => this.receiptLabelInput() ?? this.labels.t("messagingSendMessagePanel.receiptLabel"), ...ngDevMode ? [{ debugName: "receiptLabel" }] : /* istanbul ignore next */ []);
7986
8060
  this.receiptDefault = input(false, ...ngDevMode ? [{ debugName: "receiptDefault" }] : /* istanbul ignore next */ []);
7987
8061
  this.documents = input(false, ...ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []);
7988
8062
  this.documentsEnabled = input(true, ...ngDevMode ? [{ debugName: "documentsEnabled" }] : /* istanbul ignore next */ []);
@@ -7996,8 +8070,6 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7996
8070
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
7997
8071
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
7998
8072
  this.inApp = computed(() => this.recipientMode() === "in-app" && !this.bulkMode(), ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7999
- this.BULK_NO_SEND_MESSAGE = BULK_NO_SEND_MESSAGE;
8000
- this.BULK_MERGE_TAG_HINT = BULK_MERGE_TAG_HINT;
8001
8073
  this.attachmentsAndScheduling = input(true, ...ngDevMode ? [{ debugName: "attachmentsAndScheduling" }] : /* istanbul ignore next */ []);
8002
8074
  this.allowAttachments = input(null, ...ngDevMode ? [{ debugName: "allowAttachments" }] : /* istanbul ignore next */ []);
8003
8075
  this.allowScheduling = input(null, ...ngDevMode ? [{ debugName: "allowScheduling" }] : /* istanbul ignore next */ []);
@@ -8041,31 +8113,56 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8041
8113
  this.submitSnapshot = null;
8042
8114
  this.recipientWord = computed(() => {
8043
8115
  const n = this.bulkList().length;
8044
- const plural = this.recipientNoun() || "recipients";
8116
+ const plural = this.recipientNoun() || this.labels.t("messagingSendMessagePanel.recipientsWord");
8045
8117
  if (n !== 1) return plural;
8046
8118
  return this.recipientNounSingular() || (plural.endsWith("s") ? plural.slice(0, -1) : plural);
8047
8119
  }, ...ngDevMode ? [{ debugName: "recipientWord" }] : /* istanbul ignore next */ []);
8048
8120
  this.skippedText = computed(() => {
8049
8121
  const n = this.skippedRecipients().length;
8050
8122
  if (n === 0) return "";
8051
- const what = this.channel() === "email" ? "email" : "text";
8052
- return `${n} ${n === 1 ? "has" : "have"} ${this.skippedReason()} and will be skipped (${this.skippedNames()}). Only the others get this ${what}.`;
8123
+ const what = this.channel() === "email" ? this.labels.t("messagingSendMessagePanel.whatEmail") : this.labels.t("messagingSendMessagePanel.whatText");
8124
+ return this.labels.t(n === 1 ? "messagingSendMessagePanel.skippedText_one" : "messagingSendMessagePanel.skippedText_other", {
8125
+ count: n,
8126
+ reason: this.skippedReason(n),
8127
+ names: this.skippedNames(),
8128
+ what
8129
+ });
8053
8130
  }, ...ngDevMode ? [{ debugName: "skippedText" }] : /* istanbul ignore next */ []);
8054
8131
  this.noneEligibleText = computed(() => {
8055
- return `None of the ${this.bulkList().length} selected can receive this ${this.channel() === "email" ? "email" : "text message"} (${this.skippedReason()}), so there is nothing to send.`;
8132
+ const n = this.bulkList().length;
8133
+ const what = this.channel() === "email" ? this.labels.t("messagingSendMessagePanel.whatEmail") : this.labels.t("messagingSendMessagePanel.whatTextMessage");
8134
+ return this.labels.t(n === 1 ? "messagingSendMessagePanel.noneEligible_one" : "messagingSendMessagePanel.noneEligible_other", {
8135
+ count: n,
8136
+ what,
8137
+ reason: this.skippedReason(n)
8138
+ });
8056
8139
  }, ...ngDevMode ? [{ debugName: "noneEligibleText" }] : /* istanbul ignore next */ []);
8057
8140
  this.bulkAnnouncement = computed(() => {
8058
8141
  if (!this.bulkMode()) return "";
8059
8142
  const n = this.bulkList().length;
8060
8143
  const ok = this.activeRecipients().length;
8061
- const ch = this.channel() === "email" ? "email" : "text";
8062
- if (ok === 0) return `${n} ${this.recipientWord()} selected. None can receive the ${ch}.`;
8144
+ const ch = this.channel() === "email" ? this.labels.t("messagingSendMessagePanel.whatEmail") : this.labels.t("messagingSendMessagePanel.whatText");
8145
+ if (ok === 0) return this.labels.t(n === 1 ? "messagingSendMessagePanel.announceNone_one" : "messagingSendMessagePanel.announceNone_other", {
8146
+ count: n,
8147
+ word: this.recipientWord(),
8148
+ channel: ch
8149
+ });
8063
8150
  const skipped = this.skippedRecipients().length;
8064
- return `${n} ${this.recipientWord()} selected. ${ok} can receive the ${ch}.${skipped ? ` ${skipped} will be skipped.` : ""}`;
8151
+ const someKey = n === 1 ? "messagingSendMessagePanel.announceSome_one" : ok === 1 ? "messagingSendMessagePanel.announceSome_okOne" : "messagingSendMessagePanel.announceSome_other";
8152
+ return this.labels.t(someKey, {
8153
+ count: n,
8154
+ word: this.recipientWord(),
8155
+ ok,
8156
+ channel: ch
8157
+ }) + (skipped ? " " + this.labels.t(skipped === 1 ? "messagingSendMessagePanel.announceSkipped_one" : "messagingSendMessagePanel.announceSkipped_other", { count: skipped }) : "");
8065
8158
  }, ...ngDevMode ? [{ debugName: "bulkAnnouncement" }] : /* istanbul ignore next */ []);
8066
8159
  this.lastBulkResult = void 0;
8067
8160
  this.showJobPicker = input(false, ...ngDevMode ? [{ debugName: "showJobPicker" }] : /* istanbul ignore next */ []);
8068
- this.jobLabel = input("Job", ...ngDevMode ? [{ debugName: "jobLabel" }] : /* istanbul ignore next */ []);
8161
+ this.jobLabelInput = input(null, {
8162
+ ...ngDevMode ? { debugName: "jobLabelInput" } : /* istanbul ignore next */ {},
8163
+ alias: "jobLabel"
8164
+ });
8165
+ this.jobLabel = computed(() => this.jobLabelInput() ?? this.labels.t("messagingSendMessagePanel.jobLabel"), ...ngDevMode ? [{ debugName: "jobLabel" }] : /* istanbul ignore next */ []);
8069
8166
  this.jobHint = input("", ...ngDevMode ? [{ debugName: "jobHint" }] : /* istanbul ignore next */ []);
8070
8167
  this.jobSearch = input(null, ...ngDevMode ? [{ debugName: "jobSearch" }] : /* istanbul ignore next */ []);
8071
8168
  this.jobSelected = output();
@@ -8097,10 +8194,10 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8097
8194
  if (!c || this.inApp()) return [];
8098
8195
  const notices = [];
8099
8196
  const hasEmail = !!c.email, hasPhone = !!c.phone;
8100
- if (!this.textEnabled()) notices.push("Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.");
8101
- else if (!c.smsOptedIn) notices.push(`${c.name} has not opted in to receive text messages, so this can only be sent by email.${!hasEmail && hasPhone ? " Add an email address below to send." : ""}`);
8102
- else if (!hasPhone) notices.push(`${c.name} has no phone number on file, so a text needs one added first.`);
8103
- if (!hasEmail && !hasPhone) notices.push(`${c.name} has no email address or phone number on file. Add an email address below to send.`);
8197
+ if (!this.textEnabled()) notices.push(this.labels.t("messagingSendMessagePanel.noticeTextDisabled"));
8198
+ else if (!c.smsOptedIn) notices.push(this.labels.t("messagingSendMessagePanel.noticeNoOptIn", { name: c.name }) + (!hasEmail && hasPhone ? " " + this.labels.t("messagingSendMessagePanel.noticeAddEmailBelow") : ""));
8199
+ else if (!hasPhone) notices.push(this.labels.t("messagingSendMessagePanel.noticeNoPhone", { name: c.name }));
8200
+ if (!hasEmail && !hasPhone) notices.push(this.labels.t("messagingSendMessagePanel.noticeNoContact", { name: c.name }));
8104
8201
  return notices;
8105
8202
  }, ...ngDevMode ? [{ debugName: "contactNotices" }] : /* istanbul ignore next */ []);
8106
8203
  this.channel = signal("email", ...ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []);
@@ -8300,8 +8397,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8300
8397
  isRequired: false,
8301
8398
  transformFunction: null
8302
8399
  },
8303
- title: {
8304
- classPropertyName: "title",
8400
+ titleInput: {
8401
+ classPropertyName: "titleInput",
8305
8402
  publicName: "title",
8306
8403
  isSignal: true,
8307
8404
  isRequired: false,
@@ -8356,8 +8453,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8356
8453
  isRequired: false,
8357
8454
  transformFunction: null
8358
8455
  },
8359
- receiptLabel: {
8360
- classPropertyName: "receiptLabel",
8456
+ receiptLabelInput: {
8457
+ classPropertyName: "receiptLabelInput",
8361
8458
  publicName: "receiptLabel",
8362
8459
  isSignal: true,
8363
8460
  isRequired: false,
@@ -8580,8 +8677,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8580
8677
  isRequired: false,
8581
8678
  transformFunction: null
8582
8679
  },
8583
- jobLabel: {
8584
- classPropertyName: "jobLabel",
8680
+ jobLabelInput: {
8681
+ classPropertyName: "jobLabelInput",
8585
8682
  publicName: "jobLabel",
8586
8683
  isSignal: true,
8587
8684
  isRequired: false,
@@ -8641,8 +8738,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8641
8738
  [open]="open()"
8642
8739
  [title]="title()"
8643
8740
  [wide]="wide()"
8644
- cancelLabel="Cancel"
8645
- [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8741
+ [cancelLabel]="labels.t('common.cancel')"
8742
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? labels.t('messagingSendMessagePanel.sendMessage') : channel() === 'email' ? labels.t('messagingSendMessagePanel.sendEmail') : labels.t('messagingSendMessagePanel.sendText'))) : ''"
8646
8743
  primaryVariant="warning"
8647
8744
  [primaryDisabled]="!valid() || bulkBlocked()"
8648
8745
  (closed)="onClose()"
@@ -8659,25 +8756,25 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8659
8756
  @if (!loading() && !hasTarget() && !inApp()) {
8660
8757
  <div class="msg-step">
8661
8758
  <span class="msg-step-badge">1</span>
8662
- <span class="msg-step-text">Who would you like to message?</span>
8759
+ <span class="msg-step-text">{{ labels.t('messagingSendMessagePanel.whoWouldYouLikeTo') }}</span>
8663
8760
  </div>
8664
8761
  <ui-contact-picker
8665
- placeholder="Search by contact name..."
8762
+ [placeholder]="labels.t('messagingSendMessagePanel.searchByContactName')"
8666
8763
  (contactSelected)="onContactSelected($event)" />
8667
8764
  }
8668
8765
 
8669
8766
  @if (!loading() && hasTarget()) {
8670
8767
  @if (bulkMode()) {
8671
- <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
8672
- <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
8768
+ <div class="msg-bulk" role="group" [attr.aria-label]="labels.t('messagingSendMessagePanel.recipients')" data-testid="bulk-summary">
8769
+ <p class="msg-bulk-to" data-testid="bulk-to">{{ labels.t('messagingSendMessagePanel.toLabel') }} {{ bulkList().length }} {{ recipientWord() }}</p>
8673
8770
  <div class="msg-bulk-counts">
8674
- <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
8771
+ <ui-badge [label]="labels.t(emailRecipients().length === 1 ? 'messagingSendMessagePanel.canReceiveEmail_one' : 'messagingSendMessagePanel.canReceiveEmail_other', { count: emailRecipients().length })" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
8675
8772
  @if (textEnabled()) {
8676
- <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
8773
+ <ui-badge [label]="labels.t(textRecipients().length === 1 ? 'messagingSendMessagePanel.canReceiveText_one' : 'messagingSendMessagePanel.canReceiveText_other', { count: textRecipients().length })" [variant]="textRecipients().length ? 'blue' : 'gray'" />
8677
8774
  }
8678
8775
  </div>
8679
8776
  @if (!textEnabled()) {
8680
- <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
8777
+ <ui-hint>{{ labels.t('messagingSendMessagePanel.textMessagingIsNotEnabled') }}</ui-hint>
8681
8778
  }
8682
8779
  <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
8683
8780
  @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
@@ -8687,7 +8784,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8687
8784
  <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
8688
8785
  }
8689
8786
  @if (bulkMissingSend()) {
8690
- <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
8787
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ labels.t('messagingSendMessagePanel.bulkNoSend') }}</ui-alert>
8691
8788
  }
8692
8789
  @if (!bulkNoneEligible()) {
8693
8790
  <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
@@ -8697,23 +8794,19 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8697
8794
 
8698
8795
  @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
8699
8796
  <div class="msg-channel-row" data-testid="bulk-channel-row">
8700
- <label class="form-label msg-via-label">Send via</label>
8797
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
8701
8798
  @if (sending()) {
8702
8799
  <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
8703
8800
  <div class="msg-via-tabs">
8704
- <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
8705
- <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
8801
+ <ui-badge [label]="channel() === 'email' ? labels.t('common.email') : labels.t('common.text')" variant="blue" />
8802
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">{{ labels.t('messagingSendMessagePanel.channelLockedWhileSending') }}</span>
8706
8803
  </div>
8707
8804
  } @else {
8708
8805
  <div class="msg-via-tabs">
8709
8806
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8710
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
8711
- Email
8712
- </ui-toggle-badge>
8807
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('common.email') }}</ui-toggle-badge>
8713
8808
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8714
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
8715
- Text
8716
- </ui-toggle-badge>
8809
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('common.text') }}</ui-toggle-badge>
8717
8810
  </div>
8718
8811
  }
8719
8812
  </div>
@@ -8721,16 +8814,12 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8721
8814
 
8722
8815
  @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8723
8816
  <div class="msg-channel-row">
8724
- <label class="form-label msg-via-label">Send via</label>
8817
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
8725
8818
  <div class="msg-via-tabs">
8726
8819
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8727
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
8728
- Email
8729
- </ui-toggle-badge>
8820
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('common.email') }}</ui-toggle-badge>
8730
8821
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8731
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
8732
- Text
8733
- </ui-toggle-badge>
8822
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('common.text') }}</ui-toggle-badge>
8734
8823
  </div>
8735
8824
  </div>
8736
8825
  }
@@ -8747,8 +8836,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8747
8836
  @for (epoch of [formEpoch()]; track epoch) {
8748
8837
  <div class="msg-job">
8749
8838
  <ui-combobox
8750
- [label]="jobLabel()" placeholder="Search by job name…" [hint]="jobHint()"
8751
- [options]="jobOptions()" [loading]="jobLoading()" noResultsText="No jobs found" [minChars]="2" [debounce]="300"
8839
+ [label]="jobLabel()" [placeholder]="labels.t('messagingSendMessagePanel.searchByJobName')" [hint]="jobHint()"
8840
+ [options]="jobOptions()" [loading]="jobLoading()" [noResultsText]="labels.t('messagingSendMessagePanel.noJobsFound')" [minChars]="2" [debounce]="300"
8752
8841
  [value]="jobValue()"
8753
8842
  (searchChange)="jobSearch$.next($event)" (valueChange)="onJobPick($event)" />
8754
8843
  </div>
@@ -9117,8 +9206,8 @@ i0.ɵɵngDeclareClassMetadata({
9117
9206
  [open]="open()"
9118
9207
  [title]="title()"
9119
9208
  [wide]="wide()"
9120
- cancelLabel="Cancel"
9121
- [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
9209
+ [cancelLabel]="labels.t('common.cancel')"
9210
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? labels.t('messagingSendMessagePanel.sendMessage') : channel() === 'email' ? labels.t('messagingSendMessagePanel.sendEmail') : labels.t('messagingSendMessagePanel.sendText'))) : ''"
9122
9211
  primaryVariant="warning"
9123
9212
  [primaryDisabled]="!valid() || bulkBlocked()"
9124
9213
  (closed)="onClose()"
@@ -9135,25 +9224,25 @@ i0.ɵɵngDeclareClassMetadata({
9135
9224
  @if (!loading() && !hasTarget() && !inApp()) {
9136
9225
  <div class="msg-step">
9137
9226
  <span class="msg-step-badge">1</span>
9138
- <span class="msg-step-text">Who would you like to message?</span>
9227
+ <span class="msg-step-text">{{ labels.t('messagingSendMessagePanel.whoWouldYouLikeTo') }}</span>
9139
9228
  </div>
9140
9229
  <ui-contact-picker
9141
- placeholder="Search by contact name..."
9230
+ [placeholder]="labels.t('messagingSendMessagePanel.searchByContactName')"
9142
9231
  (contactSelected)="onContactSelected($event)" />
9143
9232
  }
9144
9233
 
9145
9234
  @if (!loading() && hasTarget()) {
9146
9235
  @if (bulkMode()) {
9147
- <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
9148
- <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
9236
+ <div class="msg-bulk" role="group" [attr.aria-label]="labels.t('messagingSendMessagePanel.recipients')" data-testid="bulk-summary">
9237
+ <p class="msg-bulk-to" data-testid="bulk-to">{{ labels.t('messagingSendMessagePanel.toLabel') }} {{ bulkList().length }} {{ recipientWord() }}</p>
9149
9238
  <div class="msg-bulk-counts">
9150
- <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
9239
+ <ui-badge [label]="labels.t(emailRecipients().length === 1 ? 'messagingSendMessagePanel.canReceiveEmail_one' : 'messagingSendMessagePanel.canReceiveEmail_other', { count: emailRecipients().length })" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
9151
9240
  @if (textEnabled()) {
9152
- <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
9241
+ <ui-badge [label]="labels.t(textRecipients().length === 1 ? 'messagingSendMessagePanel.canReceiveText_one' : 'messagingSendMessagePanel.canReceiveText_other', { count: textRecipients().length })" [variant]="textRecipients().length ? 'blue' : 'gray'" />
9153
9242
  }
9154
9243
  </div>
9155
9244
  @if (!textEnabled()) {
9156
- <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
9245
+ <ui-hint>{{ labels.t('messagingSendMessagePanel.textMessagingIsNotEnabled') }}</ui-hint>
9157
9246
  }
9158
9247
  <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
9159
9248
  @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
@@ -9163,7 +9252,7 @@ i0.ɵɵngDeclareClassMetadata({
9163
9252
  <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
9164
9253
  }
9165
9254
  @if (bulkMissingSend()) {
9166
- <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
9255
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ labels.t('messagingSendMessagePanel.bulkNoSend') }}</ui-alert>
9167
9256
  }
9168
9257
  @if (!bulkNoneEligible()) {
9169
9258
  <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
@@ -9173,23 +9262,19 @@ i0.ɵɵngDeclareClassMetadata({
9173
9262
 
9174
9263
  @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
9175
9264
  <div class="msg-channel-row" data-testid="bulk-channel-row">
9176
- <label class="form-label msg-via-label">Send via</label>
9265
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
9177
9266
  @if (sending()) {
9178
9267
  <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
9179
9268
  <div class="msg-via-tabs">
9180
- <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
9181
- <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
9269
+ <ui-badge [label]="channel() === 'email' ? labels.t('common.email') : labels.t('common.text')" variant="blue" />
9270
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">{{ labels.t('messagingSendMessagePanel.channelLockedWhileSending') }}</span>
9182
9271
  </div>
9183
9272
  } @else {
9184
9273
  <div class="msg-via-tabs">
9185
9274
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9186
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
9187
- Email
9188
- </ui-toggle-badge>
9275
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('common.email') }}</ui-toggle-badge>
9189
9276
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9190
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
9191
- Text
9192
- </ui-toggle-badge>
9277
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('common.text') }}</ui-toggle-badge>
9193
9278
  </div>
9194
9279
  }
9195
9280
  </div>
@@ -9197,16 +9282,12 @@ i0.ɵɵngDeclareClassMetadata({
9197
9282
 
9198
9283
  @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
9199
9284
  <div class="msg-channel-row">
9200
- <label class="form-label msg-via-label">Send via</label>
9285
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
9201
9286
  <div class="msg-via-tabs">
9202
9287
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9203
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
9204
- Email
9205
- </ui-toggle-badge>
9288
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>{{ labels.t('common.email') }}</ui-toggle-badge>
9206
9289
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9207
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
9208
- Text
9209
- </ui-toggle-badge>
9290
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>{{ labels.t('common.text') }}</ui-toggle-badge>
9210
9291
  </div>
9211
9292
  </div>
9212
9293
  }
@@ -9223,8 +9304,8 @@ i0.ɵɵngDeclareClassMetadata({
9223
9304
  @for (epoch of [formEpoch()]; track epoch) {
9224
9305
  <div class="msg-job">
9225
9306
  <ui-combobox
9226
- [label]="jobLabel()" placeholder="Search by job name…" [hint]="jobHint()"
9227
- [options]="jobOptions()" [loading]="jobLoading()" noResultsText="No jobs found" [minChars]="2" [debounce]="300"
9307
+ [label]="jobLabel()" [placeholder]="labels.t('messagingSendMessagePanel.searchByJobName')" [hint]="jobHint()"
9308
+ [options]="jobOptions()" [loading]="jobLoading()" [noResultsText]="labels.t('messagingSendMessagePanel.noJobsFound')" [minChars]="2" [debounce]="300"
9228
9309
  [value]="jobValue()"
9229
9310
  (searchChange)="jobSearch$.next($event)" (valueChange)="onJobPick($event)" />
9230
9311
  </div>
@@ -9402,7 +9483,7 @@ i0.ɵɵngDeclareClassMetadata({
9402
9483
  required: false
9403
9484
  }]
9404
9485
  }],
9405
- title: [{
9486
+ titleInput: [{
9406
9487
  type: i0.Input,
9407
9488
  args: [{
9408
9489
  isSignal: true,
@@ -9466,7 +9547,7 @@ i0.ɵɵngDeclareClassMetadata({
9466
9547
  required: false
9467
9548
  }]
9468
9549
  }],
9469
- receiptLabel: [{
9550
+ receiptLabelInput: [{
9470
9551
  type: i0.Input,
9471
9552
  args: [{
9472
9553
  isSignal: true,
@@ -9722,7 +9803,7 @@ i0.ɵɵngDeclareClassMetadata({
9722
9803
  required: false
9723
9804
  }]
9724
9805
  }],
9725
- jobLabel: [{
9806
+ jobLabelInput: [{
9726
9807
  type: i0.Input,
9727
9808
  args: [{
9728
9809
  isSignal: true,