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
@@ -1,6 +1,6 @@
1
1
  import type { FastifyInstance, FastifyReply } from 'fastify';
2
2
  import { owner } from '../db.ts';
3
- import { html, raw, type Raw } from '../html.ts';
3
+ import { esc, html, raw, type Raw } from '../html.ts';
4
4
  import { icon } from '../icons.ts';
5
5
  import {
6
6
  applyMapping,
@@ -27,6 +27,7 @@ import type { Session } from '../session.ts';
27
27
  import type { ComponentSpec } from './components.ts';
28
28
  import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
29
29
  import { appWorkspace, currentWorkspace, workshopDb } from './workspaces.ts';
30
+ import { tr, translatorFor, type BuilderTranslate } from './i18n.ts';
30
31
 
31
32
  // SQL Workshop → Load Data: upload a CSV/TSV/XLSX/JSON/XML file, preview it,
32
33
  // and load it into a new table (column types inferred) or an existing one
@@ -125,21 +126,21 @@ export function preview(sheet: Sheet) {
125
126
  <tbody>${sheet.rows.slice(0, PREVIEW_ROWS).map((r) => html`<tr>${r.map((v) => html`<td>${v === null ? html`<span class="null">null</span>` : v}</td>`)}</tr>`)}</tbody></table></div>`;
126
127
  }
127
128
 
128
- export function resultHtml(r: LoadResult, failed: boolean) {
129
+ export function resultHtml(r: LoadResult, failed: boolean, tx: BuilderTranslate = translatorFor('en')) {
129
130
  const errors = r.errors.length
130
- ? html`<div class="table-wrap"><table class="report"><thead><tr><th class="num">Row</th><th>Error</th></tr></thead>
131
+ ? html`<div class="table-wrap"><table class="report"><thead><tr><th class="num">${tx('dl.row')}</th><th>${tx('dl.error')}</th></tr></thead>
131
132
  <tbody>${r.errors.map((e) => html`<tr><td class="num">${e.row}</td><td>${e.message}</td></tr>`)}</tbody></table></div>
132
- ${r.failed > r.errors.length ? html`<p class="muted">The first ${r.errors.length} errors are shown.</p>` : ''}`
133
+ ${r.failed > r.errors.length ? html`<p class="muted">${tx('dl.first_errors', { n: r.errors.length })}</p>` : ''}`
133
134
  : '';
134
135
  const summary = failed
135
- ? html`<div class="alert alert-error" role="alert">${r.failed}${r.errors.length >= 100 ? '+' : ''} row(s) could not be loaded, so nothing was loaded. Fix the file, or tick “Skip rows with errors”.</div>`
136
- : html`<div class="alert alert-success" role="status">${r.table}: ${r.inserted} row(s) inserted${r.updated ? `, ${r.updated} updated` : ''}${r.failed ? `, ${r.failed} skipped` : ''}.</div>`;
136
+ ? html`<div class="alert alert-error" role="alert">${tx('dl.failed', { n: `${r.failed}${r.errors.length >= 100 ? '+' : ''}` })}</div>`
137
+ : html`<div class="alert alert-success" role="status">${r.table}: ${tx('dl.inserted', { n: r.inserted })}${r.updated ? `, ${tx('dl.updated', { n: r.updated })}` : ''}${r.failed ? `, ${tx('dl.skipped', { n: r.failed })}` : ''}.</div>`;
137
138
  return html`${summary}${errors}`;
138
139
  }
139
140
 
140
141
  export async function dataLoadRoutes(app: FastifyInstance) {
141
142
  const page = (s: Session, reply: FastifyReply, main: Raw) =>
142
- send(reply, s, shell(s, 'Load Data', [['SQL Workshop', `${BASE}/sql`], ['Load Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('load', s)}${main}`, 'sql'));
143
+ send(reply, s, shell(s, tr(s)('sql.tool.load'), [[tr(s)('rail.sql_workshop'), `${BASE}/sql`], [tr(s)('sql.tool.load')]], html`<h1 class="u-mb1">${tr(s)('rail.sql_workshop')}</h1>${workshopTabs('load', s)}${main}`, 'sql'));
143
144
 
144
145
  // step 1: choose a file
145
146
  app.get(`${BASE}/sql/load`, async (req: Req, reply) => {
@@ -150,28 +151,28 @@ export async function dataLoadRoutes(app: FastifyInstance) {
150
151
 
151
152
  const uploadForm = async (s: Session, error?: string, chosen?: string) => {
152
153
  const defs = await definitions(s);
154
+ const tx = tr(s);
153
155
  return region(
154
- 'Load data from a file',
156
+ tx('dl.from_file'),
155
157
  html`${error ? html`<div class="alert alert-error" role="alert">${error}</div>` : ''}
156
- <p class="muted">CSV or TSV (UTF-8 or Windows-1252; the delimiter is detected), Excel .xlsx (the first sheet), JSON (an array of objects, or JSON Lines) or XML (rows from a repeating element), up to ${MAX_MB} MB.
157
- Next you choose a new or existing table. Loading runs as the builder's owner connection.
158
- To download a table or a query as a file, use <a href="${BASE}/sql/unload">Unload Data</a>.</p>
158
+ <p class="muted">${tx('dl.intro', { mb: MAX_MB })}
159
+ ${tx.rich('dl.intro_unload', { link: raw(`<a href="${BASE}/sql/unload">${esc(tx('sql.tool.unload'))}</a>`) })}</p>
159
160
  <form method="post" enctype="multipart/form-data">${csrf(s)}
160
161
  <div class="form-grid">
161
- <div class="field"><label class="label" for="f_file">File</label>
162
+ <div class="field"><label class="label" for="f_file">${tx('dl.file')}</label>
162
163
  <input type="file" id="f_file" name="file" accept=".csv,.tsv,.txt,.xlsx,.json,.jsonl,.xml,text/csv,application/json,application/xml,text/xml" required></div>
163
164
  <div class="field"><span class="label" aria-hidden="true"></span>
164
- <label class="check"><input type="checkbox" name="headers" value="true" checked> First row contains column names</label></div>
165
- <div class="field"><label class="label" for="f_row_tag">XML row element</label>
166
- <input id="f_row_tag" name="row_tag" placeholder="e.g. employee (empty: detected)">
167
- <small class="help">XML only. DTDs and entity declarations are not accepted.</small></div>
168
- <div class="field"><label class="label" for="f_definition">Data load definition</label>
169
- <select id="f_definition" name="definition"><option value="">- none: choose the table next -</option>${defs.map(
165
+ <label class="check"><input type="checkbox" name="headers" value="true" checked> ${tx('dl.headers')}</label></div>
166
+ <div class="field"><label class="label" for="f_row_tag">${tx('dl.row_tag')}</label>
167
+ <input id="f_row_tag" name="row_tag" placeholder="${tx('dl.row_tag_ph')}">
168
+ <small class="help">${tx('dl.row_tag_help')}</small></div>
169
+ <div class="field"><label class="label" for="f_definition">${tx('comp.data_load_def')}</label>
170
+ <select id="f_definition" name="definition"><option value="">${tx('dl.no_definition')}</option>${defs.map(
170
171
  (d) => html`<option value="${d.id}"${String(d.id) === chosen ? raw(' selected') : ''}>${d.app_name} · ${d.name} → ${d.table_name}</option>`,
171
172
  )}</select>
172
- <small class="help">Shared Components of an application: table, column mapping and transformations.</small></div>
173
+ <small class="help">${tx('dl.definition_help')}</small></div>
173
174
  </div>
174
- <div class="buttons"><button class="btn btn-hot">${icon('upload')} Next</button></div>
175
+ <div class="buttons"><button class="btn btn-hot">${icon('upload')} ${tx('dl.next')}</button></div>
175
176
  </form>`,
176
177
  );
177
178
  };
@@ -185,8 +186,8 @@ export async function dataLoadRoutes(app: FastifyInstance) {
185
186
  }
186
187
  const s = await developer(req, reply);
187
188
  if (!s) return;
188
- if (!file) return page(s, reply, await uploadForm(s, 'Choose a file.'));
189
- if (file.truncated) return page(s, reply, await uploadForm(s, `The file is larger than ${MAX_MB} MB.`));
189
+ if (!file) return page(s, reply, await uploadForm(s, tr(s)('dl.e.no_file')));
190
+ if (file.truncated) return page(s, reply, await uploadForm(s, tr(s)('dl.e.too_large', { mb: MAX_MB })));
190
191
  const def = await definition(s, req.body?.definition);
191
192
  const rowTag = (req.body?.row_tag ?? '').trim();
192
193
  try {
@@ -220,11 +221,14 @@ export async function dataLoadRoutes(app: FastifyInstance) {
220
221
  if (e instanceof LoadError) return page(s, reply, await uploadForm(s, e.message));
221
222
  throw e;
222
223
  }
224
+ const tx = tr(s);
223
225
  const action = `${BASE}/sql/load/${req.params.id}`;
224
226
  const hidden = html`${csrf(s)}<input type="hidden" name="h" value="${headers ? '1' : '0'}"><input type="hidden" name="rt" value="${rowTag}">`;
225
- const info = `${f.filename}: ${sheet.rows.length} row(s), ${sheet.headers.length} column(s)${
226
- sheet.format === 'xlsx' ? ', Excel' : sheet.format === 'json' ? ', JSON' : sheet.format === 'xml' ? `, XML rows <${sheet.rowPath}>` : `, delimiter ${sheet.delimiter === '\t' ? 'tab' : `“${sheet.delimiter}”`}`
227
+ const info = `${f.filename}: ${tx('dl.info', { rows: sheet.rows.length, columns: sheet.headers.length })}${
228
+ sheet.format === 'xlsx' ? ', Excel' : sheet.format === 'json' ? ', JSON' : sheet.format === 'xml' ? `, ${tx('dl.xml_rows', { path: `<${sheet.rowPath}>` })}` : `, ${tx('dl.delimiter', { d: sheet.delimiter === '\t' ? tx('dl.tab') : `“${sheet.delimiter}”` })}`
227
229
  }`;
230
+ const firstShown = sheet.rows.length > PREVIEW_ROWS ? ` ${tx('dl.first_shown', { n: PREVIEW_ROWS })}` : '';
231
+ const anotherFile = html`<a href="${BASE}/sql/load">${tx('dl.another_file')}</a>`;
228
232
  if (def) {
229
233
  let mapped: Sheet = sheet;
230
234
  let problem = '';
@@ -234,34 +238,34 @@ export async function dataLoadRoutes(app: FastifyInstance) {
234
238
  if (!(e instanceof LoadError)) throw e;
235
239
  problem = e.message;
236
240
  }
237
- const defHtml = html`<p>Definition <a href="${BASE}/apps/${def.app_id}/shared?c=data_load_def-${def.id}">${def.name}</a>: into <b>${def.table_name}</b>, mode ${def.mode}${def.skip_errors ? ', skipping rows with errors' : ''}.
238
- ${def.columns?.length ? '' : ' File columns are matched to table columns by name.'}</p>
239
- ${problem ? html`<div class="alert alert-error" role="alert">${problem}</div>` : html`<p class="muted">After the mapping and transformations${mapped.rows.length > PREVIEW_ROWS ? ` (first ${PREVIEW_ROWS} rows)` : ''}:</p>${preview(mapped)}`}
241
+ const defHtml = html`<p>${tx.rich('dl.def_into', { def: raw(`<a href="${BASE}/apps/${def.app_id}/shared?c=data_load_def-${def.id}">${esc(def.name)}</a>`), table: raw(`<b>${esc(def.table_name)}</b>`), mode: def.mode })}${def.skip_errors ? `, ${tx('dl.def_skipping')}` : ''}.
242
+ ${def.columns?.length ? '' : ` ${tx('dl.def_by_name')}`}</p>
243
+ ${problem ? html`<div class="alert alert-error" role="alert">${problem}</div>` : html`<p class="muted">${mapped.rows.length > PREVIEW_ROWS ? tx('dl.after_mapping_first', { n: PREVIEW_ROWS }) : tx('dl.after_mapping')}</p>${preview(mapped)}`}
240
244
  <form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="definition"><input type="hidden" name="d" value="${def.id}">
241
- <div class="buttons"><button class="btn btn-hot"${problem ? raw(' disabled') : ''}>${icon('upload')} Load into ${def.table_name}</button></div></form>`;
245
+ <div class="buttons"><button class="btn btn-hot"${problem ? raw(' disabled') : ''}>${icon('upload')} ${tx('dl.load_into', { table: def.table_name })}</button></div></form>`;
242
246
  return page(
243
247
  s,
244
248
  reply,
245
249
  html`${result}
246
- ${region('Preview', html`<p class="muted">${info}${sheet.rows.length > PREVIEW_ROWS ? ` (first ${PREVIEW_ROWS} shown)` : ''}. <a href="${BASE}/sql/load">Choose another file</a></p>${preview(sheet)}`)}
250
+ ${region(tx('dl.preview'), html`<p class="muted">${info}${firstShown}. ${anotherFile}</p>${preview(sheet)}`)}
247
251
  <div class="u-spacer"></div>
248
- ${region(`Load with ${def.name}`, defHtml)}`,
252
+ ${region(tx('dl.load_with', { name: def.name }), defHtml)}`,
249
253
  );
250
254
  }
251
- const skip = html`<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="skip_errors" value="true"> Skip rows with errors (otherwise nothing is loaded when a row fails)</label></div>`;
255
+ const skip = html`<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="skip_errors" value="true"> ${tx('dl.skip_errors')}</label></div>`;
252
256
 
253
257
  const newCols = suggestColumns(sheet);
254
258
  const newTable = html`<form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="new">
255
- <div class="form-grid"><div class="field"><label class="label" for="f_new_table">Table name</label>
259
+ <div class="form-grid"><div class="field"><label class="label" for="f_new_table">${tx('dl.table_name')}</label>
256
260
  <input id="f_new_table" name="new_table" value="${tableName(f.filename)}" required>
257
- <small class="help">schema.table; the table gets an identity primary key <code>id</code>.</small></div></div>
258
- <div class="table-wrap"><table class="report"><thead><tr><th>File column</th><th>Column name (empty = skip)</th><th>Type</th></tr></thead><tbody>
261
+ <small class="help">${tx.code('dl.table_name_help')}</small></div></div>
262
+ <div class="table-wrap"><table class="report"><thead><tr><th>${tx('dl.file_column')}</th><th>${tx('dl.column_name')}</th><th>${tx('dl.type')}</th></tr></thead><tbody>
259
263
  ${newCols.map((c) => html`<tr><td>${sheet.headers[c.index]}</td>
260
- <td><input name="name_${c.index}" value="${c.name}" aria-label="Column name for ${sheet.headers[c.index]}"></td>
261
- <td><select name="type_${c.index}" aria-label="Type of ${sheet.headers[c.index]}">${COLUMN_TYPES.map((t) => html`<option${t === c.type ? raw(' selected') : ''}>${t}</option>`)}</select></td></tr>`)}
264
+ <td><input name="name_${c.index}" value="${c.name}" aria-label="${tx('dl.aria_name', { column: sheet.headers[c.index] })}"></td>
265
+ <td><select name="type_${c.index}" aria-label="${tx('dl.aria_type', { column: sheet.headers[c.index] })}">${COLUMN_TYPES.map((t) => html`<option${t === c.type ? raw(' selected') : ''}>${t}</option>`)}</select></td></tr>`)}
262
266
  </tbody></table></div>
263
267
  ${skip}
264
- <div class="buttons"><button class="btn btn-hot">Create table and load</button></div></form>`;
268
+ <div class="buttons"><button class="btn btn-hot">${tx('dl.create_and_load')}</button></div></form>`;
265
269
 
266
270
  const db = await workshopDb(s);
267
271
  const tables = (
@@ -279,44 +283,44 @@ export async function dataLoadRoutes(app: FastifyInstance) {
279
283
  const auto = autoMap(sheet.headers, cols);
280
284
  const hasPk = cols.some((c) => c.pk);
281
285
  mapping = html`<form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="existing"><input type="hidden" name="table" value="${chosen}">
282
- <div class="table-wrap"><table class="report"><thead><tr><th>File column</th><th>Table column</th></tr></thead><tbody>
286
+ <div class="table-wrap"><table class="report"><thead><tr><th>${tx('dl.file_column')}</th><th>${tx('dl.table_column')}</th></tr></thead><tbody>
283
287
  ${sheet.headers.map((h, i) => {
284
288
  const cur = auto.find((m) => m.index === i)?.column ?? '';
285
- return html`<tr><td>${h}</td><td><select name="map_${i}" aria-label="Table column for ${h}"><option value="">- skip -</option>${cols
289
+ return html`<tr><td>${h}</td><td><select name="map_${i}" aria-label="${tx('dl.aria_map', { column: h })}"><option value="">${tx('dl.skip')}</option>${cols
286
290
  .filter((c) => !c.generated)
287
- .map((c) => html`<option value="${c.name}"${c.name === cur ? raw(' selected') : ''}>${c.name} (${c.type}${c.pk ? ', key' : ''})</option>`)}</select></td></tr>`;
291
+ .map((c) => html`<option value="${c.name}"${c.name === cur ? raw(' selected') : ''}>${c.name} (${c.type}${c.pk ? `, ${tx('dl.key')}` : ''})</option>`)}</select></td></tr>`;
288
292
  })}
289
293
  </tbody></table></div>
290
- <fieldset class="field"><legend class="label">Mode</legend><div class="radio-group">
291
- <label class="check"><input type="radio" name="mode" value="append" checked> Append: insert all rows</label>
292
- <label class="check"><input type="radio" name="mode" value="merge"${hasPk ? '' : raw(' disabled')}> Merge: update rows with the same primary key, insert the others</label>
293
- <label class="check"><input type="radio" name="mode" value="replace"> Replace: delete all rows first</label>
294
+ <fieldset class="field"><legend class="label">${tx('dl.mode')}</legend><div class="radio-group">
295
+ <label class="check"><input type="radio" name="mode" value="append" checked> ${tx('dl.mode_append')}</label>
296
+ <label class="check"><input type="radio" name="mode" value="merge"${hasPk ? '' : raw(' disabled')}> ${tx('dl.mode_merge')}</label>
297
+ <label class="check"><input type="radio" name="mode" value="replace"> ${tx('dl.mode_replace')}</label>
294
298
  </div></fieldset>
295
299
  ${skip}
296
- <div class="buttons"><button class="btn btn-hot">Load into ${chosen}</button></div>
297
- <details class="u-mt1"><summary>Save this mapping as a data load definition</summary>
298
- <p class="muted">For the data_load process of an application, and to load files like this one again.</p>
300
+ <div class="buttons"><button class="btn btn-hot">${tx('dl.load_into', { table: chosen })}</button></div>
301
+ <details class="u-mt1"><summary>${tx('dl.save_mapping')}</summary>
302
+ <p class="muted">${tx('dl.save_mapping_help')}</p>
299
303
  <div class="form-grid">
300
- <div class="field"><label class="label" for="f_def_app">Application</label>
304
+ <div class="field"><label class="label" for="f_def_app">${tx('rm.application')}</label>
301
305
  <select id="f_def_app" name="def_app">${apps.map((a) => html`<option value="${a.id}">${a.name}</option>`)}</select></div>
302
- <div class="field"><label class="label" for="f_def_name">Name</label><input id="f_def_name" name="def_name" placeholder="e.g. ${defName(f.filename)}"></div>
306
+ <div class="field"><label class="label" for="f_def_name">${tx('dl.name')}</label><input id="f_def_name" name="def_name" placeholder="${tx('rm.eg', { example: defName(f.filename) })}"></div>
303
307
  </div>
304
- <div class="buttons"><button class="btn" name="target" value="save_definition">${icon('check')} Save as definition</button></div>
308
+ <div class="buttons"><button class="btn" name="target" value="save_definition">${icon('check')} ${tx('dl.save_definition')}</button></div>
305
309
  </details></form>`;
306
310
  }
307
311
  const existing = html`<form method="get" action="${action}" class="search u-mwnone">
308
312
  <input type="hidden" name="h" value="${headers ? '1' : '0'}"><input type="hidden" name="rt" value="${rowTag}">
309
- <select name="table" aria-label="Table"><option value="">- choose a table -</option>${tables.map((t) => html`<option${t === chosen ? raw(' selected') : ''}>${t}</option>`)}</select>
310
- <button class="btn">Map columns</button></form>${mapping}`;
313
+ <select name="table" aria-label="${tx('dl.table')}"><option value="">${tx('dl.choose_table')}</option>${tables.map((t) => html`<option${t === chosen ? raw(' selected') : ''}>${t}</option>`)}</select>
314
+ <button class="btn">${tx('dl.map_columns')}</button></form>${mapping}`;
311
315
 
312
316
  return page(
313
317
  s,
314
318
  reply,
315
319
  html`${result}
316
- ${region(`Preview`, html`<p class="muted">${info}${sheet.rows.length > PREVIEW_ROWS ? ` (first ${PREVIEW_ROWS} shown)` : ''}. <a href="${BASE}/sql/load">Choose another file</a></p>${preview(sheet)}`)}
320
+ ${region(tx('dl.preview'), html`<p class="muted">${info}${firstShown}. ${anotherFile}</p>${preview(sheet)}`)}
317
321
  <div class="columns u-mt1">
318
- ${region('Load into a new table', newTable)}
319
- ${region('Load into an existing table', existing)}
322
+ ${region(tx('dl.into_new'), newTable)}
323
+ ${region(tx('dl.into_existing'), existing)}
320
324
  </div>`,
321
325
  );
322
326
  };
@@ -344,20 +348,20 @@ export async function dataLoadRoutes(app: FastifyInstance) {
344
348
  const mode = ['append', 'replace', 'merge'].includes(b.mode ?? '') ? b.mode : 'append';
345
349
  const format = sheet instanceof Error ? 'auto' : sheet.format;
346
350
  try {
347
- if (!/^\d{1,9}$/.test(String(b.def_app ?? '')) || (await appWorkspace(String(b.def_app))) !== currentWorkspace(s)) throw new LoadError('Choose an application of this workspace.');
351
+ if (!/^\d{1,9}$/.test(String(b.def_app ?? '')) || (await appWorkspace(String(b.def_app))) !== currentWorkspace(s)) throw new LoadError(tr(s)('rm.e.choose_app'));
348
352
  const r = await owner.one(
349
353
  `insert into meta.data_load_def (app_id, name, table_name, format, headers, row_tag, mode, skip_errors, columns)
350
354
  values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id, app_id`,
351
355
  [b.def_app, name, b.table, format, headers, format === 'xml' && !(sheet instanceof Error) ? sheet.rowPath : null, mode, b.skip_errors === 'true', JSON.stringify(columns)],
352
356
  );
353
- flash(s, `Data load definition ${name} saved.`);
357
+ flash(s, tr(s)('dl.def_saved', { name }));
354
358
  return back(reply, s, `${BASE}/apps/${r!.app_id}/shared?c=data_load_def-${r!.id}`);
355
359
  } catch (e) {
356
360
  const code = (e as { code?: string }).code;
357
361
  if (!(e instanceof LoadError) && !code?.startsWith('23') && code !== '22P02') throw e;
358
362
  reply.code(422);
359
- return targetPage(req, reply, s, html`<div class="alert alert-error" role="alert">The definition could not be saved: ${
360
- code === '23505' ? `the application already has a definition named ${name}.` : (e as Error).message}</div>`);
363
+ return targetPage(req, reply, s, html`<div class="alert alert-error" role="alert">${tr(s)('dl.def_not_saved', {
364
+ reason: code === '23505' ? tr(s)('dl.def_exists', { name }) : (e as Error).message })}</div>`);
361
365
  }
362
366
  }
363
367
  let result: LoadResult | undefined;
@@ -369,12 +373,12 @@ export async function dataLoadRoutes(app: FastifyInstance) {
369
373
  const opts = { skipErrors: b.skip_errors === 'true', firstRow: headers && sheet && (sheet.format === 'csv' || sheet.format === 'xlsx') ? 2 : 1 };
370
374
  result = await (await workshopDb(s)).tx(async (c) => {
371
375
  if (def) return loadWithDefinition(c, def, f);
372
- if (!sheet) throw new LoadError('The data load definition no longer exists.');
376
+ if (!sheet) throw new LoadError(tr(s)('dl.e.def_gone'));
373
377
  if (b.target === 'new') {
374
378
  const cols: NewColumn[] = sheet.headers
375
379
  .map((_, index) => ({ index, name: (b[`name_${index}`] ?? '').trim(), type: (b[`type_${index}`] ?? 'text') as NewColumn['type'] }))
376
380
  .filter((x) => x.name);
377
- if (!cols.length) throw new LoadError('Give at least one column a name.');
381
+ if (!cols.length) throw new LoadError(tr(s)('dl.e.no_columns'));
378
382
  await c.query(createTableSql((b.new_table ?? '').trim(), cols));
379
383
  const table = (b.new_table ?? '').trim();
380
384
  return loadRows(c, sheet, { ...opts, table, mode: 'append', columns: cols.map((x) => ({ index: x.index, column: x.name })) });
@@ -395,12 +399,12 @@ export async function dataLoadRoutes(app: FastifyInstance) {
395
399
  return page(
396
400
  s,
397
401
  reply,
398
- html`${resultHtml(result, false)}<div class="buttons">
399
- <a class="btn btn-hot" href="${BASE}/sql/objects?o=${encodeURIComponent(result.table)}">${icon('table')} View ${result.table}</a>
400
- <a class="btn" href="${BASE}/sql/load">${icon('upload')} Load another file</a></div>`,
402
+ html`${resultHtml(result, false, tr(s))}<div class="buttons">
403
+ <a class="btn btn-hot" href="${BASE}/sql/objects?o=${encodeURIComponent(result.table)}">${icon('table')} ${tr(s)('dl.view', { table: result.table })}</a>
404
+ <a class="btn" href="${BASE}/sql/load">${icon('upload')} ${tr(s)('dl.load_another')}</a></div>`,
401
405
  );
402
406
  }
403
407
  reply.code(422);
404
- return targetPage(req, reply, s, result ? resultHtml(result, true) : html`<div class="alert alert-error" role="alert">${error}</div>`);
408
+ return targetPage(req, reply, s, result ? resultHtml(result, true, tr(s)) : html`<div class="alert alert-error" role="alert">${error}</div>`);
405
409
  });
406
410
  }