pgkiln 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/db/migrations/153_component_events.sql +21 -0
  2. package/docs/apex-feature-parity.md +3 -3
  3. package/docs/guide/03-builder.md +8 -1
  4. package/docs/guide/04-pages-and-regions.md +2 -1
  5. package/docs/guide/07-dynamic-actions.md +27 -0
  6. package/docs/guide/11-from-apex.md +2 -1
  7. package/docs/guide/12-development.md +9 -1
  8. package/package.json +1 -1
  9. package/public/app.css +2 -0
  10. package/public/app.js +85 -4
  11. package/public/builder.css +25 -2
  12. package/public/data-modeler.js +8 -5
  13. package/public/sql-monitor.js +5 -2
  14. package/src/builder/ai-builder.ts +50 -46
  15. package/src/builder/api.ts +49 -49
  16. package/src/builder/autorest.ts +92 -87
  17. package/src/builder/changelog.ts +39 -37
  18. package/src/builder/components.ts +5 -4
  19. package/src/builder/data-modeler.ts +89 -84
  20. package/src/builder/data-security.ts +39 -37
  21. package/src/builder/dataload.ts +69 -65
  22. package/src/builder/datapump.ts +108 -97
  23. package/src/builder/ddl-tools.ts +38 -34
  24. package/src/builder/document-view.ts +11 -11
  25. package/src/builder/i18n/de.ts +1627 -0
  26. package/src/builder/i18n/en.ts +1627 -0
  27. package/src/builder/i18n/es.ts +1627 -0
  28. package/src/builder/i18n/fr.ts +1627 -0
  29. package/src/builder/i18n/it.ts +1627 -0
  30. package/src/builder/i18n/ja.ts +1627 -0
  31. package/src/builder/i18n/ko.ts +1627 -0
  32. package/src/builder/i18n/nl.ts +1627 -0
  33. package/src/builder/i18n/pt.ts +1627 -0
  34. package/src/builder/i18n/zh-cn.ts +1627 -0
  35. package/src/builder/i18n/zh-tw.ts +1627 -0
  36. package/src/builder/i18n.ts +18 -0
  37. package/src/builder/object-actions.ts +18 -16
  38. package/src/builder/object-browser.ts +2 -2
  39. package/src/builder/querybuilder.ts +42 -36
  40. package/src/builder/quicksql.ts +16 -18
  41. package/src/builder/region-settings.ts +201 -199
  42. package/src/builder/report-settings.ts +64 -60
  43. package/src/builder/rest-modules.ts +145 -143
  44. package/src/builder/sampledata.ts +59 -52
  45. package/src/builder/scheduling.ts +180 -159
  46. package/src/builder/scripts.ts +71 -65
  47. package/src/builder/sql-charts.ts +36 -30
  48. package/src/builder/sql-monitor.ts +95 -89
  49. package/src/builder/sql-utilities.ts +47 -40
  50. package/src/builder/top-sql.ts +21 -17
  51. package/src/builder/unload.ts +48 -44
  52. package/src/builder/worksheet.ts +3 -3
  53. package/src/cron-schedule.ts +47 -17
  54. package/src/metadata.ts +3 -1
  55. package/src/runtime/calendar.ts +3 -2
  56. package/src/runtime/context.ts +2 -0
  57. package/src/runtime/grid.ts +1 -0
  58. package/src/runtime/maps.ts +7 -4
  59. package/src/runtime/render.ts +6 -1
  60. package/src/runtime/routes.ts +4 -0
  61. package/src/runtime/tree.ts +4 -3
@@ -32,6 +32,7 @@ import {
32
32
  import { clientIp, logActivity, type Session } from '../session.ts';
33
33
  import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
34
34
  import { currentWorkspace, workshopDb } from './workspaces.ts';
35
+ import { tr, type BuilderTranslate } from './i18n.ts';
35
36
 
36
37
  // SQL Workshop → Sample Data (APEX 26.1: Data Generator): generate realistic
37
38
  // rows for one or more tables of a schema (src/sampledata.ts). Step 1 picks a
@@ -92,28 +93,31 @@ function notes(c: ColumnInfo) {
92
93
  return n.join(' · ');
93
94
  }
94
95
 
95
- const generatorHelp = html`<details class="sd-help u-mt1"><summary>Generators and their options</summary>
96
+ /** (sprint 162) a generator's name and its options' help in the builder's language (sd.gen.<kind>, sd.opt.<kind>) */
97
+ const genLabel = (tx: BuilderTranslate, k: string, label: string) => tx.dyn(`sd.gen.${k}`, label);
98
+ const generatorHelp = (tx: BuilderTranslate) => html`<details class="sd-help u-mt1"><summary>${tx('sd.help')}</summary>
96
99
  <dl class="sd-help-list">${Object.entries(GENERATORS).map(
97
- ([k, label]) => html`<dt>${label}</dt><dd>${OPTION_HELP[k as keyof typeof GENERATORS] ?? (k === 'foreign_key' ? 'a random existing parent row; parents generated in the same run are inserted first' : k === 'skip' ? 'not inserted: the column default, identity or null' : 'no options')}</dd>`,
100
+ ([k, label]) => html`<dt>${genLabel(tx, k, label)}</dt><dd>${tx.dyn(`sd.opt.${k}`, OPTION_HELP[k as keyof typeof GENERATORS] ?? (k === 'foreign_key' ? 'a random existing parent row; parents generated in the same run are inserted first' : k === 'skip' ? 'not inserted: the column default, identity or null' : 'no options'))}</dd>`,
98
101
  )}</dl></details>`;
99
102
 
100
103
  export async function sampleDataRoutes(app: FastifyInstance) {
101
104
  const page = (s: Session, reply: FastifyReply, title: string, main: Raw, code = 200) => {
102
105
  reply.code(code);
106
+ const tx = tr(s);
107
+ const home = tx('sql.tool.sample');
103
108
  return send(
104
109
  reply,
105
110
  s,
106
- shell(s, title, [['SQL Workshop', `${BASE}/sql`], ['Sample Data', title === 'Sample Data' ? undefined : sdUrl], ...(title === 'Sample Data' ? [] : ([[title]] as [string][]))], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('sample', s)}${main}`, 'sql'),
111
+ shell(s, title, [[tx('rail.sql_workshop'), `${BASE}/sql`], [home, title === home ? undefined : sdUrl], ...(title === home ? [] : ([[title]] as [string][]))], html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('sample', s)}${main}`, 'sql'),
107
112
  );
108
113
  };
109
114
 
110
- const intro = html`<p class="muted">Generate realistic rows for the tables of a schema: names, e-mail addresses, dates and numbers in a range, values from a list,
111
- foreign keys that pick existing parent rows. Preview them, insert them in one transaction (parents first) or download them as SQL or CSV.
112
- Runs as the builder's owner connection (an isolated workspace's developer login for its developers), like the <a href="${BASE}/sql/worksheet">SQL Worksheet</a>.</p>`;
115
+ const introOf = (tx: BuilderTranslate) => html`<p class="muted">${tx.rich('sd.intro', { worksheet: html`<a href="${BASE}/sql/worksheet">${tx('sql.tool.worksheet')}</a>` })}</p>`;
113
116
 
114
117
  // ------------------------------------------------------------ step 1 and 2: schema and tables
115
118
  const start = async (s: Session, reply: FastifyReply, schema: string | undefined) => {
116
119
  const db = await workshopDb(s);
120
+ const tx = tr(s);
117
121
  const all = (await schemas(db.pool)).filter((x) => db === owner || x.usable);
118
122
  const gens = (await owner.query<Saved & { updated_at: string; updated_by: string }>('select id, name, description, schema_name, tables, updated_at, updated_by from meta.data_generator where workspace_id = $1 order by lower(name)', [currentWorkspace(s)])).rows;
119
123
  const savedList = gens.length
@@ -121,28 +125,28 @@ export async function sampleDataRoutes(app: FastifyInstance) {
121
125
  (g) => html`<li><a href="${sdUrl}/${g.id}">${g.name}</a> <span class="muted small">${g.schema_name}: ${g.tables.map((t) => `${t.table} (${t.rows})`).join(', ')}</span>
122
126
  ${g.description ? html`<br><span class="muted small">${g.description}</span>` : ''}</li>`,
123
127
  )}</ul>`
124
- : html`<p class="muted">No saved generators yet: generate rows below, then save the definition to run it again.</p>`;
128
+ : html`<p class="muted">${tx('sd.none_saved')}</p>`;
125
129
  const chosen = schema && allowedSchema(schema) && all.some((x) => x.name === schema) ? schema : '';
126
130
  const schemaForm = html`<form method="get" action="${sdUrl}" class="unload-pick">
127
- <label class="label" for="f_schema">Schema</label>
128
- <select id="f_schema" name="schema" required><option value="">- choose a schema -</option>${all.map(
129
- (x) => html`<option value="${x.name}"${x.name === chosen ? raw(' selected') : ''}>${x.name} (${x.tables} table${x.tables === 1 ? '' : 's'})</option>`,
131
+ <label class="label" for="f_schema">${tx('sm.schema')}</label>
132
+ <select id="f_schema" name="schema" required><option value="">${tx('ddl.choose_schema')}</option>${all.map(
133
+ (x) => html`<option value="${x.name}"${x.name === chosen ? raw(' selected') : ''}>${x.name} (${x.tables === 1 ? tx('sd.table_one') : tx('sd.tables_n', { n: x.tables })})</option>`,
130
134
  )}</select>
131
- <button class="btn">Choose</button></form>`;
135
+ <button class="btn">${tx('ul.choose')}</button></form>`;
132
136
  let tablesForm: Raw | '' = '';
133
137
  if (chosen) {
134
138
  const tables = await schemaTables(db.pool, chosen);
135
139
  tablesForm = html`<form method="get" action="${sdUrl}" class="u-mt1"><input type="hidden" name="schema" value="${chosen}">
136
- <fieldset class="prop-group"><legend>Tables of ${chosen}</legend><div class="qb-pick">${tables.map(
137
- (t) => html`<label class="check"><input type="checkbox" name="t" value="${t.name}"> ${t.name} <span class="muted small">${t.estimate >= 0 ? `~${t.estimate.toLocaleString('en')} rows` : ''}</span></label>`,
140
+ <fieldset class="prop-group"><legend>${tx('sd.tables_of', { schema: chosen })}</legend><div class="qb-pick">${tables.map(
141
+ (t) => html`<label class="check"><input type="checkbox" name="t" value="${t.name}"> ${t.name} <span class="muted small">${t.estimate >= 0 ? tx('sd.about_rows', { n: t.estimate.toLocaleString('en') }) : ''}</span></label>`,
138
142
  )}</div></fieldset>
139
- <div class="buttons"><button class="btn btn-hot">${icon('layers')} Propose generators</button></div></form>`;
140
- } else if (schema) tablesForm = html`<div class="alert alert-error" role="alert">Choose a schema of your own: not meta, information_schema or pg_*.</div>`;
143
+ <div class="buttons"><button class="btn btn-hot">${icon('layers')} ${tx('sd.propose')}</button></div></form>`;
144
+ } else if (schema) tablesForm = html`<div class="alert alert-error" role="alert">${tx('sd.own_schema')}</div>`;
141
145
  return page(
142
146
  s,
143
147
  reply,
144
- 'Sample Data',
145
- html`${intro}${region('New generator', html`${schemaForm}${tablesForm}`)}<div class="u-spacer"></div>${region('Saved generators', savedList, html`<span class="count">${gens.length}</span>`)}`,
148
+ tx('sql.tool.sample'),
149
+ html`${introOf(tx)}${region(tx('sd.new'), html`${schemaForm}${tablesForm}`)}<div class="u-spacer"></div>${region(tx('sd.saved_title'), savedList, html`<span class="count">${gens.length}</span>`)}`,
146
150
  );
147
151
  };
148
152
 
@@ -175,23 +179,24 @@ export async function sampleDataRoutes(app: FastifyInstance) {
175
179
 
176
180
  const editor = async (s: Session, reply: FastifyReply, st: FormState, code = 200) => {
177
181
  const { tables, missing } = await describeAll(s, st.def);
178
- const title = st.id ? (st.name ?? 'Generator') : 'New generator';
182
+ const tx = tr(s);
183
+ const title = st.id ? (st.name ?? tx('sd.generator')) : tx('sd.new');
179
184
  const columnRow = (i: number, j: number, c: ColumnInfo, spec: TableSpec) => {
180
185
  const cs = spec.columns.find((x) => x.column === c.name)!;
181
186
  const id = `${i}_${j}`;
182
187
  return html`<div class="sd-col">
183
188
  <div class="sd-col-name"><strong>${c.name}</strong><span class="muted small">${notes(c)}</span><input type="hidden" name="c_${id}" value="${c.name}"></div>
184
- <div class="sd-col-gen"><select name="g_${id}" aria-label="${c.name}: generator">${Object.entries(GENERATORS).map(
185
- ([k, label]) => html`<option value="${k}"${cs.generator === k ? raw(' selected') : ''}>${label}</option>`,
189
+ <div class="sd-col-gen"><select name="g_${id}" aria-label="${tx('sd.col_generator', { column: c.name })}">${Object.entries(GENERATORS).map(
190
+ ([k, label]) => html`<option value="${k}"${cs.generator === k ? raw(' selected') : ''}>${genLabel(tx, k, label)}</option>`,
186
191
  )}</select></div>
187
- <div class="sd-col-opt"><input name="o_${id}" value="${cs.options}" maxlength="4000" aria-label="${c.name}: options" placeholder="options" spellcheck="false"></div>
188
- <div class="sd-col-nulls"><input type="number" name="n_${id}" value="${cs.nulls}" min="0" max="100" step="1" aria-label="${c.name}: % nulls"><span class="muted small" aria-hidden="true">% null</span></div>
192
+ <div class="sd-col-opt"><input name="o_${id}" value="${cs.options}" maxlength="4000" aria-label="${tx('sd.col_options', { column: c.name })}" placeholder="${tx('sd.options')}" spellcheck="false"></div>
193
+ <div class="sd-col-nulls"><input type="number" name="n_${id}" value="${cs.nulls}" min="0" max="100" step="1" aria-label="${tx('sd.col_nulls', { column: c.name })}"><span class="muted small" aria-hidden="true">${tx('sd.pct_null')}</span></div>
189
194
  </div>`;
190
195
  };
191
196
  const tableRegions = tables.map(
192
197
  ({ info, spec }, i) => html`<fieldset class="prop-group sd-table"><legend>${info.schema}.${info.name}</legend>
193
198
  <input type="hidden" name="t" value="${info.name}">
194
- <div class="field sd-rows"><label class="label" for="f_rows_${i}">Rows</label>
199
+ <div class="field sd-rows"><label class="label" for="f_rows_${i}">${tx('sm.col.rows')}</label>
195
200
  <input type="number" id="f_rows_${i}" name="rows_${i}" value="${spec.rows}" min="0" max="${MAX_ROWS()}" step="1" required></div>
196
201
  <div class="sd-cols">${info.columns.map((c, j) => columnRow(i, j, c, spec))}</div></fieldset>`,
197
202
  );
@@ -199,42 +204,44 @@ export async function sampleDataRoutes(app: FastifyInstance) {
199
204
  const form = html`<form method="post" action="${sdUrl}">${csrf(s)}
200
205
  <input type="hidden" name="schema" value="${st.def.schema}">${st.id ? html`<input type="hidden" name="id" value="${st.id}">` : ''}
201
206
  ${st.error ? html`<div class="alert alert-error" role="alert">${st.error}</div>` : ''}
202
- ${missing.length ? html`<div class="alert alert-note">Not found any more, left out: ${missing.join(', ')}.</div>` : ''}
207
+ ${missing.length ? html`<div class="alert alert-note">${tx('sd.missing', { tables: missing.join(', ') })}</div>` : ''}
203
208
  <div class="form-grid">
204
- <div class="field"><label class="label" for="f_seed">Seed</label>
209
+ <div class="field"><label class="label" for="f_seed">${tx('sd.seed')}</label>
205
210
  <input type="number" id="f_seed" name="seed" value="${st.def.seed ?? ''}" min="0" max="4294967295" step="1">
206
- <small class="help">The same seed gives the same rows (for the same tables and parent rows). Empty: a new seed each run; a preview fills it in, so Insert then adds the rows you saw.</small></div>
207
- <div class="field"><span class="label">Schema</span><p>${st.def.schema} <a class="small" href="${tablesLink}">change tables</a></p></div>
211
+ <small class="help">${tx('sd.seed_help')}</small></div>
212
+ <div class="field"><span class="label">${tx('sm.schema')}</span><p>${st.def.schema} <a class="small" href="${tablesLink}">${tx('sd.change_tables')}</a></p></div>
208
213
  </div>
209
214
  ${tableRegions}
210
- ${generatorHelp}
215
+ ${generatorHelp(tx)}
211
216
  <div class="buttons u-mt1">
212
- <button class="btn" name="action" value="preview">${icon('search')} Preview</button>
213
- <button class="btn btn-hot" name="action" value="insert">${icon('play')} Insert rows</button>
214
- <button class="btn" name="action" value="sql">${icon('download')} Download SQL</button>
215
- <button class="btn" name="action" value="csv">${icon('download')} Download CSV</button></div>
216
- <fieldset class="prop-group u-mt1"><legend>Save the definition</legend><div class="form-grid">
217
- <div class="field"><label class="label" for="f_name">Name</label><input id="f_name" name="name" value="${st.name ?? ''}" maxlength="100"></div>
218
- <div class="field"><label class="label" for="f_description">Description</label><input id="f_description" name="description" value="${st.description ?? ''}" maxlength="1000"></div>
219
- </div><div class="buttons"><button class="btn" name="action" value="save">${icon('check')} ${st.id ? 'Save' : 'Save as new generator'}</button></div></fieldset>
217
+ <button class="btn" name="action" value="preview">${icon('search')} ${tx('sd.preview')}</button>
218
+ <button class="btn btn-hot" name="action" value="insert">${icon('play')} ${tx('sd.insert')}</button>
219
+ <button class="btn" name="action" value="sql">${icon('download')} ${tx('sd.download_sql')}</button>
220
+ <button class="btn" name="action" value="csv">${icon('download')} ${tx('sd.download_csv')}</button></div>
221
+ <fieldset class="prop-group u-mt1"><legend>${tx('sd.save_def')}</legend><div class="form-grid">
222
+ <div class="field"><label class="label" for="f_name">${tx('scr.name')}</label><input id="f_name" name="name" value="${st.name ?? ''}" maxlength="100"></div>
223
+ <div class="field"><label class="label" for="f_description">${tx('scr.description')}</label><input id="f_description" name="description" value="${st.description ?? ''}" maxlength="1000"></div>
224
+ </div><div class="buttons"><button class="btn" name="action" value="save">${icon('check')} ${st.id ? tx('scr.save') : tx('sd.save_new')}</button></div></fieldset>
220
225
  </form>`;
221
226
  const del = st.id
222
- ? html`<form method="post" action="${sdUrl}/${st.id}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="Delete this generator definition? Rows already inserted stay.">Delete generator</button></form>`
227
+ ? html`<form method="post" action="${sdUrl}/${st.id}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="${tx('sd.delete_confirm')}">${tx('sd.delete')}</button></form>`
223
228
  : '';
224
- return page(s, reply, title, html`${intro}${st.result ? html`${st.result}<div class="u-spacer"></div>` : ''}${region(title, html`${form}${del}`)}`, code);
229
+ return page(s, reply, title, html`${introOf(tx)}${st.result ? html`${st.result}<div class="u-spacer"></div>` : ''}${region(title, html`${form}${del}`)}`, code);
225
230
  };
226
231
 
227
232
  const sampleTable = (columns: string[], rows: (string | null)[][]) =>
228
233
  html`<div class="table-wrap"><table class="report"><thead><tr>${columns.map((c) => html`<th>${c}</th>`)}</tr></thead>
229
234
  <tbody>${rows.map((r) => html`<tr>${r.map((v) => html`<td>${v === null ? html`<span class="null">null</span>` : v.length > 200 ? `${v.slice(0, 200)}…` : v}</td>`)}</tr>`)}</tbody></table></div>`;
230
235
 
231
- const resultHtml = (r: RunResult, committed: boolean) =>
232
- region(
233
- committed ? 'Inserted' : 'Preview',
236
+ const resultHtml = (tx: BuilderTranslate, r: RunResult, committed: boolean) => {
237
+ const counts = r.tables.map((t) => `${t.table}: ${tx('scr.rows', { n: t.rows })}`).join(', ');
238
+ return region(
239
+ committed ? tx('sd.inserted') : tx('sd.preview'),
234
240
  html`<div class="alert ${committed ? 'alert-success' : 'alert-note'}" role="status">
235
- ${r.tables.map((t) => `${t.table}: ${t.rows} row(s)`).join(', ')}${committed ? ' inserted' : html`. <strong>Rolled back: nothing was saved.</strong>`} (seed ${r.seed}).</div>
236
- ${r.tables.map((t) => html`<h3 class="u-mt1">${t.table} <span class="muted small">${t.rows > PREVIEW_ROWS ? `first ${PREVIEW_ROWS} of ${t.rows}` : ''}</span></h3>${t.rows ? sampleTable(t.columns, t.sample) : html`<p class="muted">No rows.</p>`}`)}`,
241
+ ${committed ? tx('sd.inserted_counts', { counts, seed: r.seed }) : html`${counts}. <strong>${tx('sd.rolled_back')}</strong> (${tx('sd.seed_is', { seed: r.seed })}).`}</div>
242
+ ${r.tables.map((t) => html`<h3 class="u-mt1">${t.table} <span class="muted small">${t.rows > PREVIEW_ROWS ? tx('sd.first_of', { shown: PREVIEW_ROWS, n: t.rows }) : ''}</span></h3>${t.rows ? sampleTable(t.columns, t.sample) : html`<p class="muted">${tx('sd.no_rows')}</p>`}`)}`,
237
243
  );
244
+ };
238
245
 
239
246
  // ------------------------------------------------------------ routes
240
247
  app.get(sdUrl, async (req: Req, reply) => {
@@ -266,24 +273,24 @@ export async function sampleDataRoutes(app: FastifyInstance) {
266
273
  const action = b.action ?? 'preview';
267
274
 
268
275
  if (action === 'save') {
269
- if (!st.name) return refuse('Give the generator a name to save it.');
270
- if (st.name.length > 100 || (st.description ?? '').length > 1000) return refuse('The name is at most 100 characters, the description 1000.');
271
- if (!allowedSchema(def.schema)) return refuse('Choose a schema of your own: not meta, information_schema or pg_*.');
276
+ if (!st.name) return refuse(tr(s)('sd.err.name'));
277
+ if (st.name.length > 100 || (st.description ?? '').length > 1000) return refuse(tr(s)('sd.err.length'));
278
+ if (!allowedSchema(def.schema)) return refuse(tr(s)('sd.own_schema'));
272
279
  const { tables } = await describeAll(s, def);
273
- if (!tables.length) return refuse('Choose at least one table.');
280
+ if (!tables.length) return refuse(tr(s)('sd.err.table'));
274
281
  const json = JSON.stringify(tables.map(({ spec }) => spec));
275
282
  try {
276
283
  const id = g
277
284
  ? (await owner.one('update meta.data_generator set name = $2, description = $3, schema_name = $4, seed = $5, tables = $6, updated_by = $7, updated_at = now() where id = $1 returning id', [g.id, st.name, st.description || null, def.schema, def.seed, json, s.username])).id
278
285
  : (await owner.one('insert into meta.data_generator (name, description, schema_name, seed, tables, created_by, updated_by, workspace_id) values ($1, $2, $3, $4, $5, $6, $6, $7) returning id', [st.name, st.description || null, def.schema, def.seed, json, s.username, currentWorkspace(s)])).id;
279
- flash(s, `Generator ${st.name} saved.`);
286
+ flash(s, tr(s)('sd.saved', { name: st.name }));
280
287
  return back(reply, s, `${sdUrl}/${id}`);
281
288
  } catch (e) {
282
289
  if ((e as { code?: string }).code === '23505') return refuse(`A generator named ${st.name} exists already.`);
283
290
  throw e;
284
291
  }
285
292
  }
286
- if (!['preview', 'insert', 'sql', 'csv'].includes(action)) return refuse('Unknown action.');
293
+ if (!['preview', 'insert', 'sql', 'csv'].includes(action)) return refuse(tr(s)('sd.err.action'));
287
294
 
288
295
  const db = await workshopDb(s);
289
296
  const p = await plan(db.pool, def);
@@ -324,7 +331,7 @@ export async function sampleDataRoutes(app: FastifyInstance) {
324
331
  await c.query('rollback').catch(() => {});
325
332
  const err = e as { message: string; detail?: string; table?: string };
326
333
  return refuse(
327
- `${action === 'insert' ? 'Nothing was inserted: the transaction was rolled back. ' : ''}${err.table ? `${err.table}: ` : ''}${err.message}${err.detail ? ` (${err.detail})` : ''}`,
334
+ `${action === 'insert' ? `${tr(s)('sd.nothing_inserted')} ` : ''}${err.table ? `${err.table}: ` : ''}${err.message}${err.detail ? ` (${err.detail})` : ''}`,
328
335
  );
329
336
  } finally {
330
337
  // a connection of its own, closed: triggers may have changed settings
@@ -338,7 +345,7 @@ export async function sampleDataRoutes(app: FastifyInstance) {
338
345
  elapsedMs: Math.round(performance.now() - started),
339
346
  detail: `${def.schema}, seed ${seed}: ${result.tables.map((t) => `${t.table} ${t.rows}`).join(', ')}`.slice(0, 2000),
340
347
  });
341
- return editor(s, reply, { ...st, result: resultHtml(result, action === 'insert') });
348
+ return editor(s, reply, { ...st, result: resultHtml(tr(s), result, action === 'insert') });
342
349
  });
343
350
 
344
351
  app.post(`${sdUrl}/:id/delete`, async (req: Req, reply) => {
@@ -347,7 +354,7 @@ export async function sampleDataRoutes(app: FastifyInstance) {
347
354
  const g = await saved(s, req.params.id);
348
355
  if (!g) return reply.code(404).send('Not found');
349
356
  await owner.query('delete from meta.data_generator where id = $1', [g.id]);
350
- flash(s, `Generator ${g.name} deleted.`);
357
+ flash(s, tr(s)('sd.deleted', { name: g.name }));
351
358
  return back(reply, s, sdUrl);
352
359
  });
353
360
  }