pgkiln 0.81.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 (109) hide show
  1. package/db/migrations/147_condition_types.sql +80 -0
  2. package/db/migrations/148_validation_types.sql +44 -0
  3. package/db/migrations/149_dynamic_action_events.sql +45 -0
  4. package/db/migrations/150_process_types.sql +23 -0
  5. package/db/migrations/151_item_types.sql +16 -0
  6. package/db/migrations/152_workflow_diagram_region.sql +10 -0
  7. package/db/migrations/153_component_events.sql +21 -0
  8. package/docs/apex-feature-parity.md +11 -9
  9. package/docs/guide/02-concepts.md +3 -3
  10. package/docs/guide/03-builder.md +8 -1
  11. package/docs/guide/04-pages-and-regions.md +19 -4
  12. package/docs/guide/05-items.md +5 -0
  13. package/docs/guide/06-processing.md +104 -17
  14. package/docs/guide/07-dynamic-actions.md +43 -1
  15. package/docs/guide/11-from-apex.md +8 -0
  16. package/docs/guide/12-development.md +9 -1
  17. package/examples/hr/hr_79_condition_types.sql +17 -0
  18. package/package.json +1 -1
  19. package/public/app.css +14 -0
  20. package/public/app.js +242 -13
  21. package/public/builder.css +25 -2
  22. package/public/data-modeler.js +8 -5
  23. package/public/sql-monitor.js +5 -2
  24. package/src/appfiles.ts +4 -4
  25. package/src/builder/advisor.ts +43 -7
  26. package/src/builder/ai-builder.ts +50 -46
  27. package/src/builder/api.ts +49 -49
  28. package/src/builder/autorest.ts +92 -87
  29. package/src/builder/changelog.ts +39 -37
  30. package/src/builder/code-editor.ts +14 -10
  31. package/src/builder/components.ts +54 -23
  32. package/src/builder/conditions.ts +64 -0
  33. package/src/builder/data-modeler.ts +89 -84
  34. package/src/builder/data-security.ts +39 -37
  35. package/src/builder/dataload.ts +69 -65
  36. package/src/builder/datapump.ts +108 -97
  37. package/src/builder/ddl-tools.ts +38 -34
  38. package/src/builder/designer.ts +9 -1
  39. package/src/builder/document-view.ts +11 -11
  40. package/src/builder/i18n/de.ts +1662 -0
  41. package/src/builder/i18n/en.ts +1662 -0
  42. package/src/builder/i18n/es.ts +1662 -0
  43. package/src/builder/i18n/fr.ts +1662 -0
  44. package/src/builder/i18n/it.ts +1662 -0
  45. package/src/builder/i18n/ja.ts +1662 -0
  46. package/src/builder/i18n/ko.ts +1662 -0
  47. package/src/builder/i18n/nl.ts +1662 -0
  48. package/src/builder/i18n/pt.ts +1662 -0
  49. package/src/builder/i18n/zh-cn.ts +1662 -0
  50. package/src/builder/i18n/zh-tw.ts +1662 -0
  51. package/src/builder/i18n.ts +18 -0
  52. package/src/builder/object-actions.ts +18 -16
  53. package/src/builder/object-browser.ts +2 -2
  54. package/src/builder/querybuilder.ts +42 -36
  55. package/src/builder/quicksql.ts +16 -18
  56. package/src/builder/region-help.ts +3 -1
  57. package/src/builder/region-settings.ts +225 -198
  58. package/src/builder/report-settings.ts +64 -60
  59. package/src/builder/rest-modules.ts +145 -143
  60. package/src/builder/sampledata.ts +59 -52
  61. package/src/builder/scheduling.ts +180 -159
  62. package/src/builder/scripts.ts +71 -65
  63. package/src/builder/sql-charts.ts +36 -30
  64. package/src/builder/sql-monitor.ts +95 -89
  65. package/src/builder/sql-utilities.ts +47 -40
  66. package/src/builder/top-sql.ts +21 -17
  67. package/src/builder/unload.ts +48 -44
  68. package/src/builder/worksheet.ts +3 -3
  69. package/src/cron-schedule.ts +47 -17
  70. package/src/i18n/ar.ts +5 -0
  71. package/src/i18n/cs.ts +5 -0
  72. package/src/i18n/da.ts +5 -0
  73. package/src/i18n/de.ts +5 -0
  74. package/src/i18n/el.ts +5 -0
  75. package/src/i18n/es.ts +5 -0
  76. package/src/i18n/fi.ts +5 -0
  77. package/src/i18n/fr.ts +5 -0
  78. package/src/i18n/he.ts +5 -0
  79. package/src/i18n/it.ts +5 -0
  80. package/src/i18n/ja.ts +5 -0
  81. package/src/i18n/ko.ts +5 -0
  82. package/src/i18n/nb.ts +5 -0
  83. package/src/i18n/pl.ts +5 -0
  84. package/src/i18n/pt.ts +5 -0
  85. package/src/i18n/ru.ts +5 -0
  86. package/src/i18n/sv.ts +5 -0
  87. package/src/i18n/tr.ts +5 -0
  88. package/src/i18n/uk.ts +5 -0
  89. package/src/i18n/zh.ts +5 -0
  90. package/src/i18n.ts +10 -0
  91. package/src/metadata.ts +74 -9
  92. package/src/runtime/authz.ts +9 -5
  93. package/src/runtime/calendar.ts +3 -2
  94. package/src/runtime/context.ts +6 -0
  95. package/src/runtime/da-styles.ts +37 -0
  96. package/src/runtime/documents.ts +3 -2
  97. package/src/runtime/engine.ts +95 -13
  98. package/src/runtime/grid.ts +1 -0
  99. package/src/runtime/items.ts +33 -0
  100. package/src/runtime/logic.ts +79 -8
  101. package/src/runtime/maps.ts +56 -7
  102. package/src/runtime/process-types.ts +139 -0
  103. package/src/runtime/processes.ts +2 -0
  104. package/src/runtime/regions.ts +4 -0
  105. package/src/runtime/render.ts +19 -3
  106. package/src/runtime/routes.ts +6 -2
  107. package/src/runtime/tree.ts +4 -3
  108. package/src/runtime/workflow-diagram.ts +45 -0
  109. package/src/workflow.ts +10 -2
@@ -3,6 +3,7 @@ import { owner } from '../db.ts';
3
3
  import { html, raw } from '../html.ts';
4
4
  import { icon } from '../icons.ts';
5
5
  import { appHeader, back, BASE, csrf, developer, flash, region, send, shell, type Req } from './ui.ts';
6
+ import { tr, type BuilderTranslate } from './i18n.ts';
6
7
 
7
8
  // App Builder → Activity → Top SQL: the statements the application's
8
9
  // database role ran, from pg_stat_statements (APEX: Top SQL in the
@@ -27,12 +28,14 @@ export async function statementsState(): Promise<{ ok: true } | { ok: false; rea
27
28
  }
28
29
 
29
30
  /** Why the statistics can't be shown, and how to turn them on. */
30
- export function statementsNote(reason: 'not_installed' | 'not_loaded' | 'error', message?: string) {
31
+ export function statementsNote(tx: BuilderTranslate, reason: 'not_installed' | 'not_loaded' | 'error', message?: string) {
32
+ const ext = html`<code>pg_stat_statements</code>`;
33
+ const setting = html`<code>shared_preload_libraries = 'pg_stat_statements'</code>`;
31
34
  return html`<div class="alert alert-error" role="alert">${reason === 'not_installed'
32
- ? html`The <code>pg_stat_statements</code> extension isn't installed in this database. To turn it on: set <code>shared_preload_libraries = 'pg_stat_statements'</code> (postgresql.conf, or <code>alter system set shared_preload_libraries = 'pg_stat_statements'</code>), restart PostgreSQL, then as a superuser: <code>create extension pg_stat_statements;</code>`
35
+ ? tx.rich('topsql.not_installed', { ext, setting, alter: html`<code>alter system set shared_preload_libraries = 'pg_stat_statements'</code>`, create: html`<code>create extension pg_stat_statements;</code>` })
33
36
  : reason === 'not_loaded'
34
- ? html`The server doesn't load <code>pg_stat_statements</code> yet. Set <code>shared_preload_libraries = 'pg_stat_statements'</code> (the development <code>docker-compose.yml</code> does) and restart PostgreSQL.`
35
- : html`The statistics could not be read: ${message ?? ''}`}</div>`;
37
+ ? tx.rich('topsql.not_loaded', { ext, setting, compose: html`<code>docker-compose.yml</code>` })
38
+ : tx('topsql.read_error', { message: message ?? '' })}</div>`;
36
39
  }
37
40
 
38
41
  /** The role's statements, slowest in total first (or by mean time, calls or rows). */
@@ -66,28 +69,29 @@ export async function topSqlRoutes(app: FastifyInstance) {
66
69
  if (!s) return;
67
70
  const a = await appOf(req.params.id);
68
71
  if (!a) return reply.code(404).send('Not found');
72
+ const tx = tr(s);
69
73
  const sort = SORTS[req.query.sort ?? ''] ? req.query.sort! : 'total';
70
74
  const top = await topSql(a.db_role, sort);
71
75
  const sortLink = (key: string, label: string) =>
72
76
  html`<a href="${BASE}/apps/${a.id}/top-sql?sort=${key}"${sort === key ? raw(' aria-current="true"') : ''}>${label}</a>`;
73
77
  const body = top.ok
74
78
  ? top.rows.length
75
- ? html`<div class="seg u-mb1">Sort by ${sortLink('total', 'total time')} ${sortLink('mean', 'time per call')} ${sortLink('calls', 'calls')} ${sortLink('rows', 'rows')}</div>
76
- <div class="table-wrap"><table class="report report-reflow top-sql"><thead><tr><th class="num">Calls</th><th class="num">Total ms</th><th class="num">Mean ms</th><th class="num">Rows</th><th class="num">Share</th><th>Statement</th></tr></thead>
79
+ ? html`<div class="seg u-mb1">${tx.rich('topsql.sort_by', { total: sortLink('total', tx('topsql.sort.total')), mean: sortLink('mean', tx('topsql.sort.mean')), calls: sortLink('calls', tx('topsql.sort.calls')), rows: sortLink('rows', tx('topsql.sort.rows')) })}</div>
80
+ <div class="table-wrap"><table class="report report-reflow top-sql"><thead><tr><th class="num">${tx('sm.col.calls')}</th><th class="num">${tx('sm.col.total_ms')}</th><th class="num">${tx('sm.col.mean_ms')}</th><th class="num">${tx('sm.col.rows')}</th><th class="num">${tx('topsql.share')}</th><th>${tx('sm.col.statement')}</th></tr></thead>
77
81
  <tbody>${top.rows.map((r) => html`<tr>
78
- <td class="num" data-label="Calls">${r.calls}</td><td class="num" data-label="Total ms">${r.total_ms}</td><td class="num" data-label="Mean ms">${r.mean_ms}</td>
79
- <td class="num" data-label="Rows">${r.rows}</td><td class="num" data-label="Share">${r.share ?? 0}%</td>
80
- <td data-label="Statement"><code class="sql-text">${r.query}</code></td></tr>`)}</tbody></table></div>
82
+ <td class="num" data-label="${tx('sm.col.calls')}">${r.calls}</td><td class="num" data-label="${tx('sm.col.total_ms')}">${r.total_ms}</td><td class="num" data-label="${tx('sm.col.mean_ms')}">${r.mean_ms}</td>
83
+ <td class="num" data-label="${tx('sm.col.rows')}">${r.rows}</td><td class="num" data-label="${tx('topsql.share')}">${r.share ?? 0}%</td>
84
+ <td data-label="${tx('sm.col.statement')}"><code class="sql-text">${r.query}</code></td></tr>`)}</tbody></table></div>
81
85
  <form method="post" action="${BASE}/apps/${a.id}/top-sql/reset" class="u-mt1">${csrf(s)}
82
- <button class="btn" data-confirm="Clear the statistics of role ${a.db_role}?">${icon('history')} Reset the statistics</button></form>`
83
- : html`<p class="muted">No statements recorded for <code>${a.db_role}</code> yet. Use the application, then come back.</p>`
86
+ <button class="btn" data-confirm="${tx('topsql.reset_confirm', { role: a.db_role })}">${icon('history')} ${tx('topsql.reset')}</button></form>`
87
+ : html`<p class="muted">${tx.rich('topsql.none_yet', { role: html`<code>${a.db_role}</code>` })}</p>`
84
88
  : top.reason === 'no_role'
85
- ? html`<div class="alert alert-error" role="alert">This application runs as the owner role, so its statements can't be told apart. Give it a database role in Settings.</div>`
86
- : statementsNote(top.reason, top.message);
89
+ ? html`<div class="alert alert-error" role="alert">${tx('topsql.no_role')}</div>`
90
+ : statementsNote(tx, top.reason, top.message);
87
91
  const main = html`${appHeader(a, 'activity', s)}
88
- ${region('Top SQL', html`<p class="muted u-mt0">Statements run as the role <code>${a.db_role ?? '(owner)'}</code> since the statistics were last reset, from <code>pg_stat_statements</code>. Literals and binds show as <code>$1</code>, <code>$2</code>…; applications that share a role share this list. <a href="${BASE}/apps/${a.id}/activity">Back to Activity</a></p>
92
+ ${region(tx('sql.tool.topsql'), html`<p class="muted u-mt0">${tx.rich('topsql.intro', { role: html`<code>${a.db_role ?? tx('topsql.owner')}</code>`, ext: html`<code>pg_stat_statements</code>`, p1: html`<code>$1</code>`, p2: html`<code>$2</code>` })} <a href="${BASE}/apps/${a.id}/activity">${tx('topsql.back_activity')}</a></p>
89
93
  ${body}`)}`;
90
- return send(reply, s, shell(s, `Top SQL · ${a.name}`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Activity', `${BASE}/apps/${a.id}/activity`], ['Top SQL']], main));
94
+ return send(reply, s, shell(s, `${tx('sql.tool.topsql')} · ${a.name}`, [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('topsql.activity'), `${BASE}/apps/${a.id}/activity`], [tx('sql.tool.topsql')]], main));
91
95
  });
92
96
 
93
97
  app.post(`${BASE}/apps/:id/top-sql/reset`, async (req: Req, reply) => {
@@ -96,9 +100,9 @@ export async function topSqlRoutes(app: FastifyInstance) {
96
100
  const a = await appOf(req.params.id);
97
101
  if (!a) return reply.code(404).send('Not found');
98
102
  try {
99
- if (!a.db_role) throw new Error('The application has no database role.');
103
+ if (!a.db_role) throw new Error(tr(s)('topsql.no_db_role'));
100
104
  await owner.query(`select pg_stat_statements_reset((select oid from pg_roles where rolname = $1), (select oid from pg_database where datname = current_database()), 0)`, [a.db_role]);
101
- flash(s, 'Statistics reset.');
105
+ flash(s, tr(s)('topsql.reset_done'));
102
106
  } catch (e) {
103
107
  flash(s, (e as Error).message, 'error');
104
108
  }
@@ -22,6 +22,7 @@ import {
22
22
  } from '../unload.ts';
23
23
  import { BASE, csrf, developer, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
24
24
  import { sessionSchema, workshopDb } from './workspaces.ts';
25
+ import { tr, type BuilderTranslate } from './i18n.ts';
25
26
 
26
27
  // SQL Workshop → Unload Data (APEX: Data Workshop → Unload Data): a table or
27
28
  // view (chosen columns, an optional WHERE and ORDER BY) or a query, to CSV,
@@ -88,86 +89,89 @@ const list = (v: unknown) => (Array.isArray(v) ? v : v === undefined || v === nu
88
89
  export async function unloadRoutes(app: FastifyInstance) {
89
90
  const page = (s: Session, reply: FastifyReply, main: Raw, code = 200) => {
90
91
  reply.code(code);
91
- return send(reply, s, shell(s, 'Unload Data', [['SQL Workshop', `${BASE}/sql`], ['Unload Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('unload', s)}${main}`, 'sql'));
92
+ const tx = tr(s);
93
+ return send(reply, s, shell(s, tx('sql.tool.unload'), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.unload')]], html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('unload', s)}${main}`, 'sql'));
92
94
  };
93
95
 
94
- const formatFields = (b: Body) => {
96
+ const formatFields = (tx: BuilderTranslate, b: Body) => {
95
97
  const fmt = (UNLOAD_FORMATS as string[]).includes(b.format ?? '') ? b.format : 'csv';
96
98
  const posted = b.format !== undefined; // unticked checkboxes post nothing
97
99
  const on = (name: string) => (!posted || b[name] === '1' ? raw(' checked') : '');
98
100
  const opt = (map: Record<string, string>, field: string, labels: Record<string, string>) =>
99
101
  Object.keys(map).map((k) => html`<option value="${k}"${(b[field] ?? Object.keys(map)[0]) === k ? raw(' selected') : ''}>${labels[k]}</option>`);
100
102
  return html`
101
- <fieldset class="prop-group u-mt125"><legend>Format</legend>
102
- <div class="unload-formats" role="radiogroup" aria-label="Format">${UNLOAD_FORMATS.map(
103
- (f) => html`<label class="check"><input type="radio" name="format" value="${f}"${fmt === f ? raw(' checked') : ''}> ${{ csv: 'CSV', json: 'JSON', xlsx: 'Excel (.xlsx)', xml: 'XML', sql: 'SQL inserts' }[f]}</label>`,
103
+ <fieldset class="prop-group u-mt125"><legend>${tx('cl.format')}</legend>
104
+ <div class="unload-formats" role="radiogroup" aria-label="${tx('cl.format')}">${UNLOAD_FORMATS.map(
105
+ (f) => html`<label class="check"><input type="radio" name="format" value="${f}"${fmt === f ? raw(' checked') : ''}> ${{ csv: 'CSV', json: 'JSON', xlsx: tx('ul.fmt.xlsx'), xml: 'XML', sql: tx('ul.fmt.sql') }[f]}</label>`,
104
106
  )}</div>
105
107
  </fieldset>
106
108
  <fieldset class="prop-group"><legend>CSV</legend><div class="form-grid">
107
- <div class="field"><label class="label" for="f_delimiter">Separator</label>
108
- <select id="f_delimiter" name="delimiter">${opt(CSV_DELIMITERS, 'delimiter', { comma: 'Comma ,', semicolon: 'Semicolon ;', tab: 'Tab', pipe: 'Pipe |' })}</select></div>
109
- <div class="field"><label class="label" for="f_enclosure">Enclosed by</label>
110
- <select id="f_enclosure" name="enclosure">${opt(CSV_ENCLOSURES, 'enclosure', { double: 'Double quote "', single: "Single quote '" })}</select>
111
- <small class="help">Only values that contain the separator, the enclosure or a line break are enclosed.</small></div>
109
+ <div class="field"><label class="label" for="f_delimiter">${tx('ul.separator')}</label>
110
+ <select id="f_delimiter" name="delimiter">${opt(CSV_DELIMITERS, 'delimiter', { comma: tx('ul.sep.comma'), semicolon: tx('ul.sep.semicolon'), tab: tx('ul.sep.tab'), pipe: tx('ul.sep.pipe') })}</select></div>
111
+ <div class="field"><label class="label" for="f_enclosure">${tx('ul.enclosed')}</label>
112
+ <select id="f_enclosure" name="enclosure">${opt(CSV_ENCLOSURES, 'enclosure', { double: tx('ul.enc.double'), single: tx('ul.enc.single') })}</select>
113
+ <small class="help">${tx('ul.enclosed_help')}</small></div>
112
114
  <div class="field"><span class="label" aria-hidden="true"></span>
113
- <label class="check"><input type="checkbox" name="header" value="1"${on('header')}> First row: column names</label>
114
- <label class="check"><input type="checkbox" name="bom" value="1"${on('bom')}> UTF-8 byte order mark (for Excel)</label></div>
115
+ <label class="check"><input type="checkbox" name="header" value="1"${on('header')}> ${tx('ul.header')}</label>
116
+ <label class="check"><input type="checkbox" name="bom" value="1"${on('bom')}> ${tx('ul.bom')}</label></div>
115
117
  </div></fieldset>
116
118
  <fieldset class="prop-group"><legend>XML</legend><div class="form-grid">
117
- <div class="field"><label class="label" for="f_root_tag">Root element</label><input id="f_root_tag" name="root_tag" value="${b.root_tag ?? DEFAULT_UNLOAD.rootTag}" maxlength="64"></div>
118
- <div class="field"><label class="label" for="f_row_tag">Row element</label><input id="f_row_tag" name="row_tag" value="${b.row_tag ?? DEFAULT_UNLOAD.rowTag}" maxlength="64">
119
- <small class="help">One child element per column; empty (null) values are left out.</small></div>
119
+ <div class="field"><label class="label" for="f_root_tag">${tx('ul.root')}</label><input id="f_root_tag" name="root_tag" value="${b.root_tag ?? DEFAULT_UNLOAD.rootTag}" maxlength="64"></div>
120
+ <div class="field"><label class="label" for="f_row_tag">${tx('ul.row')}</label><input id="f_row_tag" name="row_tag" value="${b.row_tag ?? DEFAULT_UNLOAD.rowTag}" maxlength="64">
121
+ <small class="help">${tx('ul.row_help')}</small></div>
120
122
  </div></fieldset>`;
121
123
  };
122
124
 
123
- const sourceTabs = (active: 'table' | 'query') =>
124
- html`<nav class="ide-tabs u-mb1" aria-label="Unload from">
125
- <a class="ide-tab" href="${BASE}/sql/unload"${active === 'table' ? raw(' aria-current="page"') : ''}>${icon('table')}<span>Table or view</span></a>
126
- <a class="ide-tab" href="${BASE}/sql/unload?source=query"${active === 'query' ? raw(' aria-current="page"') : ''}>${icon('code')}<span>Query</span></a></nav>`;
125
+ const sourceTabs = (tx: BuilderTranslate, active: 'table' | 'query') =>
126
+ html`<nav class="ide-tabs u-mb1" aria-label="${tx('ul.from')}">
127
+ <a class="ide-tab" href="${BASE}/sql/unload"${active === 'table' ? raw(' aria-current="page"') : ''}>${icon('table')}<span>${tx('ul.table_or_view')}</span></a>
128
+ <a class="ide-tab" href="${BASE}/sql/unload?source=query"${active === 'query' ? raw(' aria-current="page"') : ''}>${icon('code')}<span>${tx('sm.col.query')}</span></a></nav>`;
127
129
 
128
- const intro = html`<p class="muted">Download the rows of a table, a view or a query as CSV, JSON, Excel or XML (up to ${DOWNLOAD_MAX_ROWS.toLocaleString('en')} rows).
129
- Runs as the builder's owner connection (an isolated workspace's developer login for its developers) in a read-only transaction. To load a file into a table, use <a href="${BASE}/sql/load">Load Data</a>.</p>`;
130
+ const introOf = (tx: BuilderTranslate) => html`<p class="muted">${tx('ul.intro', { n: DOWNLOAD_MAX_ROWS.toLocaleString('en') })}
131
+ ${tx.rich('ul.intro2', { load: html`<a href="${BASE}/sql/load">${tx('sql.tool.load')}</a>` })}</p>`;
130
132
 
131
133
  const form = async (s: Session, b: Body, error?: string) => {
134
+ const tx = tr(s);
135
+ const intro = introOf(tx);
132
136
  const alert = error ? html`<div class="alert alert-error" role="alert">${error}</div>` : '';
133
137
  if (b.source === 'query')
134
- return html`${intro}${sourceTabs('query')}${region(
135
- 'Unload a query',
138
+ return html`${intro}${sourceTabs(tx, 'query')}${region(
139
+ tx('ul.unload_query'),
136
140
  html`${alert}<form method="post" action="${BASE}/sql/unload">${csrf(s)}<input type="hidden" name="source" value="query">
137
- <div class="field"><label class="label" for="f_query">Query</label>
141
+ <div class="field"><label class="label" for="f_query">${tx('sm.col.query')}</label>
138
142
  <textarea id="f_query" name="query" class="code sql-editor" rows="10" spellcheck="false" data-code="plpgsql" required>${b.query ?? ''}</textarea>
139
- <small class="help">One SELECT (or WITH … SELECT) statement.</small></div>
140
- ${formatFields(b)}
141
- <div class="buttons"><button class="btn btn-hot">${icon('download')} Download</button></div></form>`,
143
+ <small class="help">${tx('ul.one_select')}</small></div>
144
+ ${formatFields(tx, b)}
145
+ <div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('scr.download')}</button></div></form>`,
142
146
  )}`;
143
147
  const rels = await relations();
144
148
  const rel = rels.find((r) => r.qname === b.table);
145
149
  let schema = '';
146
150
  const chooser = html`<form method="get" action="${BASE}/sql/unload" class="unload-pick">
147
- <label class="sr-only" for="f_table">Table or view</label>
148
- <select id="f_table" name="table" required><option value="">- choose a table or view -</option>${rels.map((r) => {
151
+ <label class="sr-only" for="f_table">${tx('ul.table_or_view')}</label>
152
+ <select id="f_table" name="table" required><option value="">${tx('ul.choose_table')}</option>${rels.map((r) => {
149
153
  const group = r.schema !== schema ? html`${schema ? raw('</optgroup>') : ''}<optgroup label="${(schema = r.schema)}">` : '';
150
- return html`${group}<option value="${r.qname}"${r === rel ? raw(' selected') : ''}>${r.name}${r.kind === 'view' ? ' (view)' : ''}</option>`;
154
+ return html`${group}<option value="${r.qname}"${r === rel ? raw(' selected') : ''}>${r.name}${r.kind === 'view' ? ` ${tx('ul.view')}` : ''}</option>`;
151
155
  })}${schema ? raw('</optgroup>') : ''}</select>
152
- <button class="btn">Choose</button></form>`;
153
- if (!rel) return html`${intro}${sourceTabs('table')}${region('Unload a table or view', html`${alert}${chooser}`)}`;
156
+ <button class="btn">${tx('ul.choose')}</button></form>`;
157
+ if (!rel) return html`${intro}${sourceTabs(tx, 'table')}${region(tx('ul.unload_table'), html`${alert}${chooser}`)}`;
154
158
  const cols = await columns(rel.qname);
155
159
  const chosen = new Set(b.format !== undefined ? list(b.columns) : cols.map((c) => c.name));
156
- return html`${intro}${sourceTabs('table')}${region(
157
- `Unload ${rel.qname}`,
160
+ return html`${intro}${sourceTabs(tx, 'table')}${region(
161
+ tx('ul.unload_of', { name: rel.qname }),
158
162
  html`${alert}${chooser}
159
163
  <form method="post" action="${BASE}/sql/unload" class="u-mt1">${csrf(s)}<input type="hidden" name="source" value="table"><input type="hidden" name="table" value="${rel.qname}">
160
- <fieldset class="prop-group"><legend>Columns</legend><div class="qb-pick">${cols.map(
164
+ <fieldset class="prop-group"><legend>${tx('ul.columns')}</legend><div class="qb-pick">${cols.map(
161
165
  (c) => html`<label class="check"><input type="checkbox" name="columns" value="${c.name}"${chosen.has(c.name) ? raw(' checked') : ''}> ${c.name} <span class="muted small">${c.type}</span></label>`,
162
166
  )}</div></fieldset>
163
167
  <div class="form-grid">
164
- <div class="field"><label class="label" for="f_where">Where</label><input id="f_where" name="where" value="${b.where ?? ''}" placeholder="e.g. deptno = 10" class="code" spellcheck="false">
165
- <small class="help">Optional: a condition, without the word WHERE.</small></div>
166
- <div class="field"><label class="label" for="f_order">Order by</label><input id="f_order" name="order" value="${b.order ?? ''}" placeholder="e.g. 1, created desc" class="code" spellcheck="false">
167
- <small class="help">Optional: columns, without the words ORDER BY.</small></div>
168
+ <div class="field"><label class="label" for="f_where">${tx('ul.where')}</label><input id="f_where" name="where" value="${b.where ?? ''}" placeholder="${tx('ul.eg', { x: 'deptno = 10' })}" class="code" spellcheck="false">
169
+ <small class="help">${tx('ul.where_help')}</small></div>
170
+ <div class="field"><label class="label" for="f_order">${tx('ul.order')}</label><input id="f_order" name="order" value="${b.order ?? ''}" placeholder="${tx('ul.eg', { x: '1, created desc' })}" class="code" spellcheck="false">
171
+ <small class="help">${tx('ul.order_help')}</small></div>
168
172
  </div>
169
- ${formatFields(b)}
170
- <div class="buttons"><button class="btn btn-hot">${icon('download')} Download</button></div></form>`,
173
+ ${formatFields(tx, b)}
174
+ <div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('scr.download')}</button></div></form>`,
171
175
  )}`;
172
176
  };
173
177
 
@@ -192,10 +196,10 @@ export async function unloadRoutes(app: FastifyInstance) {
192
196
  name = 'query';
193
197
  } else {
194
198
  const rel = (await relations()).find((r) => r.qname === b.table);
195
- if (!rel) return refuse('Choose a table or view.');
199
+ if (!rel) return refuse(tr(s)('ul.err.table'));
196
200
  const known = new Set((await columns(rel.qname)).map((c) => c.name));
197
201
  const cols = list(b.columns).filter((c) => known.has(c));
198
- if (!cols.length) return refuse('Choose at least one column.');
202
+ if (!cols.length) return refuse(tr(s)('ul.err.column'));
199
203
  const where = (b.where ?? '').trim();
200
204
  const order = (b.order ?? '').trim();
201
205
  // the WHERE and ORDER BY text is the developer's SQL: the whole statement must still be one SELECT
@@ -226,7 +230,7 @@ export async function unloadRoutes(app: FastifyInstance) {
226
230
  } catch (e) {
227
231
  await c.query('rollback').catch(() => {});
228
232
  c.release(true);
229
- return refuse(`The query failed: ${(e as Error).message}`);
233
+ return refuse(tr(s)('ul.err.failed', { error: (e as Error).message }));
230
234
  }
231
235
  await logActivity({ username: s.username, event: 'sql_unload', ip: clientIp(req), detail: `${opts.format}: ${sql.length > 1900 ? `${sql.slice(0, 1900)}…` : sql}` });
232
236
  const out = new PassThrough();
@@ -121,9 +121,9 @@ async function navigatorTree(db: Awaited<ReturnType<typeof workshopDb>>, schema:
121
121
  return html`<ul class="ws-tree" id="ws-tree">${objects.map((o) => {
122
122
  const list = cols.get(o.ref);
123
123
  const obLink = `${BASE}/sql/objects?${new URLSearchParams(o.type === 'policy' ? { schema, k: 'table', o: o.ref, tab: 'rls' } : { schema, k: o.type, o: o.ref })}`;
124
- if (!list) return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}"><a href="${obLink}" title="${label(o.type)}">${icon(iconOf(o.type))}<span>${o.name}</span></a>${objectMenu(schema, o)}</li>`;
124
+ if (!list) return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}"><a href="${obLink}" title="${label(o.type)}">${icon(iconOf(o.type))}<span>${o.name}</span></a>${objectMenu(schema, o, tx)}</li>`;
125
125
  const st = objectStatements(schema, o.name, list, o.type);
126
- return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}">${objectMenu(schema, o)}<details>
126
+ return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}">${objectMenu(schema, o, tx)}<details>
127
127
  <summary draggable="true" data-ws-sql="${st.select}" title="${tx('ws.nav.drag', { type: tx.lang === 'en' ? label(o.type).replace(/s$/, '') : label(o.type) })}">${icon(iconOf(o.type))}<span>${o.name}</span>${o.rls ? html`<span class="tag">RLS</span>` : ''}</summary>
128
128
  <ul class="ws-cols">${list.map((c) => html`<li title="${c.type}${c.notnull ? ' not null' : ''}">${c.pk ? icon('key') : icon('minus')}<span>${c.name}</span><small class="muted">${c.type}</small></li>`)}</ul>
129
129
  <div class="ws-obj-actions">
@@ -525,7 +525,7 @@ async function worksheet(req: Req, reply: FastifyReply) {
525
525
  else {
526
526
  const d = drawChart('', lastQuery.columns, lastQuery.data ?? [], spec);
527
527
  chartCss = d.css;
528
- chartPane = html`${chartControls(lastQuery.columns, spec)}
528
+ chartPane = html`${chartControls(lastQuery.columns, spec, 'ch', tx)}
529
529
  <div class="ch-actions"><button class="btn btn-sm" name="mode" value="chart">${icon('refresh')} ${tx('ws.chart.again')}</button>
530
530
  <input type="hidden" name="chart_sql" value="${lastQuery.sql}">
531
531
  <label class="sr-only" for="ws-chart-name">${tx('ws.chart.name')}</label><input id="ws-chart-name" name="chart_name" placeholder="${tx('ws.chart.name')}" maxlength="120">
@@ -231,12 +231,12 @@ const pad = (n: number) => String(n).padStart(2, '0');
231
231
  const sorted = (s: Set<number>) => [...s].sort((a, b) => a - b);
232
232
  const isAll = (s: Set<number>, min: number, max: number) => s.size === max - min + 1;
233
233
  /** a list of numbers with runs as ranges: 1-5, 7 */
234
- function compact(values: number[], name: (n: number) => string = String) {
234
+ function compact(values: number[], name: (n: number) => string = String, t: CronWords = EN_WORDS) {
235
235
  const out: string[] = [];
236
236
  for (let i = 0; i < values.length; i++) {
237
237
  let j = i;
238
238
  while (j + 1 < values.length && values[j + 1] === values[j] + 1) j++;
239
- out.push(j - i >= 2 ? `${name(values[i])} to ${name(values[j])}` : j > i ? `${name(values[i])}, ${name(values[j])}` : name(values[i]));
239
+ out.push(j - i >= 2 ? fillWords(t('cron.range'), { from: name(values[i]), to: name(values[j]) }) : j > i ? `${name(values[i])}, ${name(values[j])}` : name(values[i]));
240
240
  i = j;
241
241
  }
242
242
  return out.join(', ');
@@ -250,33 +250,63 @@ function stepOf(s: Set<number>, min: number, max: number) {
250
250
  }
251
251
 
252
252
  /** The schedule in words ("At 02:00, every day"). */
253
- export function describeSchedule(s: Schedule): string {
254
- if (s.kind === 'reboot') return 'When the database server starts';
255
- if (s.kind === 'seconds') return s.seconds === 1 ? 'Every second' : `Every ${s.seconds} seconds`;
253
+ /**
254
+ * (sprint 162, CD-8) The phrases of a schedule in words, by key: English here; the builder passes its translator
255
+ * (the same keys, cron.*, in src/builder/i18n/*.ts) and the language for the day and month names.
256
+ */
257
+ export const EN_WORDS_TEXT: Record<string, string> = {
258
+ 'cron.reboot': 'When the database server starts', 'cron.every_second': 'Every second', 'cron.every_seconds': 'Every {n} seconds',
259
+ 'cron.every_minute': 'Every minute', 'cron.every_minutes': 'Every {n} minutes', 'cron.every_hour': 'Every hour',
260
+ 'cron.minute_of_hour': 'At minute {m} of every hour', 'cron.minutes_of_hour': 'At minutes {list} of every hour', 'cron.at': 'At {times}',
261
+ 'cron.at_minute': 'At minute {m}', 'cron.at_minutes': 'At minutes {list}', 'cron.of_every_hours': '{mins} of every {n} hours',
262
+ 'cron.past_hours': '{mins} past hours {list}', 'cron.day_and_last': 'on day {list} and the last day of the month',
263
+ 'cron.last_day': 'on the last day of the month', 'cron.day_of_month': 'on day {list} of the month', 'cron.weekdays': 'on weekdays',
264
+ 'cron.on_days': 'on {list}', 'cron.a_days': 'a {list}', 'cron.either': '{dom} or {dow}', 'cron.if': '{dom} if it is {dow}',
265
+ 'cron.every_day': 'every day', 'cron.in_months': 'in {list}', 'cron.range': '{from} to {to}',
266
+ };
267
+ export type CronWords = (key: string) => string;
268
+ const EN_WORDS: CronWords = (k) => EN_WORDS_TEXT[k] ?? k;
269
+ const fillWords = (text: string, p: Record<string, string | number>) => text.replace(/\{(\w+)\}/g, (m, k: string) => (k in p ? String(p[k]) : m));
270
+
271
+ /** Day (0 = Sunday) and month (1 = January) names in a language (English: as before). */
272
+ const dayName = (lang: string, d: number) => (lang === 'en' ? DAY_NAMES[d] : new Intl.DateTimeFormat(lang, { weekday: 'long', timeZone: 'UTC' }).format(new Date(Date.UTC(2023, 0, 1 + d))));
273
+ const monthName = (lang: string, m: number) => (lang === 'en' ? MONTH_NAMES[m - 1] : new Intl.DateTimeFormat(lang, { month: 'long', timeZone: 'UTC' }).format(new Date(Date.UTC(2023, m - 1, 1))));
274
+
275
+ export function describeSchedule(s: Schedule, t: CronWords = EN_WORDS, lang = 'en'): string {
276
+ const w = (key: string, p: Record<string, string | number> = {}) => fillWords(t(key), p);
277
+ const list = (v: number[], name?: (n: number) => string) => compact(v, name, t);
278
+ if (s.kind === 'reboot') return w('cron.reboot');
279
+ if (s.kind === 'seconds') return s.seconds === 1 ? w('cron.every_second') : w('cron.every_seconds', { n: s.seconds });
256
280
  const minutes = sorted(s.minutes);
257
281
  const hours = sorted(s.hours);
258
282
  let time: string;
259
- if (isAll(s.minutes, 0, 59) && isAll(s.hours, 0, 23)) time = 'Every minute';
260
- else if (isAll(s.hours, 0, 23)) {
283
+ // whether the time already says how often (then no "every day")
284
+ let often = false;
285
+ if (isAll(s.minutes, 0, 59) && isAll(s.hours, 0, 23)) {
286
+ time = w('cron.every_minute');
287
+ often = true;
288
+ } else if (isAll(s.hours, 0, 23)) {
261
289
  const step = stepOf(s.minutes, 0, 59);
262
- time = step ? `Every ${step} minutes` : minutes.length === 1 ? (minutes[0] === 0 ? 'Every hour' : `At minute ${minutes[0]} of every hour`) : `At minutes ${compact(minutes)} of every hour`;
263
- } else if (hours.length * minutes.length <= 6) time = `At ${hours.flatMap((h) => minutes.map((m) => `${pad(h)}:${pad(m)}`)).join(', ')}`;
290
+ often = true;
291
+ time = step ? w('cron.every_minutes', { n: step }) : minutes.length === 1 ? (minutes[0] === 0 ? w('cron.every_hour') : w('cron.minute_of_hour', { m: minutes[0] })) : w('cron.minutes_of_hour', { list: list(minutes) });
292
+ } else if (hours.length * minutes.length <= 6) time = w('cron.at', { times: hours.flatMap((h) => minutes.map((m) => `${pad(h)}:${pad(m)}`)).join(', ') });
264
293
  else {
265
294
  const step = stepOf(s.hours, 0, 23);
266
- const mins = isAll(s.minutes, 0, 59) ? 'Every minute' : minutes.length === 1 ? `At minute ${minutes[0]}` : `At minutes ${compact(minutes)}`;
267
- time = `${mins} ${step ? `of every ${step} hours` : `past hours ${compact(hours)}`}`;
295
+ const mins = isAll(s.minutes, 0, 59) ? w('cron.every_minute') : minutes.length === 1 ? w('cron.at_minute', { m: minutes[0] }) : w('cron.at_minutes', { list: list(minutes) });
296
+ time = step ? w('cron.of_every_hours', { mins, n: step }) : w('cron.past_hours', { mins, list: list(hours) });
268
297
  }
269
298
  const parts = [time];
270
299
  const domAll = isAll(s.days, 1, 31) && !s.lastDay;
271
300
  const dowAll = isAll(s.weekdays, 0, 6);
272
- const dom = s.lastDay ? (s.days.size ? `on day ${compact(sorted(s.days))} and the last day of the month` : 'on the last day of the month') : `on day ${compact(sorted(s.days))} of the month`;
301
+ const dom = s.lastDay ? (s.days.size ? w('cron.day_and_last', { list: list(sorted(s.days)) }) : w('cron.last_day')) : w('cron.day_of_month', { list: list(sorted(s.days)) });
273
302
  const weekdays = sorted(s.weekdays);
274
- const dow = weekdays.join() === '1,2,3,4,5' ? 'on weekdays' : `on ${compact(weekdays, (d) => DAY_NAMES[d])}`;
275
- if (s.either) parts.push(`${dom} or ${dow}`);
276
- else if (!domAll && !dowAll) parts.push(`${dom} if it is ${dow.replace(/^on /, 'a ')}`);
303
+ const days = list(weekdays, (d) => dayName(lang, d));
304
+ const dow = weekdays.join() === '1,2,3,4,5' ? w('cron.weekdays') : w('cron.on_days', { list: days });
305
+ if (s.either) parts.push(w('cron.either', { dom, dow }));
306
+ else if (!domAll && !dowAll) parts.push(w('cron.if', { dom, dow: weekdays.join() === '1,2,3,4,5' ? w('cron.weekdays') : w('cron.a_days', { list: days }) }));
277
307
  else if (!domAll) parts.push(dom);
278
308
  else if (!dowAll) parts.push(dow);
279
- else if (time !== 'Every minute' && !/^Every \d+ minutes$|hour$/.test(time)) parts.push('every day');
280
- if (!isAll(s.months, 1, 12)) parts.push(`in ${compact(sorted(s.months), (m) => MONTH_NAMES[m - 1])}`);
309
+ else if (!often) parts.push(w('cron.every_day'));
310
+ if (!isAll(s.months, 1, 12)) parts.push(w('cron.in_months', { list: list(sorted(s.months), (m) => monthName(lang, m)) }));
281
311
  return parts.join(', ');
282
312
  }
package/src/i18n/ar.ts CHANGED
@@ -676,6 +676,11 @@ export const ar: Record<MessageKey, string> = {
676
676
  'process.queued': "بدأ في الخلفية (المهمة {id}).",
677
677
  'process.background_type': "لا يمكن تشغيل العملية «{name}» ({type}) في الخلفية.",
678
678
  'workflow.started': "بدأ مسار العمل {id}.",
679
+ 'process.no_geocoder': 'لم يُعدّ أي محدِّد مواقع جغرافية لهذا التثبيت.',
680
+ 'process.address_not_found': 'لم يُعثر على مكان لـ "{address}".',
681
+ 'process.task_created': 'تم إنشاء المهمة {id}.',
682
+ 'item.map_of': 'خريطة {position}',
683
+ 'process.no_task': 'لا توجد مهمة للتعامل معها.',
679
684
  'workflow.terminated': "أُنهي مسار العمل {id}.",
680
685
  'workflow.retried': "يعمل مسار العمل {id} من جديد.",
681
686
  'workflow.no_instance': "لم يُحدد أي مسار عمل.",
package/src/i18n/cs.ts CHANGED
@@ -676,6 +676,11 @@ export const cs: Record<MessageKey, string> = {
676
676
  'process.queued': "Spuštěno na pozadí (úloha {id}).",
677
677
  'process.background_type': "Proces „{name}“ ({type}) nemůže běžet na pozadí.",
678
678
  'workflow.started': "Pracovní postup {id} byl spuštěn.",
679
+ 'process.no_geocoder': 'Tato instalace nemá nastavený geokodér.',
680
+ 'process.address_not_found': 'Pro „{address}“ nebylo nalezeno žádné místo.',
681
+ 'process.task_created': 'Úkol {id} byl vytvořen.',
682
+ 'item.map_of': 'Mapa: {position}',
683
+ 'process.no_task': 'Žádný úkol ke zpracování.',
679
684
  'workflow.terminated': "Pracovní postup {id} byl ukončen.",
680
685
  'workflow.retried': "Pracovní postup {id} běží znovu.",
681
686
  'workflow.no_instance': "Není vybrán žádný pracovní postup.",
package/src/i18n/da.ts CHANGED
@@ -676,6 +676,11 @@ export const da: Record<MessageKey, string> = {
676
676
  'process.queued': "Startet i baggrunden (job {id}).",
677
677
  'process.background_type': "Processen “{name}” ({type}) kan ikke køre i baggrunden.",
678
678
  'workflow.started': "Arbejdsgang {id} er startet.",
679
+ 'process.no_geocoder': 'Denne installation har ingen geokoder.',
680
+ 'process.address_not_found': 'Intet sted fundet for "{address}".',
681
+ 'process.task_created': 'Opgave {id} er oprettet.',
682
+ 'item.map_of': 'Kort over {position}',
683
+ 'process.no_task': 'Ingen opgave at behandle.',
679
684
  'workflow.terminated': "Arbejdsgang {id} er afsluttet.",
680
685
  'workflow.retried': "Arbejdsgang {id} kører igen.",
681
686
  'workflow.no_instance': "Ingen arbejdsgang er valgt.",
package/src/i18n/de.ts CHANGED
@@ -686,6 +686,11 @@ export const de: Record<MessageKey, string> = {
686
686
  'process.queued': 'Im Hintergrund gestartet (Job {id}).',
687
687
  'process.background_type': 'Der Prozess "{name}" ({type}) kann nicht im Hintergrund laufen.',
688
688
  'workflow.started': 'Workflow {id} gestartet.',
689
+ 'process.no_geocoder': 'Für diese Installation ist kein Geocoder eingerichtet.',
690
+ 'process.address_not_found': 'Kein Ort für „{address}“ gefunden.',
691
+ 'process.task_created': 'Aufgabe {id} wurde erstellt.',
692
+ 'item.map_of': 'Karte von {position}',
693
+ 'process.no_task': 'Keine Aufgabe zum Bearbeiten.',
689
694
  'workflow.terminated': 'Workflow {id} beendet.',
690
695
  'workflow.retried': 'Workflow {id} läuft wieder.',
691
696
  'workflow.no_instance': 'Es ist kein Workflow ausgewählt.',
package/src/i18n/el.ts CHANGED
@@ -676,6 +676,11 @@ export const el: Record<MessageKey, string> = {
676
676
  'process.queued': "Ξεκίνησε στο παρασκήνιο (εργασία {id}).",
677
677
  'process.background_type': "Η διαδικασία «{name}» ({type}) δεν μπορεί να εκτελεστεί στο παρασκήνιο.",
678
678
  'workflow.started': "Η ροή εργασίας {id} ξεκίνησε.",
679
+ 'process.no_geocoder': 'Δεν έχει ρυθμιστεί γεωκωδικοποιητής για αυτή την εγκατάσταση.',
680
+ 'process.address_not_found': 'Δεν βρέθηκε τοποθεσία για «{address}».',
681
+ 'process.task_created': 'Η εργασία {id} δημιουργήθηκε.',
682
+ 'item.map_of': 'Χάρτης του {position}',
683
+ 'process.no_task': 'Δεν υπάρχει εργασία για ενέργεια.',
679
684
  'workflow.terminated': "Η ροή εργασίας {id} τερματίστηκε.",
680
685
  'workflow.retried': "Η ροή εργασίας {id} εκτελείται ξανά.",
681
686
  'workflow.no_instance': "Δεν έχει επιλεγεί ροή εργασίας.",
package/src/i18n/es.ts CHANGED
@@ -686,6 +686,11 @@ export const es: Record<MessageKey, string> = {
686
686
  'process.queued': 'Iniciado en segundo plano (trabajo {id}).',
687
687
  'process.background_type': 'El proceso "{name}" ({type}) no se puede ejecutar en segundo plano.',
688
688
  'workflow.started': 'Flujo de trabajo {id} iniciado.',
689
+ 'process.no_geocoder': 'Esta instalación no tiene un geocodificador configurado.',
690
+ 'process.address_not_found': 'No se encontró ningún lugar para "{address}".',
691
+ 'process.task_created': 'Tarea {id} creada.',
692
+ 'item.map_of': 'Mapa de {position}',
693
+ 'process.no_task': 'No hay ninguna tarea sobre la que actuar.',
689
694
  'workflow.terminated': 'Flujo de trabajo {id} terminado.',
690
695
  'workflow.retried': 'El flujo de trabajo {id} se está ejecutando de nuevo.',
691
696
  'workflow.no_instance': 'No hay ningún flujo de trabajo seleccionado.',
package/src/i18n/fi.ts CHANGED
@@ -676,6 +676,11 @@ export const fi: Record<MessageKey, string> = {
676
676
  'process.queued': "Käynnistetty taustalla (työ {id}).",
677
677
  'process.background_type': "Prosessia ”{name}” ({type}) ei voi suorittaa taustalla.",
678
678
  'workflow.started': "Työnkulku {id} käynnistetty.",
679
+ 'process.no_geocoder': 'Tälle asennukselle ei ole määritetty geokoodaajaa.',
680
+ 'process.address_not_found': 'Paikkaa ei löytynyt haulle "{address}".',
681
+ 'process.task_created': 'Tehtävä {id} luotiin.',
682
+ 'item.map_of': 'Kartta: {position}',
683
+ 'process.no_task': 'Ei käsiteltävää tehtävää.',
679
684
  'workflow.terminated': "Työnkulku {id} lopetettu.",
680
685
  'workflow.retried': "Työnkulku {id} on taas käynnissä.",
681
686
  'workflow.no_instance': "Työnkulkua ei ole valittu.",
package/src/i18n/fr.ts CHANGED
@@ -686,6 +686,11 @@ export const fr: Record<MessageKey, string> = {
686
686
  'process.queued': 'Démarré en arrière-plan (tâche {id}).',
687
687
  'process.background_type': 'Le processus « {name} » ({type}) ne peut pas s’exécuter en arrière-plan.',
688
688
  'workflow.started': 'Workflow {id} démarré.',
689
+ 'process.no_geocoder': 'Aucun géocodeur n\'est configuré pour cette installation.',
690
+ 'process.address_not_found': 'Aucun lieu trouvé pour « {address} ».',
691
+ 'process.task_created': 'Tâche {id} créée.',
692
+ 'item.map_of': 'Carte de {position}',
693
+ 'process.no_task': 'Aucune tâche à traiter.',
689
694
  'workflow.terminated': 'Workflow {id} arrêté.',
690
695
  'workflow.retried': 'Workflow {id} relancé.',
691
696
  'workflow.no_instance': 'Aucun workflow n’est sélectionné.',
package/src/i18n/he.ts CHANGED
@@ -676,6 +676,11 @@ export const he: Record<MessageKey, string> = {
676
676
  'process.queued': "הופעל ברקע (עבודה {id}).",
677
677
  'process.background_type': "התהליך \"{name}\" ({type}) אינו יכול לפעול ברקע.",
678
678
  'workflow.started': "תהליך העבודה {id} הופעל.",
679
+ 'process.no_geocoder': 'לא הוגדר מקודד גאוגרפי בהתקנה זו.',
680
+ 'process.address_not_found': 'לא נמצא מקום עבור "{address}".',
681
+ 'process.task_created': 'המשימה {id} נוצרה.',
682
+ 'item.map_of': 'מפה של {position}',
683
+ 'process.no_task': 'אין משימה לטיפול.',
679
684
  'workflow.terminated': "תהליך העבודה {id} הופסק.",
680
685
  'workflow.retried': "תהליך העבודה {id} פועל שוב.",
681
686
  'workflow.no_instance': "לא נבחר תהליך עבודה.",
package/src/i18n/it.ts CHANGED
@@ -676,6 +676,11 @@ export const it: Record<MessageKey, string> = {
676
676
  'process.queued': "Avviato in background (job {id}).",
677
677
  'process.background_type': "Il processo “{name}” ({type}) non può essere eseguito in background.",
678
678
  'workflow.started': "Workflow {id} avviato.",
679
+ 'process.no_geocoder': 'Nessun geocodificatore configurato per questa installazione.',
680
+ 'process.address_not_found': 'Nessun luogo trovato per "{address}".',
681
+ 'process.task_created': 'Attività {id} creata.',
682
+ 'item.map_of': 'Mappa di {position}',
683
+ 'process.no_task': 'Nessuna attività da gestire.',
679
684
  'workflow.terminated': "Workflow {id} terminato.",
680
685
  'workflow.retried': "Il workflow {id} è di nuovo in esecuzione.",
681
686
  'workflow.no_instance': "Nessun workflow selezionato.",
package/src/i18n/ja.ts CHANGED
@@ -676,6 +676,11 @@ export const ja: Record<MessageKey, string> = {
676
676
  'process.queued': "バックグラウンドで開始しました (ジョブ {id})。",
677
677
  'process.background_type': "プロセス「{name}」({type}) はバックグラウンドで実行できません。",
678
678
  'workflow.started': "ワークフロー {id} を開始しました。",
679
+ 'process.no_geocoder': 'このインストールにはジオコーダーが設定されていません。',
680
+ 'process.address_not_found': '「{address}」の場所が見つかりません。',
681
+ 'process.task_created': 'タスク {id} を作成しました。',
682
+ 'item.map_of': '{position} の地図',
683
+ 'process.no_task': '処理するタスクがありません。',
679
684
  'workflow.terminated': "ワークフロー {id} を終了しました。",
680
685
  'workflow.retried': "ワークフロー {id} を再実行しています。",
681
686
  'workflow.no_instance': "ワークフローが選択されていません。",
package/src/i18n/ko.ts CHANGED
@@ -676,6 +676,11 @@ export const ko: Record<MessageKey, string> = {
676
676
  'process.queued': "백그라운드에서 시작했습니다(작업 {id}).",
677
677
  'process.background_type': "프로세스 '{name}'({type})은(는) 백그라운드에서 실행할 수 없습니다.",
678
678
  'workflow.started': "워크플로 {id}을(를) 시작했습니다.",
679
+ 'process.no_geocoder': '이 설치에는 지오코더가 설정되어 있지 않습니다.',
680
+ 'process.address_not_found': '"{address}"에 대한 장소를 찾을 수 없습니다.',
681
+ 'process.task_created': '작업 {id}이(가) 생성되었습니다.',
682
+ 'item.map_of': '{position} 지도',
683
+ 'process.no_task': '처리할 작업이 없습니다.',
679
684
  'workflow.terminated': "워크플로 {id}을(를) 종료했습니다.",
680
685
  'workflow.retried': "워크플로 {id}이(가) 다시 실행 중입니다.",
681
686
  'workflow.no_instance': "선택한 워크플로가 없습니다.",
package/src/i18n/nb.ts CHANGED
@@ -676,6 +676,11 @@ export const nb: Record<MessageKey, string> = {
676
676
  'process.queued': "Startet i bakgrunnen (jobb {id}).",
677
677
  'process.background_type': "Prosessen «{name}» ({type}) kan ikke kjøre i bakgrunnen.",
678
678
  'workflow.started': "Arbeidsflyt {id} er startet.",
679
+ 'process.no_geocoder': 'Denne installasjonen har ingen geokoder.',
680
+ 'process.address_not_found': 'Fant ikke noe sted for «{address}».',
681
+ 'process.task_created': 'Oppgave {id} er opprettet.',
682
+ 'item.map_of': 'Kart over {position}',
683
+ 'process.no_task': 'Ingen oppgave å behandle.',
679
684
  'workflow.terminated': "Arbeidsflyt {id} er avsluttet.",
680
685
  'workflow.retried': "Arbeidsflyt {id} kjører igjen.",
681
686
  'workflow.no_instance': "Ingen arbeidsflyt er valgt.",
package/src/i18n/pl.ts CHANGED
@@ -676,6 +676,11 @@ export const pl: Record<MessageKey, string> = {
676
676
  'process.queued': "Uruchomiono w tle (zadanie {id}).",
677
677
  'process.background_type': "Proces „{name}” ({type}) nie może działać w tle.",
678
678
  'workflow.started': "Przepływ pracy {id} uruchomiony.",
679
+ 'process.no_geocoder': 'Ta instalacja nie ma skonfigurowanego geokodera.',
680
+ 'process.address_not_found': 'Nie znaleziono miejsca dla „{address}”.',
681
+ 'process.task_created': 'Utworzono zadanie {id}.',
682
+ 'item.map_of': 'Mapa: {position}',
683
+ 'process.no_task': 'Brak zadania do obsłużenia.',
679
684
  'workflow.terminated': "Przepływ pracy {id} przerwany.",
680
685
  'workflow.retried': "Przepływ pracy {id} działa ponownie.",
681
686
  'workflow.no_instance': "Nie wybrano przepływu pracy.",
package/src/i18n/pt.ts CHANGED
@@ -676,6 +676,11 @@ export const pt: Record<MessageKey, string> = {
676
676
  'process.queued': "Iniciado em segundo plano (tarefa {id}).",
677
677
  'process.background_type': "O processo “{name}” ({type}) não pode ser executado em segundo plano.",
678
678
  'workflow.started': "Fluxo de trabalho {id} iniciado.",
679
+ 'process.no_geocoder': 'Esta instalação não tem um geocodificador configurado.',
680
+ 'process.address_not_found': 'Nenhum lugar encontrado para "{address}".',
681
+ 'process.task_created': 'Tarefa {id} criada.',
682
+ 'item.map_of': 'Mapa de {position}',
683
+ 'process.no_task': 'Nenhuma tarefa para tratar.',
679
684
  'workflow.terminated': "Fluxo de trabalho {id} terminado.",
680
685
  'workflow.retried': "O fluxo de trabalho {id} está novamente em execução.",
681
686
  'workflow.no_instance': "Nenhum fluxo de trabalho selecionado.",