@hozu/devtools 0.17.2 → 0.18.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.
@@ -1,3 +1,4 @@
1
+ import { t } from '../messages.js';
1
2
  import { describeFor, friendlyName, questionFor } from '../plain.js';
2
3
  import { joinRequests, labelOf, openRequestsLine, requestMarkdown, scopesFor, } from '../prompt.js';
3
4
  import { held, hold, node, one, page, remove, save, saved, theme, tree } from './api.js';
@@ -44,7 +45,7 @@ const dock = h('div', { class: 'dock', role: 'toolbar', 'aria-label': 'Hozu DevT
44
45
  const panel = h('div', {
45
46
  class: 'panel',
46
47
  role: 'dialog',
47
- 'aria-label': 'Hozu DevTools inspector',
48
+ 'aria-label': t('panel.aria'),
48
49
  hidden: true,
49
50
  });
50
51
  const bench = h('div', { class: 'bench', hidden: true });
@@ -146,7 +147,7 @@ async function screensSection() {
146
147
  if (!out?.route || !out.previews.length)
147
148
  return null;
148
149
  return h('div', { class: 'sec' }, [
149
- h('div', { class: 'label' }, ['Previews (previews.ts)']),
150
+ h('div', { class: 'label' }, [t('screens.title')]),
150
151
  ...out.previews.map((p) => {
151
152
  const value = `${out.route}:${p.name}`;
152
153
  const on = out.current === value;
@@ -159,7 +160,7 @@ async function screensSection() {
159
160
  setScreen(on ? null : value);
160
161
  win.location.reload();
161
162
  },
162
- }, [on ? 'Exit' : 'Preview']),
163
+ }, [t(on ? 'screens.exit' : 'screens.preview')]),
163
164
  ]);
164
165
  }),
165
166
  ]);
@@ -271,7 +272,9 @@ function tagText(n, el) {
271
272
  if (state.audience === 'builder')
272
273
  return [
273
274
  friendlyName(n),
274
- n.component && n.component.uses > 1 ? h('b', {}, [` shared · ${n.component.uses} places`]) : '',
275
+ n.component && n.component.uses > 1
276
+ ? h('b', {}, [` ${t('hover.shared', { count: n.component.uses })}`])
277
+ : '',
275
278
  ];
276
279
  return [
277
280
  h('b', {}, [n.kind === 'element' ? '' : `${n.kind} `]),
@@ -580,8 +583,8 @@ function renderDock() {
580
583
  role: 'button',
581
584
  tabindex: '0',
582
585
  'aria-expanded': String(!state.folded),
583
- 'aria-label': state.folded ? 'Open Hozu DevTools' : 'Fold Hozu DevTools',
584
- title: 'Hozu DevTools · click to fold · drag to move · Alt+Shift+S toggles Select',
586
+ 'aria-label': t(state.folded ? 'dock.open' : 'dock.fold'),
587
+ title: t('dock.grip'),
585
588
  onkeydown: (e) => {
586
589
  const key = e.key;
587
590
  if (key === 'Enter' || key === ' ') {
@@ -598,12 +601,12 @@ function renderDock() {
598
601
  }
599
602
  dock.replaceChildren(...present([
600
603
  grip,
601
- h('div', { class: 'seg' }, [button('Browse', 'browse'), button('Select', 'select')]),
604
+ h('div', { class: 'seg' }, [button(t('dock.browse'), 'browse'), button(t('dock.select'), 'select')]),
602
605
  agent.count()
603
606
  ? h('button', {
604
607
  class: 'act agent',
605
608
  type: 'button',
606
- title: 'Notes from your agent on this page (hozu show)',
609
+ title: t('dock.agent.title'),
607
610
  'aria-pressed': String(state.panel === 'notes'),
608
611
  onclick: () => {
609
612
  if (state.panel === 'notes')
@@ -611,7 +614,7 @@ function renderDock() {
611
614
  open('notes');
612
615
  agent.focus();
613
616
  },
614
- }, ['Agent ', h('span', { class: 'agent-count' }, [String(agent.count())])])
617
+ }, [t('dock.agent'), h('span', { class: 'agent-count' }, [String(agent.count())])])
615
618
  : null,
616
619
  h('button', {
617
620
  class: 'act',
@@ -622,54 +625,54 @@ function renderDock() {
622
625
  else
623
626
  open('changes', null, drafted().length ? 'draft' : 'saved');
624
627
  },
625
- }, ['Changes', h('span', { class: 'count', 'data-count': true })]),
628
+ }, [t('dock.changes'), h('span', { class: 'count', 'data-count': true })]),
626
629
  h('button', {
627
630
  class: 'act',
628
631
  type: 'button',
629
- title: 'Select this page: its title, description and other head fields',
632
+ title: t('dock.page.title'),
630
633
  onclick: async () => {
631
634
  const n = await page(win.location.pathname);
632
635
  if (n)
633
636
  select({ id: n.id, index: 0, visible: doc.title }, state.mode === 'select' && state.picks.length > 0);
634
637
  },
635
- }, ['Page']),
638
+ }, [t('dock.page')]),
636
639
  h('button', {
637
640
  class: 'act',
638
641
  type: 'button',
639
- title: 'The parts of this page, and its states that are not on screen',
642
+ title: t('dock.layers.title'),
640
643
  onclick: () => {
641
644
  state.panel = state.panel === 'layers' ? (state.picks.length ? 'inspector' : null) : 'layers';
642
645
  persist();
643
646
  void renderPanel();
644
647
  },
645
- }, ['Layers']),
648
+ }, [t('dock.layers')]),
646
649
  h('button', {
647
650
  class: 'act',
648
651
  type: 'button',
649
- title: 'Every component of the app, its variants, previews and uses; and the design tokens',
652
+ title: t('dock.assets.title'),
650
653
  onclick: () => void assets.open(),
651
- }, ['Assets']),
654
+ }, [t('dock.assets')]),
652
655
  h('button', {
653
656
  class: 'act',
654
657
  type: 'button',
655
- title: 'The data this page reads and the changes it can make: run them with your own input',
658
+ title: t('dock.api.title'),
656
659
  'aria-pressed': api.isOpen() ? 'true' : 'false',
657
660
  onclick: () => api.toggle(),
658
- }, ['API']),
661
+ }, [t('dock.api')]),
659
662
  h('button', {
660
663
  class: 'act',
661
664
  type: 'button',
662
- title: 'The page at an exact size, with its parts on the left and the inspector on the right',
665
+ title: t('dock.frame.title'),
663
666
  onclick: () => {
664
667
  state.panel = state.picks.length ? 'inspector' : null;
665
668
  openBench();
666
669
  },
667
- }, ['Frame']),
670
+ }, [t('dock.frame')]),
668
671
  h('button', {
669
672
  class: 'act gear',
670
673
  type: 'button',
671
- title: 'Settings',
672
- 'aria-label': 'Settings',
674
+ title: t('dock.settings'),
675
+ 'aria-label': t('dock.settings'),
673
676
  onclick: () => {
674
677
  state.panel = state.panel === 'settings' ? (state.picks.length ? 'inspector' : null) : 'settings';
675
678
  persist();
@@ -680,37 +683,40 @@ function renderDock() {
680
683
  ? h('button', {
681
684
  class: 'previewing',
682
685
  type: 'button',
683
- title: 'Exit the preview from previews.ts',
686
+ title: t('dock.screen.exit'),
684
687
  onclick: () => {
685
688
  setScreen(null);
686
689
  win.location.reload();
687
690
  },
688
- }, [`Screen: ${screenOf().slice(screenOf().indexOf(':') + 1)}`, ' · Exit'])
691
+ }, [
692
+ t('dock.screen', { name: screenOf().slice(screenOf().indexOf(':') + 1) }),
693
+ ` · ${t('dock.exit')}`,
694
+ ])
689
695
  : null,
690
696
  held()
691
697
  ? h('button', {
692
698
  class: 'previewing',
693
699
  type: 'button',
694
- title: 'Exit the preview',
700
+ title: t('dock.preview.exit'),
695
701
  onclick: () => hold(null, reload),
696
- }, [h('span', { 'data-preview': true }, ['Preview']), ' · Exit'])
702
+ }, [h('span', { 'data-preview': true }, [t('dock.preview')]), ` · ${t('dock.exit')}`])
697
703
  : null,
698
704
  state.mode === 'select' && !held() && !state.panel
699
705
  ? h('div', { class: 'tip', role: 'note' }, [
700
- h('span', {}, [h('kbd', {}, ['Click']), ' a part to select it']),
701
- h('span', {}, [h('kbd', {}, ['Shift']), '+click adds another']),
702
- h('span', {}, [h('kbd', {}, ['⇧ Enter']), ' picks its parent']),
703
- h('span', {}, [h('kbd', {}, ['Alt']), ' measures']),
704
- h('span', {}, [h('kbd', {}, ['Esc']), ' stops']),
706
+ h('span', {}, inline(t('tip.click'), 'key', h('kbd', {}, ['Click']))),
707
+ h('span', {}, inline(t('tip.shift'), 'key', h('kbd', {}, ['Shift']))),
708
+ h('span', {}, inline(t('tip.parent'), 'key', h('kbd', {}, ['⇧ Enter']))),
709
+ h('span', {}, inline(t('tip.measure'), 'key', h('kbd', {}, ['Alt']))),
710
+ h('span', {}, inline(t('tip.stop'), 'key', h('kbd', {}, ['Esc']))),
705
711
  ])
706
712
  : null,
707
713
  ]));
708
714
  const holding = held();
709
715
  if (holding)
710
- void tree(win.location.pathname).then((t) => {
716
+ void tree(win.location.pathname).then((found) => {
711
717
  const label = dock.querySelector('[data-preview]');
712
718
  if (label)
713
- label.textContent = `Preview: ${previewLabel(t, holding)}`;
719
+ label.textContent = t('dock.preview.named', { name: previewLabel(found, holding) });
714
720
  });
715
721
  const count = dock.querySelector('[data-count]');
716
722
  if (count)
@@ -817,9 +823,9 @@ const where = (what, l) => l
817
823
  h('span', {}, [`${l.file}:${l.line}:${l.column}`]),
818
824
  h('button', {
819
825
  type: 'button',
820
- title: 'Copy the location',
826
+ title: t('inspector.copyLocation'),
821
827
  onclick: () => void copy(`${l.file}:${l.line}:${l.column}`),
822
- }, ['Copy']),
828
+ }, [t('inspector.copy')]),
823
829
  ])
824
830
  : null;
825
831
  function excerpt(n) {
@@ -838,28 +844,35 @@ const section = (label, children) => children.some(Boolean)
838
844
  ? h('div', { class: 'sec' }, [h('div', { class: 'label' }, [label]), ...children])
839
845
  : null;
840
846
  const kindTitle = {
841
- element: 'Element',
842
- component: 'Component use',
843
- text: 'Text',
844
- query: 'Query',
845
- when: 'State branch',
846
- if: 'Branch',
847
- list: 'List',
848
- html: 'Raw HTML',
849
- embed: 'Embed',
850
- other: 'Node',
847
+ element: 'kind.element',
848
+ component: 'kind.component',
849
+ text: 'kind.text',
850
+ query: 'kind.query',
851
+ when: 'kind.when',
852
+ if: 'kind.if',
853
+ list: 'kind.list',
854
+ html: 'kind.html',
855
+ embed: 'kind.embed',
856
+ other: 'kind.other',
857
+ };
858
+ const kindOf = (kind) => (kindTitle[kind] ? t(kindTitle[kind]) : kind);
859
+ const inline = (text, name, node) => {
860
+ const [before, after] = text.split(`{${name}}`);
861
+ return after === undefined ? [text] : [before || null, node, after || null].filter((x) => x !== null);
851
862
  };
852
863
  function kicker(n) {
853
864
  if (state.audience === 'builder')
854
- return n.page ? `Page ${n.page.path}` : `On ${win.location.pathname}`;
865
+ return n.page
866
+ ? t('inspector.kicker.page', { path: n.page.path })
867
+ : t('inspector.kicker.on', { path: win.location.pathname });
855
868
  const kind = n.component
856
- ? 'Component use'
869
+ ? t('kind.component')
857
870
  : n.kind === 'text' && n.source?.kind === 'data'
858
- ? 'Data text'
859
- : (kindTitle[n.kind] ?? n.kind);
871
+ ? t('kind.dataText')
872
+ : kindOf(n.kind);
860
873
  if (n.page)
861
- return `Page · route ${n.page.path}`;
862
- return `${kind} · view ${n.owner?.feature}.${n.owner?.view}`;
874
+ return t('inspector.kicker.route', { path: n.page.path });
875
+ return t('inspector.kicker.view', { kind, view: `${n.owner?.feature}.${n.owner?.view}` });
863
876
  }
864
877
  function titleFor(n, active) {
865
878
  const text = (active.visible || n.text || '').slice(0, 40);
@@ -872,7 +885,7 @@ function titleFor(n, active) {
872
885
  ? `<${n.tag}>`
873
886
  : n.kind === 'text'
874
887
  ? `“${text}”`
875
- : (kindTitle[n.kind] ?? n.kind),
888
+ : kindOf(n.kind),
876
889
  n.component ? h('code', {}, [n.component.ref]) : null,
877
890
  ];
878
891
  }
@@ -884,7 +897,9 @@ function textRows(n) {
884
897
  h('b', {}, [source.kind === 'literal' ? 'text ' : `${source.kind} `]),
885
898
  source.kind === 'literal' ? `“${source.detail}”` : source.detail,
886
899
  source.location ? ` ${source.location.file}:${source.location.line}` : '',
887
- (source.uses ?? 1) > 1 ? h('span', { class: 'warn' }, [` · shared by ${source.uses} places`]) : null,
900
+ (source.uses ?? 1) > 1
901
+ ? h('span', { class: 'warn' }, [` · ${t('inspector.sharedBy', { count: source.uses })}`])
902
+ : null,
888
903
  ]));
889
904
  }
890
905
  async function copy(text) {
@@ -938,7 +953,7 @@ const open = (panelName, opened = null, tab = state.tab) => {
938
953
  persist();
939
954
  void renderPanel();
940
955
  };
941
- const closeButton = (onclick) => h('button', { class: 'close', type: 'button', 'aria-label': 'Close', onclick }, ['×']);
956
+ const closeButton = (onclick) => h('button', { class: 'close', type: 'button', 'aria-label': t('common.close'), onclick }, ['×']);
942
957
  function choose(active, id, visible) {
943
958
  state.picks[state.active] = { ...active, id, scope: null, visible };
944
959
  persist();
@@ -959,6 +974,19 @@ function removeActive() {
959
974
  drawPicks();
960
975
  void renderPanel();
961
976
  }
977
+ function scopeLabel(n, scope) {
978
+ const c = n.component;
979
+ if (scope === 'component' && c)
980
+ return t(c.uses === 1 ? 'scope.component.one' : 'scope.component.other', {
981
+ name: c.ref.split('.').pop() ?? '',
982
+ count: c.uses,
983
+ });
984
+ if (scope === 'items')
985
+ return t('scope.items');
986
+ if (n.conditions.some((x) => x.kind === 'each'))
987
+ return t('scope.thisItem');
988
+ return t(c ? 'scope.instance' : 'scope.thisOne');
989
+ }
962
990
  function scopeField(n, active) {
963
991
  const scopes = scopesFor(n);
964
992
  if (scopes.length < 2)
@@ -978,7 +1006,7 @@ function scopeField(n, active) {
978
1006
  persist();
979
1007
  },
980
1008
  }),
981
- asked?.options[s.scope] ?? s.label,
1009
+ asked?.options[s.scope] ?? scopeLabel(n, s.scope),
982
1010
  ])),
983
1011
  ]);
984
1012
  }
@@ -998,7 +1026,7 @@ function exportBlock() {
998
1026
  const result = h('div', { class: 'status', role: 'status' });
999
1027
  const draft = h('textarea', {
1000
1028
  class: 'draft',
1001
- 'aria-label': 'The request, as the agent will read it',
1029
+ 'aria-label': t('export.draft'),
1002
1030
  hidden: true,
1003
1031
  });
1004
1032
  const generate = async () => requestMarkdown(await request(), { excerpt: state.excerpt, theme: await theme() });
@@ -1010,10 +1038,10 @@ function exportBlock() {
1010
1038
  if (draft.hidden)
1011
1039
  draft.value = await generate();
1012
1040
  draft.hidden = !draft.hidden;
1013
- edit.textContent = draft.hidden ? 'Edit before sending' : 'Discard edits';
1041
+ edit.textContent = t(draft.hidden ? 'export.edit' : 'export.discard');
1014
1042
  },
1015
- }, ['Edit before sending']);
1016
- const many = count > 1 ? ` (${count})` : '';
1043
+ }, [t('export.edit')]);
1044
+ const many = count > 1;
1017
1045
  return h('div', { class: 'export' }, [
1018
1046
  h('div', { class: 'row-end' }, [edit]),
1019
1047
  draft,
@@ -1023,11 +1051,9 @@ function exportBlock() {
1023
1051
  type: 'button',
1024
1052
  onclick: async () => {
1025
1053
  await copy(await markdown());
1026
- status(result, 'ok', [
1027
- `Copied ${count === 1 ? 'the change' : `all ${count} changes`}. Paste it to your agent.`,
1028
- ]);
1054
+ status(result, 'ok', [t(count === 1 ? 'export.copied.one' : 'export.copied.other', { count })]);
1029
1055
  },
1030
- }, [`Copy for AI${many}`]),
1056
+ }, [many ? t('export.copy.many', { count }) : t('export.copy')]),
1031
1057
  h('button', {
1032
1058
  type: 'button',
1033
1059
  onclick: async () => {
@@ -1035,16 +1061,16 @@ function exportBlock() {
1035
1061
  const done = await save(await markdown());
1036
1062
  const ask = `Do the Hozu request ${done.file}`;
1037
1063
  await copy(ask);
1038
- notice = ['Saved ', h('code', {}, [done.file]), `. Copied “${ask}”: paste it to your agent.`];
1064
+ notice = inline(t('export.saved', { ask }), 'file', h('code', {}, [done.file]));
1039
1065
  clearDraft();
1040
1066
  renderDock();
1041
1067
  open('changes', null, 'saved');
1042
1068
  }
1043
1069
  catch (e) {
1044
- status(result, 'err', [`Not saved: ${e.message}`]);
1070
+ status(result, 'err', [t('export.notSaved', { error: e.message })]);
1045
1071
  }
1046
1072
  },
1047
- }, [`Save request${many}`]),
1073
+ }, [many ? t('export.save.many', { count }) : t('export.save')]),
1048
1074
  h('button', {
1049
1075
  type: 'button',
1050
1076
  onclick: async () => {
@@ -1053,9 +1079,9 @@ function exportBlock() {
1053
1079
  download: 'hozu-request.md',
1054
1080
  });
1055
1081
  a.click();
1056
- status(result, 'ok', ['Downloaded hozu-request.md']);
1082
+ status(result, 'ok', [t('export.downloaded', { file: 'hozu-request.md' })]);
1057
1083
  },
1058
- }, ['Download .md']),
1084
+ }, [t('export.download')]),
1059
1085
  ]),
1060
1086
  result,
1061
1087
  ]);
@@ -1081,8 +1107,8 @@ function textFor(n, active) {
1081
1107
  function requestSection(n, active) {
1082
1108
  const others = state.picks.filter((p) => p !== active && hasContent(p)).length;
1083
1109
  const note = h('textarea', {
1084
- 'aria-label': 'What should change?',
1085
- placeholder: 'What should change? For example: “Make it bigger and use the brand red”',
1110
+ 'aria-label': t('change.aria'),
1111
+ placeholder: t('change.placeholder'),
1086
1112
  oninput: (event) => {
1087
1113
  const had = hasContent(active);
1088
1114
  active.note = event.target.value;
@@ -1093,33 +1119,31 @@ function requestSection(n, active) {
1093
1119
  });
1094
1120
  note.value = active.note;
1095
1121
  return h('div', { class: 'sec' }, [
1096
- h('div', { class: 'label' }, ['Change']),
1122
+ h('div', { class: 'label' }, [t('change.label')]),
1097
1123
  scopeField(n, active),
1098
1124
  note,
1099
1125
  others
1100
1126
  ? h('div', { class: 'plain more' }, [
1101
- `This request also has ${others} other ${others === 1 ? 'change' : 'changes'}. `,
1127
+ t(others === 1 ? 'change.others.one' : 'change.others.other', { count: others }),
1102
1128
  h('button', { class: 'link', type: 'button', onclick: () => open('changes', null, 'draft') }, [
1103
- 'Review all',
1129
+ t('change.review'),
1104
1130
  ]),
1105
1131
  ])
1106
- : h('div', { class: 'hint-text' }, [
1107
- 'Described parts stay in this request when you select the next one.',
1108
- ]),
1132
+ : h('div', { class: 'hint-text' }, [t('change.hint')]),
1109
1133
  exportBlock(),
1110
1134
  ]);
1111
1135
  }
1112
1136
  function builderSections(n) {
1113
1137
  const children = n.children.filter((c) => c.kind !== 'text');
1114
1138
  return [
1115
- section('About it', describeFor(n).map((line) => h('div', { class: 'plain' }, [line]))),
1139
+ section(t('inspector.about'), describeFor(n).map((line) => h('div', { class: 'plain' }, [line]))),
1116
1140
  h('div', { class: 'sec' }, [
1117
1141
  h('div', { class: 'nav' }, [
1118
- h('button', { type: 'button', onclick: () => walk('up') }, ['↑ Select the area around it']),
1142
+ h('button', { type: 'button', onclick: () => walk('up') }, [t('inspector.around')]),
1119
1143
  children.length && !isText(n)
1120
- ? h('button', { type: 'button', onclick: () => walk('down') }, ['↓ Inside'])
1144
+ ? h('button', { type: 'button', onclick: () => walk('down') }, [t('inspector.inside')])
1121
1145
  : null,
1122
- h('button', { type: 'button', onclick: removeActive }, ['Remove']),
1146
+ h('button', { type: 'button', onclick: removeActive }, [t('inspector.remove')]),
1123
1147
  ]),
1124
1148
  n.location
1125
1149
  ? h('div', { class: 'file' }, [
@@ -1128,7 +1152,7 @@ function builderSections(n) {
1128
1152
  class: 'link',
1129
1153
  type: 'button',
1130
1154
  onclick: () => void copy(`${n.location.file}:${n.location.line}`),
1131
- }, ['Copy']),
1155
+ }, [t('inspector.copy')]),
1132
1156
  ])
1133
1157
  : null,
1134
1158
  ]),
@@ -1137,25 +1161,27 @@ function builderSections(n) {
1137
1161
  function developerSections(n, active) {
1138
1162
  const children = n.children.filter((c) => c.kind !== 'text' || c.text);
1139
1163
  return [
1140
- section('Where', [
1141
- where(n.page ? 'page' : 'view', n.location),
1142
- n.page ? where('route', n.page.routeLocation) : null,
1143
- n.component ? where('component', n.component.declaration) : null,
1144
- n.events.length ? where('machine', n.machine) : null,
1164
+ section(t('inspector.where'), [
1165
+ where(t(n.page ? 'inspector.where.page' : 'inspector.where.view'), n.location),
1166
+ n.page ? where(t('inspector.where.route'), n.page.routeLocation) : null,
1167
+ n.component ? where(t('inspector.where.component'), n.component.declaration) : null,
1168
+ n.events.length ? where(t('inspector.where.machine'), n.machine) : null,
1145
1169
  excerpt(n),
1146
1170
  ]),
1147
- section('Component', n.component
1171
+ section(t('inspector.component'), n.component
1148
1172
  ? [
1149
1173
  h('div', {}, [
1150
1174
  h('span', { class: 'chip red' }, [n.component.ref]),
1151
1175
  h('span', { class: 'chip' }, [
1152
- `used in ${n.component.uses} ${n.component.uses === 1 ? 'place' : 'places'}`,
1176
+ t(n.component.uses === 1 ? 'inspector.usedIn.one' : 'inspector.usedIn.other', {
1177
+ count: n.component.uses,
1178
+ }),
1153
1179
  ]),
1154
1180
  ...Object.entries(n.component.variant).map(([k, v]) => h('span', { class: 'chip' }, [`${k}=${v}`])),
1155
1181
  ]),
1156
1182
  ]
1157
1183
  : []),
1158
- section('Head', n.page
1184
+ section(t('inspector.head'), n.page
1159
1185
  ? [
1160
1186
  ...['title', 'description', 'image'].map((f) => h('div', { class: 'row' }, [h('b', {}, [`${f} `]), n.page.head[f] ?? '—'])),
1161
1187
  h('div', { class: 'row' }, [h('b', {}, ['views ']), n.page.views.join(', ')]),
@@ -1165,18 +1191,18 @@ function developerSections(n, active) {
1165
1191
  : null,
1166
1192
  ]
1167
1193
  : []),
1168
- section('Text', textRows(n)),
1169
- section('Shown when', n.conditions.map((c) => h('div', { class: 'row' }, [
1194
+ section(t('inspector.text'), textRows(n)),
1195
+ section(t('inspector.shownWhen'), n.conditions.map((c) => h('div', { class: 'row' }, [
1170
1196
  h('b', {}, [`${c.kind} `]),
1171
1197
  c.detail,
1172
1198
  c.location ? ` ${c.location.file}:${c.location.line}` : '',
1173
1199
  ]))),
1174
- section('Behaviour', n.events.map((e) => h('div', { class: 'row' }, [
1200
+ section(t('inspector.behaviour'), n.events.map((e) => h('div', { class: 'row' }, [
1175
1201
  h('span', { class: 'chip red' }, [`on ${e.dom}`]),
1176
1202
  h('span', { class: 'chip' }, [e.event]),
1177
1203
  ...e.transitions.map((t) => h('span', { class: 'chip' }, [`${t.from} → ${t.to}${t.guarded ? ' if …' : ''}`])),
1178
1204
  ]))),
1179
- section('Inside', [
1205
+ section(t('inspector.insideIt'), [
1180
1206
  children.length
1181
1207
  ? h('div', { class: 'kids' }, children.map((c) => h('button', {
1182
1208
  type: 'button',
@@ -1184,9 +1210,11 @@ function developerSections(n, active) {
1184
1210
  }, [c.kind === 'text' ? `text ${c.text}` : `${c.kind} ${c.id.split('/').slice(-1)[0]}`])))
1185
1211
  : null,
1186
1212
  h('div', { class: 'nav' }, [
1187
- h('button', { type: 'button', onclick: () => walk('up') }, ['↑ Parent']),
1188
- isText(n) ? null : h('button', { type: 'button', onclick: () => walk('down') }, ['↓ Child']),
1189
- h('button', { type: 'button', onclick: removeActive }, ['Remove']),
1213
+ h('button', { type: 'button', onclick: () => walk('up') }, [t('inspector.parent')]),
1214
+ isText(n)
1215
+ ? null
1216
+ : h('button', { type: 'button', onclick: () => walk('down') }, [t('inspector.child')]),
1217
+ h('button', { type: 'button', onclick: removeActive }, [t('inspector.remove')]),
1190
1218
  ]),
1191
1219
  ]),
1192
1220
  h('div', { class: 'sec meta' }, [`node ${n.id}`, n.pointer ? ` · ${n.pointer}` : '']),
@@ -1212,19 +1240,13 @@ async function renderPanel() {
1212
1240
  if (state.panel !== 'inspector' || !active) {
1213
1241
  panel.hidden = state.view !== 'workbench';
1214
1242
  if (state.view === 'workbench')
1215
- panel.replaceChildren(h('div', { class: 'empty' }, [
1216
- h('b', {}, ['Nothing selected. ']),
1217
- 'Choose Select and click a part of the page, or pick one in Layers.',
1218
- ]));
1243
+ panel.replaceChildren(h('div', { class: 'empty' }, [h('b', {}, [t('inspector.empty.title')]), t('inspector.empty.text')]));
1219
1244
  return;
1220
1245
  }
1221
1246
  const n = await node(active.id);
1222
1247
  if (!n) {
1223
1248
  panel.hidden = false;
1224
- panel.replaceChildren(h('div', { class: 'empty' }, [
1225
- h('b', {}, ['This part is gone.']),
1226
- ' The code changed since you selected it: select it again.',
1227
- ]));
1249
+ panel.replaceChildren(h('div', { class: 'empty' }, [h('b', {}, [t('inspector.gone.title')]), t('inspector.gone.text')]));
1228
1250
  return;
1229
1251
  }
1230
1252
  const nodes = await Promise.all(state.picks.map((p) => node(p.id)));
@@ -1269,8 +1291,8 @@ function tabs(current, saved) {
1269
1291
  onclick: () => open('changes', null, name),
1270
1292
  }, [label]);
1271
1293
  return h('div', { class: 'tabs' }, [
1272
- tab('draft', `This request · ${drafted().length}`),
1273
- tab('saved', `Saved · ${saved}`),
1294
+ tab('draft', t('changes.tab.draft', { count: drafted().length })),
1295
+ tab('saved', t('changes.tab.saved', { count: saved })),
1274
1296
  ]);
1275
1297
  }
1276
1298
  async function renderChanges() {
@@ -1278,8 +1300,8 @@ async function renderChanges() {
1278
1300
  const current = state.tab;
1279
1301
  panel.hidden = false;
1280
1302
  const head = h('div', { class: 'head' }, [
1281
- h('div', { class: 'kicker' }, [current === 'draft' ? 'Not sent yet' : '.hozu/requests']),
1282
- h('h2', { class: 'title' }, ['Changes']),
1303
+ h('div', { class: 'kicker' }, [current === 'draft' ? t('changes.notSent') : '.hozu/requests']),
1304
+ h('h2', { class: 'title' }, [t('changes.title')]),
1283
1305
  closeButton(() => open(state.picks.length ? 'inspector' : null)),
1284
1306
  tabs(current, list.length),
1285
1307
  ]);
@@ -1292,17 +1314,17 @@ async function renderChanges() {
1292
1314
  const done = h('button', {
1293
1315
  class: 'link done',
1294
1316
  type: 'button',
1295
- title: 'Resolved or not needed: remove it',
1317
+ title: t('changes.resolve.title'),
1296
1318
  onclick: async () => {
1297
1319
  if (!armed) {
1298
1320
  armed = true;
1299
- done.textContent = 'Remove?';
1321
+ done.textContent = t('changes.resolve.confirm');
1300
1322
  return;
1301
1323
  }
1302
1324
  await remove(r.number);
1303
1325
  void renderPanel();
1304
1326
  },
1305
- }, ['Resolve']);
1327
+ }, [t('changes.resolve')]);
1306
1328
  return h('div', { class: 'req' }, [
1307
1329
  h('span', { class: 'n' }, [r.number]),
1308
1330
  h('button', { class: 't open-req', type: 'button', onclick: () => open('changes', r.number, 'saved') }, [r.title]),
@@ -1315,7 +1337,7 @@ async function renderChanges() {
1315
1337
  message ? h('div', { class: 'notice' }, message) : null,
1316
1338
  ...(list.length
1317
1339
  ? [...list].reverse().map(row)
1318
- : [h('div', { class: 'empty' }, ['No open requests. Resolved ones are removed.'])]),
1340
+ : [h('div', { class: 'empty' }, [t('changes.noRequests')])]),
1319
1341
  list.length
1320
1342
  ? h('div', { class: 'sec' }, [
1321
1343
  h('div', { class: 'choice' }, [
@@ -1325,24 +1347,18 @@ async function renderChanges() {
1325
1347
  onclick: async () => {
1326
1348
  const full = await Promise.all(list.map((r) => one(r.number)));
1327
1349
  await copy(joinRequests(full));
1328
- status(result, 'ok', [
1329
- `Copied all ${list.length} as one prompt. Paste it to your agent.`,
1330
- ]);
1350
+ status(result, 'ok', [t('changes.copiedAll', { count: list.length })]);
1331
1351
  },
1332
- }, [`Copy all ${list.length} for AI`]),
1333
- h('span', { class: 'why' }, [
1334
- 'One prompt with every open request: the fewest tokens and no file reads. Works in any chat.',
1335
- ]),
1352
+ }, [t('changes.copyAll', { count: list.length })]),
1353
+ h('span', { class: 'why' }, [t('changes.copyAll.why')]),
1336
1354
  h('button', {
1337
1355
  type: 'button',
1338
1356
  onclick: async () => {
1339
1357
  await copy(openRequestsLine(list));
1340
- status(result, 'ok', ['Copied the file paths.']);
1358
+ status(result, 'ok', [t('changes.copiedPaths')]);
1341
1359
  },
1342
- }, ['Copy file paths only']),
1343
- h('span', { class: 'why' }, [
1344
- 'For an agent in this project that should read the files itself (or run hozu requests --full).',
1345
- ]),
1360
+ }, [t('changes.copyPaths')]),
1361
+ h('span', { class: 'why' }, [t('changes.copyPaths.why')]),
1346
1362
  ]),
1347
1363
  result,
1348
1364
  ])
@@ -1365,7 +1381,9 @@ async function renderChanges() {
1365
1381
  [
1366
1382
  p.note.trim(),
1367
1383
  p.style?.length
1368
- ? `${p.style.length} style ${p.style.length === 1 ? 'change' : 'changes'}`
1384
+ ? t(p.style.length === 1 ? 'changes.styles.one' : 'changes.styles.other', {
1385
+ count: p.style.length,
1386
+ })
1369
1387
  : '',
1370
1388
  ]
1371
1389
  .filter(Boolean)
@@ -1379,7 +1397,7 @@ async function renderChanges() {
1379
1397
  state.active = state.picks.indexOf(p);
1380
1398
  open('inspector');
1381
1399
  },
1382
- }, ['Edit']),
1400
+ }, [t('changes.edit')]),
1383
1401
  h('button', {
1384
1402
  class: 'link',
1385
1403
  type: 'button',
@@ -1393,13 +1411,10 @@ async function renderChanges() {
1393
1411
  renderDock();
1394
1412
  void renderPanel();
1395
1413
  },
1396
- }, ['Remove']),
1414
+ }, [t('changes.remove')]),
1397
1415
  ]);
1398
1416
  }))
1399
- : h('div', { class: 'empty' }, [
1400
- h('b', {}, ['Nothing yet. ']),
1401
- 'Choose Select, click a part and describe the change. Every described part is added here.',
1402
- ]),
1417
+ : h('div', { class: 'empty' }, [h('b', {}, [t('changes.empty.title')]), t('changes.empty.text')]),
1403
1418
  items.length ? h('div', { class: 'sec' }, [exportBlock()]) : null,
1404
1419
  ]));
1405
1420
  }
@@ -1420,17 +1435,17 @@ async function renderRequest(number) {
1420
1435
  onclick: async () => {
1421
1436
  if (!armed) {
1422
1437
  armed = true;
1423
- del.textContent = 'Click again to remove';
1438
+ del.textContent = t('request.remove.confirm');
1424
1439
  return;
1425
1440
  }
1426
1441
  await remove(r.number);
1427
1442
  renderDock();
1428
1443
  open('changes', null, 'saved');
1429
1444
  },
1430
- }, ['Resolve · remove']);
1445
+ }, [t('request.remove')]);
1431
1446
  panel.replaceChildren(h('div', { class: 'head' }, [
1432
1447
  h('button', { class: 'back', type: 'button', onclick: () => open('changes', null, 'saved') }, [
1433
- '← Saved',
1448
+ t('request.back'),
1434
1449
  ]),
1435
1450
  h('h2', { class: 'title' }, [r.title]),
1436
1451
  h('div', { class: 'kicker' }, [r.file]),
@@ -1442,16 +1457,16 @@ async function renderRequest(number) {
1442
1457
  type: 'button',
1443
1458
  onclick: async () => {
1444
1459
  await copy(`Do the Hozu request ${r.file}`);
1445
- status(result, 'ok', [`Copied “Do the Hozu request ${r.file}”.`]);
1460
+ status(result, 'ok', [t('request.copiedAsk', { ask: `Do the Hozu request ${r.file}` })]);
1446
1461
  },
1447
- }, ['Copy “do this request”']),
1462
+ }, [t('request.copyAsk')]),
1448
1463
  h('button', {
1449
1464
  type: 'button',
1450
1465
  onclick: async () => {
1451
1466
  await copy(r.markdown);
1452
- status(result, 'ok', ['Copied the whole request.']);
1467
+ status(result, 'ok', [t('request.copied')]);
1453
1468
  },
1454
- }, ['Copy request']),
1469
+ }, [t('request.copy')]),
1455
1470
  del,
1456
1471
  ]),
1457
1472
  result,
@@ -1475,14 +1490,14 @@ function renderSettings() {
1475
1490
  ]);
1476
1491
  panel.replaceChildren(h('div', { class: 'head' }, [
1477
1492
  h('div', { class: 'kicker' }, ['Hozu DevTools']),
1478
- h('h2', { class: 'title' }, ['Settings']),
1493
+ h('h2', { class: 'title' }, [t('settings.title')]),
1479
1494
  closeButton(() => open(state.picks.length ? 'inspector' : null)),
1480
1495
  ]), h('div', { class: 'sec' }, [
1481
- h('div', { class: 'label' }, ['Appearance']),
1482
- h('div', { class: 'tabs', role: 'radiogroup', 'aria-label': 'Appearance' }, [
1483
- ['system', 'System'],
1484
- ['light', 'Light'],
1485
- ['dark', 'Dark'],
1496
+ h('div', { class: 'label' }, [t('settings.appearance')]),
1497
+ h('div', { class: 'tabs', role: 'radiogroup', 'aria-label': t('settings.appearance') }, [
1498
+ ['system', t('settings.system')],
1499
+ ['light', t('settings.light')],
1500
+ ['dark', t('settings.dark')],
1486
1501
  ].map(([value, label]) => h('button', {
1487
1502
  class: 'tab',
1488
1503
  type: 'button',
@@ -1497,13 +1512,13 @@ function renderSettings() {
1497
1512
  },
1498
1513
  }, [label]))),
1499
1514
  ]), h('div', { class: 'sec' }, [
1500
- h('div', { class: 'label' }, ['Show']),
1515
+ h('div', { class: 'label' }, [t('settings.show')]),
1501
1516
  h('fieldset', {}, [
1502
- choice('builder', 'Builder', 'Plain words: what it is, what a change reaches. No code names.'),
1503
- choice('developer', 'Developer', 'Files, the code excerpt, components, conditions, transitions and node ids.'),
1517
+ choice('builder', t('settings.builder'), t('settings.builder.detail')),
1518
+ choice('developer', t('settings.developer'), t('settings.developer.detail')),
1504
1519
  ]),
1505
1520
  ]), h('div', { class: 'sec' }, [
1506
- h('div', { class: 'label' }, ['Requests']),
1521
+ h('div', { class: 'label' }, [t('settings.requests')]),
1507
1522
  h('label', { class: 'option' }, [
1508
1523
  h('input', {
1509
1524
  type: 'checkbox',
@@ -1513,26 +1528,21 @@ function renderSettings() {
1513
1528
  persist();
1514
1529
  },
1515
1530
  }),
1516
- h('span', {}, [
1517
- h('b', {}, ['Add the code excerpt']),
1518
- h('span', {}, [
1519
- 'Seven lines around each place. The agent reads the file anyway, so it is off by default.',
1520
- ]),
1521
- ]),
1531
+ h('span', {}, [h('b', {}, [t('settings.excerpt')]), h('span', {}, [t('settings.excerpt.detail')])]),
1522
1532
  ]),
1523
1533
  ]), h('div', { class: 'sec' }, [
1524
- h('div', { class: 'label' }, ['Keys']),
1534
+ h('div', { class: 'label' }, [t('settings.keys')]),
1525
1535
  ...[
1526
- ['Alt+Shift+S', 'Select on or off'],
1527
- ['Click', 'Select'],
1528
- ['Shift+click', 'Add to the request'],
1529
- ['Shift+Enter', 'Select the area around it'],
1530
- ['Enter', 'Select inside it'],
1531
- ['Tab / Shift+Tab', 'Next / previous part beside it'],
1532
- ['Alt (hold)', 'Measure: W × H and the distance to the part under the pointer'],
1533
- ['Double-click', 'Select a text'],
1534
- ['↑ ↓', 'Around it / inside it'],
1535
- ['Esc', 'Back to Browse'],
1536
+ ['Alt+Shift+S', t('keys.toggle')],
1537
+ ['Click', t('keys.click')],
1538
+ ['Shift+click', t('keys.add')],
1539
+ ['Shift+Enter', t('keys.around')],
1540
+ ['Enter', t('keys.inside')],
1541
+ ['Tab / Shift+Tab', t('keys.beside')],
1542
+ ['Alt (hold)', t('keys.measure')],
1543
+ ['Double-click', t('keys.text')],
1544
+ ['↑ ↓', t('keys.arrows')],
1545
+ ['Esc', t('keys.escape')],
1536
1546
  ].map(([k, v]) => h('div', { class: 'keyrow' }, [h('kbd', {}, [k]), v])),
1537
1547
  ]));
1538
1548
  }
@@ -1542,6 +1552,12 @@ const devices = [
1542
1552
  { name: 'Laptop', width: 1280, height: 800 },
1543
1553
  { name: 'Desktop', width: 1440, height: 900 },
1544
1554
  ];
1555
+ const deviceNames = {
1556
+ Phone: 'bench.phone',
1557
+ Tablet: 'bench.tablet',
1558
+ Laptop: 'bench.laptop',
1559
+ Desktop: 'bench.desktop',
1560
+ };
1545
1561
  const left = h('div', { class: 'bench-left' });
1546
1562
  const stage = h('div', { class: 'stage' });
1547
1563
  const sizer = h('div', { class: 'sizer' });
@@ -1595,21 +1611,23 @@ function renderBar() {
1595
1611
  onclick: () => setMode(value),
1596
1612
  }, [label]);
1597
1613
  const select = h('select', {
1598
- 'aria-label': 'Device',
1614
+ 'aria-label': t('bench.device'),
1599
1615
  onchange: (e) => {
1600
1616
  const d = devices.find((x) => x.name === e.target.value);
1601
1617
  if (d)
1602
1618
  setDevice(d);
1603
1619
  },
1604
1620
  }, [
1605
- ...devices.map((d) => h('option', { value: d.name, selected: d.name === state.device.name }, [`${d.name} ${d.width}`])),
1621
+ ...devices.map((d) => h('option', { value: d.name, selected: d.name === state.device.name }, [
1622
+ t('bench.option', { name: t(deviceNames[d.name]), width: d.width }),
1623
+ ])),
1606
1624
  devices.some((d) => d.name === state.device.name)
1607
1625
  ? null
1608
- : h('option', { value: 'Custom', selected: true }, ['Custom']),
1626
+ : h('option', { value: 'Custom', selected: true }, [t('bench.custom')]),
1609
1627
  ]);
1610
1628
  bar.replaceChildren(...present([
1611
1629
  h('div', { class: 'grip' }, [h('img', { src: logo, alt: 'Hozu', width: '18', height: '18' })]),
1612
- h('div', { class: 'seg' }, [mode('Browse', 'browse'), mode('Select', 'select')]),
1630
+ h('div', { class: 'seg' }, [mode(t('dock.browse'), 'browse'), mode(t('dock.select'), 'select')]),
1613
1631
  h('button', {
1614
1632
  class: 'act layers-toggle',
1615
1633
  type: 'button',
@@ -1618,47 +1636,50 @@ function renderBar() {
1618
1636
  bench.classList.toggle('layers-open');
1619
1637
  renderBar();
1620
1638
  },
1621
- }, ['Layers']),
1639
+ }, [t('dock.layers')]),
1622
1640
  select,
1623
1641
  h('button', {
1624
1642
  class: 'act',
1625
1643
  type: 'button',
1626
- title: 'Rotate',
1627
- 'aria-label': 'Rotate',
1644
+ title: t('bench.rotate'),
1645
+ 'aria-label': t('bench.rotate'),
1628
1646
  onclick: () => setDevice({ name: state.device.name, width: state.device.height, height: state.device.width }),
1629
1647
  }, ['⟲']),
1630
1648
  h('span', { class: 'size', 'data-size': true }),
1631
1649
  h('span', { class: 'spacer' }),
1632
1650
  held()
1633
1651
  ? h('button', { class: 'previewing', type: 'button', onclick: () => hold(null, reload) }, [
1634
- 'Preview · Exit',
1652
+ `${t('dock.preview')} · ${t('dock.exit')}`,
1635
1653
  ])
1636
1654
  : null,
1637
1655
  h('button', {
1638
1656
  class: 'act',
1639
1657
  type: 'button',
1640
1658
  onclick: () => open(state.panel === 'changes' ? null : 'changes', null, drafted().length ? 'draft' : 'saved'),
1641
- }, ['Changes', h('span', { class: 'count' }, [drafted().length ? String(drafted().length) : ''])]),
1659
+ }, [
1660
+ t('dock.changes'),
1661
+ h('span', { class: 'count' }, [drafted().length ? String(drafted().length) : '']),
1662
+ ]),
1642
1663
  h('button', {
1643
1664
  class: 'act gear',
1644
1665
  type: 'button',
1645
- 'aria-label': 'Settings',
1666
+ 'aria-label': t('dock.settings'),
1646
1667
  onclick: () => open(state.panel === 'settings' ? null : 'settings'),
1647
1668
  }, ['⚙']),
1648
1669
  h('button', {
1649
1670
  class: 'act',
1650
1671
  type: 'button',
1651
- title: 'The data this page reads and the changes it can make',
1672
+ title: t('bench.api.title'),
1652
1673
  'aria-pressed': api.isOpen() ? 'true' : 'false',
1653
1674
  onclick: () => api.toggle(),
1654
- }, ['API']),
1675
+ }, [t('dock.api')]),
1655
1676
  h('button', {
1656
1677
  class: 'act',
1657
1678
  type: 'button',
1658
- title: 'Every component of the app, its variants, previews and uses; and the design tokens',
1679
+ title: t('dock.assets.title'),
1659
1680
  onclick: () => void assets.open(),
1660
- }, ['Assets']),
1661
- h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, ['Exit frame']),
1681
+ }, [t('dock.assets')]),
1682
+ h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, [t('bench.exit')]),
1662
1683
  ]));
1663
1684
  }
1664
1685
  async function renderLeft() {
@@ -1675,7 +1696,7 @@ function openBench() {
1675
1696
  bench.hidden = false;
1676
1697
  const frame = h('iframe', {
1677
1698
  src: location.href,
1678
- title: 'The page in Frame',
1699
+ title: t('bench.frame'),
1679
1700
  'data-hozu-bench': true,
1680
1701
  });
1681
1702
  frame.addEventListener('load', () => {
@@ -1686,7 +1707,7 @@ function openBench() {
1686
1707
  void renderLeft();
1687
1708
  void renderPanel();
1688
1709
  });
1689
- const grip = h('div', { class: 'resize', title: 'Drag to resize', 'aria-hidden': 'true' });
1710
+ const grip = h('div', { class: 'resize', title: t('common.resize'), 'aria-hidden': 'true' });
1690
1711
  grip.addEventListener('pointerdown', (event) => {
1691
1712
  event.preventDefault();
1692
1713
  grip.setPointerCapture(event.pointerId);