agentvibes 4.4.1 → 4.5.7

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.
Files changed (55) hide show
  1. package/.agentvibes/config.json +4 -4
  2. package/.claude/config/audio-effects.cfg +1 -0
  3. package/.claude/config/background-music-enabled.txt +1 -0
  4. package/.claude/config/reverb-level.txt +1 -1
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/audio-processor.sh +1 -1
  7. package/.claude/hooks/bmad-speak.sh +16 -2
  8. package/.claude/hooks-windows/bmad-speak.ps1 +200 -0
  9. package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
  10. package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
  11. package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
  12. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
  13. package/.claude/hooks-windows/play-tts.ps1 +14 -6
  14. package/.claude/hooks-windows/provider-manager.ps1 +16 -1
  15. package/CLAUDE.md +4 -0
  16. package/README.md +39 -9
  17. package/RELEASE_NOTES.md +78 -0
  18. package/bin/agent-vibes +1 -1
  19. package/bin/agentvibes-voice-browser.js +1 -1
  20. package/bin/bmad-speak.js +52 -0
  21. package/bin/mcp-server.js +1 -1
  22. package/bin/test-bmad-pr +1 -1
  23. package/package.json +1 -1
  24. package/setup-windows.ps1 +4 -4
  25. package/src/console/app.js +63 -12
  26. package/src/console/navigation.js +5 -2
  27. package/src/console/tabs/agents-tab.js +72 -76
  28. package/src/console/tabs/help-tab.js +107 -54
  29. package/src/console/tabs/install-tab.js +132 -56
  30. package/src/console/tabs/music-tab.js +1039 -1011
  31. package/src/console/tabs/placeholder-tab.js +27 -0
  32. package/src/console/tabs/readme-tab.js +9 -7
  33. package/src/console/tabs/receiver-tab.js +23 -12
  34. package/src/console/tabs/settings-tab.js +4001 -3783
  35. package/src/console/tabs/voices-tab.js +1680 -1653
  36. package/src/console/widgets/personality-picker.js +35 -7
  37. package/src/console/widgets/reverb-picker.js +9 -6
  38. package/src/console/widgets/track-picker.js +7 -2
  39. package/src/i18n/de.js +203 -0
  40. package/src/i18n/en.js +203 -0
  41. package/src/i18n/es.js +203 -0
  42. package/src/i18n/fr.js +203 -0
  43. package/src/i18n/hi.js +203 -0
  44. package/src/i18n/ja.js +203 -0
  45. package/src/i18n/ko.js +203 -0
  46. package/src/i18n/pt.js +203 -0
  47. package/src/i18n/strings.js +54 -0
  48. package/src/i18n/zh-CN.js +203 -0
  49. package/src/installer/language-screen.js +31 -0
  50. package/src/installer.js +79 -25
  51. package/src/services/language-service.js +47 -0
  52. package/src/utils/file-ownership-verifier.js +2 -2
  53. package/src/utils/provider-validator.js +9 -13
  54. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
  55. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +0 -108
@@ -19,6 +19,7 @@ import { promisify } from 'node:util';
19
19
  import fs from 'node:fs';
20
20
  import { promises as _fsP } from 'node:fs';
21
21
  import { buildAudioEnv } from '../audio-env.js';
22
+ import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
22
23
  import {
23
24
  copyCommandFiles, copyHookFiles, copyPersonalityFiles,
24
25
  copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
@@ -140,7 +141,7 @@ function createTestStub() {
140
141
  hide: () => {},
141
142
  onFocus: () => {},
142
143
  onBlur: () => {},
143
- getFooterText: () => FOOTER_TEXT,
144
+ getFooterText: () => t('en', 'footerText'),
144
145
  getFooterColor: () => COLORS.footerBg,
145
146
  };
146
147
  }
@@ -158,7 +159,7 @@ function createTestStub() {
158
159
  export function createInstallTab(screen, services) {
159
160
  if (IS_TEST) return createTestStub();
160
161
 
161
- const { configService, providerService, navigationService, focusMainTabBar } = services;
162
+ const { configService, providerService, navigationService, focusMainTabBar, languageService } = services;
162
163
 
163
164
  // -------------------------------------------------------------------------
164
165
  // Container
@@ -178,8 +179,13 @@ export function createInstallTab(screen, services) {
178
179
  // -------------------------------------------------------------------------
179
180
  // Wizard state
180
181
 
181
- let _screen = 1;
182
- let _lastScreen = 0;
182
+ let _screen = 0;
183
+ let _lastScreen = -1;
184
+ // _lang is now owned by languageService; keep a local helper for convenience
185
+ // and a local _langIdx for the language-picker UI (Screen 0).
186
+ const _getLang = () => languageService?.getLang() ?? 'en';
187
+ const _tl = (key) => languageService?.t(key) ?? t('en', key);
188
+ let _langIdx = 0;
183
189
  let _deps = null;
184
190
  let _checking = false;
185
191
  let _selectedProvider = null;
@@ -437,9 +443,10 @@ export function createInstallTab(screen, services) {
437
443
 
438
444
  _renderScreen5();
439
445
 
440
- // Show OK button now that install is done (success or error)
441
- _s5OkBtn.show();
442
- _s5OkBtn.focus();
446
+ // Show buttons now that install is done (success or error)
447
+ _s5QuitBtn.show();
448
+ _s5CustomizeBtn.show();
449
+ _s5QuitBtn.focus();
443
450
  screen.render();
444
451
 
445
452
  // Play TTS greeting on success
@@ -525,8 +532,8 @@ export function createInstallTab(screen, services) {
525
532
  return btn;
526
533
  }
527
534
 
528
- const _editBtn = _createInstallBtn('Edit', '#1565c0', _doEdit);
529
- const _acceptBtn = _createInstallBtn('✓ Accept & Install', COLORS.btnDefault, _doAccept);
535
+ const _editBtn = _createInstallBtn(_tl('editInstallBtn'), '#1565c0', _doEdit);
536
+ const _acceptBtn = _createInstallBtn(_tl('acceptInstallBtn'), COLORS.btnDefault, _doAccept);
530
537
 
531
538
  // Edit sits inline with the intro text row; Accept & Install is below
532
539
  _editBtn.top = 8; _editBtn.left = 36;
@@ -540,11 +547,11 @@ export function createInstallTab(screen, services) {
540
547
  // -------------------------------------------------------------------------
541
548
  // Screen 1 buttons — Begin (cyan) and Exit (grey)
542
549
 
543
- const _s1BeginBtn = _createInstallBtn('▶ Begin', '#00838f', () => {
550
+ const _s1BeginBtn = _createInstallBtn(_tl('beginBtn'), '#00838f', () => {
544
551
  _screen++;
545
552
  _showCurrentScreen();
546
553
  });
547
- const _s1ExitBtn = _createInstallBtn('✗ Exit', '#546e7a', () => {
554
+ const _s1ExitBtn = _createInstallBtn(_tl('exitBtn'), '#546e7a', () => {
548
555
  box.hide();
549
556
  screen.render();
550
557
  if (typeof focusMainTabBar === 'function') focusMainTabBar();
@@ -562,7 +569,7 @@ export function createInstallTab(screen, services) {
562
569
  // -------------------------------------------------------------------------
563
570
  // Screen 2 button — Continue (shown after deps check passes)
564
571
 
565
- const _s2ContinueBtn = _createInstallBtn('Continue →', '#1565c0', () => {
572
+ const _s2ContinueBtn = _createInstallBtn(_tl('continueArrowBtn'), '#1565c0', () => {
566
573
  _screen++;
567
574
  _showCurrentScreen();
568
575
  });
@@ -573,12 +580,25 @@ export function createInstallTab(screen, services) {
573
580
  // Screen 3: no Continue button — Enter/→ on the list confirms selection and advances
574
581
 
575
582
  // -------------------------------------------------------------------------
576
- // Screen 5 buttonOK (summary page only, config already saved on screen 4)
583
+ // Screen 5 buttonsCustomize More (left) + Done - Quit (right, default focused)
584
+ // Layout: [ Customize More ] [ Done - Quit ]
585
+ // Button width = label + 2 padding chars; gap between buttons = 3 chars
577
586
 
578
- const _s5OkBtn = _createInstallBtn('✓ OK — Done', '#1565c0', () => {
587
+ const _s5CustomizeBtn = _createInstallBtn(_tl('doneCustomizeBtn'), '#1565c0', () => {
579
588
  _dismissCompletionModal();
580
589
  });
581
- _s5OkBtn.bottom = 3; _s5OkBtn.left = 4; // bottom-anchored: sits above hintLine (bottom:2)
590
+ _s5CustomizeBtn.bottom = 3; _s5CustomizeBtn.left = 4;
591
+
592
+ const _s5QuitBtn = _createInstallBtn(_tl('doneQuitBtn'), '#b71c1c', () => {
593
+ screen.destroy();
594
+ process.exit(0);
595
+ });
596
+ // left = start(4) + customizeLabel + 2 padding + 3 gap
597
+ _s5QuitBtn.bottom = 3; _s5QuitBtn.left = 4 + _tl('doneCustomizeBtn').length + 2 + 3;
598
+
599
+ // Arrow/Tab navigation between the two buttons
600
+ _s5CustomizeBtn.key(['tab', 'right'], () => { _s5QuitBtn.focus(); screen.render(); });
601
+ _s5QuitBtn.key(['tab', 'left', 'S-tab'], () => { _s5CustomizeBtn.focus(); screen.render(); });
582
602
 
583
603
  // -------------------------------------------------------------------------
584
604
  // Screen renderers
@@ -586,15 +606,35 @@ export function createInstallTab(screen, services) {
586
606
  const _HDR = (emoji, label) =>
587
607
  `{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'─'.repeat(100)}{/${COLORS.sectionHdr}-fg}`;
588
608
 
609
+ function _renderScreen0() {
610
+ const lines = [
611
+ _HDR('🌐', 'Language / Idioma / Langue / Sprache / 言語 / भाषा / 语言 / 언어'),
612
+ '',
613
+ ' Select your language:',
614
+ '',
615
+ ...SUPPORTED_LANGUAGES.map((l, i) =>
616
+ i === _langIdx
617
+ ? ` {green-fg}► ${l.name}{/green-fg}`
618
+ : ` ${l.name}`
619
+ ),
620
+ ];
621
+ contentBox.setContent(_c(lines));
622
+ hintLine.setContent(' Screen 0: Language | [↑/↓] Select | [Enter] Apply & Continue | [→] Skip (English)');
623
+ screen.render();
624
+ }
625
+
589
626
  function _renderScreen1() {
627
+ // Update button labels to current language before focus triggers the decorator
628
+ _s1BeginBtn.setContent(t(_getLang(), 'beginBtn'));
629
+ _s1ExitBtn.setContent(t(_getLang(), 'exitBtn'));
590
630
  contentBox.setContent(_c([
591
- _HDR('🔧', 'Setup Wizard'),
631
+ _HDR('🔧', t(_getLang(), 'setupWizard')),
592
632
  '',
593
- ` {${COLORS.noticeFg}-fg}TTS for AI assistants with personality.{/${COLORS.noticeFg}-fg}`,
633
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'setupWizardSubtitle')}{/${COLORS.noticeFg}-fg}`,
594
634
  '',
595
635
  '', // ← [▶ Begin] [✗ Exit] buttons here (box row 5)
596
636
  ]));
597
- hintLine.setContent(' Screen 1/5: Welcome | [←/→] Navigate | [Enter] Begin | [Esc] Exit');
637
+ hintLine.setContent(` ${t(_getLang(), 'screen1Hint')}`);
598
638
  _s1BeginBtn.focus();
599
639
  screen.render();
600
640
  }
@@ -606,19 +646,19 @@ export function createInstallTab(screen, services) {
606
646
  _s2ContinueBtn.hide(); // hidden during spinner
607
647
 
608
648
  contentBox.setContent(_c([
609
- _HDR('🔍', 'Dependency Check'),
649
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
610
650
  '',
611
- ` {${COLORS.noticeFg}-fg}${frames[0]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
651
+ ` {${COLORS.noticeFg}-fg}${frames[0]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
612
652
  ]));
613
- hintLine.setContent(' Screen 2/5: Dependencies | [←] Back | [Enter] Next');
653
+ hintLine.setContent(` ${t(_getLang(), 'screen2Hint')}`);
614
654
  screen.render();
615
655
 
616
656
  const spinInterval = setInterval(() => {
617
657
  frameIdx = (frameIdx + 1) % frames.length;
618
658
  contentBox.setContent(_c([
619
- _HDR('🔍', 'Dependency Check'),
659
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
620
660
  '',
621
- ` {${COLORS.noticeFg}-fg}${frames[frameIdx]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
661
+ ` {${COLORS.noticeFg}-fg}${frames[frameIdx]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
622
662
  ]));
623
663
  screen.render();
624
664
  }, 100);
@@ -630,28 +670,29 @@ export function createInstallTab(screen, services) {
630
670
  _checking = false;
631
671
  }
632
672
 
633
- const ok = () => `{${COLORS.successFg}-fg}✅ Installed{/${COLORS.successFg}-fg}`;
634
- const bad = () => `{${COLORS.errorFg}-fg}❌ Not found{/${COLORS.errorFg}-fg}`;
673
+ const ok = () => `{${COLORS.successFg}-fg}✅ ${t(_getLang(), 'installed')}{/${COLORS.successFg}-fg}`;
674
+ const bad = () => `{${COLORS.errorFg}-fg}❌ ${t(_getLang(), 'notFound')}{/${COLORS.errorFg}-fg}`;
635
675
 
636
676
  const ttsOk = _deps.piper || _deps.soprano;
637
677
  contentBox.setContent(_c([
638
- _HDR('🔍', 'Dependency Check'),
678
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
639
679
  '',
640
- ` {${COLORS.noticeFg}-fg}${'Dependency'.padEnd(14)}Status{/${COLORS.noticeFg}-fg}`,
680
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'depColumn').padEnd(14)}${t(_getLang(), 'statusColumn')}{/${COLORS.noticeFg}-fg}`,
641
681
  ` {${COLORS.noticeFg}-fg}${'─'.repeat(78)}{/${COLORS.noticeFg}-fg}`,
642
682
  ` {${COLORS.labelFg}-fg}${'Node.js'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.node ? ok() : bad()}`,
643
683
  ` {${COLORS.labelFg}-fg}${'npm'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.npm ? ok() : bad()}`,
644
684
  ` {${COLORS.labelFg}-fg}${'Piper TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.piper ? ok() : bad()}`,
645
685
  ` {${COLORS.labelFg}-fg}${'Soprano TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.soprano ? ok() : bad()}`,
646
- ` {${COLORS.labelFg}-fg}${'ffmpeg'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.ffmpeg ? ok() : `{${COLORS.errorFg}-fg}⚠ Not found (needed for background music){/${COLORS.errorFg}-fg}`}`,
686
+ ` {${COLORS.labelFg}-fg}${'ffmpeg'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.ffmpeg ? ok() : `{${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'ffmpegMissing')}{/${COLORS.errorFg}-fg}`}`,
647
687
  '',
648
688
  ttsOk
649
- ? ` {${COLORS.successFg}-fg}✅ TTS Providers Detected{/${COLORS.successFg}-fg}`
650
- : ` {${COLORS.errorFg}-fg}⚠ No TTS provider found. Install Piper or Soprano first.{/${COLORS.errorFg}-fg}`,
689
+ ? ` {${COLORS.successFg}-fg}✅ ${t(_getLang(), 'ttsDetected')}{/${COLORS.successFg}-fg}`
690
+ : ` {${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'noTtsFound')}{/${COLORS.errorFg}-fg}`,
651
691
  '', // blank separator
652
692
  '', // ← [Continue →] button here (box row 12) when TTS detected
653
693
  ]));
654
694
  if (ttsOk) {
695
+ _s2ContinueBtn.setContent(_tl('continueArrowBtn'));
655
696
  _s2ContinueBtn.show();
656
697
  _s2ContinueBtn.focus();
657
698
  }
@@ -685,14 +726,14 @@ export function createInstallTab(screen, services) {
685
726
  const _blank = ' '.repeat(120);
686
727
  const _trail = Array(12).fill(_blank);
687
728
  contentBox.setContent(_c([
688
- _HDR('🎤', 'Provider Selection'),
729
+ _HDR('🎤', t(_getLang(), 'providerSelection')),
689
730
  '',
690
- ` {${COLORS.noticeFg}-fg}${'Available TTS providers:'.padEnd(94)}{/${COLORS.noticeFg}-fg}`,
731
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'availableProviders').padEnd(94)}{/${COLORS.noticeFg}-fg}`,
691
732
  '',
692
733
  ...paddedItems.map(i => ` ${i}`),
693
734
  ..._trail,
694
735
  ]));
695
- hintLine.setContent(' Screen 3/5: Provider | [←] Back | [↑↓] Choose | [Enter/→] Confirm & Continue');
736
+ hintLine.setContent(` ${t(_getLang(), 'screen3Hint')}`);
696
737
  box.focus();
697
738
  screen.render();
698
739
  }
@@ -705,36 +746,38 @@ export function createInstallTab(screen, services) {
705
746
  const voiceId = providerService?.getActiveVoiceId?.() ?? 'en_US-amy-medium';
706
747
 
707
748
  contentBox.setContent(_c([
708
- _HDR('🎤', 'Provider & Voice'),
749
+ _HDR('🎤', t(_getLang(), 'providerAndVoice')),
709
750
  '',
710
- ` {${COLORS.labelFg}-fg}${'Provider:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${provider}{/${COLORS.valueFg}-fg}`,
711
- ` {${COLORS.labelFg}-fg}${'Voice:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${voiceId}{/${COLORS.valueFg}-fg} {${COLORS.noticeFg}-fg}(after installation, you can change in Settings){/${COLORS.noticeFg}-fg}`,
751
+ ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'providerLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${provider}{/${COLORS.valueFg}-fg}`,
752
+ ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'voiceLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${voiceId}{/${COLORS.valueFg}-fg} {${COLORS.noticeFg}-fg}${t(_getLang(), 'voiceChangeHint')}{/${COLORS.noticeFg}-fg}`,
712
753
  '',
713
- _HDR('✍️', 'Intro Text'),
754
+ _HDR('✍️', t(_getLang(), 'introText')),
714
755
  '',
715
- ` {${COLORS.labelFg}-fg}${'Intro text:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || '(none)'}{/${COLORS.valueFg}-fg}`,
756
+ ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'introTextLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || `(${t(_getLang(), 'none')})`}{/${COLORS.valueFg}-fg}`,
716
757
  // ↑ [Edit] button rendered inline at box row 8, left=36
717
758
  '',
718
- ` {${COLORS.noticeFg}-fg}Example:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
759
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'example')}:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
719
760
  '',
720
761
  '',
721
762
  '', // ← [✓ Accept & Install] button rendered as real widget here (box row 13)
722
763
  ]));
723
- hintLine.setContent(' Screen 4/5: Config | [Esc] Back | [E] Edit | [↓] Accept & Install');
764
+ hintLine.setContent(` ${t(_getLang(), 'screen4Hint')}`);
765
+ _editBtn.setContent(_tl('editInstallBtn'));
766
+ _acceptBtn.setContent(_tl('acceptInstallBtn'));
724
767
  _acceptBtn.focus();
725
768
  screen.render();
726
769
  }
727
770
 
728
771
  function _renderScreen5() {
729
772
  const header = _installError
730
- ? _HDR('❌', 'Installation Failed')
773
+ ? _HDR('❌', t(_getLang(), 'installationFailed'))
731
774
  : _installComplete
732
- ? _HDR('✅', 'Installation Complete')
733
- : _HDR('⚙️', 'Installing AgentVibes...');
775
+ ? _HDR('✅', t(_getLang(), 'installComplete'))
776
+ : _HDR('⚙️', t(_getLang(), 'installing'));
734
777
 
735
778
  const hint = (_installComplete || _installError)
736
- ? ' Screen 5/5: Complete | [Enter] OK — Done'
737
- : ' Screen 5/5: Installing... | Please wait';
779
+ ? ` ${t(_getLang(), 'screen5HintDone')}`
780
+ : ` ${t(_getLang(), 'screen5HintWait')}`;
738
781
 
739
782
  // Show last 18 log lines so content fits in the box
740
783
  const MAX_LINES = 18;
@@ -778,7 +821,7 @@ export function createInstallTab(screen, services) {
778
821
  _completionModalBox = null;
779
822
  }
780
823
  _completionModalOpen = false;
781
- _screen = 1;
824
+ _screen = 0;
782
825
  box.hide();
783
826
  _showInstallNotice('Installation Complete — Settings Saved');
784
827
  screen.render();
@@ -793,14 +836,18 @@ export function createInstallTab(screen, services) {
793
836
  _s1BeginBtn.hide(); _s1ExitBtn.hide();
794
837
  }
795
838
 
839
+ // Screen 0 has no button widgets — nav is handled via key handlers
840
+
796
841
  // Screen 2 continue button: hidden on other screens; _renderScreen2 manages show/focus
797
842
  if (_screen !== 2) _s2ContinueBtn.hide();
798
843
 
799
- // Screen 5 OK button: hidden during active install, shown by _runInstall() on completion
844
+ // Screen 5 buttons: hidden during active install, shown by _runInstall() on completion
800
845
  if (_screen === 5 && (_installComplete || _installError)) {
801
- _s5OkBtn.show();
846
+ _s5QuitBtn.show();
847
+ _s5CustomizeBtn.show();
802
848
  } else {
803
- _s5OkBtn.hide();
849
+ _s5QuitBtn.hide();
850
+ _s5CustomizeBtn.hide();
804
851
  }
805
852
 
806
853
  // Show Screen 4 action buttons only on screen 4
@@ -842,6 +889,7 @@ export function createInstallTab(screen, services) {
842
889
  setTimeout(() => {
843
890
  if (_screen !== targetScreen) return;
844
891
  switch (_screen) {
892
+ case 0: _renderScreen0(); break;
845
893
  case 1: _renderScreen1(); break;
846
894
  case 2: _renderScreen2(); break;
847
895
  case 3: _renderScreen3(); break;
@@ -852,6 +900,7 @@ export function createInstallTab(screen, services) {
852
900
  return;
853
901
  }
854
902
  switch (_screen) {
903
+ case 0: _renderScreen0(); break;
855
904
  case 1: _renderScreen1(); break;
856
905
  case 2: _renderScreen2(); break;
857
906
  case 3: _renderScreen3(); break;
@@ -870,6 +919,12 @@ export function createInstallTab(screen, services) {
870
919
  screen.key(['enter'], () => {
871
920
  if (box.hidden || _checking) return;
872
921
  if (_completionModalOpen) { _dismissCompletionModal(); return; } // always first
922
+ if (_screen === 0) { // Screen 0: apply selected language and advance
923
+ if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
924
+ _screen = 1;
925
+ _showCurrentScreen();
926
+ return;
927
+ }
873
928
  if (_screen === 1) return; // Screen 1: Enter handled by Begin/Exit buttons
874
929
  if (_screen === 2) return; // Screen 2: Enter handled by Continue button
875
930
  if (_screen === 4) return; // Screen 4: Enter handled by the focused button
@@ -883,7 +938,7 @@ export function createInstallTab(screen, services) {
883
938
  screen.key(['escape'], () => {
884
939
  if (box.hidden || _checking) return;
885
940
  if (_completionModalOpen) { _dismissCompletionModal(); return; }
886
- if (_screen > 1) {
941
+ if (_screen > 0) {
887
942
  _screen--;
888
943
  _showCurrentScreen();
889
944
  } else {
@@ -899,6 +954,11 @@ export function createInstallTab(screen, services) {
899
954
 
900
955
  screen.key(['up'], () => {
901
956
  if (box.hidden) return;
957
+ if (_screen === 0) {
958
+ _langIdx = Math.max(0, _langIdx - 1);
959
+ _renderScreen0();
960
+ return;
961
+ }
902
962
  if (_screen === 3 && _deps) {
903
963
  const providers = [];
904
964
  if (_deps.piper) providers.push('piper');
@@ -914,16 +974,23 @@ export function createInstallTab(screen, services) {
914
974
  screen.key(['left'], () => {
915
975
  if (box.hidden || _checking) return;
916
976
  if (_screen === 4) return;
917
- if (_screen > 1) {
977
+ if (_screen > 0) {
918
978
  _screen--;
919
979
  _showCurrentScreen();
920
980
  }
921
981
  });
922
982
 
923
983
  // Right arrow = go forward (same logic as Enter, without save/finish side-effects)
984
+ // Screen 0: → skips language selection (keeps English)
924
985
  // Screen 1: right arrow handled by button ←/→ navigation
925
986
  screen.key(['right'], () => {
926
987
  if (box.hidden || _checking) return;
988
+ if (_screen === 0) { // → skips: keep current _lang (default 'en') and advance
989
+ if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
990
+ _screen = 1;
991
+ _showCurrentScreen();
992
+ return;
993
+ }
927
994
  if (_screen === 1) return;
928
995
  if (_screen === 2) return; // Screen 2: → handled by Continue button
929
996
  if (_screen === 3) { _screen++; _showCurrentScreen(); return; } // → confirms provider and advances
@@ -931,10 +998,15 @@ export function createInstallTab(screen, services) {
931
998
  if (_screen === 5) return; // Screen 5: → handled by button nav
932
999
  });
933
1000
 
934
- // Down arrow: Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
1001
+ // Down arrow: Screen 0 language nav; Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
935
1002
  // (tab bar's el.key(['down']) → onFocus() focuses Begin, then button ↓ → Exit)
936
1003
  screen.key(['down'], () => {
937
1004
  if (box.hidden) return;
1005
+ if (_screen === 0) {
1006
+ _langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
1007
+ _renderScreen0();
1008
+ return;
1009
+ }
938
1010
  if (_screen === 3 && _deps) {
939
1011
  const providers = [];
940
1012
  if (_deps.piper) providers.push('piper');
@@ -961,7 +1033,9 @@ export function createInstallTab(screen, services) {
961
1033
  box,
962
1034
 
963
1035
  show() {
964
- _screen = 1;
1036
+ _screen = 0;
1037
+ _langIdx = 0;
1038
+ // _lang now lives in languageService — don't reset it here
965
1039
  _screen5Announced = false;
966
1040
  _installLog = [];
967
1041
  _installRunning = false;
@@ -982,12 +1056,14 @@ export function createInstallTab(screen, services) {
982
1056
 
983
1057
  onFocus() {
984
1058
  // Focus the active interactive element, not just the box container
985
- if (_screen === 1) {
1059
+ if (_screen === 0) {
1060
+ box.focus(); // Screen 0 uses key handlers, no button widgets
1061
+ } else if (_screen === 1) {
986
1062
  _s1BeginBtn.focus();
987
1063
  } else if (_screen === 4) {
988
1064
  _editBtn.focus();
989
1065
  } else if (_screen === 5 && (_installComplete || _installError)) {
990
- _s5OkBtn.focus();
1066
+ _s5QuitBtn.focus();
991
1067
  } else {
992
1068
  box.focus();
993
1069
  }
@@ -997,7 +1073,7 @@ export function createInstallTab(screen, services) {
997
1073
  onBlur() {},
998
1074
 
999
1075
  getFooterText() {
1000
- return FOOTER_TEXT;
1076
+ return _tl('footerText');
1001
1077
  },
1002
1078
 
1003
1079
  getFooterColor() {