agentvibes 4.4.1 → 4.5.0

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 (50) hide show
  1. package/.agentvibes/config.json +4 -4
  2. package/.claude/config/reverb-level.txt +1 -1
  3. package/.claude/github-star-reminder.txt +1 -1
  4. package/.claude/hooks-windows/bmad-speak.ps1 +112 -0
  5. package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
  6. package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
  7. package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
  8. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
  9. package/.claude/hooks-windows/play-tts.ps1 +14 -6
  10. package/.claude/hooks-windows/provider-manager.ps1 +16 -1
  11. package/CLAUDE.md +4 -0
  12. package/README.md +39 -9
  13. package/RELEASE_NOTES.md +39 -0
  14. package/bin/agent-vibes +1 -1
  15. package/bin/agentvibes-voice-browser.js +1 -1
  16. package/bin/bmad-speak.js +52 -0
  17. package/bin/mcp-server.js +1 -1
  18. package/bin/test-bmad-pr +1 -1
  19. package/package.json +1 -1
  20. package/setup-windows.ps1 +4 -4
  21. package/src/console/app.js +58 -11
  22. package/src/console/tabs/agents-tab.js +61 -65
  23. package/src/console/tabs/help-tab.js +107 -54
  24. package/src/console/tabs/install-tab.js +107 -47
  25. package/src/console/tabs/music-tab.js +1030 -1011
  26. package/src/console/tabs/placeholder-tab.js +27 -0
  27. package/src/console/tabs/readme-tab.js +9 -7
  28. package/src/console/tabs/receiver-tab.js +23 -12
  29. package/src/console/tabs/settings-tab.js +4001 -3783
  30. package/src/console/tabs/voices-tab.js +1680 -1653
  31. package/src/console/widgets/personality-picker.js +35 -7
  32. package/src/console/widgets/reverb-picker.js +9 -6
  33. package/src/console/widgets/track-picker.js +6 -1
  34. package/src/i18n/de.js +201 -0
  35. package/src/i18n/en.js +201 -0
  36. package/src/i18n/es.js +201 -0
  37. package/src/i18n/fr.js +201 -0
  38. package/src/i18n/hi.js +201 -0
  39. package/src/i18n/ja.js +201 -0
  40. package/src/i18n/ko.js +201 -0
  41. package/src/i18n/pt.js +201 -0
  42. package/src/i18n/strings.js +54 -0
  43. package/src/i18n/zh-CN.js +201 -0
  44. package/src/installer/language-screen.js +31 -0
  45. package/src/installer.js +79 -25
  46. package/src/services/language-service.js +47 -0
  47. package/src/utils/file-ownership-verifier.js +2 -2
  48. package/src/utils/provider-validator.js +9 -13
  49. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
  50. 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;
@@ -525,8 +531,8 @@ export function createInstallTab(screen, services) {
525
531
  return btn;
526
532
  }
527
533
 
528
- const _editBtn = _createInstallBtn('Edit', '#1565c0', _doEdit);
529
- const _acceptBtn = _createInstallBtn('✓ Accept & Install', COLORS.btnDefault, _doAccept);
534
+ const _editBtn = _createInstallBtn(_tl('editInstallBtn'), '#1565c0', _doEdit);
535
+ const _acceptBtn = _createInstallBtn(_tl('acceptInstallBtn'), COLORS.btnDefault, _doAccept);
530
536
 
531
537
  // Edit sits inline with the intro text row; Accept & Install is below
532
538
  _editBtn.top = 8; _editBtn.left = 36;
@@ -540,11 +546,11 @@ export function createInstallTab(screen, services) {
540
546
  // -------------------------------------------------------------------------
541
547
  // Screen 1 buttons — Begin (cyan) and Exit (grey)
542
548
 
543
- const _s1BeginBtn = _createInstallBtn('▶ Begin', '#00838f', () => {
549
+ const _s1BeginBtn = _createInstallBtn(_tl('beginBtn'), '#00838f', () => {
544
550
  _screen++;
545
551
  _showCurrentScreen();
546
552
  });
547
- const _s1ExitBtn = _createInstallBtn('✗ Exit', '#546e7a', () => {
553
+ const _s1ExitBtn = _createInstallBtn(_tl('exitBtn'), '#546e7a', () => {
548
554
  box.hide();
549
555
  screen.render();
550
556
  if (typeof focusMainTabBar === 'function') focusMainTabBar();
@@ -562,7 +568,7 @@ export function createInstallTab(screen, services) {
562
568
  // -------------------------------------------------------------------------
563
569
  // Screen 2 button — Continue (shown after deps check passes)
564
570
 
565
- const _s2ContinueBtn = _createInstallBtn('Continue →', '#1565c0', () => {
571
+ const _s2ContinueBtn = _createInstallBtn(_tl('continueArrowBtn'), '#1565c0', () => {
566
572
  _screen++;
567
573
  _showCurrentScreen();
568
574
  });
@@ -575,7 +581,7 @@ export function createInstallTab(screen, services) {
575
581
  // -------------------------------------------------------------------------
576
582
  // Screen 5 button — OK (summary page only, config already saved on screen 4)
577
583
 
578
- const _s5OkBtn = _createInstallBtn('✓ OK — Done', '#1565c0', () => {
584
+ const _s5OkBtn = _createInstallBtn(_tl('okDoneBtn'), '#1565c0', () => {
579
585
  _dismissCompletionModal();
580
586
  });
581
587
  _s5OkBtn.bottom = 3; _s5OkBtn.left = 4; // bottom-anchored: sits above hintLine (bottom:2)
@@ -586,15 +592,35 @@ export function createInstallTab(screen, services) {
586
592
  const _HDR = (emoji, label) =>
587
593
  `{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'─'.repeat(100)}{/${COLORS.sectionHdr}-fg}`;
588
594
 
595
+ function _renderScreen0() {
596
+ const lines = [
597
+ _HDR('🌐', 'Language / Idioma / Langue / Sprache / 言語 / भाषा / 语言 / 언어'),
598
+ '',
599
+ ' Select your language:',
600
+ '',
601
+ ...SUPPORTED_LANGUAGES.map((l, i) =>
602
+ i === _langIdx
603
+ ? ` {green-fg}► ${l.name}{/green-fg}`
604
+ : ` ${l.name}`
605
+ ),
606
+ ];
607
+ contentBox.setContent(_c(lines));
608
+ hintLine.setContent(' Screen 0: Language | [↑/↓] Select | [Enter] Apply & Continue | [→] Skip (English)');
609
+ screen.render();
610
+ }
611
+
589
612
  function _renderScreen1() {
613
+ // Update button labels to current language before focus triggers the decorator
614
+ _s1BeginBtn.setContent(t(_getLang(), 'beginBtn'));
615
+ _s1ExitBtn.setContent(t(_getLang(), 'exitBtn'));
590
616
  contentBox.setContent(_c([
591
- _HDR('🔧', 'Setup Wizard'),
617
+ _HDR('🔧', t(_getLang(), 'setupWizard')),
592
618
  '',
593
- ` {${COLORS.noticeFg}-fg}TTS for AI assistants with personality.{/${COLORS.noticeFg}-fg}`,
619
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'setupWizardSubtitle')}{/${COLORS.noticeFg}-fg}`,
594
620
  '',
595
621
  '', // ← [▶ Begin] [✗ Exit] buttons here (box row 5)
596
622
  ]));
597
- hintLine.setContent(' Screen 1/5: Welcome | [←/→] Navigate | [Enter] Begin | [Esc] Exit');
623
+ hintLine.setContent(` ${t(_getLang(), 'screen1Hint')}`);
598
624
  _s1BeginBtn.focus();
599
625
  screen.render();
600
626
  }
@@ -606,19 +632,19 @@ export function createInstallTab(screen, services) {
606
632
  _s2ContinueBtn.hide(); // hidden during spinner
607
633
 
608
634
  contentBox.setContent(_c([
609
- _HDR('🔍', 'Dependency Check'),
635
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
610
636
  '',
611
- ` {${COLORS.noticeFg}-fg}${frames[0]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
637
+ ` {${COLORS.noticeFg}-fg}${frames[0]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
612
638
  ]));
613
- hintLine.setContent(' Screen 2/5: Dependencies | [←] Back | [Enter] Next');
639
+ hintLine.setContent(` ${t(_getLang(), 'screen2Hint')}`);
614
640
  screen.render();
615
641
 
616
642
  const spinInterval = setInterval(() => {
617
643
  frameIdx = (frameIdx + 1) % frames.length;
618
644
  contentBox.setContent(_c([
619
- _HDR('🔍', 'Dependency Check'),
645
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
620
646
  '',
621
- ` {${COLORS.noticeFg}-fg}${frames[frameIdx]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
647
+ ` {${COLORS.noticeFg}-fg}${frames[frameIdx]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
622
648
  ]));
623
649
  screen.render();
624
650
  }, 100);
@@ -630,28 +656,29 @@ export function createInstallTab(screen, services) {
630
656
  _checking = false;
631
657
  }
632
658
 
633
- const ok = () => `{${COLORS.successFg}-fg}✅ Installed{/${COLORS.successFg}-fg}`;
634
- const bad = () => `{${COLORS.errorFg}-fg}❌ Not found{/${COLORS.errorFg}-fg}`;
659
+ const ok = () => `{${COLORS.successFg}-fg}✅ ${t(_getLang(), 'installed')}{/${COLORS.successFg}-fg}`;
660
+ const bad = () => `{${COLORS.errorFg}-fg}❌ ${t(_getLang(), 'notFound')}{/${COLORS.errorFg}-fg}`;
635
661
 
636
662
  const ttsOk = _deps.piper || _deps.soprano;
637
663
  contentBox.setContent(_c([
638
- _HDR('🔍', 'Dependency Check'),
664
+ _HDR('🔍', t(_getLang(), 'dependencyCheck')),
639
665
  '',
640
- ` {${COLORS.noticeFg}-fg}${'Dependency'.padEnd(14)}Status{/${COLORS.noticeFg}-fg}`,
666
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'depColumn').padEnd(14)}${t(_getLang(), 'statusColumn')}{/${COLORS.noticeFg}-fg}`,
641
667
  ` {${COLORS.noticeFg}-fg}${'─'.repeat(78)}{/${COLORS.noticeFg}-fg}`,
642
668
  ` {${COLORS.labelFg}-fg}${'Node.js'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.node ? ok() : bad()}`,
643
669
  ` {${COLORS.labelFg}-fg}${'npm'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.npm ? ok() : bad()}`,
644
670
  ` {${COLORS.labelFg}-fg}${'Piper TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.piper ? ok() : bad()}`,
645
671
  ` {${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}`}`,
672
+ ` {${COLORS.labelFg}-fg}${'ffmpeg'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.ffmpeg ? ok() : `{${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'ffmpegMissing')}{/${COLORS.errorFg}-fg}`}`,
647
673
  '',
648
674
  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}`,
675
+ ? ` {${COLORS.successFg}-fg}✅ ${t(_getLang(), 'ttsDetected')}{/${COLORS.successFg}-fg}`
676
+ : ` {${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'noTtsFound')}{/${COLORS.errorFg}-fg}`,
651
677
  '', // blank separator
652
678
  '', // ← [Continue →] button here (box row 12) when TTS detected
653
679
  ]));
654
680
  if (ttsOk) {
681
+ _s2ContinueBtn.setContent(_tl('continueArrowBtn'));
655
682
  _s2ContinueBtn.show();
656
683
  _s2ContinueBtn.focus();
657
684
  }
@@ -685,14 +712,14 @@ export function createInstallTab(screen, services) {
685
712
  const _blank = ' '.repeat(120);
686
713
  const _trail = Array(12).fill(_blank);
687
714
  contentBox.setContent(_c([
688
- _HDR('🎤', 'Provider Selection'),
715
+ _HDR('🎤', t(_getLang(), 'providerSelection')),
689
716
  '',
690
- ` {${COLORS.noticeFg}-fg}${'Available TTS providers:'.padEnd(94)}{/${COLORS.noticeFg}-fg}`,
717
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'availableProviders').padEnd(94)}{/${COLORS.noticeFg}-fg}`,
691
718
  '',
692
719
  ...paddedItems.map(i => ` ${i}`),
693
720
  ..._trail,
694
721
  ]));
695
- hintLine.setContent(' Screen 3/5: Provider | [←] Back | [↑↓] Choose | [Enter/→] Confirm & Continue');
722
+ hintLine.setContent(` ${t(_getLang(), 'screen3Hint')}`);
696
723
  box.focus();
697
724
  screen.render();
698
725
  }
@@ -705,36 +732,38 @@ export function createInstallTab(screen, services) {
705
732
  const voiceId = providerService?.getActiveVoiceId?.() ?? 'en_US-amy-medium';
706
733
 
707
734
  contentBox.setContent(_c([
708
- _HDR('🎤', 'Provider & Voice'),
735
+ _HDR('🎤', t(_getLang(), 'providerAndVoice')),
709
736
  '',
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}`,
737
+ ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'providerLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${provider}{/${COLORS.valueFg}-fg}`,
738
+ ` {${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
739
  '',
713
- _HDR('✍️', 'Intro Text'),
740
+ _HDR('✍️', t(_getLang(), 'introText')),
714
741
  '',
715
- ` {${COLORS.labelFg}-fg}${'Intro text:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || '(none)'}{/${COLORS.valueFg}-fg}`,
742
+ ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'introTextLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || `(${t(_getLang(), 'none')})`}{/${COLORS.valueFg}-fg}`,
716
743
  // ↑ [Edit] button rendered inline at box row 8, left=36
717
744
  '',
718
- ` {${COLORS.noticeFg}-fg}Example:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
745
+ ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'example')}:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
719
746
  '',
720
747
  '',
721
748
  '', // ← [✓ Accept & Install] button rendered as real widget here (box row 13)
722
749
  ]));
723
- hintLine.setContent(' Screen 4/5: Config | [Esc] Back | [E] Edit | [↓] Accept & Install');
750
+ hintLine.setContent(` ${t(_getLang(), 'screen4Hint')}`);
751
+ _editBtn.setContent(_tl('editInstallBtn'));
752
+ _acceptBtn.setContent(_tl('acceptInstallBtn'));
724
753
  _acceptBtn.focus();
725
754
  screen.render();
726
755
  }
727
756
 
728
757
  function _renderScreen5() {
729
758
  const header = _installError
730
- ? _HDR('❌', 'Installation Failed')
759
+ ? _HDR('❌', t(_getLang(), 'installationFailed'))
731
760
  : _installComplete
732
- ? _HDR('✅', 'Installation Complete')
733
- : _HDR('⚙️', 'Installing AgentVibes...');
761
+ ? _HDR('✅', t(_getLang(), 'installComplete'))
762
+ : _HDR('⚙️', t(_getLang(), 'installing'));
734
763
 
735
764
  const hint = (_installComplete || _installError)
736
- ? ' Screen 5/5: Complete | [Enter] OK — Done'
737
- : ' Screen 5/5: Installing... | Please wait';
765
+ ? ` ${t(_getLang(), 'screen5HintDone')}`
766
+ : ` ${t(_getLang(), 'screen5HintWait')}`;
738
767
 
739
768
  // Show last 18 log lines so content fits in the box
740
769
  const MAX_LINES = 18;
@@ -778,7 +807,7 @@ export function createInstallTab(screen, services) {
778
807
  _completionModalBox = null;
779
808
  }
780
809
  _completionModalOpen = false;
781
- _screen = 1;
810
+ _screen = 0;
782
811
  box.hide();
783
812
  _showInstallNotice('Installation Complete — Settings Saved');
784
813
  screen.render();
@@ -793,6 +822,8 @@ export function createInstallTab(screen, services) {
793
822
  _s1BeginBtn.hide(); _s1ExitBtn.hide();
794
823
  }
795
824
 
825
+ // Screen 0 has no button widgets — nav is handled via key handlers
826
+
796
827
  // Screen 2 continue button: hidden on other screens; _renderScreen2 manages show/focus
797
828
  if (_screen !== 2) _s2ContinueBtn.hide();
798
829
 
@@ -842,6 +873,7 @@ export function createInstallTab(screen, services) {
842
873
  setTimeout(() => {
843
874
  if (_screen !== targetScreen) return;
844
875
  switch (_screen) {
876
+ case 0: _renderScreen0(); break;
845
877
  case 1: _renderScreen1(); break;
846
878
  case 2: _renderScreen2(); break;
847
879
  case 3: _renderScreen3(); break;
@@ -852,6 +884,7 @@ export function createInstallTab(screen, services) {
852
884
  return;
853
885
  }
854
886
  switch (_screen) {
887
+ case 0: _renderScreen0(); break;
855
888
  case 1: _renderScreen1(); break;
856
889
  case 2: _renderScreen2(); break;
857
890
  case 3: _renderScreen3(); break;
@@ -870,6 +903,12 @@ export function createInstallTab(screen, services) {
870
903
  screen.key(['enter'], () => {
871
904
  if (box.hidden || _checking) return;
872
905
  if (_completionModalOpen) { _dismissCompletionModal(); return; } // always first
906
+ if (_screen === 0) { // Screen 0: apply selected language and advance
907
+ if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
908
+ _screen = 1;
909
+ _showCurrentScreen();
910
+ return;
911
+ }
873
912
  if (_screen === 1) return; // Screen 1: Enter handled by Begin/Exit buttons
874
913
  if (_screen === 2) return; // Screen 2: Enter handled by Continue button
875
914
  if (_screen === 4) return; // Screen 4: Enter handled by the focused button
@@ -883,7 +922,7 @@ export function createInstallTab(screen, services) {
883
922
  screen.key(['escape'], () => {
884
923
  if (box.hidden || _checking) return;
885
924
  if (_completionModalOpen) { _dismissCompletionModal(); return; }
886
- if (_screen > 1) {
925
+ if (_screen > 0) {
887
926
  _screen--;
888
927
  _showCurrentScreen();
889
928
  } else {
@@ -899,6 +938,11 @@ export function createInstallTab(screen, services) {
899
938
 
900
939
  screen.key(['up'], () => {
901
940
  if (box.hidden) return;
941
+ if (_screen === 0) {
942
+ _langIdx = Math.max(0, _langIdx - 1);
943
+ _renderScreen0();
944
+ return;
945
+ }
902
946
  if (_screen === 3 && _deps) {
903
947
  const providers = [];
904
948
  if (_deps.piper) providers.push('piper');
@@ -914,16 +958,23 @@ export function createInstallTab(screen, services) {
914
958
  screen.key(['left'], () => {
915
959
  if (box.hidden || _checking) return;
916
960
  if (_screen === 4) return;
917
- if (_screen > 1) {
961
+ if (_screen > 0) {
918
962
  _screen--;
919
963
  _showCurrentScreen();
920
964
  }
921
965
  });
922
966
 
923
967
  // Right arrow = go forward (same logic as Enter, without save/finish side-effects)
968
+ // Screen 0: → skips language selection (keeps English)
924
969
  // Screen 1: right arrow handled by button ←/→ navigation
925
970
  screen.key(['right'], () => {
926
971
  if (box.hidden || _checking) return;
972
+ if (_screen === 0) { // → skips: keep current _lang (default 'en') and advance
973
+ if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
974
+ _screen = 1;
975
+ _showCurrentScreen();
976
+ return;
977
+ }
927
978
  if (_screen === 1) return;
928
979
  if (_screen === 2) return; // Screen 2: → handled by Continue button
929
980
  if (_screen === 3) { _screen++; _showCurrentScreen(); return; } // → confirms provider and advances
@@ -931,10 +982,15 @@ export function createInstallTab(screen, services) {
931
982
  if (_screen === 5) return; // Screen 5: → handled by button nav
932
983
  });
933
984
 
934
- // Down arrow: Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
985
+ // Down arrow: Screen 0 language nav; Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
935
986
  // (tab bar's el.key(['down']) → onFocus() focuses Begin, then button ↓ → Exit)
936
987
  screen.key(['down'], () => {
937
988
  if (box.hidden) return;
989
+ if (_screen === 0) {
990
+ _langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
991
+ _renderScreen0();
992
+ return;
993
+ }
938
994
  if (_screen === 3 && _deps) {
939
995
  const providers = [];
940
996
  if (_deps.piper) providers.push('piper');
@@ -961,7 +1017,9 @@ export function createInstallTab(screen, services) {
961
1017
  box,
962
1018
 
963
1019
  show() {
964
- _screen = 1;
1020
+ _screen = 0;
1021
+ _langIdx = 0;
1022
+ // _lang now lives in languageService — don't reset it here
965
1023
  _screen5Announced = false;
966
1024
  _installLog = [];
967
1025
  _installRunning = false;
@@ -982,7 +1040,9 @@ export function createInstallTab(screen, services) {
982
1040
 
983
1041
  onFocus() {
984
1042
  // Focus the active interactive element, not just the box container
985
- if (_screen === 1) {
1043
+ if (_screen === 0) {
1044
+ box.focus(); // Screen 0 uses key handlers, no button widgets
1045
+ } else if (_screen === 1) {
986
1046
  _s1BeginBtn.focus();
987
1047
  } else if (_screen === 4) {
988
1048
  _editBtn.focus();
@@ -997,7 +1057,7 @@ export function createInstallTab(screen, services) {
997
1057
  onBlur() {},
998
1058
 
999
1059
  getFooterText() {
1000
- return FOOTER_TEXT;
1060
+ return _tl('footerText');
1001
1061
  },
1002
1062
 
1003
1063
  getFooterColor() {