pgkiln 0.88.0 → 0.90.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.
@@ -2,7 +2,7 @@ import type { FastifyInstance, FastifyReply } from 'fastify';
2
2
  import { owner } from '../db.ts';
3
3
  import { html, raw, type Raw } from '../html.ts';
4
4
  import { icon } from '../icons.ts';
5
- import { APP_COLORS, BASE, csrf, developer, input, region, send, shell, type Req } from './ui.ts';
5
+ import { APP_COLORS, BASE, csrf, developer, input, pageHeader, region, send, shell, type Req } from './ui.ts';
6
6
  import { crossAppLinks } from './cross-app.ts';
7
7
  import type { Session } from '../session.ts';
8
8
  import { isAdmin } from './locks.ts';
@@ -53,16 +53,6 @@ const lockedText = (tx: BuilderTranslate, by: string) => tx('ab.locked_by', { us
53
53
  const appIcon = (a: { id: number; name: string }, cls = '') =>
54
54
  html`<span class="app-icon app-color-${a.id % APP_COLORS}${cls ? ` ${cls}` : ''}" aria-hidden="true">${a.name.slice(0, 1).toUpperCase()}</span>`;
55
55
 
56
- /** The four tiles at the top of the App Builder. */
57
- const tiles = (tx: BuilderTranslate) => html`<nav class="ab-tiles" aria-label="${tx('rail.app_builder')}">
58
- ${([
59
- ['create', 'plus', tx('ab.tile.create'), tx('ab.tile.create_text')],
60
- ['import', 'upload', tx('ab.tile.import'), tx('ab.tile.import_text')],
61
- ['dashboard', 'activity', tx('ab.tile.dashboard'), tx('ab.tile.dashboard_text')],
62
- ['utilities', 'settings', tx('ab.tile.utilities'), tx('ab.tile.utilities_text')],
63
- ] as const).map(([path, ic, title, text]) => html`<a class="ab-tile" href="${BASE}/${path}"><span class="ab-tile-icon">${icon(ic)}</span><strong>${title}</strong><span>${text}</span></a>`)}
64
- </nav>`;
65
-
66
56
  /** The side column: About, Recent, Tasks. */
67
57
  function aside(tx: BuilderTranslate, recent: { id: number; name: string }[], about: string, tasks: [string, string][]) {
68
58
  return html`<aside class="ab-side" aria-label="${tx('ab.aside')}">
@@ -73,17 +63,16 @@ function aside(tx: BuilderTranslate, recent: { id: number; name: string }[], abo
73
63
  </aside>`;
74
64
  }
75
65
 
66
+ // (UX-5) what the header doesn't offer already (Create and Import are there)
76
67
  const TASKS: [BuilderKey, string][] = [
77
- ['ab.task.create', `${BASE}/create`],
78
68
  ['ab.task.create_file', `${BASE}/create/file`],
79
- ['ab.task.import', `${BASE}/import`],
80
69
  ['ab.task.users', `${BASE}/users`],
81
70
  ['ab.task.providers', `${BASE}/users/providers`],
82
71
  ['rail.sql_workshop', `${BASE}/sql`],
83
72
  ['admin.developers', `${BASE}/developers`],
84
73
  ];
85
74
 
86
- async function recentApps(s: Session) {
75
+ export async function recentApps(s: Session) {
87
76
  const ids = recentIds(s);
88
77
  const ws = currentWorkspace(s);
89
78
  if (ids.length) {
@@ -153,20 +142,20 @@ export async function homeRoutes(app: FastifyInstance) {
153
142
  <a class="tb-btn" href="${BASE}/apps/${a.id}" title="${tx('ab.edit', { name: a.name })}">${icon('edit')}<span class="sr-only">${tx('ab.edit', { name: a.name })}</span></a>
154
143
  <a class="tb-btn" href="/a/${a.alias}" target="_blank" rel="noopener" title="${tx('ab.run', { name: a.name })}">${icon('play')}<span class="sr-only">${tx('ab.run', { name: a.name })}</span></a>
155
144
  </div></article>`)}</div>`
156
- : html`<div class="table-wrap ab-table"><table class="report ab-apps">
145
+ : html`<div class="table-wrap ab-table rep-fit"><table class="report ab-apps">
157
146
  <caption class="sr-only">${tx('ab.col.apps')}</caption>
158
- <thead><tr>${th('id', tx('ab.col.app'), 'num')}${th('name', tx('ab.col.name'))}${th('alias', tx('ab.col.alias'))}${th('pages', tx('ab.col.pages'), 'num')}<th scope="col" class="num">${tx('ab.col.views')}</th><th scope="col">${tx('ab.col.signin')}</th>${th('updated', tx('ab.col.updated'))}<th scope="col">${tx('ab.col.updated_by')}</th><th scope="col" class="num">${tx('ab.col.copies')}</th><th scope="col" class="ab-actions-col">${tx('ab.col.actions')}</th></tr></thead>
147
+ <thead><tr>${th('id', tx('ab.col.app'), 'num')}${th('name', tx('ab.col.name'))}${th('alias', tx('ab.col.alias'))}${th('pages', tx('ab.col.pages'), 'num')}<th scope="col" class="num col-low">${tx('ab.col.views')}</th><th scope="col">${tx('ab.col.signin')}</th>${th('updated', tx('ab.col.updated'))}<th scope="col" class="col-low">${tx('ab.col.updated_by')}</th><th scope="col" class="num col-low">${tx('ab.col.copies')}</th><th scope="col" class="ab-actions-col col-actions">${tx('ab.col.actions')}</th></tr></thead>
159
148
  <tbody>${rows.map((a) => html`<tr data-filter-row>
160
149
  <td class="num">${a.id}</td>
161
150
  <td class="ab-name"><a href="${BASE}/apps/${a.id}">${appIcon(a)}<span>${a.name}</span></a>${a.locked_by ? html`<span class="ab-lock" role="img" aria-label="${lockedText(tx, a.locked_by)}" title="${lockedText(tx, a.locked_by)}">${icon('lock')}</span>` : ''}</td>
162
151
  <td>${a.alias}${a.group_name ? html` <span class="tag">${a.group_name}</span>` : ''}</td>
163
152
  <td class="num">${a.pages}</td>
164
- <td class="num">${a.views}</td>
153
+ <td class="num col-low">${a.views}</td>
165
154
  <td>${tx(a.authentication === 'none' ? 'ab.auth.none' : a.authentication === 'header' ? 'ab.auth.header' : a.authentication === 'database' ? 'ab.auth.database' : a.authentication === 'custom' ? 'ab.auth.custom' : 'ab.auth.app_users')}</td>
166
155
  <td title="${a.updated_at ? new Date(a.updated_at).toISOString().slice(0, 16).replace('T', ' ') : ''}">${ago(a.updated_at, Date.now(), tx)}</td>
167
- <td class="ab-by">${a.updated_by ?? ''}</td>
168
- <td class="num">${a.copies ? html`<a href="${BASE}/apps/${a.id}/working-copies" aria-label="${tx('ab.copies_of', { copies: copiesText(tx, a.copies), name: a.name })}">${a.copies}</a>` : '0'}</td>
169
- <td class="ab-actions">
156
+ <td class="ab-by col-low">${a.updated_by ?? ''}</td>
157
+ <td class="num col-low">${a.copies ? html`<a href="${BASE}/apps/${a.id}/working-copies" aria-label="${tx('ab.copies_of', { copies: copiesText(tx, a.copies), name: a.name })}">${a.copies}</a>` : '0'}</td>
158
+ <td class="ab-actions col-actions">
170
159
  <a class="tb-btn" href="${BASE}/apps/${a.id}" title="${tx('ab.edit', { name: a.name })}">${icon('edit')}<span class="sr-only">${tx('ab.edit', { name: a.name })}</span></a>
171
160
  <a class="tb-btn" href="/a/${a.alias}" target="_blank" rel="noopener" title="${tx('ab.run', { name: a.name })}">${icon('play')}<span class="sr-only">${tx('ab.run', { name: a.name })}</span></a>
172
161
  </td></tr>`)}</tbody></table></div>`;
@@ -174,9 +163,13 @@ export async function homeRoutes(app: FastifyInstance) {
174
163
  html`<a class="tb-btn" href="${link({ view: v })}" title="${label}"${view === v ? raw(' aria-current="true"') : ''}>${icon(ic)}<span class="sr-only">${label}</span></a>`;
175
164
  const main = html`<div class="ab-home">
176
165
  <div class="ab-main">
177
- <h1 class="sr-only">${tx('rail.app_builder')}</h1>
166
+ ${pageHeader(tx('rail.app_builder'), {
167
+ primary: [`${BASE}/create`, tx('ab.tile.create'), 'plus'],
168
+ secondary: [[`${BASE}/import`, tx('ab.tile.import'), 'upload']],
169
+ more: [[`${BASE}/dashboard`, tx('ab.tile.dashboard'), 'activity'], [`${BASE}/utilities`, tx('rail.admin'), 'settings']],
170
+ moreLabel: tx('ws.more'),
171
+ })}
178
172
  ${s.workspace?.message ? html`<div class="alert alert-info ab-message" role="status">${icon('bell')}<div><strong>${s.workspace.name}</strong><p>${s.workspace.message}</p></div></div>` : ''}
179
- ${tiles(tx)}
180
173
  <div class="ab-toolbar">
181
174
  <form class="ab-search" method="get" action="${BASE}/apps" role="search">
182
175
  <label class="sr-only" for="ab-q">${tx('ab.search_label')}</label>${icon('search')}
@@ -190,13 +183,8 @@ export async function homeRoutes(app: FastifyInstance) {
190
183
  <div class="menu-panel" role="menu">
191
184
  <div class="menu-section"><span class="small muted">${tx('ab.sort_by')}</span>
192
185
  ${(Object.keys(SORTS) as Sort[]).map((k) => html`<a role="menuitem" href="${link({ sort: k })}"${sort === k ? raw(' aria-current="true"') : ''}>${tx(`ab.sort.${k}`)}</a>`)}</div>
193
- <div class="menu-section menu-links">
194
- <a role="menuitem" href="${BASE}/import">${icon('upload')} ${tx('ab.task.import')}</a>
195
- <a role="menuitem" href="${BASE}/dashboard">${icon('activity')} ${tx('ab.ws_dashboard')}</a>
196
- </div>
197
186
  </div></details>
198
187
  <span class="ab-count muted" aria-live="polite">${rows.length === total.n ? tx(total.n === 1 ? 'ab.count_one' : 'ab.count', { n: total.n }) : tx('ab.count_of', { n: rows.length, total: total.n })}</span>
199
- <a class="btn btn-hot ab-create" href="${BASE}/create">${tx('ab.tile.create')}</a>
200
188
  </div>
201
189
  ${list}
202
190
  </div>
@@ -271,49 +259,72 @@ export async function homeRoutes(app: FastifyInstance) {
271
259
  });
272
260
 
273
261
  // ---------------------------------------------------------------- workspace utilities
262
+ // (UX-4) Administration: the workspace utilities in sections, with a filter; the rail's Administration entry (UX-3)
274
263
  app.get(`${BASE}/utilities`, async (req: Req, reply) => {
275
264
  const s = await developer(req, reply);
276
265
  if (!s) return;
277
266
  const tx = tr(s);
278
- const card = (href: string, ic: Raw | '', title: string, text: string | Raw) =>
279
- html`<a class="ab-tile" href="${href}"><span class="ab-tile-icon">${ic}</span><strong>${title}</strong><span>${text}</span></a>`;
280
- const main = html`<div class="ab-home"><div class="ab-main"><h1 class="u-mb1">${tx('wu.title')}</h1>
281
- <div class="ab-tiles ab-tiles-wrap">
282
- ${card(`${BASE}/users`, icon('user'), tx('wu.users'), tx('wu.users_text'))}
283
- ${card(`${BASE}/app-groups`, icon('folder'), tx('wu.app_groups'), tx('wu.app_groups_text'))}
284
- ${card(`${BASE}/gallery`, icon('package'), tx('wu.gallery'), tx('wu.gallery_text'))}
285
- ${card(`${BASE}/users/providers`, icon('key'), tx('wu.providers'), tx('wu.providers_text'))}
286
- ${card(`${BASE}/users/directories`, icon('org'), tx('wu.ldap'), tx('wu.ldap_text'))}
287
- ${card(`${BASE}/users#account-settings`, icon('shield'), tx('wu.password_policy'), tx('wu.password_policy_text'))}
288
- ${card(`${BASE}/developers`, icon('users'), tx('wu.developers'), tx('wu.developers_text'))}
289
- ${s.workspace?.sql_workshop === false ? '' : card(`${BASE}/sql`, icon('database'), tx('wu.sql'), tx('wu.sql_text'))}
290
- ${card(`${BASE}/backups`, icon('archive'), tx('wu.backups'), tx('wu.backups_text'))}
291
- ${card(`${BASE}/workspace-files`, icon('file'), tx('wu.files'), tx('wu.files_text', { tag: '#WORKSPACE_FILES#' }))}
292
- ${card(`${BASE}/remote-servers`, icon('server'), tx('wu.servers'), tx('wu.servers_text'))}
293
- ${(await isAdmin(s.username)) ? card(`${BASE}/remote-databases`, icon('database'), tx('wu.remote_dbs'), tx('wu.remote_dbs_text')) : ''}
294
- ${card(`${BASE}/workspace-credentials`, icon('key'), tx('wu.credentials'), tx('wu.credentials_text'))}
295
- ${card(`${BASE}/rest-catalogs`, icon('globe'), tx('wu.catalogs'), tx('wu.catalogs_text'))}
296
- ${s.workspace?.admin ? card(`${BASE}/workspace-export`, icon('download'), tx('wu.export'), tx('wu.export_text')) : ''}
297
- ${card(`${BASE}/dashboard`, icon('activity'), tx('wu.dashboard'), tx('wu.dashboard_text'))}
298
- ${(await isAdmin(s.username)) ? card(`${BASE}/instance`, icon('settings'), tx('wu.instance'), tx('wu.instance_text')) : ''}
299
- ${(await isAdmin(s.username))
300
- ? card(`${BASE}/workspaces`, icon('layers'), tx('wu.workspaces'), tx('wu.workspaces_text'))
301
- : s.workspace && (await workspaceAdmin(s.username, s.workspace.id))
302
- ? card(`${BASE}/workspaces/${s.workspace.id}`, icon('layers'), tx('wu.workspace', { name: s.workspace.name }), tx('wu.workspace_text'))
303
- : ''}
304
- ${card(`${BASE}/ui-defaults`, icon('sliders-horizontal'), tx('wu.ui_defaults'), tx('wu.ui_defaults_text'))}
305
- ${card(`${BASE}/icons`, icon('image'), tx('wu.icons'), tx('wu.icons_text'))}
306
- ${card(`${BASE}/vector-providers`, icon('search'), tx('wu.vectors'), tx('wu.vectors_text'))}
307
- ${(await isAdmin(s.username)) ? card(`${BASE}/ai`, icon('bolt'), tx('wu.ai'), tx('wu.ai_text')) : ''}
308
- ${(await isAdmin(s.username)) ? card(`${BASE}/mail`, icon('send'), tx('wu.mail'), tx('wu.mail_text')) : ''}
309
- ${(await isAdmin(s.username)) ? card(`${BASE}/telemetry`, icon('activity'), tx('wu.otel'), tx('wu.otel_text')) : ''}
310
- ${(await isAdmin(s.username)) ? card(`${BASE}/installation`, icon('history'), tx('wu.installation'), tx('wu.installation_text')) : ''}
311
- </div></div>
312
- <aside class="ab-side" aria-label="${tx('wu.side')}">
313
- <section><h2>${tx('wu.about')}</h2><p>${tx('wu.about_text')}</p>
314
- <a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md#workspaces" target="_blank" rel="noopener">${tx('wu.learn_more')}</a></section>
315
- <section><h2>${tx('wu.cross_app')}</h2><ul class="ab-links ab-tasks">${crossAppLinks()}</ul></section>
316
- </aside></div>`;
317
- return send(reply, s, shell(s, tx('wu.title'), [[tx('rail.app_builder'), BASE], [tx('wu.title')]], main));
267
+ const admin = await isAdmin(s.username);
268
+ const wsAdmin = !!s.workspace?.admin || (!!s.workspace && (await workspaceAdmin(s.username, s.workspace.id)));
269
+ const ws = s.workspace;
270
+ type Tile = [href: string, ic: string, title: string, text: string | Raw] | null;
271
+ const tile = (t: Tile) =>
272
+ t ? html`<li><a class="adm-tile" href="${t[0]}"><span class="adm-tile-head">${icon(t[1])}<strong>${t[2]}</strong></span>${t[3] ? html`<span class="adm-tile-text">${t[3]}</span>` : ''}</a></li>` : '';
273
+ const section = (id: string, title: string, tiles: Tile[]) => {
274
+ const shown = tiles.filter(Boolean);
275
+ return shown.length ? html`<section class="adm-section" aria-labelledby="adm-${id}"><h2 id="adm-${id}">${title}</h2><ul class="adm-tiles">${shown.map(tile)}</ul></section>` : '';
276
+ };
277
+ const main = html`<div class="adm">
278
+ <div class="title-row"><h1>${tx('rail.admin')}</h1>
279
+ <div class="adm-filter" hidden><label class="sr-only" for="adm-q">${tx('wu.filter')}</label>
280
+ <input id="adm-q" type="search" placeholder="${tx('wu.filter')}" autocomplete="off" data-adm-filter></div></div>
281
+ <p class="u-mt0 adm-lede"><a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md#workspaces" target="_blank" rel="noopener">${tx('wu.learn_more')}</a></p>
282
+ ${section('people', tx('wu.sec.people'), [
283
+ [`${BASE}/users`, 'user', tx('wu.users'), tx('wu.users_text')],
284
+ [`${BASE}/developers`, 'users', tx('wu.developers'), tx('wu.developers_text')],
285
+ [`${BASE}/users/providers`, 'key', tx('wu.providers'), tx('wu.providers_text')],
286
+ [`${BASE}/users/directories`, 'org', tx('wu.ldap'), tx('wu.ldap_text')],
287
+ [`${BASE}/users#account-settings`, 'shield', tx('wu.password_policy'), tx('wu.password_policy_text')],
288
+ ])}
289
+ ${section('connections', tx('wu.sec.connections'), [
290
+ [`${BASE}/remote-servers`, 'server', tx('wu.servers'), tx('wu.servers_text')],
291
+ admin ? [`${BASE}/remote-databases`, 'database', tx('wu.remote_dbs'), tx('wu.remote_dbs_text')] : null,
292
+ [`${BASE}/workspace-credentials`, 'key', tx('wu.credentials'), tx('wu.credentials_text')],
293
+ [`${BASE}/rest-catalogs`, 'globe', tx('wu.catalogs'), tx('wu.catalogs_text')],
294
+ admin ? [`${BASE}/mail`, 'send', tx('wu.mail'), tx('wu.mail_text')] : null,
295
+ wsAdmin ? [`${BASE}/mail-queue`, 'inbox', tx('admin.mail_queue'), ''] : null,
296
+ wsAdmin ? [`${BASE}/network`, 'globe', tx('admin.network'), ''] : null,
297
+ ])}
298
+ ${section('data', tx('wu.sec.data'), [
299
+ admin ? [`${BASE}/ai`, 'bolt', tx('wu.ai'), tx('wu.ai_text')] : null,
300
+ [`${BASE}/vector-providers`, 'search', tx('wu.vectors'), tx('wu.vectors_text')],
301
+ [`${BASE}/ui-defaults`, 'sliders-horizontal', tx('wu.ui_defaults'), tx('wu.ui_defaults_text')],
302
+ [`${BASE}/workspace-files`, 'file', tx('wu.files'), tx('wu.files_text', { tag: '#WORKSPACE_FILES#' })],
303
+ [`${BASE}/icons`, 'image', tx('wu.icons'), tx('wu.icons_text')],
304
+ ])}
305
+ ${section('operations', tx('wu.sec.operations'), [
306
+ [`${BASE}/app-groups`, 'folder', tx('wu.app_groups'), tx('wu.app_groups_text')],
307
+ [`${BASE}/backups`, 'archive', tx('wu.backups'), tx('wu.backups_text')],
308
+ wsAdmin ? [`${BASE}/workspace-export`, 'download', tx('wu.export'), tx('wu.export_text')] : null,
309
+ [`${BASE}/dashboard`, 'activity', tx('wu.dashboard'), tx('wu.dashboard_text')],
310
+ admin ? [`${BASE}/telemetry`, 'activity', tx('wu.otel'), tx('wu.otel_text')] : null,
311
+ ])}
312
+ ${ws && wsAdmin ? section('workspace', tx('wu.sec.workspace', { name: ws.name }), [
313
+ [`${BASE}/workspaces/${ws.id}`, 'layers', tx('admin.ws_devs'), tx('wu.workspace_text')],
314
+ [`${BASE}/workspaces/${ws.id}#service`, 'bell', tx('admin.ws_message'), ''],
315
+ [`${BASE}/workspaces/${ws.id}#preferences`, 'sliders-horizontal', tx('admin.ws_prefs'), ''],
316
+ [`${BASE}/workspaces/${ws.id}#links`, 'external-link', tx('admin.ext_links'), ''],
317
+ [`${BASE}/workspaces/${ws.id}/utilization`, 'chart', tx('admin.utilization'), ''],
318
+ ]) : ''}
319
+ ${admin ? section('instance', tx('wu.sec.instance'), [
320
+ [`${BASE}/instance`, 'settings', tx('wu.instance'), tx('wu.instance_text')],
321
+ [`${BASE}/workspaces`, 'layers', tx('wu.workspaces'), tx('wu.workspaces_text')],
322
+ s.instanceAdmin ? [`${BASE}/db-users`, 'key', tx('admin.db_users'), ''] : null,
323
+ [`${BASE}/installation`, 'history', tx('wu.installation'), tx('wu.installation_text')],
324
+ ]) : ''}
325
+ <section class="adm-section" aria-labelledby="adm-reports"><h2 id="adm-reports">${tx('wu.cross_app')}</h2><ul class="ab-links ab-tasks adm-reports">${crossAppLinks()}</ul></section>
326
+ <p class="adm-none muted" hidden>${tx('wu.filter_none')}</p>
327
+ </div>`;
328
+ return send(reply, s, shell(s, tx('rail.admin'), [[tx('rail.admin')]], main));
318
329
  });
319
330
  }
@@ -408,7 +408,6 @@ export const de: BuilderMessages = {
408
408
  'pd.not_found': 'Komponente nicht gefunden.',
409
409
  'pd.save_page': 'Seite speichern',
410
410
  'pd.delete_page': 'Seite löschen',
411
- 'pd.delete_page_confirm': 'Seite {n} und alle ihre Komponenten löschen?',
412
411
  'pd.filter_label': 'Eigenschaften filtern',
413
412
  'pd.filter': 'Filtern',
414
413
  'pd.help_hint': 'Wählen Sie eine Eigenschaft, um hier ihre Hilfe zu sehen.',
@@ -1427,7 +1426,6 @@ export const de: BuilderMessages = {
1427
1426
  'st.ts_format': 'Datums- und Zeitformat',
1428
1427
  'st.ts_format_ph': 'z. B. DD-MM-YYYY HH24:MI',
1429
1428
  'st.save_settings': 'Einstellungen speichern',
1430
- 'st.delete_confirm': 'Anwendung {app} mit allen Seiten löschen?',
1431
1429
  'st.delete': 'Anwendung löschen',
1432
1430
  'st.error_fn': 'Funktion zur Fehlerbehandlung',
1433
1431
  'st.error_fn_ph': 'z. B. sales.error_message',
@@ -1475,7 +1473,7 @@ export const de: BuilderMessages = {
1475
1473
  'us.status': 'Status',
1476
1474
  'us.none': 'keine',
1477
1475
  'us.password': 'Passwort',
1478
- 'us.no_password': 'kein Passwort',
1476
+ 'us.no_password': 'Kein Passwort',
1479
1477
  'us.active': 'aktiv',
1480
1478
  'us.inactive': 'inaktiv',
1481
1479
  'us.last': 'zuletzt {at}',
@@ -1845,8 +1843,8 @@ export const de: BuilderMessages = {
1845
1843
  'mfa.locked_out': 'Telefon, Schlüssel und Codes verloren? Bitten Sie einen Administrator, Ihre Zwei-Faktor-Anmeldung zurückzusetzen.',
1846
1844
  'mfa.wrong': 'Dieser Code ist falsch oder wurde schon verwendet.',
1847
1845
  'mfa.column': 'Zwei-Faktor',
1848
- 'mfa.on': 'an',
1849
- 'mfa.off': 'aus',
1846
+ 'mfa.on': 'An',
1847
+ 'mfa.off': 'Aus',
1850
1848
  'mfa.reset': 'Zurücksetzen',
1851
1849
  'mfa.reason': 'Grund',
1852
1850
  'mfa.reset_help': 'Entfernt die Apps und Wiederherstellungscodes von {user} und meldet die Person ab. Protokolliert; die Administratoren erhalten eine E-Mail.',
@@ -7516,4 +7514,41 @@ export const de: BuilderMessages = {
7516
7514
  'pwa.prefetch_hours': 'Erneut laden nach (Stunden)',
7517
7515
  'pwa.prefetch_hours_help': '1 bis 168. Die Seiten werden nach dieser Zeit beim ersten Seitenaufruf erneut geladen, sofern eine Verbindung besteht.',
7518
7516
  'pwa.prefetch_hours_range': 'Erneut laden nach: eine ganze Zahl von Stunden zwischen 1 und 168.',
7517
+ // ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
7518
+ 'ws.more': 'Weitere Aktionen',
7519
+ 'ws.suggest': 'Vorschläge',
7520
+ 'ws.suggest_title': 'Vorschläge an der Cursorposition (Strg+Leertaste)',
7521
+ 'ws.check': 'Prüfen',
7522
+ 'ws.check_title': 'Das SQL planen, ohne es auszuführen, wie der Advisor',
7523
+ 'ws.key.indent': 'Einrücken (Esc, dann Tab: nächstes Feld)',
7524
+ 'ws.search_path_plain': '{schema} zuerst im Suchpfad',
7525
+ // ------------------------------------------------------------ Rail labels (UX-3)
7526
+ 'rail.labels_show': 'Beschriftungen anzeigen',
7527
+ 'rail.labels_hide': 'Beschriftungen ausblenden',
7528
+ // ------------------------------------------------------------ Administration page (UX-4)
7529
+ 'wu.filter': 'Hilfsprogramme filtern',
7530
+ 'wu.filter_none': 'Nichts entspricht dem Filter.',
7531
+ 'wu.sec.people': 'Personen und Anmeldung',
7532
+ 'wu.sec.connections': 'Verbindungen',
7533
+ 'wu.sec.data': 'Daten und KI',
7534
+ 'wu.sec.operations': 'Betrieb',
7535
+ 'wu.sec.workspace': 'Arbeitsbereich {name}',
7536
+ 'wu.sec.instance': 'Instanz',
7537
+ // ------------------------------------------------------------ Workspace home: needs attention (UX-13)
7538
+ 'wh.attention': 'Erfordert Aufmerksamkeit',
7539
+ // ------------------------------------------------------------ Delete with a typed confirmation (UX-6)
7540
+ 'st.danger_zone': 'Gefahrenzone',
7541
+ 'st.delete_text': 'Beim Löschen von {app} werden seine Seiten, gemeinsamen Komponenten, Arbeitskopien und auch seine Sicherungen entfernt; das lässt sich nicht rückgängig machen. Exportieren Sie die Anwendung vorher, wenn Sie sie noch brauchen könnten.',
7542
+ 'st.delete_type': 'Geben Sie zur Bestätigung den Alias {alias} ein',
7543
+ 'st.delete_mismatch': 'Geben Sie den Alias der Anwendung, {alias}, ein, um sie zu löschen.',
7544
+ 'pd.delete_type': 'Geben Sie zur Bestätigung die Seitennummer {n} ein',
7545
+ 'pd.delete_mismatch': 'Geben Sie die Seitennummer {n} ein, um die Seite zu löschen.',
7546
+ // ------------------------------------------------------------ Create application: name first, a sticky footer (UX-7)
7547
+ 'aw.data_and_pages': 'Daten und Seiten',
7548
+ 'aw.sign_in': 'Anmeldung',
7549
+ 'aw.sum_on': '{n} Seiten auf {schema}',
7550
+ 'aw.sum_on_1': '{n} Seite auf {schema}',
7551
+ 'aw.sum_new': 'Eine neue Anwendung mit eigenem Schema',
7552
+ // ------------------------------------------------------------ A side drawer's Close (UX-8)
7553
+ 'common.close': 'Schließen',
7519
7554
  };
@@ -411,7 +411,6 @@ export const en = {
411
411
  'pd.not_found': 'Component not found.',
412
412
  'pd.save_page': 'Save page',
413
413
  'pd.delete_page': 'Delete page',
414
- 'pd.delete_page_confirm': 'Delete page {n} and all its components?',
415
414
  'pd.filter_label': 'Filter properties',
416
415
  'pd.filter': 'Filter',
417
416
  'pd.help_hint': 'Select a property to see its help here.',
@@ -1431,7 +1430,6 @@ export const en = {
1431
1430
  'st.ts_format': 'Date and time format',
1432
1431
  'st.ts_format_ph': 'e.g. DD-MM-YYYY HH24:MI',
1433
1432
  'st.save_settings': 'Save settings',
1434
- 'st.delete_confirm': 'Delete application {app} and all its pages?',
1435
1433
  'st.delete': 'Delete application',
1436
1434
  'st.error_fn': 'Error handling function',
1437
1435
  'st.error_fn_ph': 'e.g. sales.error_message',
@@ -1478,8 +1476,8 @@ export const en = {
1478
1476
  'us.signin': 'Sign-in',
1479
1477
  'us.status': 'Status',
1480
1478
  'us.none': 'none',
1481
- 'us.password': 'password',
1482
- 'us.no_password': 'no password',
1479
+ 'us.password': 'Password',
1480
+ 'us.no_password': 'No password',
1483
1481
  'us.active': 'active',
1484
1482
  'us.inactive': 'inactive',
1485
1483
  'us.last': 'last {at}',
@@ -1849,8 +1847,8 @@ export const en = {
1849
1847
  'mfa.locked_out': 'Lost your phone, key and codes? Ask an administrator to reset your two-factor sign-in.',
1850
1848
  'mfa.wrong': 'That code is not right, or was already used.',
1851
1849
  'mfa.column': 'Two-factor',
1852
- 'mfa.on': 'on',
1853
- 'mfa.off': 'off',
1850
+ 'mfa.on': 'On',
1851
+ 'mfa.off': 'Off',
1854
1852
  'mfa.reset': 'Reset',
1855
1853
  'mfa.reason': 'Reason',
1856
1854
  'mfa.reset_help': 'Removes the apps and recovery codes of {user} and signs them out. Logged; the administrators are told by mail.',
@@ -7520,4 +7518,41 @@ export const en = {
7520
7518
  'pwa.prefetch_hours': 'Fetch them again after (hours)',
7521
7519
  'pwa.prefetch_hours_help': '1 to 168. The pages are fetched again on the first page opened after this time, while there is a connection.',
7522
7520
  'pwa.prefetch_hours_range': 'Fetch them again after: a whole number of hours from 1 to 168.',
7521
+ // ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
7522
+ 'ws.more': 'More actions',
7523
+ 'ws.suggest': 'Suggest',
7524
+ 'ws.suggest_title': 'Suggestions at the cursor (Ctrl+Space)',
7525
+ 'ws.check': 'Check',
7526
+ 'ws.check_title': 'Plan the SQL without running it, as the Advisor does',
7527
+ 'ws.key.indent': 'Indent (Esc, then Tab: next field)',
7528
+ 'ws.search_path_plain': '{schema} first in the search path',
7529
+ // ------------------------------------------------------------ Rail labels (UX-3)
7530
+ 'rail.labels_show': 'Show labels',
7531
+ 'rail.labels_hide': 'Hide labels',
7532
+ // ------------------------------------------------------------ Administration page (UX-4)
7533
+ 'wu.filter': 'Filter the utilities',
7534
+ 'wu.filter_none': 'Nothing matches the filter.',
7535
+ 'wu.sec.people': 'People and sign-in',
7536
+ 'wu.sec.connections': 'Connections',
7537
+ 'wu.sec.data': 'Data and AI',
7538
+ 'wu.sec.operations': 'Operations',
7539
+ 'wu.sec.workspace': 'Workspace {name}',
7540
+ 'wu.sec.instance': 'Instance',
7541
+ // ------------------------------------------------------------ Workspace home: needs attention (UX-13)
7542
+ 'wh.attention': 'Needs attention',
7543
+ // ------------------------------------------------------------ Delete with a typed confirmation (UX-6)
7544
+ 'st.danger_zone': 'Danger zone',
7545
+ 'st.delete_text': 'Deleting {app} removes its pages, shared components, working copies and its backups too; it can\'t be undone. Export it first if you may need it again.',
7546
+ 'st.delete_type': 'Type the alias {alias} to confirm',
7547
+ 'st.delete_mismatch': 'Type the application\'s alias, {alias}, to delete it.',
7548
+ 'pd.delete_type': 'Type the page number {n} to confirm',
7549
+ 'pd.delete_mismatch': 'Type the page number, {n}, to delete the page.',
7550
+ // ------------------------------------------------------------ Create application: name first, a sticky footer (UX-7)
7551
+ 'aw.data_and_pages': 'Data and pages',
7552
+ 'aw.sign_in': 'Sign-in',
7553
+ 'aw.sum_on': '{n} pages on {schema}',
7554
+ 'aw.sum_on_1': '{n} page on {schema}',
7555
+ 'aw.sum_new': 'A new application with its own schema',
7556
+ // ------------------------------------------------------------ A side drawer's Close (UX-8)
7557
+ 'common.close': 'Close',
7523
7558
  } as const;
@@ -408,7 +408,6 @@ export const es: BuilderMessages = {
408
408
  'pd.not_found': 'Componente no encontrado.',
409
409
  'pd.save_page': 'Guardar página',
410
410
  'pd.delete_page': 'Eliminar página',
411
- 'pd.delete_page_confirm': '¿Eliminar la página {n} y todos sus componentes?',
412
411
  'pd.filter_label': 'Filtrar propiedades',
413
412
  'pd.filter': 'Filtrar',
414
413
  'pd.help_hint': 'Seleccione una propiedad para ver aquí su ayuda.',
@@ -1427,7 +1426,6 @@ export const es: BuilderMessages = {
1427
1426
  'st.ts_format': 'Formato de fecha y hora',
1428
1427
  'st.ts_format_ph': 'p. ej. DD-MM-YYYY HH24:MI',
1429
1428
  'st.save_settings': 'Guardar ajustes',
1430
- 'st.delete_confirm': '¿Eliminar la aplicación {app} y todas sus páginas?',
1431
1429
  'st.delete': 'Eliminar aplicación',
1432
1430
  'st.error_fn': 'Función de gestión de errores',
1433
1431
  'st.error_fn_ph': 'p. ej. sales.error_message',
@@ -1474,8 +1472,8 @@ export const es: BuilderMessages = {
1474
1472
  'us.signin': 'Inicio de sesión',
1475
1473
  'us.status': 'Estado',
1476
1474
  'us.none': 'ninguna',
1477
- 'us.password': 'contraseña',
1478
- 'us.no_password': 'sin contraseña',
1475
+ 'us.password': 'Contraseña',
1476
+ 'us.no_password': 'Sin contraseña',
1479
1477
  'us.active': 'activa',
1480
1478
  'us.inactive': 'inactiva',
1481
1479
  'us.last': 'último {at}',
@@ -1845,8 +1843,8 @@ export const es: BuilderMessages = {
1845
1843
  'mfa.locked_out': '¿Perdió el teléfono, la llave y los códigos? Pida a un administrador que restablezca su inicio de sesión en dos pasos.',
1846
1844
  'mfa.wrong': 'Ese código no es correcto o ya se usó.',
1847
1845
  'mfa.column': 'Dos pasos',
1848
- 'mfa.on': 'activado',
1849
- 'mfa.off': 'desactivado',
1846
+ 'mfa.on': 'Activado',
1847
+ 'mfa.off': 'Desactivado',
1850
1848
  'mfa.reset': 'Restablecer',
1851
1849
  'mfa.reason': 'Motivo',
1852
1850
  'mfa.reset_help': 'Quita las aplicaciones y los códigos de recuperación de {user} y cierra su sesión. Se registra; los administradores reciben un correo.',
@@ -7516,4 +7514,41 @@ export const es: BuilderMessages = {
7516
7514
  'pwa.prefetch_hours': 'Volver a obtenerlas después de (horas)',
7517
7515
  'pwa.prefetch_hours_help': 'De 1 a 168. Las páginas se vuelven a obtener en la primera página abierta después de este tiempo, mientras haya conexión.',
7518
7516
  'pwa.prefetch_hours_range': 'Volver a obtenerlas después de: un número entero de horas entre 1 y 168.',
7517
+ // ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
7518
+ 'ws.more': 'Más acciones',
7519
+ 'ws.suggest': 'Sugerir',
7520
+ 'ws.suggest_title': 'Sugerencias en el cursor (Ctrl+Espacio)',
7521
+ 'ws.check': 'Comprobar',
7522
+ 'ws.check_title': 'Planificar el SQL sin ejecutarlo, como el Advisor',
7523
+ 'ws.key.indent': 'Sangrar (Esc y luego Tab: siguiente campo)',
7524
+ 'ws.search_path_plain': '{schema} primero en la ruta de búsqueda',
7525
+ // ------------------------------------------------------------ Rail labels (UX-3)
7526
+ 'rail.labels_show': 'Mostrar etiquetas',
7527
+ 'rail.labels_hide': 'Ocultar etiquetas',
7528
+ // ------------------------------------------------------------ Administration page (UX-4)
7529
+ 'wu.filter': 'Filtrar las utilidades',
7530
+ 'wu.filter_none': 'Nada coincide con el filtro.',
7531
+ 'wu.sec.people': 'Personas e inicio de sesión',
7532
+ 'wu.sec.connections': 'Conexiones',
7533
+ 'wu.sec.data': 'Datos e IA',
7534
+ 'wu.sec.operations': 'Operaciones',
7535
+ 'wu.sec.workspace': 'Espacio de trabajo {name}',
7536
+ 'wu.sec.instance': 'Instancia',
7537
+ // ------------------------------------------------------------ Workspace home: needs attention (UX-13)
7538
+ 'wh.attention': 'Requiere atención',
7539
+ // ------------------------------------------------------------ Delete with a typed confirmation (UX-6)
7540
+ 'st.danger_zone': 'Zona de peligro',
7541
+ 'st.delete_text': 'Eliminar {app} borra sus páginas, componentes compartidos, copias de trabajo y también sus copias de seguridad; no se puede deshacer. Expórtela antes si puede volver a necesitarla.',
7542
+ 'st.delete_type': 'Escriba el alias {alias} para confirmar',
7543
+ 'st.delete_mismatch': 'Escriba el alias de la aplicación, {alias}, para eliminarla.',
7544
+ 'pd.delete_type': 'Escriba el número de página {n} para confirmar',
7545
+ 'pd.delete_mismatch': 'Escriba el número de página, {n}, para eliminar la página.',
7546
+ // ------------------------------------------------------------ Create application: name first, a sticky footer (UX-7)
7547
+ 'aw.data_and_pages': 'Datos y páginas',
7548
+ 'aw.sign_in': 'Inicio de sesión',
7549
+ 'aw.sum_on': '{n} páginas sobre {schema}',
7550
+ 'aw.sum_on_1': '{n} página sobre {schema}',
7551
+ 'aw.sum_new': 'Una aplicación nueva con su propio esquema',
7552
+ // ------------------------------------------------------------ A side drawer's Close (UX-8)
7553
+ 'common.close': 'Cerrar',
7519
7554
  };
@@ -408,7 +408,6 @@ export const fr: BuilderMessages = {
408
408
  'pd.not_found': 'Composant introuvable.',
409
409
  'pd.save_page': 'Enregistrer la page',
410
410
  'pd.delete_page': 'Supprimer la page',
411
- 'pd.delete_page_confirm': 'Supprimer la page {n} et tous ses composants ?',
412
411
  'pd.filter_label': 'Filtrer les propriétés',
413
412
  'pd.filter': 'Filtrer',
414
413
  'pd.help_hint': 'Sélectionnez une propriété pour afficher son aide ici.',
@@ -1427,7 +1426,6 @@ export const fr: BuilderMessages = {
1427
1426
  'st.ts_format': 'Format de date et heure',
1428
1427
  'st.ts_format_ph': 'p. ex. DD-MM-YYYY HH24:MI',
1429
1428
  'st.save_settings': 'Enregistrer les paramètres',
1430
- 'st.delete_confirm': 'Supprimer l’application {app} et toutes ses pages ?',
1431
1429
  'st.delete': 'Supprimer l’application',
1432
1430
  'st.error_fn': 'Fonction de gestion des erreurs',
1433
1431
  'st.error_fn_ph': 'p. ex. sales.error_message',
@@ -1474,8 +1472,8 @@ export const fr: BuilderMessages = {
1474
1472
  'us.signin': 'Connexion',
1475
1473
  'us.status': 'Statut',
1476
1474
  'us.none': 'aucune',
1477
- 'us.password': 'mot de passe',
1478
- 'us.no_password': 'pas de mot de passe',
1475
+ 'us.password': 'Mot de passe',
1476
+ 'us.no_password': 'Pas de mot de passe',
1479
1477
  'us.active': 'actif',
1480
1478
  'us.inactive': 'inactif',
1481
1479
  'us.last': 'dernier {at}',
@@ -1845,8 +1843,8 @@ export const fr: BuilderMessages = {
1845
1843
  'mfa.locked_out': 'Téléphone, clé et codes perdus ? Demandez à un administrateur de réinitialiser votre connexion à deux facteurs.',
1846
1844
  'mfa.wrong': 'Ce code est incorrect ou a déjà été utilisé.',
1847
1845
  'mfa.column': 'Deux facteurs',
1848
- 'mfa.on': 'activée',
1849
- 'mfa.off': 'désactivée',
1846
+ 'mfa.on': 'Activée',
1847
+ 'mfa.off': 'Désactivée',
1850
1848
  'mfa.reset': 'Réinitialiser',
1851
1849
  'mfa.reason': 'Motif',
1852
1850
  'mfa.reset_help': 'Supprime les applications et codes de récupération de {user} et le déconnecte. Journalisé ; les administrateurs sont prévenus par e-mail.',
@@ -7516,4 +7514,41 @@ export const fr: BuilderMessages = {
7516
7514
  'pwa.prefetch_hours': 'Les récupérer à nouveau après (heures)',
7517
7515
  'pwa.prefetch_hours_help': 'De 1 à 168. Les pages sont récupérées à nouveau à la première page ouverte après ce délai, s’il y a une connexion.',
7518
7516
  'pwa.prefetch_hours_range': 'Les récupérer à nouveau après : un nombre entier d’heures entre 1 et 168.',
7517
+ // ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
7518
+ 'ws.more': 'Plus d’actions',
7519
+ 'ws.suggest': 'Suggérer',
7520
+ 'ws.suggest_title': 'Suggestions au curseur (Ctrl+Espace)',
7521
+ 'ws.check': 'Vérifier',
7522
+ 'ws.check_title': 'Planifier le SQL sans l’exécuter, comme l’Advisor',
7523
+ 'ws.key.indent': 'Indenter (Échap puis Tab : champ suivant)',
7524
+ 'ws.search_path_plain': '{schema} en premier dans le chemin de recherche',
7525
+ // ------------------------------------------------------------ Rail labels (UX-3)
7526
+ 'rail.labels_show': 'Afficher les libellés',
7527
+ 'rail.labels_hide': 'Masquer les libellés',
7528
+ // ------------------------------------------------------------ Administration page (UX-4)
7529
+ 'wu.filter': 'Filtrer les utilitaires',
7530
+ 'wu.filter_none': 'Rien ne correspond au filtre.',
7531
+ 'wu.sec.people': 'Personnes et connexion',
7532
+ 'wu.sec.connections': 'Connexions',
7533
+ 'wu.sec.data': 'Données et IA',
7534
+ 'wu.sec.operations': 'Exploitation',
7535
+ 'wu.sec.workspace': 'Espace de travail {name}',
7536
+ 'wu.sec.instance': 'Instance',
7537
+ // ------------------------------------------------------------ Workspace home: needs attention (UX-13)
7538
+ 'wh.attention': 'À traiter',
7539
+ // ------------------------------------------------------------ Delete with a typed confirmation (UX-6)
7540
+ 'st.danger_zone': 'Zone de danger',
7541
+ 'st.delete_text': 'Supprimer {app} retire ses pages, ses composants partagés, ses copies de travail et aussi ses sauvegardes ; c’est irréversible. Exportez-la d’abord si vous pourriez en avoir besoin.',
7542
+ 'st.delete_type': 'Tapez l’alias {alias} pour confirmer',
7543
+ 'st.delete_mismatch': 'Tapez l’alias de l’application, {alias}, pour la supprimer.',
7544
+ 'pd.delete_type': 'Tapez le numéro de page {n} pour confirmer',
7545
+ 'pd.delete_mismatch': 'Tapez le numéro de page, {n}, pour supprimer la page.',
7546
+ // ------------------------------------------------------------ Create application: name first, a sticky footer (UX-7)
7547
+ 'aw.data_and_pages': 'Données et pages',
7548
+ 'aw.sign_in': 'Connexion',
7549
+ 'aw.sum_on': '{n} pages sur {schema}',
7550
+ 'aw.sum_on_1': '{n} page sur {schema}',
7551
+ 'aw.sum_new': 'Une nouvelle application avec son propre schéma',
7552
+ // ------------------------------------------------------------ A side drawer's Close (UX-8)
7553
+ 'common.close': 'Fermer',
7519
7554
  };