@forgeroute/ui 0.4.77 → 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
  `,
@@ -2025,6 +2065,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
2025
2065
  "legend",
2026
2066
  "orientation",
2027
2067
  "values",
2068
+ "labelLinks",
2028
2069
  "description",
2029
2070
  "indent",
2030
2071
  "required",
@@ -2156,35 +2197,34 @@ i0.ɵɵngDeclareClassMetadata({
2156
2197
  <div class="dso" role="group" [attr.aria-label]="label()">
2157
2198
  @if (available()) {
2158
2199
  @if (locked()) {
2159
- <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>
2160
2201
  @if (envelopeStatus() === 'loading') {
2161
2202
  <div role="status"><ui-hint data-testid="envelope-loading">{{ envelopeReadMessage() }}</ui-hint></div>
2162
2203
  } @else if (envelopeStatus() === 'error') {
2163
2204
  <div class="dso-retry">
2164
2205
  <ui-error errorId="dso-envelope-error" [message]="envelopeReadMessage()" />
2165
- <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>
2166
2207
  </div>
2167
2208
  }
2168
2209
  } @else {
2169
2210
  <ui-combobox
2170
2211
  [label]="label()"
2171
2212
  [required]="required()"
2172
- placeholder="Search your documents by name…"
2213
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchYourDocumentsByName')"
2173
2214
  [options]="options()"
2174
2215
  [loading]="loading()"
2175
2216
  [minChars]="2"
2176
- [noResultsText]="searchFailed() ? 'The search failed' : 'No matching documents'"
2177
- [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') : ''"
2178
2219
  [error]="searchFailed() ? searchFailedMessage : ''"
2179
2220
  [showErrors]="searchFailed()"
2180
2221
  (searchChange)="search$.next($event)"
2181
2222
  (valueChange)="onPick($event)" />
2182
- <ui-error errorId="dso-search-error" [message]="searchFailed() ? 'Couldn\\'t search your documents. Check your connection and try again.' : ''" />
2183
2223
  }
2184
2224
 
2185
2225
  @if (selected().length > 0) {
2186
2226
  <!-- One row per document: the chip (or the plain title when the list is fixed) and, beside it, its small Preview button. -->
2187
- <div class="dso-docs" role="group" aria-label="Chosen documents">
2227
+ <div class="dso-docs" role="group" [attr.aria-label]="labels.t('messagingDocumentSendOptions.chosenDocuments')">
2188
2228
  @for (d of selected(); track d.id) {
2189
2229
  <div class="dso-doc">
2190
2230
  @if (locked()) {
@@ -2196,8 +2236,8 @@ i0.ɵɵngDeclareClassMetadata({
2196
2236
  @if (documentPreview()) {
2197
2237
  <ui-button
2198
2238
  variant="secondary" size="sm"
2199
- [ariaLabel]="selected().length === 1 ? undefined : 'Preview ' + d.title"
2200
- (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>
2201
2241
  }
2202
2242
  </div>
2203
2243
  }
@@ -2212,30 +2252,30 @@ i0.ɵɵngDeclareClassMetadata({
2212
2252
  } @else if (signerInfo()?.status === 'error') {
2213
2253
  <div class="dso-retry">
2214
2254
  <ui-error errorId="dso-roles-error" [message]="signerReadMessage()" />
2215
- <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>
2216
2256
  </div>
2217
2257
  }
2218
2258
 
2219
2259
  @if (providerChoiceShown()) {
2220
2260
  <ui-radio-group
2221
- label="Signature provider"
2261
+ [label]="labels.t('messagingDocumentSendOptions.signatureProvider')"
2222
2262
  orientation="horizontal"
2223
- [options]="providerOptions"
2263
+ [options]="providerOptions()"
2224
2264
  [ngModel]="provider()"
2225
2265
  (ngModelChange)="setProvider($event)" />
2226
2266
  }
2227
2267
 
2228
2268
  @if (provider() === 'DOCUSIGN') {
2229
- <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>
2230
2270
  @if (showCountersign()) {
2231
2271
  <ui-checkbox
2232
- label="Require my countersignature after client signs"
2272
+ [label]="labels.t('messagingDocumentSendOptions.requireMyCountersignatureAfterClient')"
2233
2273
  [ngModel]="countersignChoice()"
2234
2274
  (ngModelChange)="countersignChoice.set(!!$event)" />
2235
2275
  }
2236
2276
  } @else if (channel() === 'email' && selected().length > 0 && purposeOptions().length > 1) {
2237
2277
  <ui-select
2238
- label="What should the recipient do?"
2278
+ [label]="labels.t('messagingDocumentSendOptions.whatShouldTheRecipientDo')"
2239
2279
  [options]="purposeOptions()"
2240
2280
  [ngModel]="purpose()"
2241
2281
  (ngModelChange)="setPurpose($event)" />
@@ -2251,21 +2291,21 @@ i0.ɵɵngDeclareClassMetadata({
2251
2291
  <ui-button
2252
2292
  variant="ghost" size="xs"
2253
2293
  [ariaExpanded]="signingExpanded()"
2254
- [ariaLabel]="signingExpanded() ? 'Hide signing options' : 'Change signing options'"
2255
- (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>
2256
2296
  </div>
2257
2297
  @if (signingExpanded()) {
2258
2298
  <ui-form-section>
2259
2299
  <!-- One column: three labelled number fields do not fit side by side in a 480px panel or on a phone. -->
2260
2300
  <ui-form-row [cols]="1">
2261
2301
  <ui-text-input
2262
- 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"
2263
2303
  [showErrors]="showErrors()" [ngModel]="expiryDays()" (ngModelChange)="expiryDays.set(asText($event))" />
2264
2304
  <ui-text-input
2265
- 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"
2266
2306
  [showErrors]="showErrors()" [ngModel]="reminderIntervalDays()" (ngModelChange)="reminderIntervalDays.set(asText($event))" />
2267
2307
  <ui-text-input
2268
- 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"
2269
2309
  [showErrors]="showErrors()" [ngModel]="maxReminders()" (ngModelChange)="maxReminders.set(asText($event))" />
2270
2310
  </ui-form-row>
2271
2311
  </ui-form-section>
@@ -2273,7 +2313,7 @@ i0.ɵɵngDeclareClassMetadata({
2273
2313
  </div>
2274
2314
  }
2275
2315
  } @else {
2276
- <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>
2277
2317
  }
2278
2318
  </div>
2279
2319
  `,
@@ -2290,7 +2330,7 @@ i0.ɵɵngDeclareClassMetadata({
2290
2330
  required: false
2291
2331
  }]
2292
2332
  }],
2293
- label: [{
2333
+ labelInput: [{
2294
2334
  type: i0.Input,
2295
2335
  args: [{
2296
2336
  isSignal: true,
@@ -2402,6 +2442,7 @@ i0.ɵɵngDeclareClassMetadata({
2402
2442
  });
2403
2443
  var DocumentSignersComponent = class DocumentSignersComponent {
2404
2444
  constructor() {
2445
+ this.labels = inject(UiLabelsService);
2405
2446
  this.info = input(null, ...ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []);
2406
2447
  this.config = input(null, ...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
2407
2448
  this.recipient = input({
@@ -2442,7 +2483,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2442
2483
  return countersignPlaceLabel(title);
2443
2484
  }
2444
2485
  placeText(title) {
2445
- return this.placeDocs().length > 1 ? countersignPlaceLabel(title) : "Place your countersignature box";
2486
+ return this.placeDocs().length > 1 ? countersignPlaceLabel(title) : this.labels.t("messagingDocumentSendOptions.placeCountersign");
2446
2487
  }
2447
2488
  label(role) {
2448
2489
  return signerRoleLabel(role, this.config());
@@ -2523,7 +2564,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2523
2564
  ngImport: i0,
2524
2565
  template: `
2525
2566
  @if (rowsShown()) {
2526
- <ui-form-section label="Signers" data-testid="signers-section">
2567
+ <ui-form-section [label]="labels.t('messagingDocumentSendOptions.signers')" data-testid="signers-section">
2527
2568
  @for (role of info()!.roles; track role) {
2528
2569
  @if (role === 'signer1') {
2529
2570
  <div class="ds-row" [attr.data-testid]="'signer-row-' + role">
@@ -2538,7 +2579,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2538
2579
  [required]="true"
2539
2580
  [showErrors]="showErrors()"
2540
2581
  [errorId]="'ds-error-' + role"
2541
- placeholder="Search for a contact…"
2582
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchForAContact')"
2542
2583
  (contactSelected)="pick(role, $event)" />
2543
2584
  <!-- The picker's own "<Role> is required." (shown after blur or with showErrors), else the contact problem (no email / twice). -->
2544
2585
  <div class="ds-error" [attr.id]="'ds-error-' + role">
@@ -2547,7 +2588,7 @@ var DocumentSignersComponent = class DocumentSignersComponent {
2547
2588
  </div>
2548
2589
  }
2549
2590
  }
2550
- <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>
2551
2592
  </ui-form-section>
2552
2593
  }
2553
2594
  @if (line()) {
@@ -2672,7 +2713,7 @@ i0.ɵɵngDeclareClassMetadata({
2672
2713
  }],
2673
2714
  template: `
2674
2715
  @if (rowsShown()) {
2675
- <ui-form-section label="Signers" data-testid="signers-section">
2716
+ <ui-form-section [label]="labels.t('messagingDocumentSendOptions.signers')" data-testid="signers-section">
2676
2717
  @for (role of info()!.roles; track role) {
2677
2718
  @if (role === 'signer1') {
2678
2719
  <div class="ds-row" [attr.data-testid]="'signer-row-' + role">
@@ -2687,7 +2728,7 @@ i0.ɵɵngDeclareClassMetadata({
2687
2728
  [required]="true"
2688
2729
  [showErrors]="showErrors()"
2689
2730
  [errorId]="'ds-error-' + role"
2690
- placeholder="Search for a contact…"
2731
+ [placeholder]="labels.t('messagingDocumentSendOptions.searchForAContact')"
2691
2732
  (contactSelected)="pick(role, $event)" />
2692
2733
  <!-- The picker's own "<Role> is required." (shown after blur or with showErrors), else the contact problem (no email / twice). -->
2693
2734
  <div class="ds-error" [attr.id]="'ds-error-' + role">
@@ -2696,7 +2737,7 @@ i0.ɵɵngDeclareClassMetadata({
2696
2737
  </div>
2697
2738
  }
2698
2739
  }
2699
- <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>
2700
2741
  </ui-form-section>
2701
2742
  }
2702
2743
  @if (line()) {
@@ -2941,6 +2982,10 @@ i0.ɵɵngDeclareClassMetadata({
2941
2982
  }]
2942
2983
  }
2943
2984
  });
2985
+ const SPANISH_VIDEO_LINK_WORDS = {
2986
+ watchVideo: "Ver video",
2987
+ watchNamed: "Ver \"{{ title }}\": {{ link }}"
2988
+ };
2944
2989
  function buildVideoLink(baseLink, options = {}) {
2945
2990
  const params = [];
2946
2991
  if (options.autoplay) params.push("autoplay=1");
@@ -2961,63 +3006,110 @@ function videoLinkWithDefaults(baseLink, defaults = {}, contactId = "") {
2961
3006
  hideReactions: !!defaults.hideReactions
2962
3007
  });
2963
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
+ }
2964
3019
  function stripVideoLink(text) {
2965
- 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();
2966
3021
  }
2967
3022
  function appendVideoLink(text, link) {
2968
3023
  const base = stripVideoLink(text);
2969
- 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;
2970
3026
  }
2971
3027
  function videoMessageLine(title, link) {
2972
- 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));
2973
3065
  }
2974
3066
  function applyVideoLine(body, prev, next) {
2975
3067
  if (!prev) {
2976
3068
  if (!next) return body;
2977
3069
  return body.trim() ? `${body}\n\n${next}` : next;
2978
3070
  }
2979
- const at = body.indexOf(prev);
2980
- if (at < 0) return body;
2981
- 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);
2982
3075
  const before = body.slice(0, at);
2983
- const after = body.slice(at + prev.length);
3076
+ const after = body.slice(at + found.length);
2984
3077
  const trimmedBefore = before.replace(/\n{1,2}$/, "");
2985
3078
  return (trimmedBefore + after).trim() === "" ? "" : trimmedBefore + after;
2986
3079
  }
2987
- const VIDEO_PLAYBACK_OPTIONS = [{
2988
- value: "click",
2989
- label: "Click to Play"
2990
- }, {
2991
- value: "autoplay",
2992
- label: "Autoplay"
2993
- }];
2994
- const VIDEO_CTA_OPTIONS = [
2995
- {
2996
- value: "",
2997
- label: "None (use video default)"
2998
- },
2999
- {
3000
- value: "schedule",
3001
- label: "Schedule a Call"
3002
- },
3003
- {
3004
- value: "contact",
3005
- label: "Contact Me"
3006
- },
3007
- {
3008
- value: "custom",
3009
- label: "Custom"
3010
- }
3011
- ];
3012
3080
  var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3013
3081
  constructor() {
3082
+ this.labels = inject(UiLabelsService);
3014
3083
  this.video = input(null, ...ngDevMode ? [{ debugName: "video" }] : /* istanbul ignore next */ []);
3015
3084
  this.defaults = input({}, ...ngDevMode ? [{ debugName: "defaults" }] : /* istanbul ignore next */ []);
3016
3085
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
3017
3086
  this.linkChange = output();
3018
3087
  this.removed = output();
3019
- this.playbackOptions = VIDEO_PLAYBACK_OPTIONS;
3020
- 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 */ []);
3021
3113
  this.autoplay = signal("click", ...ngDevMode ? [{ debugName: "autoplay" }] : /* istanbul ignore next */ []);
3022
3114
  this.ctaType = signal("", ...ngDevMode ? [{ debugName: "ctaType" }] : /* istanbul ignore next */ []);
3023
3115
  this.ctaUrl = signal("", ...ngDevMode ? [{ debugName: "ctaUrl" }] : /* istanbul ignore next */ []);
@@ -3126,40 +3218,40 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3126
3218
  <ui-form-section class="vls">
3127
3219
  <div class="vls-bar">
3128
3220
  @if (v.shareLink) {
3129
- <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>
3130
3222
  }
3131
- <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">Video settings</ui-button>
3132
- <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>
3133
3225
  </div>
3134
3226
  <!-- Collapsed by default: the defaults from Communication settings are already applied. -->
3135
3227
  @if (settingsOpen()) {
3136
3228
  <ui-radio-group
3137
- label="Playback"
3229
+ [label]="labels.t('messagingVideoLinkSettings.playback')"
3138
3230
  orientation="horizontal"
3139
- [options]="playbackOptions"
3231
+ [options]="playbackOptions()"
3140
3232
  [ngModel]="autoplay()"
3141
3233
  (ngModelChange)="setAutoplay($event)" />
3142
3234
  <ui-select
3143
- label="Call-to-Action"
3144
- [options]="ctaOptions"
3235
+ [label]="labels.t('messagingVideoLinkSettings.callToAction')"
3236
+ [options]="ctaOptions()"
3145
3237
  [ngModel]="ctaType()"
3146
3238
  (ngModelChange)="setCtaType($event)" />
3147
3239
  @if (ctaType()) {
3148
3240
  <ui-text-input
3149
- label="Call-to-action URL"
3241
+ [label]="labels.t('messagingVideoLinkSettings.callToActionUrl')"
3150
3242
  type="url"
3151
3243
  placeholder="https://..."
3152
3244
  [maxlength]="500"
3153
3245
  [ngModel]="ctaUrl()"
3154
3246
  (ngModelChange)="setCtaUrl($event)" />
3155
3247
  <ui-text-input
3156
- label="Call-to-action label"
3157
- placeholder="Button label (e.g. Book a Call)"
3248
+ [label]="labels.t('messagingVideoLinkSettings.callToActionLabel')"
3249
+ [placeholder]="labels.t('messagingVideoLinkSettings.buttonLabelEGBook')"
3158
3250
  [maxlength]="100"
3159
3251
  [ngModel]="ctaLabel()"
3160
3252
  (ngModelChange)="setCtaLabel($event)" />
3161
3253
  }
3162
- <ui-checkbox label="Hide reactions" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3254
+ <ui-checkbox [label]="labels.t('messagingVideoLinkSettings.hideReactions')" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3163
3255
  }
3164
3256
  </ui-form-section>
3165
3257
  }
@@ -3241,6 +3333,7 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3241
3333
  "legend",
3242
3334
  "orientation",
3243
3335
  "values",
3336
+ "labelLinks",
3244
3337
  "description",
3245
3338
  "indent",
3246
3339
  "required",
@@ -3379,40 +3472,40 @@ i0.ɵɵngDeclareClassMetadata({
3379
3472
  <ui-form-section class="vls">
3380
3473
  <div class="vls-bar">
3381
3474
  @if (v.shareLink) {
3382
- <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>
3383
3476
  }
3384
- <ui-button variant="ghost" size="xs" [ariaExpanded]="settingsOpen()" (click)="toggleSettings()">Video settings</ui-button>
3385
- <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>
3386
3479
  </div>
3387
3480
  <!-- Collapsed by default: the defaults from Communication settings are already applied. -->
3388
3481
  @if (settingsOpen()) {
3389
3482
  <ui-radio-group
3390
- label="Playback"
3483
+ [label]="labels.t('messagingVideoLinkSettings.playback')"
3391
3484
  orientation="horizontal"
3392
- [options]="playbackOptions"
3485
+ [options]="playbackOptions()"
3393
3486
  [ngModel]="autoplay()"
3394
3487
  (ngModelChange)="setAutoplay($event)" />
3395
3488
  <ui-select
3396
- label="Call-to-Action"
3397
- [options]="ctaOptions"
3489
+ [label]="labels.t('messagingVideoLinkSettings.callToAction')"
3490
+ [options]="ctaOptions()"
3398
3491
  [ngModel]="ctaType()"
3399
3492
  (ngModelChange)="setCtaType($event)" />
3400
3493
  @if (ctaType()) {
3401
3494
  <ui-text-input
3402
- label="Call-to-action URL"
3495
+ [label]="labels.t('messagingVideoLinkSettings.callToActionUrl')"
3403
3496
  type="url"
3404
3497
  placeholder="https://..."
3405
3498
  [maxlength]="500"
3406
3499
  [ngModel]="ctaUrl()"
3407
3500
  (ngModelChange)="setCtaUrl($event)" />
3408
3501
  <ui-text-input
3409
- label="Call-to-action label"
3410
- placeholder="Button label (e.g. Book a Call)"
3502
+ [label]="labels.t('messagingVideoLinkSettings.callToActionLabel')"
3503
+ [placeholder]="labels.t('messagingVideoLinkSettings.buttonLabelEGBook')"
3411
3504
  [maxlength]="100"
3412
3505
  [ngModel]="ctaLabel()"
3413
3506
  (ngModelChange)="setCtaLabel($event)" />
3414
3507
  }
3415
- <ui-checkbox label="Hide reactions" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3508
+ <ui-checkbox [label]="labels.t('messagingVideoLinkSettings.hideReactions')" [ngModel]="hideReactions()" (ngModelChange)="setHideReactions($event)" />
3416
3509
  }
3417
3510
  </ui-form-section>
3418
3511
  }
@@ -3476,10 +3569,16 @@ function buildDocumentsPayload(v, delivery, purpose = v.purpose, extras = {}) {
3476
3569
  const DEFAULT_DOCUMENT_SUBJECT = "Document Signature Required";
3477
3570
  function suggestDocumentSubject(titles, purpose, informational = false) {
3478
3571
  if (titles.length === 0) return "";
3479
- if (informational && purpose !== "ATTACH") return titles.length === 1 ? `Document shared: ${titles[0]}` : `${titles.length} documents shared: ${titles[0]}…`;
3480
- const attachOnly = purpose === "ATTACH";
3481
- if (titles.length === 1) return `${attachOnly ? "Document attached" : "Signature required"}: ${titles[0]}`;
3482
- 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
+ });
3483
3582
  }
3484
3583
  const MAX_CC = 10;
3485
3584
  function parseCcList(raw) {
@@ -3496,9 +3595,9 @@ function parseCcList(raw) {
3496
3595
  const CC_EMAIL_RE = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/;
3497
3596
  function ccError(list) {
3498
3597
  const bad = list.find((a) => a.length > 254 || !CC_EMAIL_RE.test(a));
3499
- if (bad) return `"${bad}" isn't a valid CC email address.`;
3500
- if (list.length > 10) return `You can copy at most 10 people.`;
3501
- 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");
3502
3601
  return "";
3503
3602
  }
3504
3603
  function signingBodyFields(d) {
@@ -3553,13 +3652,14 @@ function nonBlankValues(...sources) {
3553
3652
  return out;
3554
3653
  }
3555
3654
  function dateVars(now = /* @__PURE__ */ new Date()) {
3556
- const long = now.toLocaleDateString("en-US", {
3655
+ const locale = uiLocale();
3656
+ const long = now.toLocaleDateString(locale, {
3557
3657
  month: "long",
3558
3658
  day: "numeric",
3559
3659
  year: "numeric"
3560
3660
  });
3561
3661
  return {
3562
- month: now.toLocaleString("en-US", { month: "long" }),
3662
+ month: now.toLocaleString(locale, { month: "long" }),
3563
3663
  year: now.getFullYear().toString(),
3564
3664
  date: long,
3565
3665
  today: long
@@ -3591,6 +3691,9 @@ var SendEmailComponent = class SendEmailComponent {
3591
3691
  get deferredKeys() {
3592
3692
  return this.bulk() ? PERSONAL_TAG_KEYS : [];
3593
3693
  }
3694
+ get EMAIL_DISABLED_MESSAGE() {
3695
+ return this.labels.t("messagingSendMessagePanel.emailDisabled");
3696
+ }
3594
3697
  ngOnInit() {
3595
3698
  this.includeReceipt = this.receiptDefault();
3596
3699
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
@@ -3599,6 +3702,7 @@ var SendEmailComponent = class SendEmailComponent {
3599
3702
  } });
3600
3703
  }
3601
3704
  constructor() {
3705
+ this.labels = inject(UiLabelsService);
3602
3706
  this.destroyRef = inject(DestroyRef);
3603
3707
  this.http = inject(HttpClient);
3604
3708
  this.apiBase = inject(FORGEROUTE_CONFIG, { optional: true })?.baseUrl ?? "/api";
@@ -3615,7 +3719,11 @@ var SendEmailComponent = class SendEmailComponent {
3615
3719
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
3616
3720
  this.sendFn = input(null, ...ngDevMode ? [{ debugName: "sendFn" }] : /* istanbul ignore next */ []);
3617
3721
  this.showReceiptOption = input(false, ...ngDevMode ? [{ debugName: "showReceiptOption" }] : /* istanbul ignore next */ []);
3618
- 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 */ []);
3619
3727
  this.receiptDefault = input(false, ...ngDevMode ? [{ debugName: "receiptDefault" }] : /* istanbul ignore next */ []);
3620
3728
  this.documents = input(false, ...ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []);
3621
3729
  this.documentPurposes = input([
@@ -3660,7 +3768,6 @@ var SendEmailComponent = class SendEmailComponent {
3660
3768
  this.validChange = output();
3661
3769
  this.rejected = output();
3662
3770
  this.mapFieldsClick = output();
3663
- this.EMAIL_DISABLED_MESSAGE = "Email sending is disabled, so this can't be sent. Enable it in Communication Settings.";
3664
3771
  this.mode = "compose";
3665
3772
  this.subject = "";
3666
3773
  this.body = "";
@@ -3683,13 +3790,13 @@ var SendEmailComponent = class SendEmailComponent {
3683
3790
  this.signersInvalid = false;
3684
3791
  this.mergeWarnings = [];
3685
3792
  this.savingEmail = signal(false, ...ngDevMode ? [{ debugName: "savingEmail" }] : /* istanbul ignore next */ []);
3686
- this.modeTabs = [{
3793
+ this.modeTabs = computed(() => [{
3687
3794
  key: "compose",
3688
- label: "Create Message"
3795
+ label: this.labels.t("messagingSendMessagePanel.tabCompose")
3689
3796
  }, {
3690
3797
  key: "template",
3691
- label: "Use Existing Template"
3692
- }];
3798
+ label: this.labels.t("messagingSendMessagePanel.tabTemplate")
3799
+ }], ...ngDevMode ? [{ debugName: "modeTabs" }] : /* istanbul ignore next */ []);
3693
3800
  this.marketVars = {};
3694
3801
  this._appliedDefaults = {
3695
3802
  subject: "",
@@ -3803,7 +3910,7 @@ var SendEmailComponent = class SendEmailComponent {
3803
3910
  });
3804
3911
  if (this.hasDocuments && this.signerInfo?.multiDocumentNote) out.push({
3805
3912
  key: "multi",
3806
- text: "Documents with several signers are sent one at a time.",
3913
+ text: this.labels.t("messagingSendMessagePanel.multiSignerOneAtATime"),
3807
3914
  testId: "documents-multi-signer-note"
3808
3915
  });
3809
3916
  if (this.hasDocuments) this.mergeWarnings.forEach((text, i) => out.push({
@@ -3827,8 +3934,8 @@ var SendEmailComponent = class SendEmailComponent {
3827
3934
  }
3828
3935
  this.syncSuggestedSubject();
3829
3936
  if (this.hasDocuments) {
3830
- const removed = [this.attachments.length > 0 ? "your attachments" : "", this.showSchedule ? "the schedule" : ""].filter(Boolean);
3831
- 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")} `) });
3832
3939
  this.attachments = [];
3833
3940
  this.showSchedule = false;
3834
3941
  this.scheduledAt = "";
@@ -3840,10 +3947,16 @@ var SendEmailComponent = class SendEmailComponent {
3840
3947
  this.notifyValid();
3841
3948
  }
3842
3949
  get bodyLengthError() {
3843
- 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
+ }) : "";
3844
3954
  }
3845
3955
  get subjectLengthError() {
3846
- 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
+ }) : "";
3847
3960
  }
3848
3961
  get sendLengthError() {
3849
3962
  return this.subjectLengthError || (this.mode === "compose" ? "" : this.bodyLengthError);
@@ -3905,7 +4018,7 @@ var SendEmailComponent = class SendEmailComponent {
3905
4018
  }
3906
4019
  get composeTagError() {
3907
4020
  if (this.mode !== "compose") return "";
3908
- 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") : "";
3909
4022
  }
3910
4023
  get unresolvedVars() {
3911
4024
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -4005,7 +4118,7 @@ var SendEmailComponent = class SendEmailComponent {
4005
4118
  const reader = new FileReader();
4006
4119
  reader.onload = () => {
4007
4120
  const base64 = reader.result.split(",")[1];
4008
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
4121
+ const sizeLabel = formatAttachmentSize(file.size);
4009
4122
  this.attachments = [...this.attachments, {
4010
4123
  filename: file.name,
4011
4124
  content: base64,
@@ -4024,7 +4137,7 @@ var SendEmailComponent = class SendEmailComponent {
4024
4137
  insertVideo(v) {
4025
4138
  if (!v.shareLink) return;
4026
4139
  const sep = this.body.trim() ? "\n\n" : "";
4027
- this.body = this.body + sep + `Watch "${v.title}": ${v.shareLink}`;
4140
+ this.body = this.body + sep + videoMessageLine(v.title, v.shareLink);
4028
4141
  this.notifyValid();
4029
4142
  }
4030
4143
  docuSignBody(docs, payload) {
@@ -4069,7 +4182,7 @@ var SendEmailComponent = class SendEmailComponent {
4069
4182
  submit() {
4070
4183
  if (!this.canSend || this.loading || !this.bulk() && !this.contactEmail()) return;
4071
4184
  if (this.bulk() && !this.sendFn()) {
4072
- this.toast.error(BULK_NO_SEND_MESSAGE);
4185
+ this.toast.error(this.labels.t("messagingSendMessagePanel.bulkNoSend"));
4073
4186
  return;
4074
4187
  }
4075
4188
  if (this.mode === "template") this.applyPendingVars();
@@ -4146,7 +4259,7 @@ var SendEmailComponent = class SendEmailComponent {
4146
4259
  });
4147
4260
  const onError = (err) => {
4148
4261
  if (this.bulk() && (destroyed || !this.loading)) return;
4149
- 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"));
4150
4263
  this.loading = false;
4151
4264
  this.cdr.markForCheck();
4152
4265
  if (this.bulk()) this.bulkSending.emit(null);
@@ -4162,7 +4275,7 @@ var SendEmailComponent = class SendEmailComponent {
4162
4275
  provider: docs.provider,
4163
4276
  informational: docs.signerInfo?.informational === true
4164
4277
  }) : void 0;
4165
- 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")));
4166
4279
  this.loading = false;
4167
4280
  this.attachments = [];
4168
4281
  this.includeReceipt = this.receiptDefault();
@@ -4267,8 +4380,8 @@ var SendEmailComponent = class SendEmailComponent {
4267
4380
  isRequired: false,
4268
4381
  transformFunction: null
4269
4382
  },
4270
- receiptLabel: {
4271
- classPropertyName: "receiptLabel",
4383
+ receiptLabelInput: {
4384
+ classPropertyName: "receiptLabelInput",
4272
4385
  publicName: "receiptLabel",
4273
4386
  isSignal: true,
4274
4387
  isRequired: false,
@@ -4443,25 +4556,21 @@ var SendEmailComponent = class SendEmailComponent {
4443
4556
  <!-- Missing email guard -->
4444
4557
  @if (!bulk() && !contactEmail()) {
4445
4558
  <div class="sm-missing-banner">
4446
- <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>
4447
- No email address on file for this contact.
4448
- </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>
4449
4560
  <ui-text-input
4450
- label="Add Email Address"
4561
+ [label]="labels.t('messagingSendMessagePanel.addEmailAddress')"
4451
4562
  type="email"
4452
4563
  [required]="true"
4453
4564
  placeholder="email@example.com"
4454
4565
  [(ngModel)]="pendingEmail"
4455
4566
  (ngModelChange)="notifyValid()"
4456
- [error]="pendingEmail && !pendingEmailValid ? 'Please enter a valid email address.' : ''" />
4457
- <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">
4458
- Save &amp; Continue
4459
- </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>
4460
4569
  } @else {
4461
4570
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
4462
4571
  @if (!bulk()) {
4463
4572
  <div class="sm-group">
4464
- <span class="sm-group-label">To</span>
4573
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
4465
4574
  <div class="sm-to-card">
4466
4575
  <span class="sm-to-name">{{ contactName() }}</span>
4467
4576
  <span class="sm-to-detail">{{ contactEmail() }}</span>
@@ -4489,7 +4598,7 @@ var SendEmailComponent = class SendEmailComponent {
4489
4598
  <ui-text-input
4490
4599
  label="CC"
4491
4600
  placeholder="name@example.com, other@example.com"
4492
- hint="Separate addresses with commas. Everyone listed receives a copy."
4601
+ [hint]="labels.t('messagingSendMessagePanel.separateAddressesWithCommasEveryone')"
4493
4602
  [(ngModel)]="ccText"
4494
4603
  [error]="ccError"
4495
4604
  (ngModelChange)="notifyValid()" />
@@ -4498,24 +4607,24 @@ var SendEmailComponent = class SendEmailComponent {
4498
4607
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
4499
4608
  @if (showTemplates()) {
4500
4609
  <div class="sm-mode-row">
4501
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
4610
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
4502
4611
  </div>
4503
4612
  }
4504
4613
 
4505
4614
  @if (mode === 'compose') {
4506
4615
  @if (showSubject()) {
4507
4616
  <ui-text-input
4508
- label="Subject"
4617
+ [label]="labels.t('common.subject')"
4509
4618
  [required]="!viaDocuSign"
4510
- placeholder="Email subject..."
4619
+ [placeholder]="labels.t('messagingSendMessagePanel.emailSubject')"
4511
4620
  [maxlength]="MAX_SUBJECT"
4512
4621
  [(ngModel)]="subject"
4513
4622
  (ngModelChange)="notifyValid()" />
4514
4623
  }
4515
4624
  <ui-textarea-input
4516
- label="Message"
4625
+ [label]="labels.t('common.message')"
4517
4626
  [required]="!viaDocuSign"
4518
- placeholder="Write your message..."
4627
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
4519
4628
  [rows]="8"
4520
4629
  [(ngModel)]="body"
4521
4630
  [error]="composeTagError || bodyLengthError"
@@ -4523,7 +4632,7 @@ var SendEmailComponent = class SendEmailComponent {
4523
4632
 
4524
4633
  @if (!hasDocuments && showAttachments()) {
4525
4634
  <div class="sm-field">
4526
- <label class="sm-field-label">Attachments</label>
4635
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</label>
4527
4636
  @if (attachments.length > 0) {
4528
4637
  <div class="attach-chips">
4529
4638
  @for (a of attachments; track a.filename; let i = $index) {
@@ -4531,25 +4640,23 @@ var SendEmailComponent = class SendEmailComponent {
4531
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>
4532
4641
  <span class="attach-name">{{ a.filename }}</span>
4533
4642
  <span class="attach-size">{{ a.sizeLabel }}</span>
4534
- <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>
4535
4644
  </div>
4536
4645
  }
4537
4646
  </div>
4538
4647
  }
4539
4648
  <input #emailFileInput type="file" multiple hidden (change)="onFilesSelected($event)" />
4540
4649
  <button type="button" class="attach-add-btn" (click)="emailFileInput.click()">
4541
- <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>
4542
- Attach Files
4543
- </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>
4544
4651
  </div>
4545
4652
 
4546
4653
  }
4547
4654
 
4548
4655
  @if (!hasDocuments && showScheduling()) {
4549
- <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()" />
4550
4657
  @if (showSchedule) {
4551
4658
  <ui-datetime-input
4552
- label="Send at"
4659
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
4553
4660
  [min]="minScheduleTime"
4554
4661
  [(ngModel)]="scheduledAt"
4555
4662
  (ngModelChange)="notifyValid()" />
@@ -4559,14 +4666,12 @@ var SendEmailComponent = class SendEmailComponent {
4559
4666
  @if (missingVars.length > 0) {
4560
4667
  <div class="mv-section">
4561
4668
  <p class="mv-hint">
4562
- <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>
4563
- This template has fields that need values before sending.
4564
- </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>
4565
4670
  @for (key of missingVars; track key) {
4566
4671
  <div class="mv-row">
4567
4672
  <div class="mv-input-wrap">
4568
- <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)" />
4569
- <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')">
4570
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>
4571
4676
  </ui-button>
4572
4677
  </div>
@@ -4577,22 +4682,18 @@ var SendEmailComponent = class SendEmailComponent {
4577
4682
 
4578
4683
  <div class="tpl-preview-header">
4579
4684
  <ui-button variant="ghost" size="sm" (click)="clearTemplate()">
4580
- <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>
4581
- Back to templates
4582
- </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>
4583
4686
  <div class="tpl-view-toggle">
4584
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">Preview</ui-button>
4585
- <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>
4586
4689
  </div>
4587
4690
  </div>
4588
4691
 
4589
4692
  <div class="tpl-subject-card">
4590
4693
  <div class="tpl-subject-card-top">
4591
- <span class="tpl-subject-label">SUBJECT</span>
4694
+ <span class="tpl-subject-label">{{ labels.t('messagingSendMessagePanel.subject') }}</span>
4592
4695
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
4593
- <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>
4594
- View Send Preview
4595
- </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>
4596
4697
  </div>
4597
4698
  <span class="tpl-subject-text">{{ resolvedSubject }}</span>
4598
4699
  </div>
@@ -4600,9 +4701,9 @@ var SendEmailComponent = class SendEmailComponent {
4600
4701
  <div class="tpl-body-preview" [innerHTML]="previewBodyHtml"></div>
4601
4702
  } @else {
4602
4703
  <!-- Raw: the template source exactly as written, {{tags}} included — subject first, then the body. -->
4603
- <div class="tpl-raw-label">Subject</div>
4704
+ <div class="tpl-raw-label">{{ labels.t('common.subject') }}</div>
4604
4705
  <pre class="tpl-subject-raw">{{ subject }}</pre>
4605
- <div class="tpl-raw-label">Body</div>
4706
+ <div class="tpl-raw-label">{{ labels.t('messagingSendMessagePanel.body') }}</div>
4606
4707
  <pre class="tpl-body-raw">{{ body }}</pre>
4607
4708
  }
4608
4709
  } @else {
@@ -4639,10 +4740,10 @@ var SendEmailComponent = class SendEmailComponent {
4639
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. -->
4640
4741
  <div class="sm-docs-live" role="status" aria-live="polite">
4641
4742
  @if (hasDocuments) {
4642
- <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>
4643
4744
  }
4644
4745
  @if (warningLines.length > 0) {
4645
- <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">
4646
4747
  <ul class="sm-warn-list">
4647
4748
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
4648
4749
  </ul>
@@ -4665,7 +4766,7 @@ var SendEmailComponent = class SendEmailComponent {
4665
4766
 
4666
4767
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
4667
4768
  @if (mode === 'compose' && showVideo()) {
4668
- <ui-form-section label="Video" [divider]="true">
4769
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
4669
4770
  @for (k of [videoPickerKey]; track k) {
4670
4771
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
4671
4772
  }
@@ -4687,20 +4788,18 @@ var SendEmailComponent = class SendEmailComponent {
4687
4788
  <div class="sp-modal" (click)="$event.stopPropagation()">
4688
4789
  <div class="sp-modal-header">
4689
4790
  <div class="sp-modal-title">
4690
- <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>
4691
- Email Preview
4692
- </div>
4693
- <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')">
4694
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>
4695
4794
  </ui-button>
4696
4795
  </div>
4697
4796
  <div class="sp-email-meta">
4698
4797
  <div class="sp-meta-row">
4699
- <span class="sp-meta-label">To</span>
4700
- <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>
4701
4800
  </div>
4702
4801
  <div class="sp-meta-row">
4703
- <span class="sp-meta-label">Subject</span>
4802
+ <span class="sp-meta-label">{{ labels.t('common.subject') }}</span>
4704
4803
  <span class="sp-meta-value sp-meta-subject">{{ sendPreviewSubject }}</span>
4705
4804
  </div>
4706
4805
  </div>
@@ -4792,6 +4891,7 @@ var SendEmailComponent = class SendEmailComponent {
4792
4891
  "legend",
4793
4892
  "orientation",
4794
4893
  "values",
4894
+ "labelLinks",
4795
4895
  "description",
4796
4896
  "indent",
4797
4897
  "required",
@@ -5065,25 +5165,21 @@ i0.ɵɵngDeclareClassMetadata({
5065
5165
  <!-- Missing email guard -->
5066
5166
  @if (!bulk() && !contactEmail()) {
5067
5167
  <div class="sm-missing-banner">
5068
- <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>
5069
- No email address on file for this contact.
5070
- </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>
5071
5169
  <ui-text-input
5072
- label="Add Email Address"
5170
+ [label]="labels.t('messagingSendMessagePanel.addEmailAddress')"
5073
5171
  type="email"
5074
5172
  [required]="true"
5075
5173
  placeholder="email@example.com"
5076
5174
  [(ngModel)]="pendingEmail"
5077
5175
  (ngModelChange)="notifyValid()"
5078
- [error]="pendingEmail && !pendingEmailValid ? 'Please enter a valid email address.' : ''" />
5079
- <ui-button variant="primary" [disabled]="!pendingEmailValid || savingEmail()" [loading]="savingEmail()" (click)="saveEmail()">
5080
- Save &amp; Continue
5081
- </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>
5082
5178
  } @else {
5083
5179
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
5084
5180
  @if (!bulk()) {
5085
5181
  <div class="sm-group">
5086
- <span class="sm-group-label">To</span>
5182
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
5087
5183
  <div class="sm-to-card">
5088
5184
  <span class="sm-to-name">{{ contactName() }}</span>
5089
5185
  <span class="sm-to-detail">{{ contactEmail() }}</span>
@@ -5111,7 +5207,7 @@ i0.ɵɵngDeclareClassMetadata({
5111
5207
  <ui-text-input
5112
5208
  label="CC"
5113
5209
  placeholder="name@example.com, other@example.com"
5114
- hint="Separate addresses with commas. Everyone listed receives a copy."
5210
+ [hint]="labels.t('messagingSendMessagePanel.separateAddressesWithCommasEveryone')"
5115
5211
  [(ngModel)]="ccText"
5116
5212
  [error]="ccError"
5117
5213
  (ngModelChange)="notifyValid()" />
@@ -5120,24 +5216,24 @@ i0.ɵɵngDeclareClassMetadata({
5120
5216
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
5121
5217
  @if (showTemplates()) {
5122
5218
  <div class="sm-mode-row">
5123
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
5219
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
5124
5220
  </div>
5125
5221
  }
5126
5222
 
5127
5223
  @if (mode === 'compose') {
5128
5224
  @if (showSubject()) {
5129
5225
  <ui-text-input
5130
- label="Subject"
5226
+ [label]="labels.t('common.subject')"
5131
5227
  [required]="!viaDocuSign"
5132
- placeholder="Email subject..."
5228
+ [placeholder]="labels.t('messagingSendMessagePanel.emailSubject')"
5133
5229
  [maxlength]="MAX_SUBJECT"
5134
5230
  [(ngModel)]="subject"
5135
5231
  (ngModelChange)="notifyValid()" />
5136
5232
  }
5137
5233
  <ui-textarea-input
5138
- label="Message"
5234
+ [label]="labels.t('common.message')"
5139
5235
  [required]="!viaDocuSign"
5140
- placeholder="Write your message..."
5236
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5141
5237
  [rows]="8"
5142
5238
  [(ngModel)]="body"
5143
5239
  [error]="composeTagError || bodyLengthError"
@@ -5145,7 +5241,7 @@ i0.ɵɵngDeclareClassMetadata({
5145
5241
 
5146
5242
  @if (!hasDocuments && showAttachments()) {
5147
5243
  <div class="sm-field">
5148
- <label class="sm-field-label">Attachments</label>
5244
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</label>
5149
5245
  @if (attachments.length > 0) {
5150
5246
  <div class="attach-chips">
5151
5247
  @for (a of attachments; track a.filename; let i = $index) {
@@ -5153,25 +5249,23 @@ i0.ɵɵngDeclareClassMetadata({
5153
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>
5154
5250
  <span class="attach-name">{{ a.filename }}</span>
5155
5251
  <span class="attach-size">{{ a.sizeLabel }}</span>
5156
- <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>
5157
5253
  </div>
5158
5254
  }
5159
5255
  </div>
5160
5256
  }
5161
5257
  <input #emailFileInput type="file" multiple hidden (change)="onFilesSelected($event)" />
5162
5258
  <button type="button" class="attach-add-btn" (click)="emailFileInput.click()">
5163
- <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>
5164
- Attach Files
5165
- </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>
5166
5260
  </div>
5167
5261
 
5168
5262
  }
5169
5263
 
5170
5264
  @if (!hasDocuments && showScheduling()) {
5171
- <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()" />
5172
5266
  @if (showSchedule) {
5173
5267
  <ui-datetime-input
5174
- label="Send at"
5268
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
5175
5269
  [min]="minScheduleTime"
5176
5270
  [(ngModel)]="scheduledAt"
5177
5271
  (ngModelChange)="notifyValid()" />
@@ -5181,14 +5275,12 @@ i0.ɵɵngDeclareClassMetadata({
5181
5275
  @if (missingVars.length > 0) {
5182
5276
  <div class="mv-section">
5183
5277
  <p class="mv-hint">
5184
- <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>
5185
- This template has fields that need values before sending.
5186
- </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>
5187
5279
  @for (key of missingVars; track key) {
5188
5280
  <div class="mv-row">
5189
5281
  <div class="mv-input-wrap">
5190
- <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)" />
5191
- <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')">
5192
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>
5193
5285
  </ui-button>
5194
5286
  </div>
@@ -5199,22 +5291,18 @@ i0.ɵɵngDeclareClassMetadata({
5199
5291
 
5200
5292
  <div class="tpl-preview-header">
5201
5293
  <ui-button variant="ghost" size="sm" (click)="clearTemplate()">
5202
- <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>
5203
- Back to templates
5204
- </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>
5205
5295
  <div class="tpl-view-toggle">
5206
- <ui-button variant="tab" size="sm" [active]="previewFormat === 'preview'" [ariaPressed]="previewFormat === 'preview'" (click)="setPreviewFormat('preview')">Preview</ui-button>
5207
- <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>
5208
5298
  </div>
5209
5299
  </div>
5210
5300
 
5211
5301
  <div class="tpl-subject-card">
5212
5302
  <div class="tpl-subject-card-top">
5213
- <span class="tpl-subject-label">SUBJECT</span>
5303
+ <span class="tpl-subject-label">{{ labels.t('messagingSendMessagePanel.subject') }}</span>
5214
5304
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
5215
- <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>
5216
- View Send Preview
5217
- </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>
5218
5306
  </div>
5219
5307
  <span class="tpl-subject-text">{{ resolvedSubject }}</span>
5220
5308
  </div>
@@ -5222,9 +5310,9 @@ i0.ɵɵngDeclareClassMetadata({
5222
5310
  <div class="tpl-body-preview" [innerHTML]="previewBodyHtml"></div>
5223
5311
  } @else {
5224
5312
  <!-- Raw: the template source exactly as written, {{tags}} included — subject first, then the body. -->
5225
- <div class="tpl-raw-label">Subject</div>
5313
+ <div class="tpl-raw-label">{{ labels.t('common.subject') }}</div>
5226
5314
  <pre class="tpl-subject-raw">{{ subject }}</pre>
5227
- <div class="tpl-raw-label">Body</div>
5315
+ <div class="tpl-raw-label">{{ labels.t('messagingSendMessagePanel.body') }}</div>
5228
5316
  <pre class="tpl-body-raw">{{ body }}</pre>
5229
5317
  }
5230
5318
  } @else {
@@ -5261,10 +5349,10 @@ i0.ɵɵngDeclareClassMetadata({
5261
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. -->
5262
5350
  <div class="sm-docs-live" role="status" aria-live="polite">
5263
5351
  @if (hasDocuments) {
5264
- <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>
5265
5353
  }
5266
5354
  @if (warningLines.length > 0) {
5267
- <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">
5268
5356
  <ul class="sm-warn-list">
5269
5357
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
5270
5358
  </ul>
@@ -5287,7 +5375,7 @@ i0.ɵɵngDeclareClassMetadata({
5287
5375
 
5288
5376
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
5289
5377
  @if (mode === 'compose' && showVideo()) {
5290
- <ui-form-section label="Video" [divider]="true">
5378
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
5291
5379
  @for (k of [videoPickerKey]; track k) {
5292
5380
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
5293
5381
  }
@@ -5309,20 +5397,18 @@ i0.ɵɵngDeclareClassMetadata({
5309
5397
  <div class="sp-modal" (click)="$event.stopPropagation()">
5310
5398
  <div class="sp-modal-header">
5311
5399
  <div class="sp-modal-title">
5312
- <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>
5313
- Email Preview
5314
- </div>
5315
- <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')">
5316
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>
5317
5403
  </ui-button>
5318
5404
  </div>
5319
5405
  <div class="sp-email-meta">
5320
5406
  <div class="sp-meta-row">
5321
- <span class="sp-meta-label">To</span>
5322
- <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>
5323
5409
  </div>
5324
5410
  <div class="sp-meta-row">
5325
- <span class="sp-meta-label">Subject</span>
5411
+ <span class="sp-meta-label">{{ labels.t('common.subject') }}</span>
5326
5412
  <span class="sp-meta-value sp-meta-subject">{{ sendPreviewSubject }}</span>
5327
5413
  </div>
5328
5414
  </div>
@@ -5416,7 +5502,7 @@ i0.ɵɵngDeclareClassMetadata({
5416
5502
  required: false
5417
5503
  }]
5418
5504
  }],
5419
- receiptLabel: [{
5505
+ receiptLabelInput: [{
5420
5506
  type: i0.Input,
5421
5507
  args: [{
5422
5508
  isSignal: true,
@@ -5633,6 +5719,7 @@ i0.ɵɵngDeclareClassMetadata({
5633
5719
  const MAX_SUBJECT = 255;
5634
5720
  var SendInAppComponent = class SendInAppComponent {
5635
5721
  constructor() {
5722
+ this.labels = inject(UiLabelsService);
5636
5723
  this.destroyRef = inject(DestroyRef);
5637
5724
  this.toast = inject(ToastService);
5638
5725
  this.cdr = inject(ChangeDetectorRef);
@@ -5681,7 +5768,7 @@ var SendInAppComponent = class SendInAppComponent {
5681
5768
  const reader = new FileReader();
5682
5769
  reader.onload = () => {
5683
5770
  const base64 = String(reader.result).split(",")[1] ?? "";
5684
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
5771
+ const sizeLabel = formatAttachmentSize(file.size);
5685
5772
  this.attachments = [...this.attachments, {
5686
5773
  filename: file.name,
5687
5774
  content: base64,
@@ -5719,13 +5806,13 @@ var SendInAppComponent = class SendInAppComponent {
5719
5806
  if (this.showSubject()) payload.subject = this.subject.trim();
5720
5807
  send(payload).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
5721
5808
  next: () => {
5722
- this.toast.show("Message sent");
5809
+ this.toast.show(this.labels.t("messagingSendMessagePanel.messageSent"));
5723
5810
  this.sending.set(false);
5724
5811
  this.attachments = [];
5725
5812
  this.sent.emit();
5726
5813
  },
5727
5814
  error: (err) => {
5728
- this.toast.error(serverMessage(err) || "Failed to send message");
5815
+ this.toast.error(serverMessage(err) || this.labels.t("messagingSendMessagePanel.failedSendMessage"));
5729
5816
  this.sending.set(false);
5730
5817
  this.notifyValid();
5731
5818
  }
@@ -5827,7 +5914,7 @@ var SendInAppComponent = class SendInAppComponent {
5827
5914
  ngImport: i0,
5828
5915
  template: `
5829
5916
  <div class="sm-group">
5830
- <span class="sm-group-label">To</span>
5917
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
5831
5918
  <div class="sm-to-card">
5832
5919
  <span class="sm-to-name">{{ contactName() }}</span>
5833
5920
  @if (contactEmail()) { <span class="sm-to-email">{{ contactEmail() }}</span> }
@@ -5835,56 +5922,56 @@ var SendInAppComponent = class SendInAppComponent {
5835
5922
  </div>
5836
5923
  @if (showSubject()) {
5837
5924
  <ui-text-input
5838
- label="Subject"
5925
+ [label]="labels.t('common.subject')"
5839
5926
  [required]="true"
5840
- placeholder="Subject..."
5927
+ [placeholder]="labels.t('messagingSendMessagePanel.subject2')"
5841
5928
  [maxlength]="MAX_SUBJECT"
5842
5929
  [(ngModel)]="subject"
5843
5930
  (ngModelChange)="notifyValid()" />
5844
5931
  }
5845
5932
  @if (richText()) {
5846
5933
  <ui-rich-text-editor
5847
- label="Message"
5934
+ [label]="labels.t('common.message')"
5848
5935
  [required]="true"
5849
- placeholder="Write your message..."
5936
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5850
5937
  [minHeight]="180"
5851
- [error]="richTooLong() ? 'Too long (max ' + limit() + ' characters).' : ''"
5938
+ [error]="richTooLong() ? labels.t('validation.tooLong', { max: limit() }) : ''"
5852
5939
  [(ngModel)]="body"
5853
5940
  (ngModelChange)="notifyValid()" />
5854
5941
  } @else if (limit(); as max) {
5855
5942
  <ui-textarea-input
5856
- label="Message"
5943
+ [label]="labels.t('common.message')"
5857
5944
  [required]="true"
5858
- placeholder="Write your message..."
5945
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5859
5946
  [rows]="8"
5860
5947
  [maxlength]="max"
5861
5948
  [(ngModel)]="body"
5862
5949
  (ngModelChange)="notifyValid()" />
5863
5950
  } @else {
5864
5951
  <ui-textarea-input
5865
- label="Message"
5952
+ [label]="labels.t('common.message')"
5866
5953
  [required]="true"
5867
- placeholder="Write your message..."
5954
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
5868
5955
  [rows]="8"
5869
5956
  [(ngModel)]="body"
5870
5957
  (ngModelChange)="notifyValid()" />
5871
5958
  }
5872
5959
  @if (allowAttachments()) {
5873
5960
  <div class="sm-group" data-testid="in-app-attachments">
5874
- <span class="sm-group-label">Attachments</span>
5961
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</span>
5875
5962
  @if (attachments.length > 0) {
5876
5963
  <div class="attach-chips">
5877
5964
  @for (a of attachments; track $index; let i = $index) {
5878
5965
  <div class="attach-chip">
5879
5966
  <span class="attach-name">{{ a.filename }}</span>
5880
5967
  <span class="attach-size">{{ a.sizeLabel }}</span>
5881
- <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>
5882
5969
  </div>
5883
5970
  }
5884
5971
  </div>
5885
5972
  }
5886
5973
  <input #fileInput type="file" multiple hidden data-testid="in-app-file-input" (change)="onFilesSelected($event)" />
5887
- <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>
5888
5975
  </div>
5889
5976
  }
5890
5977
  `,
@@ -6035,7 +6122,7 @@ i0.ɵɵngDeclareClassMetadata({
6035
6122
  ],
6036
6123
  template: `
6037
6124
  <div class="sm-group">
6038
- <span class="sm-group-label">To</span>
6125
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
6039
6126
  <div class="sm-to-card">
6040
6127
  <span class="sm-to-name">{{ contactName() }}</span>
6041
6128
  @if (contactEmail()) { <span class="sm-to-email">{{ contactEmail() }}</span> }
@@ -6043,56 +6130,56 @@ i0.ɵɵngDeclareClassMetadata({
6043
6130
  </div>
6044
6131
  @if (showSubject()) {
6045
6132
  <ui-text-input
6046
- label="Subject"
6133
+ [label]="labels.t('common.subject')"
6047
6134
  [required]="true"
6048
- placeholder="Subject..."
6135
+ [placeholder]="labels.t('messagingSendMessagePanel.subject2')"
6049
6136
  [maxlength]="MAX_SUBJECT"
6050
6137
  [(ngModel)]="subject"
6051
6138
  (ngModelChange)="notifyValid()" />
6052
6139
  }
6053
6140
  @if (richText()) {
6054
6141
  <ui-rich-text-editor
6055
- label="Message"
6142
+ [label]="labels.t('common.message')"
6056
6143
  [required]="true"
6057
- placeholder="Write your message..."
6144
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6058
6145
  [minHeight]="180"
6059
- [error]="richTooLong() ? 'Too long (max ' + limit() + ' characters).' : ''"
6146
+ [error]="richTooLong() ? labels.t('validation.tooLong', { max: limit() }) : ''"
6060
6147
  [(ngModel)]="body"
6061
6148
  (ngModelChange)="notifyValid()" />
6062
6149
  } @else if (limit(); as max) {
6063
6150
  <ui-textarea-input
6064
- label="Message"
6151
+ [label]="labels.t('common.message')"
6065
6152
  [required]="true"
6066
- placeholder="Write your message..."
6153
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6067
6154
  [rows]="8"
6068
6155
  [maxlength]="max"
6069
6156
  [(ngModel)]="body"
6070
6157
  (ngModelChange)="notifyValid()" />
6071
6158
  } @else {
6072
6159
  <ui-textarea-input
6073
- label="Message"
6160
+ [label]="labels.t('common.message')"
6074
6161
  [required]="true"
6075
- placeholder="Write your message..."
6162
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6076
6163
  [rows]="8"
6077
6164
  [(ngModel)]="body"
6078
6165
  (ngModelChange)="notifyValid()" />
6079
6166
  }
6080
6167
  @if (allowAttachments()) {
6081
6168
  <div class="sm-group" data-testid="in-app-attachments">
6082
- <span class="sm-group-label">Attachments</span>
6169
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.attachments') }}</span>
6083
6170
  @if (attachments.length > 0) {
6084
6171
  <div class="attach-chips">
6085
6172
  @for (a of attachments; track $index; let i = $index) {
6086
6173
  <div class="attach-chip">
6087
6174
  <span class="attach-name">{{ a.filename }}</span>
6088
6175
  <span class="attach-size">{{ a.sizeLabel }}</span>
6089
- <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>
6090
6177
  </div>
6091
6178
  }
6092
6179
  </div>
6093
6180
  }
6094
6181
  <input #fileInput type="file" multiple hidden data-testid="in-app-file-input" (change)="onFilesSelected($event)" />
6095
- <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>
6096
6183
  </div>
6097
6184
  }
6098
6185
  `,
@@ -6214,6 +6301,7 @@ var SendTextComponent = class SendTextComponent {
6214
6301
  } });
6215
6302
  }
6216
6303
  constructor() {
6304
+ this.labels = inject(UiLabelsService);
6217
6305
  this.formatPhone = formatPhone;
6218
6306
  this.destroyRef = inject(DestroyRef);
6219
6307
  this.http = inject(HttpClient);
@@ -6287,13 +6375,13 @@ var SendTextComponent = class SendTextComponent {
6287
6375
  this.mediaAttachment = signal(null, ...ngDevMode ? [{ debugName: "mediaAttachment" }] : /* istanbul ignore next */ []);
6288
6376
  this.mediaUploading = signal(false, ...ngDevMode ? [{ debugName: "mediaUploading" }] : /* istanbul ignore next */ []);
6289
6377
  this.savingPhone = signal(false, ...ngDevMode ? [{ debugName: "savingPhone" }] : /* istanbul ignore next */ []);
6290
- this.modeTabs = [{
6378
+ this.modeTabs = computed(() => [{
6291
6379
  key: "compose",
6292
- label: "Create Message"
6380
+ label: this.labels.t("messagingSendMessagePanel.tabCompose")
6293
6381
  }, {
6294
6382
  key: "template",
6295
- label: "Use Existing Template"
6296
- }];
6383
+ label: this.labels.t("messagingSendMessagePanel.tabTemplate")
6384
+ }], ...ngDevMode ? [{ debugName: "modeTabs" }] : /* istanbul ignore next */ []);
6297
6385
  this.marketVars = {};
6298
6386
  this._appliedDefaultBody = "";
6299
6387
  this.forwardedContext = computed(() => forwardContext(this.sendContext(), this.documentContextTypes()).context, ...ngDevMode ? [{ debugName: "forwardedContext" }] : /* istanbul ignore next */ []);
@@ -6356,8 +6444,8 @@ var SendTextComponent = class SendTextComponent {
6356
6444
  onDocumentsChange(v) {
6357
6445
  this.documentsValue = v;
6358
6446
  if (this.hasDocuments) {
6359
- const removed = [this.mediaAttachment() ? "your image" : "", this.showSchedule ? "the schedule" : ""].filter(Boolean);
6360
- 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")} `) });
6361
6449
  this.showSchedule = false;
6362
6450
  this.scheduledAt = "";
6363
6451
  this.mediaAttachment.set(null);
@@ -6409,10 +6497,10 @@ var SendTextComponent = class SendTextComponent {
6409
6497
  return mergeTagKeys(this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
6410
6498
  }
6411
6499
  get videoLengthError() {
6412
- 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 }) : "";
6413
6501
  }
6414
6502
  get composeTagError() {
6415
- 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") : "";
6416
6504
  }
6417
6505
  get unresolvedVars() {
6418
6506
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -6494,7 +6582,7 @@ var SendTextComponent = class SendTextComponent {
6494
6582
  onFileSelected(event) {
6495
6583
  const file = event.target.files?.[0];
6496
6584
  if (!file) return;
6497
- const sizeLabel = file.size < 1048576 ? `${Math.round(file.size / 1024)} KB` : `${(file.size / 1048576).toFixed(1)} MB`;
6585
+ const sizeLabel = formatAttachmentSize(file.size);
6498
6586
  this.mediaUploading.set(true);
6499
6587
  this.cdr.detectChanges();
6500
6588
  const form = new FormData();
@@ -6511,7 +6599,7 @@ var SendTextComponent = class SendTextComponent {
6511
6599
  this.cdr.detectChanges();
6512
6600
  },
6513
6601
  error: () => {
6514
- this.toast.error("Failed to upload attachment");
6602
+ this.toast.error(this.labels.t("messagingSendMessagePanel.failedUpload"));
6515
6603
  this.mediaUploading.set(false);
6516
6604
  this.cdr.detectChanges();
6517
6605
  }
@@ -6524,7 +6612,7 @@ var SendTextComponent = class SendTextComponent {
6524
6612
  insertVideo(v) {
6525
6613
  if (!v.shareLink) return;
6526
6614
  const sep = this.body.trim() ? "\n\n" : "";
6527
- 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);
6528
6616
  this.notifyValid();
6529
6617
  }
6530
6618
  onVideoSelected(v) {
@@ -6559,7 +6647,7 @@ var SendTextComponent = class SendTextComponent {
6559
6647
  submit() {
6560
6648
  if (!this.canSend || this.loading || !this.bulk() && !this.contactPhone()) return;
6561
6649
  if (this.bulk() && !this.sendFn()) {
6562
- this.toast.error(BULK_NO_SEND_MESSAGE);
6650
+ this.toast.error(this.labels.t("messagingSendMessagePanel.bulkNoSend"));
6563
6651
  return;
6564
6652
  }
6565
6653
  if (this.mode === "template") this.applyPendingVars();
@@ -6631,7 +6719,7 @@ var SendTextComponent = class SendTextComponent {
6631
6719
  });
6632
6720
  const onError = (err) => {
6633
6721
  if (this.bulk() && (destroyed || !this.loading)) return;
6634
- 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"));
6635
6723
  this.loading = false;
6636
6724
  this.cdr.detectChanges();
6637
6725
  if (this.bulk()) this.bulkSending.emit(null);
@@ -6646,7 +6734,7 @@ var SendTextComponent = class SendTextComponent {
6646
6734
  channel: "text",
6647
6735
  informational: docs.signerInfo?.informational === true
6648
6736
  }) : void 0;
6649
- 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")));
6650
6738
  this.loading = false;
6651
6739
  this.mediaAttachment.set(null);
6652
6740
  if (this.bulk()) this.bulkResult.emit({
@@ -6869,25 +6957,21 @@ var SendTextComponent = class SendTextComponent {
6869
6957
  <!-- Missing phone guard -->
6870
6958
  @if (!bulk() && !contactPhone()) {
6871
6959
  <div class="sm-missing-banner">
6872
- <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>
6873
- No phone number on file for this contact.
6874
- </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>
6875
6961
  <ui-text-input
6876
- label="Add Phone Number"
6962
+ [label]="labels.t('messagingSendMessagePanel.addPhoneNumber')"
6877
6963
  type="tel"
6878
6964
  [required]="true"
6879
6965
  placeholder="(555) 000-0000"
6880
6966
  [(ngModel)]="pendingPhone"
6881
6967
  (ngModelChange)="notifyValid()"
6882
- [error]="pendingPhone && !pendingPhoneValid ? 'Please enter a valid phone number.' : ''" />
6883
- <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">
6884
- Save &amp; Continue
6885
- </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>
6886
6970
  } @else {
6887
6971
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
6888
6972
  @if (!bulk()) {
6889
6973
  <div class="sm-group">
6890
- <span class="sm-group-label">To</span>
6974
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
6891
6975
  <div class="sm-to-card">
6892
6976
  <span class="sm-to-name">{{ contactName() }}</span>
6893
6977
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
@@ -6898,23 +6982,23 @@ var SendTextComponent = class SendTextComponent {
6898
6982
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
6899
6983
  @if (showTemplates()) {
6900
6984
  <div class="sm-mode-row">
6901
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
6985
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
6902
6986
  </div>
6903
6987
  }
6904
6988
 
6905
6989
  @if (mode === 'compose') {
6906
6990
  <div class="sm-field">
6907
6991
  <ui-textarea-input
6908
- label="Message"
6992
+ [label]="labels.t('common.message')"
6909
6993
  [required]="true"
6910
- placeholder="Write your message..."
6994
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
6911
6995
  [rows]="8"
6912
6996
  [maxlength]="MAX_LENGTH"
6913
6997
  [(ngModel)]="body"
6914
6998
  [error]="composeTagError || videoLengthError"
6915
6999
  (ngModelChange)="notifyValid()" />
6916
7000
  <div class="sm-char-row">
6917
- <span class="sm-hint">Sent as SMS. Keep messages concise for best delivery.</span>
7001
+ <span class="sm-hint">{{ labels.t('messagingSendMessagePanel.sentAsSmsKeepMessages') }}</span>
6918
7002
  <span class="sm-char-count" [class.sm-char-warn]="body.length > MAX_LENGTH * 0.9">
6919
7003
  {{ body.length }} / {{ MAX_LENGTH }}
6920
7004
  </span>
@@ -6923,14 +7007,14 @@ var SendTextComponent = class SendTextComponent {
6923
7007
 
6924
7008
  @if (!hasDocuments && showAttachments()) {
6925
7009
  <div class="sm-field">
6926
- <label class="sm-field-label">Add Image / Media (MMS)</label>
7010
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.addImageMediaMms') }}</label>
6927
7011
  @if (mediaAttachment()) {
6928
7012
  <div class="attach-chips">
6929
7013
  <div class="attach-chip">
6930
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>
6931
7015
  <span class="attach-name">{{ mediaAttachment()!.mediaFileName }}</span>
6932
7016
  <span class="attach-size">{{ mediaAttachment()!.sizeLabel }}</span>
6933
- <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>
6934
7018
  </div>
6935
7019
  </div>
6936
7020
  }
@@ -6938,10 +7022,10 @@ var SendTextComponent = class SendTextComponent {
6938
7022
  <input #smsFileInput type="file" accept="image/*,video/*,application/pdf" hidden (change)="onFileSelected($event)" />
6939
7023
  <button type="button" class="attach-add-btn" [disabled]="mediaUploading()" (click)="smsFileInput.click()">
6940
7024
  @if (mediaUploading()) {
6941
- <span class="attach-spinner"></span> Uploading...
7025
+ <span class="attach-spinner"></span> {{ labels.t('messagingSendText.uploading') }}
6942
7026
  } @else {
6943
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>
6944
- Attach Image
7028
+ {{ labels.t('messagingSendMessagePanel.attachImage') }}
6945
7029
  }
6946
7030
  </button>
6947
7031
  }
@@ -6950,10 +7034,10 @@ var SendTextComponent = class SendTextComponent {
6950
7034
  }
6951
7035
 
6952
7036
  @if (!hasDocuments && showScheduling()) {
6953
- <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()" />
6954
7038
  @if (showSchedule) {
6955
7039
  <ui-datetime-input
6956
- label="Send at"
7040
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
6957
7041
  [min]="minScheduleTime"
6958
7042
  [(ngModel)]="scheduledAt"
6959
7043
  (ngModelChange)="notifyValid()" />
@@ -6963,14 +7047,12 @@ var SendTextComponent = class SendTextComponent {
6963
7047
  @if (missingVars.length > 0) {
6964
7048
  <div class="mv-section">
6965
7049
  <p class="mv-hint">
6966
- <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>
6967
- This template has fields that need values before sending.
6968
- </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>
6969
7051
  @for (key of missingVars; track key) {
6970
7052
  <div class="mv-row">
6971
7053
  <div class="mv-input-wrap">
6972
- <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)" />
6973
- <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')">
6974
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>
6975
7057
  </button>
6976
7058
  </div>
@@ -6981,20 +7063,16 @@ var SendTextComponent = class SendTextComponent {
6981
7063
 
6982
7064
  <div class="tpl-preview-header">
6983
7065
  <button class="tpl-back-btn" type="button" (click)="clearTemplate()">
6984
- <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>
6985
- Back to templates
6986
- </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>
6987
7067
  <div class="tpl-view-toggle">
6988
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">Preview</button>
6989
- <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>
6990
7070
  </div>
6991
7071
  </div>
6992
7072
 
6993
7073
  <div class="tpl-send-preview-row">
6994
7074
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
6995
- <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>
6996
- View Send Preview
6997
- </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>
6998
7076
  </div>
6999
7077
 
7000
7078
  @if (previewFormat === 'preview') {
@@ -7034,10 +7112,10 @@ var SendTextComponent = class SendTextComponent {
7034
7112
  <!-- One live region, at most ONE warning banner (every current non-blocking warning is a line in it). -->
7035
7113
  <div class="sm-docs-live" role="status" aria-live="polite">
7036
7114
  @if (hasDocuments) {
7037
- <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>
7038
7116
  }
7039
7117
  @if (warningLines.length > 0) {
7040
- <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">
7041
7119
  <ul class="sm-warn-list">
7042
7120
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
7043
7121
  </ul>
@@ -7048,7 +7126,7 @@ var SendTextComponent = class SendTextComponent {
7048
7126
 
7049
7127
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
7050
7128
  @if (mode === 'compose' && showVideo()) {
7051
- <ui-form-section label="Video" [divider]="true">
7129
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
7052
7130
  @for (k of [videoPickerKey]; track k) {
7053
7131
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
7054
7132
  }
@@ -7070,17 +7148,15 @@ var SendTextComponent = class SendTextComponent {
7070
7148
  <div class="sp-modal" (click)="$event.stopPropagation()">
7071
7149
  <div class="sp-modal-header">
7072
7150
  <div class="sp-modal-title">
7073
- <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>
7074
- Text Preview
7075
- </div>
7076
- <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')">
7077
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>
7078
7154
  </button>
7079
7155
  </div>
7080
7156
  <div class="sp-text-meta">
7081
7157
  <div class="sp-meta-row">
7082
- <span class="sp-meta-label">To</span>
7083
- <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>
7084
7160
  </div>
7085
7161
  </div>
7086
7162
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7094,10 +7170,8 @@ var SendTextComponent = class SendTextComponent {
7094
7170
  <div class="sp-modal" (click)="$event.stopPropagation()">
7095
7171
  <div class="sp-modal-header">
7096
7172
  <div class="sp-modal-title">
7097
- <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>
7098
- Text Preview
7099
- </div>
7100
- <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')">
7101
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>
7102
7176
  </button>
7103
7177
  </div>
@@ -7189,6 +7263,7 @@ var SendTextComponent = class SendTextComponent {
7189
7263
  "legend",
7190
7264
  "orientation",
7191
7265
  "values",
7266
+ "labelLinks",
7192
7267
  "description",
7193
7268
  "indent",
7194
7269
  "required",
@@ -7437,25 +7512,21 @@ i0.ɵɵngDeclareClassMetadata({
7437
7512
  <!-- Missing phone guard -->
7438
7513
  @if (!bulk() && !contactPhone()) {
7439
7514
  <div class="sm-missing-banner">
7440
- <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>
7441
- No phone number on file for this contact.
7442
- </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>
7443
7516
  <ui-text-input
7444
- label="Add Phone Number"
7517
+ [label]="labels.t('messagingSendMessagePanel.addPhoneNumber')"
7445
7518
  type="tel"
7446
7519
  [required]="true"
7447
7520
  placeholder="(555) 000-0000"
7448
7521
  [(ngModel)]="pendingPhone"
7449
7522
  (ngModelChange)="notifyValid()"
7450
- [error]="pendingPhone && !pendingPhoneValid ? 'Please enter a valid phone number.' : ''" />
7451
- <ui-button variant="primary" [disabled]="!pendingPhoneValid || savingPhone()" [loading]="savingPhone()" (click)="savePhone()">
7452
- Save &amp; Continue
7453
- </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>
7454
7525
  } @else {
7455
7526
  <!-- To (bulk mode: the panel shows the recipient summary instead) -->
7456
7527
  @if (!bulk()) {
7457
7528
  <div class="sm-group">
7458
- <span class="sm-group-label">To</span>
7529
+ <span class="sm-group-label">{{ labels.t('messagingSendMessagePanel.to') }}</span>
7459
7530
  <div class="sm-to-card">
7460
7531
  <span class="sm-to-name">{{ contactName() }}</span>
7461
7532
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
@@ -7466,23 +7537,23 @@ i0.ɵɵngDeclareClassMetadata({
7466
7537
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
7467
7538
  @if (showTemplates()) {
7468
7539
  <div class="sm-mode-row">
7469
- <ui-pill-tabs [tabs]="modeTabs" [activeKey]="mode" (tabChange)="setMode($event)" />
7540
+ <ui-pill-tabs [tabs]="modeTabs()" [activeKey]="mode" (tabChange)="setMode($event)" />
7470
7541
  </div>
7471
7542
  }
7472
7543
 
7473
7544
  @if (mode === 'compose') {
7474
7545
  <div class="sm-field">
7475
7546
  <ui-textarea-input
7476
- label="Message"
7547
+ [label]="labels.t('common.message')"
7477
7548
  [required]="true"
7478
- placeholder="Write your message..."
7549
+ [placeholder]="labels.t('messagingSendMessagePanel.writeYourMessage')"
7479
7550
  [rows]="8"
7480
7551
  [maxlength]="MAX_LENGTH"
7481
7552
  [(ngModel)]="body"
7482
7553
  [error]="composeTagError || videoLengthError"
7483
7554
  (ngModelChange)="notifyValid()" />
7484
7555
  <div class="sm-char-row">
7485
- <span class="sm-hint">Sent as SMS. Keep messages concise for best delivery.</span>
7556
+ <span class="sm-hint">{{ labels.t('messagingSendMessagePanel.sentAsSmsKeepMessages') }}</span>
7486
7557
  <span class="sm-char-count" [class.sm-char-warn]="body.length > MAX_LENGTH * 0.9">
7487
7558
  {{ body.length }} / {{ MAX_LENGTH }}
7488
7559
  </span>
@@ -7491,14 +7562,14 @@ i0.ɵɵngDeclareClassMetadata({
7491
7562
 
7492
7563
  @if (!hasDocuments && showAttachments()) {
7493
7564
  <div class="sm-field">
7494
- <label class="sm-field-label">Add Image / Media (MMS)</label>
7565
+ <label class="sm-field-label">{{ labels.t('messagingSendMessagePanel.addImageMediaMms') }}</label>
7495
7566
  @if (mediaAttachment()) {
7496
7567
  <div class="attach-chips">
7497
7568
  <div class="attach-chip">
7498
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>
7499
7570
  <span class="attach-name">{{ mediaAttachment()!.mediaFileName }}</span>
7500
7571
  <span class="attach-size">{{ mediaAttachment()!.sizeLabel }}</span>
7501
- <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>
7502
7573
  </div>
7503
7574
  </div>
7504
7575
  }
@@ -7506,10 +7577,10 @@ i0.ɵɵngDeclareClassMetadata({
7506
7577
  <input #smsFileInput type="file" accept="image/*,video/*,application/pdf" hidden (change)="onFileSelected($event)" />
7507
7578
  <button type="button" class="attach-add-btn" [disabled]="mediaUploading()" (click)="smsFileInput.click()">
7508
7579
  @if (mediaUploading()) {
7509
- <span class="attach-spinner"></span> Uploading...
7580
+ <span class="attach-spinner"></span> {{ labels.t('messagingSendText.uploading') }}
7510
7581
  } @else {
7511
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>
7512
- Attach Image
7583
+ {{ labels.t('messagingSendMessagePanel.attachImage') }}
7513
7584
  }
7514
7585
  </button>
7515
7586
  }
@@ -7518,10 +7589,10 @@ i0.ɵɵngDeclareClassMetadata({
7518
7589
  }
7519
7590
 
7520
7591
  @if (!hasDocuments && showScheduling()) {
7521
- <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()" />
7522
7593
  @if (showSchedule) {
7523
7594
  <ui-datetime-input
7524
- label="Send at"
7595
+ [label]="labels.t('messagingSendMessagePanel.sendAt')"
7525
7596
  [min]="minScheduleTime"
7526
7597
  [(ngModel)]="scheduledAt"
7527
7598
  (ngModelChange)="notifyValid()" />
@@ -7531,14 +7602,12 @@ i0.ɵɵngDeclareClassMetadata({
7531
7602
  @if (missingVars.length > 0) {
7532
7603
  <div class="mv-section">
7533
7604
  <p class="mv-hint">
7534
- <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>
7535
- This template has fields that need values before sending.
7536
- </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>
7537
7606
  @for (key of missingVars; track key) {
7538
7607
  <div class="mv-row">
7539
7608
  <div class="mv-input-wrap">
7540
- <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)" />
7541
- <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')">
7542
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>
7543
7612
  </button>
7544
7613
  </div>
@@ -7549,20 +7618,16 @@ i0.ɵɵngDeclareClassMetadata({
7549
7618
 
7550
7619
  <div class="tpl-preview-header">
7551
7620
  <button class="tpl-back-btn" type="button" (click)="clearTemplate()">
7552
- <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>
7553
- Back to templates
7554
- </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>
7555
7622
  <div class="tpl-view-toggle">
7556
- <button class="tpl-toggle-btn" [class.active]="previewFormat === 'preview'" [attr.aria-pressed]="previewFormat === 'preview'" type="button" (click)="setPreviewFormat('preview')">Preview</button>
7557
- <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>
7558
7625
  </div>
7559
7626
  </div>
7560
7627
 
7561
7628
  <div class="tpl-send-preview-row">
7562
7629
  <ui-button variant="secondary" size="sm" (click)="openSendPreview()">
7563
- <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>
7564
- View Send Preview
7565
- </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>
7566
7631
  </div>
7567
7632
 
7568
7633
  @if (previewFormat === 'preview') {
@@ -7602,10 +7667,10 @@ i0.ɵɵngDeclareClassMetadata({
7602
7667
  <!-- One live region, at most ONE warning banner (every current non-blocking warning is a line in it). -->
7603
7668
  <div class="sm-docs-live" role="status" aria-live="polite">
7604
7669
  @if (hasDocuments) {
7605
- <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>
7606
7671
  }
7607
7672
  @if (warningLines.length > 0) {
7608
- <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">
7609
7674
  <ul class="sm-warn-list">
7610
7675
  @for (w of warningLines; track w.key) { <li [attr.data-testid]="w.testId">{{ w.text }}</li> }
7611
7676
  </ul>
@@ -7616,7 +7681,7 @@ i0.ɵɵngDeclareClassMetadata({
7616
7681
 
7617
7682
  <!-- Video: the LAST section. The picker, and once a video is attached its settings, Watch Preview and Remove Video. -->
7618
7683
  @if (mode === 'compose' && showVideo()) {
7619
- <ui-form-section label="Video" [divider]="true">
7684
+ <ui-form-section [label]="labels.t('common.video')" [divider]="true">
7620
7685
  @for (k of [videoPickerKey]; track k) {
7621
7686
  <ui-video-picker (videoSelected)="onVideoSelected($event)" />
7622
7687
  }
@@ -7638,17 +7703,15 @@ i0.ɵɵngDeclareClassMetadata({
7638
7703
  <div class="sp-modal" (click)="$event.stopPropagation()">
7639
7704
  <div class="sp-modal-header">
7640
7705
  <div class="sp-modal-title">
7641
- <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>
7642
- Text Preview
7643
- </div>
7644
- <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')">
7645
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>
7646
7709
  </button>
7647
7710
  </div>
7648
7711
  <div class="sp-text-meta">
7649
7712
  <div class="sp-meta-row">
7650
- <span class="sp-meta-label">To</span>
7651
- <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>
7652
7715
  </div>
7653
7716
  </div>
7654
7717
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7662,10 +7725,8 @@ i0.ɵɵngDeclareClassMetadata({
7662
7725
  <div class="sp-modal" (click)="$event.stopPropagation()">
7663
7726
  <div class="sp-modal-header">
7664
7727
  <div class="sp-modal-title">
7665
- <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>
7666
- Text Preview
7667
- </div>
7668
- <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')">
7669
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>
7670
7731
  </button>
7671
7732
  </div>
@@ -7908,13 +7969,21 @@ i0.ɵɵngDeclareClassMetadata({
7908
7969
  const BULK_MERGE_TAG_HINT = "Personal details such as {{first_name}} are filled in for each recipient when sent.";
7909
7970
  const SKIPPED_NAMES_SHOWN = 5;
7910
7971
  var SendMessagePanelComponent = class SendMessagePanelComponent {
7911
- skippedReason() {
7912
- 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");
7913
7979
  }
7914
7980
  skippedNames() {
7915
- const names = this.skippedRecipients().map((r) => r.name || "Unnamed");
7981
+ const names = this.skippedRecipients().map((r) => r.name || this.labels.t("messagingSendMessagePanel.unnamed"));
7916
7982
  const shown = names.slice(0, SKIPPED_NAMES_SHOWN).join(", ");
7917
- 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;
7918
7987
  }
7919
7988
  bulkRecipientsFor(ch) {
7920
7989
  if (!this.bulkMode()) return null;
@@ -7962,15 +8031,20 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7962
8031
  this.jobOptions.set([]);
7963
8032
  }
7964
8033
  constructor() {
8034
+ this.labels = inject(UiLabelsService);
7965
8035
  this.open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
7966
8036
  this.contact = input(null, ...ngDevMode ? [{ debugName: "contact" }] : /* istanbul ignore next */ []);
7967
8037
  this.loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
7968
8038
  this.recipients = input(null, ...ngDevMode ? [{ debugName: "recipients" }] : /* istanbul ignore next */ []);
7969
8039
  this.initialChannel = input(null, ...ngDevMode ? [{ debugName: "initialChannel" }] : /* istanbul ignore next */ []);
7970
- this.recipientNoun = input("recipients", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
8040
+ this.recipientNoun = input("", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
7971
8041
  this.recipientNounSingular = input("", ...ngDevMode ? [{ debugName: "recipientNounSingular" }] : /* istanbul ignore next */ []);
7972
8042
  this.textEnabled = input(false, ...ngDevMode ? [{ debugName: "textEnabled" }] : /* istanbul ignore next */ []);
7973
- 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 */ []);
7974
8048
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
7975
8049
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
7976
8050
  this.defaultMessage = input("", ...ngDevMode ? [{ debugName: "defaultMessage" }] : /* istanbul ignore next */ []);
@@ -7978,7 +8052,11 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7978
8052
  this.sendEmail = input(null, ...ngDevMode ? [{ debugName: "sendEmail" }] : /* istanbul ignore next */ []);
7979
8053
  this.sendText = input(null, ...ngDevMode ? [{ debugName: "sendText" }] : /* istanbul ignore next */ []);
7980
8054
  this.showReceiptOption = input(false, ...ngDevMode ? [{ debugName: "showReceiptOption" }] : /* istanbul ignore next */ []);
7981
- 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 */ []);
7982
8060
  this.receiptDefault = input(false, ...ngDevMode ? [{ debugName: "receiptDefault" }] : /* istanbul ignore next */ []);
7983
8061
  this.documents = input(false, ...ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []);
7984
8062
  this.documentsEnabled = input(true, ...ngDevMode ? [{ debugName: "documentsEnabled" }] : /* istanbul ignore next */ []);
@@ -7992,8 +8070,6 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7992
8070
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
7993
8071
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
7994
8072
  this.inApp = computed(() => this.recipientMode() === "in-app" && !this.bulkMode(), ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7995
- this.BULK_NO_SEND_MESSAGE = BULK_NO_SEND_MESSAGE;
7996
- this.BULK_MERGE_TAG_HINT = BULK_MERGE_TAG_HINT;
7997
8073
  this.attachmentsAndScheduling = input(true, ...ngDevMode ? [{ debugName: "attachmentsAndScheduling" }] : /* istanbul ignore next */ []);
7998
8074
  this.allowAttachments = input(null, ...ngDevMode ? [{ debugName: "allowAttachments" }] : /* istanbul ignore next */ []);
7999
8075
  this.allowScheduling = input(null, ...ngDevMode ? [{ debugName: "allowScheduling" }] : /* istanbul ignore next */ []);
@@ -8037,31 +8113,56 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8037
8113
  this.submitSnapshot = null;
8038
8114
  this.recipientWord = computed(() => {
8039
8115
  const n = this.bulkList().length;
8040
- const plural = this.recipientNoun() || "recipients";
8116
+ const plural = this.recipientNoun() || this.labels.t("messagingSendMessagePanel.recipientsWord");
8041
8117
  if (n !== 1) return plural;
8042
8118
  return this.recipientNounSingular() || (plural.endsWith("s") ? plural.slice(0, -1) : plural);
8043
8119
  }, ...ngDevMode ? [{ debugName: "recipientWord" }] : /* istanbul ignore next */ []);
8044
8120
  this.skippedText = computed(() => {
8045
8121
  const n = this.skippedRecipients().length;
8046
8122
  if (n === 0) return "";
8047
- const what = this.channel() === "email" ? "email" : "text";
8048
- 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
+ });
8049
8130
  }, ...ngDevMode ? [{ debugName: "skippedText" }] : /* istanbul ignore next */ []);
8050
8131
  this.noneEligibleText = computed(() => {
8051
- 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
+ });
8052
8139
  }, ...ngDevMode ? [{ debugName: "noneEligibleText" }] : /* istanbul ignore next */ []);
8053
8140
  this.bulkAnnouncement = computed(() => {
8054
8141
  if (!this.bulkMode()) return "";
8055
8142
  const n = this.bulkList().length;
8056
8143
  const ok = this.activeRecipients().length;
8057
- const ch = this.channel() === "email" ? "email" : "text";
8058
- 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
+ });
8059
8150
  const skipped = this.skippedRecipients().length;
8060
- 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 }) : "");
8061
8158
  }, ...ngDevMode ? [{ debugName: "bulkAnnouncement" }] : /* istanbul ignore next */ []);
8062
8159
  this.lastBulkResult = void 0;
8063
8160
  this.showJobPicker = input(false, ...ngDevMode ? [{ debugName: "showJobPicker" }] : /* istanbul ignore next */ []);
8064
- 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 */ []);
8065
8166
  this.jobHint = input("", ...ngDevMode ? [{ debugName: "jobHint" }] : /* istanbul ignore next */ []);
8066
8167
  this.jobSearch = input(null, ...ngDevMode ? [{ debugName: "jobSearch" }] : /* istanbul ignore next */ []);
8067
8168
  this.jobSelected = output();
@@ -8093,10 +8194,10 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8093
8194
  if (!c || this.inApp()) return [];
8094
8195
  const notices = [];
8095
8196
  const hasEmail = !!c.email, hasPhone = !!c.phone;
8096
- if (!this.textEnabled()) notices.push("Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.");
8097
- 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." : ""}`);
8098
- else if (!hasPhone) notices.push(`${c.name} has no phone number on file, so a text needs one added first.`);
8099
- 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 }));
8100
8201
  return notices;
8101
8202
  }, ...ngDevMode ? [{ debugName: "contactNotices" }] : /* istanbul ignore next */ []);
8102
8203
  this.channel = signal("email", ...ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []);
@@ -8296,8 +8397,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8296
8397
  isRequired: false,
8297
8398
  transformFunction: null
8298
8399
  },
8299
- title: {
8300
- classPropertyName: "title",
8400
+ titleInput: {
8401
+ classPropertyName: "titleInput",
8301
8402
  publicName: "title",
8302
8403
  isSignal: true,
8303
8404
  isRequired: false,
@@ -8352,8 +8453,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8352
8453
  isRequired: false,
8353
8454
  transformFunction: null
8354
8455
  },
8355
- receiptLabel: {
8356
- classPropertyName: "receiptLabel",
8456
+ receiptLabelInput: {
8457
+ classPropertyName: "receiptLabelInput",
8357
8458
  publicName: "receiptLabel",
8358
8459
  isSignal: true,
8359
8460
  isRequired: false,
@@ -8576,8 +8677,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8576
8677
  isRequired: false,
8577
8678
  transformFunction: null
8578
8679
  },
8579
- jobLabel: {
8580
- classPropertyName: "jobLabel",
8680
+ jobLabelInput: {
8681
+ classPropertyName: "jobLabelInput",
8581
8682
  publicName: "jobLabel",
8582
8683
  isSignal: true,
8583
8684
  isRequired: false,
@@ -8637,8 +8738,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8637
8738
  [open]="open()"
8638
8739
  [title]="title()"
8639
8740
  [wide]="wide()"
8640
- cancelLabel="Cancel"
8641
- [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'))) : ''"
8642
8743
  primaryVariant="warning"
8643
8744
  [primaryDisabled]="!valid() || bulkBlocked()"
8644
8745
  (closed)="onClose()"
@@ -8655,25 +8756,25 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8655
8756
  @if (!loading() && !hasTarget() && !inApp()) {
8656
8757
  <div class="msg-step">
8657
8758
  <span class="msg-step-badge">1</span>
8658
- <span class="msg-step-text">Who would you like to message?</span>
8759
+ <span class="msg-step-text">{{ labels.t('messagingSendMessagePanel.whoWouldYouLikeTo') }}</span>
8659
8760
  </div>
8660
8761
  <ui-contact-picker
8661
- placeholder="Search by contact name..."
8762
+ [placeholder]="labels.t('messagingSendMessagePanel.searchByContactName')"
8662
8763
  (contactSelected)="onContactSelected($event)" />
8663
8764
  }
8664
8765
 
8665
8766
  @if (!loading() && hasTarget()) {
8666
8767
  @if (bulkMode()) {
8667
- <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
8668
- <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>
8669
8770
  <div class="msg-bulk-counts">
8670
- <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'" />
8671
8772
  @if (textEnabled()) {
8672
- <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'" />
8673
8774
  }
8674
8775
  </div>
8675
8776
  @if (!textEnabled()) {
8676
- <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>
8677
8778
  }
8678
8779
  <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
8679
8780
  @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
@@ -8683,7 +8784,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8683
8784
  <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
8684
8785
  }
8685
8786
  @if (bulkMissingSend()) {
8686
- <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>
8687
8788
  }
8688
8789
  @if (!bulkNoneEligible()) {
8689
8790
  <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
@@ -8693,23 +8794,19 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8693
8794
 
8694
8795
  @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
8695
8796
  <div class="msg-channel-row" data-testid="bulk-channel-row">
8696
- <label class="form-label msg-via-label">Send via</label>
8797
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
8697
8798
  @if (sending()) {
8698
8799
  <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
8699
8800
  <div class="msg-via-tabs">
8700
- <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
8701
- <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>
8702
8803
  </div>
8703
8804
  } @else {
8704
8805
  <div class="msg-via-tabs">
8705
8806
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8706
- <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>
8707
- Email
8708
- </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>
8709
8808
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8710
- <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>
8711
- Text
8712
- </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>
8713
8810
  </div>
8714
8811
  }
8715
8812
  </div>
@@ -8717,16 +8814,12 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8717
8814
 
8718
8815
  @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8719
8816
  <div class="msg-channel-row">
8720
- <label class="form-label msg-via-label">Send via</label>
8817
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
8721
8818
  <div class="msg-via-tabs">
8722
8819
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8723
- <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>
8724
- Email
8725
- </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>
8726
8821
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8727
- <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>
8728
- Text
8729
- </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>
8730
8823
  </div>
8731
8824
  </div>
8732
8825
  }
@@ -8743,8 +8836,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8743
8836
  @for (epoch of [formEpoch()]; track epoch) {
8744
8837
  <div class="msg-job">
8745
8838
  <ui-combobox
8746
- [label]="jobLabel()" placeholder="Search by job name…" [hint]="jobHint()"
8747
- [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"
8748
8841
  [value]="jobValue()"
8749
8842
  (searchChange)="jobSearch$.next($event)" (valueChange)="onJobPick($event)" />
8750
8843
  </div>
@@ -9113,8 +9206,8 @@ i0.ɵɵngDeclareClassMetadata({
9113
9206
  [open]="open()"
9114
9207
  [title]="title()"
9115
9208
  [wide]="wide()"
9116
- cancelLabel="Cancel"
9117
- [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'))) : ''"
9118
9211
  primaryVariant="warning"
9119
9212
  [primaryDisabled]="!valid() || bulkBlocked()"
9120
9213
  (closed)="onClose()"
@@ -9131,25 +9224,25 @@ i0.ɵɵngDeclareClassMetadata({
9131
9224
  @if (!loading() && !hasTarget() && !inApp()) {
9132
9225
  <div class="msg-step">
9133
9226
  <span class="msg-step-badge">1</span>
9134
- <span class="msg-step-text">Who would you like to message?</span>
9227
+ <span class="msg-step-text">{{ labels.t('messagingSendMessagePanel.whoWouldYouLikeTo') }}</span>
9135
9228
  </div>
9136
9229
  <ui-contact-picker
9137
- placeholder="Search by contact name..."
9230
+ [placeholder]="labels.t('messagingSendMessagePanel.searchByContactName')"
9138
9231
  (contactSelected)="onContactSelected($event)" />
9139
9232
  }
9140
9233
 
9141
9234
  @if (!loading() && hasTarget()) {
9142
9235
  @if (bulkMode()) {
9143
- <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
9144
- <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>
9145
9238
  <div class="msg-bulk-counts">
9146
- <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'" />
9147
9240
  @if (textEnabled()) {
9148
- <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'" />
9149
9242
  }
9150
9243
  </div>
9151
9244
  @if (!textEnabled()) {
9152
- <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>
9153
9246
  }
9154
9247
  <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
9155
9248
  @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
@@ -9159,7 +9252,7 @@ i0.ɵɵngDeclareClassMetadata({
9159
9252
  <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
9160
9253
  }
9161
9254
  @if (bulkMissingSend()) {
9162
- <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>
9163
9256
  }
9164
9257
  @if (!bulkNoneEligible()) {
9165
9258
  <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
@@ -9169,23 +9262,19 @@ i0.ɵɵngDeclareClassMetadata({
9169
9262
 
9170
9263
  @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
9171
9264
  <div class="msg-channel-row" data-testid="bulk-channel-row">
9172
- <label class="form-label msg-via-label">Send via</label>
9265
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
9173
9266
  @if (sending()) {
9174
9267
  <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
9175
9268
  <div class="msg-via-tabs">
9176
- <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
9177
- <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>
9178
9271
  </div>
9179
9272
  } @else {
9180
9273
  <div class="msg-via-tabs">
9181
9274
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9182
- <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>
9183
- Email
9184
- </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>
9185
9276
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9186
- <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>
9187
- Text
9188
- </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>
9189
9278
  </div>
9190
9279
  }
9191
9280
  </div>
@@ -9193,16 +9282,12 @@ i0.ɵɵngDeclareClassMetadata({
9193
9282
 
9194
9283
  @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
9195
9284
  <div class="msg-channel-row">
9196
- <label class="form-label msg-via-label">Send via</label>
9285
+ <label class="form-label msg-via-label">{{ labels.t('messagingSendMessagePanel.sendVia') }}</label>
9197
9286
  <div class="msg-via-tabs">
9198
9287
  <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9199
- <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>
9200
- Email
9201
- </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>
9202
9289
  <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9203
- <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>
9204
- Text
9205
- </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>
9206
9291
  </div>
9207
9292
  </div>
9208
9293
  }
@@ -9219,8 +9304,8 @@ i0.ɵɵngDeclareClassMetadata({
9219
9304
  @for (epoch of [formEpoch()]; track epoch) {
9220
9305
  <div class="msg-job">
9221
9306
  <ui-combobox
9222
- [label]="jobLabel()" placeholder="Search by job name…" [hint]="jobHint()"
9223
- [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"
9224
9309
  [value]="jobValue()"
9225
9310
  (searchChange)="jobSearch$.next($event)" (valueChange)="onJobPick($event)" />
9226
9311
  </div>
@@ -9398,7 +9483,7 @@ i0.ɵɵngDeclareClassMetadata({
9398
9483
  required: false
9399
9484
  }]
9400
9485
  }],
9401
- title: [{
9486
+ titleInput: [{
9402
9487
  type: i0.Input,
9403
9488
  args: [{
9404
9489
  isSignal: true,
@@ -9462,7 +9547,7 @@ i0.ɵɵngDeclareClassMetadata({
9462
9547
  required: false
9463
9548
  }]
9464
9549
  }],
9465
- receiptLabel: [{
9550
+ receiptLabelInput: [{
9466
9551
  type: i0.Input,
9467
9552
  args: [{
9468
9553
  isSignal: true,
@@ -9718,7 +9803,7 @@ i0.ɵɵngDeclareClassMetadata({
9718
9803
  required: false
9719
9804
  }]
9720
9805
  }],
9721
- jobLabel: [{
9806
+ jobLabelInput: [{
9722
9807
  type: i0.Input,
9723
9808
  args: [{
9724
9809
  isSignal: true,