@verbaly/studio 0.56.1 → 0.57.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbaly/studio",
3
- "version": "0.56.1",
3
+ "version": "0.57.0",
4
4
  "description": "Local review server for Verbaly: read, edit and approve translations over the JSON files already in your repo. No account, no database.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -26,17 +26,7 @@
26
26
  ".": {
27
27
  "types": "./dist/index.d.ts",
28
28
  "import": "./dist/index.js"
29
- },
30
- "./ui": {
31
- "types": "./dist/panel/index.ts",
32
- "import": "./dist/panel/index.ts"
33
- },
34
- "./Panel.svelte": {
35
- "types": "./dist/panel/Panel.svelte.d.ts",
36
- "svelte": "./dist/panel/Panel.svelte",
37
- "default": "./dist/panel/Panel.svelte"
38
- },
39
- "./tokens.css": "./dist/panel/tokens.css"
29
+ }
40
30
  },
41
31
  "bin": {
42
32
  "verbaly-studio": "./dist/cli.js"
@@ -53,7 +43,7 @@
53
43
  "access": "public"
54
44
  },
55
45
  "dependencies": {
56
- "@verbaly/compiler": "^0.56.1"
46
+ "@verbaly/compiler": "^0.57.0"
57
47
  },
58
48
  "devDependencies": {
59
49
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
@@ -62,22 +52,10 @@
62
52
  "svelte-check": "^4.7.6",
63
53
  "typescript": "npm:@typescript/typescript6@^6.0.2",
64
54
  "vite": "^8.3.0",
65
- "verbaly": "^0.56.1"
66
- },
67
- "peerDependencies": {
68
- "svelte": "^5.0.0",
69
- "verbaly": "^0.56.1"
70
- },
71
- "peerDependenciesMeta": {
72
- "svelte": {
73
- "optional": true
74
- },
75
- "verbaly": {
76
- "optional": true
77
- }
55
+ "verbaly": "^0.57.0"
78
56
  },
79
57
  "scripts": {
80
- "build": "tsdown && vite build && node scripts/copy-panel.mjs",
58
+ "build": "tsdown && vite build",
81
59
  "dev": "vite",
82
60
  "test": "vitest run --passWithNoTests",
83
61
  "typecheck": "tsc --noEmit && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
@@ -1,255 +0,0 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { ACTION, messages } from './words';
4
- import type { Job, Plan, StudioApi } from './api';
5
-
6
- interface Props {
7
- api: StudioApi;
8
- scanning: boolean;
9
- targets: number;
10
- shown: string[];
11
- onDone: (said: string) => Promise<void> | void;
12
- }
13
-
14
- const { api, scanning, targets, shown, onDone }: Props = $props();
15
-
16
- type Phase =
17
- | { at: 'idle' }
18
- | { at: 'finding' }
19
- | { at: 'planning' }
20
- | { at: 'plan'; plan: Plan }
21
- | { at: 'running'; job: Job }
22
- | { at: 'said'; text: string; bad: boolean };
23
-
24
- // the annotation goes on the rune: on the declaration, phase narrows to its first member
25
- let phase = $state<Phase>({ at: 'idle' });
26
- const busy = $derived(phase.at === 'finding' || phase.at === 'planning' || phase.at === 'running');
27
-
28
- function said(text: string, bad = false) {
29
- phase = { at: 'said', text, bad };
30
- }
31
-
32
- async function find() {
33
- phase = { at: 'finding' };
34
- const answer = await api.extract();
35
- if (answer.error || !answer.value) return said(answer.error ?? 'no answer', true);
36
- const { found } = answer.value;
37
- if (!found) return said(ACTION.nothingNew);
38
- const wrote = `${messages(found)} your code uses and your catalogs did not have`;
39
- await onDone(wrote);
40
- said(wrote);
41
- }
42
-
43
- async function plan() {
44
- phase = { at: 'planning' };
45
- const answer = await api.plan(shown);
46
- if (answer.error || !answer.value) return said(answer.error ?? 'no answer', true);
47
- if (!answer.value.total) return said(ACTION.nothingMissing);
48
- phase = { at: 'plan', plan: answer.value };
49
- }
50
-
51
- // the bar is asked, never pushed: translate writes partial results, so a reload must not lose it
52
- async function poll(id: string) {
53
- const answer = await api.job(id);
54
- if (answer.error || !answer.value) return said(answer.error ?? 'no answer', true);
55
- const job = answer.value;
56
- if (job.state === 'running') {
57
- phase = { at: 'running', job };
58
- return;
59
- }
60
- // every translation a machine wrote is a draft, and the panel has to say so out loud
61
- const wrote = `${messages(job.done)} translated, unread until you read them`;
62
- await onDone(wrote);
63
- said(job.state === 'failed' ? (job.message ?? 'the run failed') : wrote, job.state === 'failed');
64
- }
65
-
66
- async function go() {
67
- const answer = await api.translate(shown);
68
- if (answer.error || !answer.value) return said(answer.error ?? 'no answer', true);
69
- phase = { at: 'running', job: answer.value };
70
- }
71
-
72
- // a run outlives the page that started it, so the bar comes back instead of the job hiding
73
- onMount(() => {
74
- void api.job().then((answer) => {
75
- if (answer.value && phase.at === 'idle') phase = { at: 'running', job: answer.value };
76
- });
77
- });
78
-
79
- $effect(() => {
80
- if (phase.at !== 'running') return;
81
- const id = phase.job.id;
82
- const timer = setInterval(() => void poll(id), 1000);
83
- return () => clearInterval(timer);
84
- });
85
-
86
- // a disabled control still owes you the reason, and the reason is not always the same one
87
- const why = $derived(
88
- !targets ? ACTION.onlyLocale : shown.length ? ACTION.translateWhy : ACTION.pickFirst,
89
- );
90
-
91
- const share = $derived(
92
- phase.at === 'running' && phase.job.total ? phase.job.done / phase.job.total : 0,
93
- );
94
- </script>
95
-
96
- <section class="acts" aria-label="Commands">
97
- <h2 class="cap">Commands</h2>
98
-
99
- {#if phase.at === 'plan'}
100
- <div class="bill">
101
- <p class="lede">{messages(phase.plan.total)} in {Object.keys(phase.plan.pending).join(', ')}</p>
102
- <p class="why">{ACTION.translateWhy}</p>
103
- <div class="pair">
104
- <button class="go" onclick={go}>{ACTION.translate}</button>
105
- <button class="plain" onclick={() => (phase = { at: 'idle' })}>{ACTION.cancel}</button>
106
- </div>
107
- </div>
108
- {:else if phase.at === 'running'}
109
- <div class="bill">
110
- <p class="lede">{ACTION.running} {phase.job.locale ?? ''}</p>
111
- <div class="track"><span class="fill" style="width: {Math.round(share * 100)}%"></span></div>
112
- <p class="why">{phase.job.done} of {phase.job.total}</p>
113
- </div>
114
- {:else}
115
- <button class="act" disabled={busy || !scanning} onclick={find}>
116
- <span class="name">{phase.at === 'finding' ? ACTION.working : ACTION.find}</span>
117
- <span class="why">{scanning ? ACTION.findWhy : ACTION.scanOff}</span>
118
- </button>
119
- <button class="act" disabled={busy || !shown.length} onclick={plan}>
120
- <span class="name">{ACTION.translate}</span>
121
- <span class="why">{why}</span>
122
- </button>
123
- {/if}
124
-
125
- {#if phase.at === 'said'}
126
- <p class="said" class:bad={phase.bad}>{phase.text}</p>
127
- {/if}
128
- </section>
129
-
130
- <style>
131
- .acts {
132
- margin-bottom: 22px;
133
- }
134
-
135
- .cap {
136
- margin: 0 0 8px;
137
- padding: 0 6px;
138
- font-size: 11px;
139
- font-weight: 600;
140
- letter-spacing: 0.08em;
141
- text-transform: uppercase;
142
- color: var(--muted);
143
- }
144
-
145
- .act {
146
- display: flex;
147
- flex-direction: column;
148
- gap: 1px;
149
- width: 100%;
150
- margin-bottom: 4px;
151
- padding: 7px 8px;
152
- border: 0;
153
- border-radius: var(--radius);
154
- background: none;
155
- font: inherit;
156
- color: var(--body);
157
- cursor: pointer;
158
- text-align: left;
159
- }
160
-
161
- .act:hover:not(:disabled) {
162
- background: var(--hollow);
163
- }
164
-
165
- .act:disabled {
166
- cursor: default;
167
- }
168
-
169
- /* only the name dims: the line under it is where a disabled button says why it is disabled */
170
- .act:disabled .name {
171
- opacity: 0.55;
172
- }
173
-
174
- .name {
175
- font-size: 13px;
176
- color: var(--ink);
177
- }
178
-
179
- .why {
180
- margin: 0;
181
- font-size: 11.5px;
182
- line-height: 1.35;
183
- color: var(--muted);
184
- }
185
-
186
- .bill {
187
- padding: 10px;
188
- border: 1px solid var(--rule);
189
- border-radius: var(--radius);
190
- background: var(--paper);
191
- }
192
-
193
- .lede {
194
- margin: 0 0 4px;
195
- font-size: 13px;
196
- color: var(--ink);
197
- }
198
-
199
- .pair {
200
- display: flex;
201
- gap: 6px;
202
- margin-top: 10px;
203
- }
204
-
205
- .go {
206
- flex: 1;
207
- padding: 5px 10px;
208
- border: 0;
209
- border-radius: var(--radius);
210
- background: var(--done);
211
- /* paper turns with the theme, so the label stays legible on both greens */
212
- color: var(--paper);
213
- font: inherit;
214
- font-size: 12px;
215
- cursor: pointer;
216
- }
217
-
218
- .plain {
219
- padding: 5px 10px;
220
- border: 1px solid var(--rule-strong);
221
- border-radius: var(--radius);
222
- background: none;
223
- color: var(--body);
224
- font: inherit;
225
- font-size: 12px;
226
- cursor: pointer;
227
- }
228
-
229
- /* the one thing on this screen that moves, because it reports a run that is really happening */
230
- .track {
231
- height: 4px;
232
- margin: 8px 0 6px;
233
- border-radius: 999px;
234
- background: var(--hollow);
235
- overflow: hidden;
236
- }
237
-
238
- .fill {
239
- display: block;
240
- height: 100%;
241
- background: var(--done);
242
- transition: width 0.3s linear;
243
- }
244
-
245
- .said {
246
- margin: 6px 0 0;
247
- padding: 0 8px;
248
- font-size: 11.5px;
249
- color: var(--muted);
250
- }
251
-
252
- .said.bad {
253
- color: var(--broken);
254
- }
255
- </style>
@@ -1,184 +0,0 @@
1
- <script lang="ts">
2
- interface Entry {
3
- level: 'ok' | 'warn' | 'error';
4
- check: string;
5
- message: string;
6
- fix?: string;
7
- }
8
-
9
- interface Props {
10
- entries: Entry[];
11
- ok: boolean;
12
- onClose: () => void;
13
- }
14
-
15
- const { entries, ok, onClose }: Props = $props();
16
-
17
- // doctor names the part of the compiler that answered; this says what it means for your project
18
- const TITLE: Record<string, string> = {
19
- config: 'Your settings',
20
- cli: 'The verbaly command',
21
- catalogs: 'Your catalogs',
22
- routing: 'Where the language lives in your urls',
23
- plugin: 'The piece that wires your build',
24
- types: 'The generated types',
25
- sources: 'Reading your code',
26
- orphans: 'Keys nothing in your code uses',
27
- bundle: 'What the browser downloads',
28
- translations: 'The translations themselves',
29
- };
30
-
31
- const bad = $derived(entries.filter((e) => e.level !== 'ok').length);
32
- </script>
33
-
34
- <section class="health" aria-label="Health">
35
- <header>
36
- <h2>{ok ? 'Nothing is broken' : `${bad} of ${entries.length} need you`}</h2>
37
- <p>
38
- This is the same check <code>npx verbaly doctor</code> runs, read out loud. It looks at your
39
- settings, your catalogs and the way your build is wired, and it never changes anything.
40
- </p>
41
- <button class="close" onclick={onClose}>Back to the messages</button>
42
- </header>
43
-
44
- <ul>
45
- <!-- doctor can report the same check twice, so the position is the only unique key here -->
46
- {#each entries as entry, i (i)}
47
- <li data-level={entry.level}>
48
- <span class="mark" aria-hidden="true">
49
- {#if entry.level === 'ok'}
50
- <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3.4 8.4 3 3 6.2-6.8" /></svg>
51
- {:else if entry.level === 'warn'}
52
- <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 4v5" /><path d="M8 12h.01" /></svg>
53
- {:else}
54
- <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8" /></svg>
55
- {/if}
56
- </span>
57
- <span class="body">
58
- <b>{TITLE[entry.check] ?? entry.check}</b>
59
- <span class="msg">{entry.message}</span>
60
- {#if entry.fix}
61
- <span class="fix">{entry.fix}</span>
62
- {/if}
63
- </span>
64
- <span class="sr">{entry.level}</span>
65
- </li>
66
- {/each}
67
- </ul>
68
- </section>
69
-
70
- <style>
71
- .health {
72
- flex: 1;
73
- min-width: 0;
74
- overflow-y: auto;
75
- background: var(--paper);
76
- }
77
-
78
- header {
79
- padding: 28px 24px 20px;
80
- border-bottom: 1px solid var(--rule);
81
- }
82
-
83
- h2 {
84
- margin: 0;
85
- font-size: 21px;
86
- font-weight: 500;
87
- letter-spacing: -0.014em;
88
- color: var(--ink);
89
- }
90
-
91
- header p {
92
- margin: 8px 0 0;
93
- max-width: 64ch;
94
- font-size: 13px;
95
- color: var(--muted);
96
- }
97
-
98
- .close {
99
- margin-top: 16px;
100
- padding: 5px 12px;
101
- border: 1px solid var(--rule-strong);
102
- border-radius: var(--radius);
103
- background: var(--paper);
104
- color: var(--body);
105
- font: inherit;
106
- font-size: 12.5px;
107
- cursor: pointer;
108
- }
109
-
110
- .close:hover {
111
- background: var(--ground);
112
- }
113
-
114
- ul {
115
- margin: 0;
116
- padding: 0;
117
- list-style: none;
118
- }
119
-
120
- li {
121
- display: flex;
122
- align-items: flex-start;
123
- gap: 12px;
124
- padding: 14px 24px;
125
- border-bottom: 1px solid var(--rule);
126
- }
127
-
128
- .mark {
129
- width: 20px;
130
- height: 20px;
131
- flex: none;
132
- margin-top: 1px;
133
- display: inline-flex;
134
- align-items: center;
135
- justify-content: center;
136
- border-radius: 50%;
137
- background: var(--done-tint);
138
- color: var(--done);
139
- }
140
-
141
- [data-level='warn'] .mark {
142
- background: var(--missing-tint);
143
- color: var(--missing);
144
- }
145
-
146
- [data-level='error'] .mark {
147
- background: var(--broken-tint);
148
- color: var(--broken);
149
- }
150
-
151
- .body {
152
- display: flex;
153
- flex-direction: column;
154
- gap: 2px;
155
- min-width: 0;
156
- }
157
-
158
- b {
159
- font-size: 13.5px;
160
- font-weight: 500;
161
- color: var(--ink);
162
- }
163
-
164
- .msg {
165
- font-size: 13px;
166
- color: var(--body);
167
- }
168
-
169
- /* the remedy is the whole point of a diagnosis, so it never hides behind a disclosure */
170
- .fix {
171
- margin-top: 4px;
172
- font-family: var(--mono);
173
- font-size: 12px;
174
- color: var(--draft);
175
- }
176
-
177
- .sr {
178
- position: absolute;
179
- width: 1px;
180
- height: 1px;
181
- overflow: hidden;
182
- clip-path: inset(50%);
183
- }
184
- </style>
@@ -1,56 +0,0 @@
1
- <script lang="ts">
2
- import type { MessageState } from './model';
3
-
4
- // Each ink carries its own mark, so the ladder reads in greyscale and to a colourblind eye.
5
- const { state }: { state: MessageState } = $props();
6
-
7
- const WORD: Record<MessageState, string> = {
8
- done: 'translated',
9
- draft: 'written by a machine',
10
- missing: 'not translated, shows in your source language',
11
- broken: 'breaks your site',
12
- };
13
- </script>
14
-
15
- {#if state === 'done'}
16
- <svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true">
17
- <path d="M5 11h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
18
- </svg>
19
- {:else if state === 'missing'}
20
- <svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true" fill="none">
21
- <path
22
- d="M6 12.5 11 7l5 5.5"
23
- stroke="currentColor"
24
- stroke-width="1.7"
25
- stroke-linecap="round"
26
- stroke-linejoin="round"
27
- />
28
- <path d="M11 8v7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
29
- </svg>
30
- {:else if state === 'draft'}
31
- <svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true" fill="none">
32
- <path
33
- d="M4.5 12.5c1.6-2.2 3.2-2.2 4.8 0s3.2 2.2 4.8 0 3.2-2.2 4.8 0"
34
- stroke="currentColor"
35
- stroke-width="1.7"
36
- stroke-linecap="round"
37
- />
38
- </svg>
39
- {:else}
40
- <svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true">
41
- <path d="M11 6v6.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
42
- <circle cx="11" cy="16" r="1.2" fill="currentColor" />
43
- </svg>
44
- {/if}
45
- <span class="sr">{WORD[state]}</span>
46
-
47
- <style>
48
- .sr {
49
- position: absolute;
50
- width: 1px;
51
- height: 1px;
52
- overflow: hidden;
53
- clip-path: inset(50%);
54
- white-space: nowrap;
55
- }
56
- </style>