@hozu/devtools 0.17.2 → 0.18.1

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,36 @@ 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'), [
1140
+ ...describeFor(n).map((line) => h('div', { class: 'plain' }, [line])),
1141
+ n.component?.client
1142
+ ? h('div', { class: 'plain' }, [t('plain.about.client', { file: n.component.client.file })])
1143
+ : null,
1144
+ ]),
1116
1145
  h('div', { class: 'sec' }, [
1117
1146
  h('div', { class: 'nav' }, [
1118
- h('button', { type: 'button', onclick: () => walk('up') }, ['↑ Select the area around it']),
1119
- children.length && !isText(n)
1120
- ? h('button', { type: 'button', onclick: () => walk('down') }, ['↓ Inside'])
1147
+ h('button', { type: 'button', onclick: () => walk('up') }, [t('inspector.around')]),
1148
+ children.length && !isText(n) && !n.component?.client
1149
+ ? h('button', { type: 'button', onclick: () => walk('down') }, [t('inspector.inside')])
1121
1150
  : null,
1122
- h('button', { type: 'button', onclick: removeActive }, ['Remove']),
1151
+ h('button', { type: 'button', onclick: removeActive }, [t('inspector.remove')]),
1123
1152
  ]),
1124
1153
  n.location
1125
1154
  ? h('div', { class: 'file' }, [
@@ -1128,7 +1157,7 @@ function builderSections(n) {
1128
1157
  class: 'link',
1129
1158
  type: 'button',
1130
1159
  onclick: () => void copy(`${n.location.file}:${n.location.line}`),
1131
- }, ['Copy']),
1160
+ }, [t('inspector.copy')]),
1132
1161
  ])
1133
1162
  : null,
1134
1163
  ]),
@@ -1137,25 +1166,30 @@ function builderSections(n) {
1137
1166
  function developerSections(n, active) {
1138
1167
  const children = n.children.filter((c) => c.kind !== 'text' || c.text);
1139
1168
  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,
1169
+ section(t('inspector.where'), [
1170
+ where(t(n.page ? 'inspector.where.page' : 'inspector.where.view'), n.location),
1171
+ n.page ? where(t('inspector.where.route'), n.page.routeLocation) : null,
1172
+ n.component ? where(t('inspector.where.component'), n.component.declaration) : null,
1173
+ n.events.length ? where(t('inspector.where.machine'), n.machine) : null,
1145
1174
  excerpt(n),
1146
1175
  ]),
1147
- section('Component', n.component
1176
+ section(t('inspector.component'), n.component
1148
1177
  ? [
1149
1178
  h('div', {}, [
1150
1179
  h('span', { class: 'chip red' }, [n.component.ref]),
1151
1180
  h('span', { class: 'chip' }, [
1152
- `used in ${n.component.uses} ${n.component.uses === 1 ? 'place' : 'places'}`,
1181
+ t(n.component.uses === 1 ? 'inspector.usedIn.one' : 'inspector.usedIn.other', {
1182
+ count: n.component.uses,
1183
+ }),
1153
1184
  ]),
1154
1185
  ...Object.entries(n.component.variant).map(([k, v]) => h('span', { class: 'chip' }, [`${k}=${v}`])),
1155
1186
  ]),
1187
+ n.component.client
1188
+ ? h('div', { class: 'plain' }, [t('inspector.client', { file: n.component.client.file })])
1189
+ : null,
1156
1190
  ]
1157
1191
  : []),
1158
- section('Head', n.page
1192
+ section(t('inspector.head'), n.page
1159
1193
  ? [
1160
1194
  ...['title', 'description', 'image'].map((f) => h('div', { class: 'row' }, [h('b', {}, [`${f} `]), n.page.head[f] ?? '—'])),
1161
1195
  h('div', { class: 'row' }, [h('b', {}, ['views ']), n.page.views.join(', ')]),
@@ -1165,18 +1199,18 @@ function developerSections(n, active) {
1165
1199
  : null,
1166
1200
  ]
1167
1201
  : []),
1168
- section('Text', textRows(n)),
1169
- section('Shown when', n.conditions.map((c) => h('div', { class: 'row' }, [
1202
+ section(t('inspector.text'), textRows(n)),
1203
+ section(t('inspector.shownWhen'), n.conditions.map((c) => h('div', { class: 'row' }, [
1170
1204
  h('b', {}, [`${c.kind} `]),
1171
1205
  c.detail,
1172
1206
  c.location ? ` ${c.location.file}:${c.location.line}` : '',
1173
1207
  ]))),
1174
- section('Behaviour', n.events.map((e) => h('div', { class: 'row' }, [
1208
+ section(t('inspector.behaviour'), n.events.map((e) => h('div', { class: 'row' }, [
1175
1209
  h('span', { class: 'chip red' }, [`on ${e.dom}`]),
1176
1210
  h('span', { class: 'chip' }, [e.event]),
1177
1211
  ...e.transitions.map((t) => h('span', { class: 'chip' }, [`${t.from} → ${t.to}${t.guarded ? ' if …' : ''}`])),
1178
1212
  ]))),
1179
- section('Inside', [
1213
+ section(t('inspector.insideIt'), [
1180
1214
  children.length
1181
1215
  ? h('div', { class: 'kids' }, children.map((c) => h('button', {
1182
1216
  type: 'button',
@@ -1184,9 +1218,11 @@ function developerSections(n, active) {
1184
1218
  }, [c.kind === 'text' ? `text ${c.text}` : `${c.kind} ${c.id.split('/').slice(-1)[0]}`])))
1185
1219
  : null,
1186
1220
  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']),
1221
+ h('button', { type: 'button', onclick: () => walk('up') }, [t('inspector.parent')]),
1222
+ isText(n) || n.component?.client
1223
+ ? null
1224
+ : h('button', { type: 'button', onclick: () => walk('down') }, [t('inspector.child')]),
1225
+ h('button', { type: 'button', onclick: removeActive }, [t('inspector.remove')]),
1190
1226
  ]),
1191
1227
  ]),
1192
1228
  h('div', { class: 'sec meta' }, [`node ${n.id}`, n.pointer ? ` · ${n.pointer}` : '']),
@@ -1212,19 +1248,13 @@ async function renderPanel() {
1212
1248
  if (state.panel !== 'inspector' || !active) {
1213
1249
  panel.hidden = state.view !== 'workbench';
1214
1250
  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
- ]));
1251
+ panel.replaceChildren(h('div', { class: 'empty' }, [h('b', {}, [t('inspector.empty.title')]), t('inspector.empty.text')]));
1219
1252
  return;
1220
1253
  }
1221
1254
  const n = await node(active.id);
1222
1255
  if (!n) {
1223
1256
  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
- ]));
1257
+ panel.replaceChildren(h('div', { class: 'empty' }, [h('b', {}, [t('inspector.gone.title')]), t('inspector.gone.text')]));
1228
1258
  return;
1229
1259
  }
1230
1260
  const nodes = await Promise.all(state.picks.map((p) => node(p.id)));
@@ -1269,8 +1299,8 @@ function tabs(current, saved) {
1269
1299
  onclick: () => open('changes', null, name),
1270
1300
  }, [label]);
1271
1301
  return h('div', { class: 'tabs' }, [
1272
- tab('draft', `This request · ${drafted().length}`),
1273
- tab('saved', `Saved · ${saved}`),
1302
+ tab('draft', t('changes.tab.draft', { count: drafted().length })),
1303
+ tab('saved', t('changes.tab.saved', { count: saved })),
1274
1304
  ]);
1275
1305
  }
1276
1306
  async function renderChanges() {
@@ -1278,8 +1308,8 @@ async function renderChanges() {
1278
1308
  const current = state.tab;
1279
1309
  panel.hidden = false;
1280
1310
  const head = h('div', { class: 'head' }, [
1281
- h('div', { class: 'kicker' }, [current === 'draft' ? 'Not sent yet' : '.hozu/requests']),
1282
- h('h2', { class: 'title' }, ['Changes']),
1311
+ h('div', { class: 'kicker' }, [current === 'draft' ? t('changes.notSent') : '.hozu/requests']),
1312
+ h('h2', { class: 'title' }, [t('changes.title')]),
1283
1313
  closeButton(() => open(state.picks.length ? 'inspector' : null)),
1284
1314
  tabs(current, list.length),
1285
1315
  ]);
@@ -1292,17 +1322,17 @@ async function renderChanges() {
1292
1322
  const done = h('button', {
1293
1323
  class: 'link done',
1294
1324
  type: 'button',
1295
- title: 'Resolved or not needed: remove it',
1325
+ title: t('changes.resolve.title'),
1296
1326
  onclick: async () => {
1297
1327
  if (!armed) {
1298
1328
  armed = true;
1299
- done.textContent = 'Remove?';
1329
+ done.textContent = t('changes.resolve.confirm');
1300
1330
  return;
1301
1331
  }
1302
1332
  await remove(r.number);
1303
1333
  void renderPanel();
1304
1334
  },
1305
- }, ['Resolve']);
1335
+ }, [t('changes.resolve')]);
1306
1336
  return h('div', { class: 'req' }, [
1307
1337
  h('span', { class: 'n' }, [r.number]),
1308
1338
  h('button', { class: 't open-req', type: 'button', onclick: () => open('changes', r.number, 'saved') }, [r.title]),
@@ -1315,7 +1345,7 @@ async function renderChanges() {
1315
1345
  message ? h('div', { class: 'notice' }, message) : null,
1316
1346
  ...(list.length
1317
1347
  ? [...list].reverse().map(row)
1318
- : [h('div', { class: 'empty' }, ['No open requests. Resolved ones are removed.'])]),
1348
+ : [h('div', { class: 'empty' }, [t('changes.noRequests')])]),
1319
1349
  list.length
1320
1350
  ? h('div', { class: 'sec' }, [
1321
1351
  h('div', { class: 'choice' }, [
@@ -1325,24 +1355,18 @@ async function renderChanges() {
1325
1355
  onclick: async () => {
1326
1356
  const full = await Promise.all(list.map((r) => one(r.number)));
1327
1357
  await copy(joinRequests(full));
1328
- status(result, 'ok', [
1329
- `Copied all ${list.length} as one prompt. Paste it to your agent.`,
1330
- ]);
1358
+ status(result, 'ok', [t('changes.copiedAll', { count: list.length })]);
1331
1359
  },
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
- ]),
1360
+ }, [t('changes.copyAll', { count: list.length })]),
1361
+ h('span', { class: 'why' }, [t('changes.copyAll.why')]),
1336
1362
  h('button', {
1337
1363
  type: 'button',
1338
1364
  onclick: async () => {
1339
1365
  await copy(openRequestsLine(list));
1340
- status(result, 'ok', ['Copied the file paths.']);
1366
+ status(result, 'ok', [t('changes.copiedPaths')]);
1341
1367
  },
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
- ]),
1368
+ }, [t('changes.copyPaths')]),
1369
+ h('span', { class: 'why' }, [t('changes.copyPaths.why')]),
1346
1370
  ]),
1347
1371
  result,
1348
1372
  ])
@@ -1365,7 +1389,9 @@ async function renderChanges() {
1365
1389
  [
1366
1390
  p.note.trim(),
1367
1391
  p.style?.length
1368
- ? `${p.style.length} style ${p.style.length === 1 ? 'change' : 'changes'}`
1392
+ ? t(p.style.length === 1 ? 'changes.styles.one' : 'changes.styles.other', {
1393
+ count: p.style.length,
1394
+ })
1369
1395
  : '',
1370
1396
  ]
1371
1397
  .filter(Boolean)
@@ -1379,7 +1405,7 @@ async function renderChanges() {
1379
1405
  state.active = state.picks.indexOf(p);
1380
1406
  open('inspector');
1381
1407
  },
1382
- }, ['Edit']),
1408
+ }, [t('changes.edit')]),
1383
1409
  h('button', {
1384
1410
  class: 'link',
1385
1411
  type: 'button',
@@ -1393,13 +1419,10 @@ async function renderChanges() {
1393
1419
  renderDock();
1394
1420
  void renderPanel();
1395
1421
  },
1396
- }, ['Remove']),
1422
+ }, [t('changes.remove')]),
1397
1423
  ]);
1398
1424
  }))
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
- ]),
1425
+ : h('div', { class: 'empty' }, [h('b', {}, [t('changes.empty.title')]), t('changes.empty.text')]),
1403
1426
  items.length ? h('div', { class: 'sec' }, [exportBlock()]) : null,
1404
1427
  ]));
1405
1428
  }
@@ -1420,17 +1443,17 @@ async function renderRequest(number) {
1420
1443
  onclick: async () => {
1421
1444
  if (!armed) {
1422
1445
  armed = true;
1423
- del.textContent = 'Click again to remove';
1446
+ del.textContent = t('request.remove.confirm');
1424
1447
  return;
1425
1448
  }
1426
1449
  await remove(r.number);
1427
1450
  renderDock();
1428
1451
  open('changes', null, 'saved');
1429
1452
  },
1430
- }, ['Resolve · remove']);
1453
+ }, [t('request.remove')]);
1431
1454
  panel.replaceChildren(h('div', { class: 'head' }, [
1432
1455
  h('button', { class: 'back', type: 'button', onclick: () => open('changes', null, 'saved') }, [
1433
- '← Saved',
1456
+ t('request.back'),
1434
1457
  ]),
1435
1458
  h('h2', { class: 'title' }, [r.title]),
1436
1459
  h('div', { class: 'kicker' }, [r.file]),
@@ -1442,16 +1465,16 @@ async function renderRequest(number) {
1442
1465
  type: 'button',
1443
1466
  onclick: async () => {
1444
1467
  await copy(`Do the Hozu request ${r.file}`);
1445
- status(result, 'ok', [`Copied “Do the Hozu request ${r.file}”.`]);
1468
+ status(result, 'ok', [t('request.copiedAsk', { ask: `Do the Hozu request ${r.file}` })]);
1446
1469
  },
1447
- }, ['Copy “do this request”']),
1470
+ }, [t('request.copyAsk')]),
1448
1471
  h('button', {
1449
1472
  type: 'button',
1450
1473
  onclick: async () => {
1451
1474
  await copy(r.markdown);
1452
- status(result, 'ok', ['Copied the whole request.']);
1475
+ status(result, 'ok', [t('request.copied')]);
1453
1476
  },
1454
- }, ['Copy request']),
1477
+ }, [t('request.copy')]),
1455
1478
  del,
1456
1479
  ]),
1457
1480
  result,
@@ -1475,14 +1498,14 @@ function renderSettings() {
1475
1498
  ]);
1476
1499
  panel.replaceChildren(h('div', { class: 'head' }, [
1477
1500
  h('div', { class: 'kicker' }, ['Hozu DevTools']),
1478
- h('h2', { class: 'title' }, ['Settings']),
1501
+ h('h2', { class: 'title' }, [t('settings.title')]),
1479
1502
  closeButton(() => open(state.picks.length ? 'inspector' : null)),
1480
1503
  ]), 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'],
1504
+ h('div', { class: 'label' }, [t('settings.appearance')]),
1505
+ h('div', { class: 'tabs', role: 'radiogroup', 'aria-label': t('settings.appearance') }, [
1506
+ ['system', t('settings.system')],
1507
+ ['light', t('settings.light')],
1508
+ ['dark', t('settings.dark')],
1486
1509
  ].map(([value, label]) => h('button', {
1487
1510
  class: 'tab',
1488
1511
  type: 'button',
@@ -1497,13 +1520,13 @@ function renderSettings() {
1497
1520
  },
1498
1521
  }, [label]))),
1499
1522
  ]), h('div', { class: 'sec' }, [
1500
- h('div', { class: 'label' }, ['Show']),
1523
+ h('div', { class: 'label' }, [t('settings.show')]),
1501
1524
  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.'),
1525
+ choice('builder', t('settings.builder'), t('settings.builder.detail')),
1526
+ choice('developer', t('settings.developer'), t('settings.developer.detail')),
1504
1527
  ]),
1505
1528
  ]), h('div', { class: 'sec' }, [
1506
- h('div', { class: 'label' }, ['Requests']),
1529
+ h('div', { class: 'label' }, [t('settings.requests')]),
1507
1530
  h('label', { class: 'option' }, [
1508
1531
  h('input', {
1509
1532
  type: 'checkbox',
@@ -1513,26 +1536,21 @@ function renderSettings() {
1513
1536
  persist();
1514
1537
  },
1515
1538
  }),
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
- ]),
1539
+ h('span', {}, [h('b', {}, [t('settings.excerpt')]), h('span', {}, [t('settings.excerpt.detail')])]),
1522
1540
  ]),
1523
1541
  ]), h('div', { class: 'sec' }, [
1524
- h('div', { class: 'label' }, ['Keys']),
1542
+ h('div', { class: 'label' }, [t('settings.keys')]),
1525
1543
  ...[
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'],
1544
+ ['Alt+Shift+S', t('keys.toggle')],
1545
+ ['Click', t('keys.click')],
1546
+ ['Shift+click', t('keys.add')],
1547
+ ['Shift+Enter', t('keys.around')],
1548
+ ['Enter', t('keys.inside')],
1549
+ ['Tab / Shift+Tab', t('keys.beside')],
1550
+ ['Alt (hold)', t('keys.measure')],
1551
+ ['Double-click', t('keys.text')],
1552
+ ['↑ ↓', t('keys.arrows')],
1553
+ ['Esc', t('keys.escape')],
1536
1554
  ].map(([k, v]) => h('div', { class: 'keyrow' }, [h('kbd', {}, [k]), v])),
1537
1555
  ]));
1538
1556
  }
@@ -1542,6 +1560,12 @@ const devices = [
1542
1560
  { name: 'Laptop', width: 1280, height: 800 },
1543
1561
  { name: 'Desktop', width: 1440, height: 900 },
1544
1562
  ];
1563
+ const deviceNames = {
1564
+ Phone: 'bench.phone',
1565
+ Tablet: 'bench.tablet',
1566
+ Laptop: 'bench.laptop',
1567
+ Desktop: 'bench.desktop',
1568
+ };
1545
1569
  const left = h('div', { class: 'bench-left' });
1546
1570
  const stage = h('div', { class: 'stage' });
1547
1571
  const sizer = h('div', { class: 'sizer' });
@@ -1595,21 +1619,23 @@ function renderBar() {
1595
1619
  onclick: () => setMode(value),
1596
1620
  }, [label]);
1597
1621
  const select = h('select', {
1598
- 'aria-label': 'Device',
1622
+ 'aria-label': t('bench.device'),
1599
1623
  onchange: (e) => {
1600
1624
  const d = devices.find((x) => x.name === e.target.value);
1601
1625
  if (d)
1602
1626
  setDevice(d);
1603
1627
  },
1604
1628
  }, [
1605
- ...devices.map((d) => h('option', { value: d.name, selected: d.name === state.device.name }, [`${d.name} ${d.width}`])),
1629
+ ...devices.map((d) => h('option', { value: d.name, selected: d.name === state.device.name }, [
1630
+ t('bench.option', { name: t(deviceNames[d.name]), width: d.width }),
1631
+ ])),
1606
1632
  devices.some((d) => d.name === state.device.name)
1607
1633
  ? null
1608
- : h('option', { value: 'Custom', selected: true }, ['Custom']),
1634
+ : h('option', { value: 'Custom', selected: true }, [t('bench.custom')]),
1609
1635
  ]);
1610
1636
  bar.replaceChildren(...present([
1611
1637
  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')]),
1638
+ h('div', { class: 'seg' }, [mode(t('dock.browse'), 'browse'), mode(t('dock.select'), 'select')]),
1613
1639
  h('button', {
1614
1640
  class: 'act layers-toggle',
1615
1641
  type: 'button',
@@ -1618,47 +1644,50 @@ function renderBar() {
1618
1644
  bench.classList.toggle('layers-open');
1619
1645
  renderBar();
1620
1646
  },
1621
- }, ['Layers']),
1647
+ }, [t('dock.layers')]),
1622
1648
  select,
1623
1649
  h('button', {
1624
1650
  class: 'act',
1625
1651
  type: 'button',
1626
- title: 'Rotate',
1627
- 'aria-label': 'Rotate',
1652
+ title: t('bench.rotate'),
1653
+ 'aria-label': t('bench.rotate'),
1628
1654
  onclick: () => setDevice({ name: state.device.name, width: state.device.height, height: state.device.width }),
1629
1655
  }, ['⟲']),
1630
1656
  h('span', { class: 'size', 'data-size': true }),
1631
1657
  h('span', { class: 'spacer' }),
1632
1658
  held()
1633
1659
  ? h('button', { class: 'previewing', type: 'button', onclick: () => hold(null, reload) }, [
1634
- 'Preview · Exit',
1660
+ `${t('dock.preview')} · ${t('dock.exit')}`,
1635
1661
  ])
1636
1662
  : null,
1637
1663
  h('button', {
1638
1664
  class: 'act',
1639
1665
  type: 'button',
1640
1666
  onclick: () => open(state.panel === 'changes' ? null : 'changes', null, drafted().length ? 'draft' : 'saved'),
1641
- }, ['Changes', h('span', { class: 'count' }, [drafted().length ? String(drafted().length) : ''])]),
1667
+ }, [
1668
+ t('dock.changes'),
1669
+ h('span', { class: 'count' }, [drafted().length ? String(drafted().length) : '']),
1670
+ ]),
1642
1671
  h('button', {
1643
1672
  class: 'act gear',
1644
1673
  type: 'button',
1645
- 'aria-label': 'Settings',
1674
+ 'aria-label': t('dock.settings'),
1646
1675
  onclick: () => open(state.panel === 'settings' ? null : 'settings'),
1647
1676
  }, ['⚙']),
1648
1677
  h('button', {
1649
1678
  class: 'act',
1650
1679
  type: 'button',
1651
- title: 'The data this page reads and the changes it can make',
1680
+ title: t('bench.api.title'),
1652
1681
  'aria-pressed': api.isOpen() ? 'true' : 'false',
1653
1682
  onclick: () => api.toggle(),
1654
- }, ['API']),
1683
+ }, [t('dock.api')]),
1655
1684
  h('button', {
1656
1685
  class: 'act',
1657
1686
  type: 'button',
1658
- title: 'Every component of the app, its variants, previews and uses; and the design tokens',
1687
+ title: t('dock.assets.title'),
1659
1688
  onclick: () => void assets.open(),
1660
- }, ['Assets']),
1661
- h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, ['Exit frame']),
1689
+ }, [t('dock.assets')]),
1690
+ h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, [t('bench.exit')]),
1662
1691
  ]));
1663
1692
  }
1664
1693
  async function renderLeft() {
@@ -1675,7 +1704,7 @@ function openBench() {
1675
1704
  bench.hidden = false;
1676
1705
  const frame = h('iframe', {
1677
1706
  src: location.href,
1678
- title: 'The page in Frame',
1707
+ title: t('bench.frame'),
1679
1708
  'data-hozu-bench': true,
1680
1709
  });
1681
1710
  frame.addEventListener('load', () => {
@@ -1686,7 +1715,7 @@ function openBench() {
1686
1715
  void renderLeft();
1687
1716
  void renderPanel();
1688
1717
  });
1689
- const grip = h('div', { class: 'resize', title: 'Drag to resize', 'aria-hidden': 'true' });
1718
+ const grip = h('div', { class: 'resize', title: t('common.resize'), 'aria-hidden': 'true' });
1690
1719
  grip.addEventListener('pointerdown', (event) => {
1691
1720
  event.preventDefault();
1692
1721
  grip.setPointerCapture(event.pointerId);