pgkiln 0.83.0 → 0.85.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 (126) hide show
  1. package/db/migrations/154_notifications.sql +186 -0
  2. package/docs/apex-feature-parity.md +3 -2
  3. package/docs/guide/03-builder.md +16 -5
  4. package/docs/guide/04-pages-and-regions.md +42 -0
  5. package/docs/guide/06-processing.md +22 -1
  6. package/docs/guide/07-dynamic-actions.md +1 -0
  7. package/docs/guide/09-reference.md +15 -0
  8. package/docs/guide/11-from-apex.md +1 -0
  9. package/docs/guide/12-development.md +13 -1
  10. package/package.json +1 -1
  11. package/public/app.css +29 -0
  12. package/public/app.js +78 -0
  13. package/public/sw.js +5 -1
  14. package/src/app.ts +11 -0
  15. package/src/builder/ai-agents.ts +86 -85
  16. package/src/builder/ai-builder.ts +30 -30
  17. package/src/builder/ai.ts +93 -92
  18. package/src/builder/api.ts +47 -47
  19. package/src/builder/app-groups.ts +33 -32
  20. package/src/builder/appfromfile.ts +57 -58
  21. package/src/builder/appsheets.ts +33 -32
  22. package/src/builder/appwizard.ts +65 -65
  23. package/src/builder/arrange.ts +23 -22
  24. package/src/builder/assistant.ts +39 -38
  25. package/src/builder/automations.ts +32 -31
  26. package/src/builder/availability.ts +24 -24
  27. package/src/builder/backups.ts +30 -31
  28. package/src/builder/blueprints.ts +62 -61
  29. package/src/builder/code-editor.ts +1 -1
  30. package/src/builder/components.ts +10 -6
  31. package/src/builder/create-page.ts +52 -50
  32. package/src/builder/cross-app.ts +37 -35
  33. package/src/builder/dataload.ts +2 -2
  34. package/src/builder/datasets.ts +107 -106
  35. package/src/builder/dependencies.ts +20 -19
  36. package/src/builder/designer.ts +36 -36
  37. package/src/builder/diagnostics.ts +58 -57
  38. package/src/builder/features.ts +39 -35
  39. package/src/builder/feedback.ts +23 -20
  40. package/src/builder/find-workspace.ts +28 -24
  41. package/src/builder/form-designer.ts +36 -35
  42. package/src/builder/gallery.ts +25 -29
  43. package/src/builder/globalization.ts +50 -49
  44. package/src/builder/i18n/de.ts +3461 -0
  45. package/src/builder/i18n/en.ts +3461 -0
  46. package/src/builder/i18n/es.ts +3461 -0
  47. package/src/builder/i18n/fr.ts +3461 -0
  48. package/src/builder/i18n/it.ts +3461 -0
  49. package/src/builder/i18n/ja.ts +3461 -0
  50. package/src/builder/i18n/ko.ts +3461 -0
  51. package/src/builder/i18n/nl.ts +3461 -0
  52. package/src/builder/i18n/pt.ts +3461 -0
  53. package/src/builder/i18n/zh-cn.ts +3461 -0
  54. package/src/builder/i18n/zh-tw.ts +3461 -0
  55. package/src/builder/i18n.ts +17 -3
  56. package/src/builder/json-sources.ts +63 -62
  57. package/src/builder/layouts.ts +12 -11
  58. package/src/builder/locks.ts +29 -28
  59. package/src/builder/login-page.ts +26 -25
  60. package/src/builder/mail.ts +148 -147
  61. package/src/builder/newapp.ts +15 -12
  62. package/src/builder/notification-settings.ts +27 -0
  63. package/src/builder/plugins.ts +47 -44
  64. package/src/builder/process-jobs.ts +11 -10
  65. package/src/builder/pwa.ts +22 -21
  66. package/src/builder/remote-databases.ts +55 -56
  67. package/src/builder/report-settings.ts +1 -1
  68. package/src/builder/reporter.ts +43 -42
  69. package/src/builder/rest-catalogs.ts +87 -86
  70. package/src/builder/rest.ts +9 -8
  71. package/src/builder/routes.ts +30 -27
  72. package/src/builder/search.ts +14 -13
  73. package/src/builder/static-files.ts +31 -32
  74. package/src/builder/subscriptions.ts +47 -45
  75. package/src/builder/supporting.ts +23 -18
  76. package/src/builder/team.ts +179 -179
  77. package/src/builder/telemetry.ts +59 -62
  78. package/src/builder/templates.ts +66 -65
  79. package/src/builder/themeroller.ts +60 -63
  80. package/src/builder/ui-defaults.ts +21 -20
  81. package/src/builder/ui.ts +2 -1
  82. package/src/builder/upgrade.ts +25 -24
  83. package/src/builder/utilities.ts +48 -47
  84. package/src/builder/vector-providers.ts +67 -66
  85. package/src/builder/websources.ts +43 -42
  86. package/src/builder/wizards.ts +95 -89
  87. package/src/builder/workflow-migrate.ts +30 -31
  88. package/src/builder/workflows.ts +40 -39
  89. package/src/builder/workingcopies.ts +41 -49
  90. package/src/builder/workspace-backups.ts +21 -20
  91. package/src/builder/workspace-mail.ts +17 -16
  92. package/src/builder/workspace-resources.ts +104 -112
  93. package/src/builder/workspace-transfer.ts +34 -26
  94. package/src/cli/replace.ts +2 -0
  95. package/src/i18n/ar.ts +9 -0
  96. package/src/i18n/cs.ts +9 -0
  97. package/src/i18n/da.ts +9 -0
  98. package/src/i18n/de.ts +9 -0
  99. package/src/i18n/el.ts +9 -0
  100. package/src/i18n/es.ts +9 -0
  101. package/src/i18n/fi.ts +9 -0
  102. package/src/i18n/fr.ts +9 -0
  103. package/src/i18n/he.ts +9 -0
  104. package/src/i18n/it.ts +9 -0
  105. package/src/i18n/ja.ts +9 -0
  106. package/src/i18n/ko.ts +9 -0
  107. package/src/i18n/nb.ts +9 -0
  108. package/src/i18n/pl.ts +9 -0
  109. package/src/i18n/pt.ts +9 -0
  110. package/src/i18n/ru.ts +9 -0
  111. package/src/i18n/sv.ts +9 -0
  112. package/src/i18n/tr.ts +9 -0
  113. package/src/i18n/uk.ts +9 -0
  114. package/src/i18n/zh.ts +9 -0
  115. package/src/i18n.ts +18 -0
  116. package/src/metadata.ts +9 -3
  117. package/src/runtime/engine.ts +2 -1
  118. package/src/runtime/feedback.ts +1 -1
  119. package/src/runtime/lists.ts +43 -11
  120. package/src/runtime/notifications.ts +237 -0
  121. package/src/runtime/processes.ts +45 -1
  122. package/src/runtime/regions.ts +11 -15
  123. package/src/runtime/render.ts +12 -3
  124. package/src/runtime/routes.ts +12 -4
  125. package/src/runtime/signin.ts +2 -1
  126. package/src/session.ts +13 -4
@@ -4,6 +4,7 @@ import { html, raw } from '../html.ts';
4
4
  import { baseLanguage, BUILTIN_LANGUAGES, LANGUAGE_NAMES } from '../i18n.ts';
5
5
  import { clearLocaleCache, translatableTexts } from '../runtime/locale.ts';
6
6
  import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
7
+ import { scopedTx as tx } from './i18n.ts';
7
8
 
8
9
  // Shared Components → Globalization (APEX: Translate Application, Text
9
10
  // Messages). Translations live in the application itself: one text in the
@@ -84,19 +85,19 @@ export async function timeZoneSettings(a: { time_zone: string | null; time_zone_
84
85
  const zones = (await owner.query<{ name: string }>(`select name from pg_timezone_names where name !~ '^(posix|right)/' order by name`)).rows.map((r) => r.name);
85
86
  const dbZone = (await owner.one<{ tz: string }>(`select current_setting('TimeZone') as tz`))?.tz ?? '';
86
87
  return html`<div class="form-grid">
87
- ${input('currency', 'Currency', a.currency ?? '', { placeholder: 'e.g. EUR (empty: per language)', help: 'ISO 4217 code for L (symbol) and C (code) in number format masks such as FML999G990D00. A text message FORMAT.CURRENCY overrides it per language.' })}
88
- ${select('time_zone', 'Time zone', a.time_zone ?? '', [['', `- the database's (${dbZone}) -`], ...zones], 'Queries run in this time zone (SET LOCAL timezone), so timestamp with time zone values show in it.')}
88
+ ${input('currency', tx('glob.currency'), a.currency ?? '', { placeholder: tx('glob.e_g_eur_empty_per'), help: tx('glob.iso_4217_code_for_l') })}
89
+ ${select('time_zone', tx('glob.time_zone'), a.time_zone ?? '', [['', `- the database's (${dbZone}) -`], ...zones], tx('glob.queries_run_in_this_time'))}
89
90
  </div>
90
- <div class="field"><label class="check"><input type="checkbox" name="time_zone_auto" value="true"${a.time_zone_auto ? raw(' checked') : ''}> Automatic time zone</label>
91
- <small class="help">Each user sees times in their own time zone: the browser's (sent once per session), or the one they choose on My account. Without JavaScript the app's time zone applies (APEX: Automatic Time Zone).</small></div>`;
91
+ <div class="field"><label class="check"><input type="checkbox" name="time_zone_auto" value="true"${a.time_zone_auto ? raw(' checked') : ''}> ${tx('glob.automatic_time_zone')}</label>
92
+ <small class="help">${tx('glob.each_user_sees_times_in')}</small></div>`;
92
93
  }
93
94
 
94
95
  /** Save the currency and time zone settings; an unknown time zone or currency is an error. */
95
96
  export async function saveTimeZoneSettings(appId: string, b: Record<string, string | undefined>) {
96
97
  const zone = b.time_zone?.trim() || null;
97
- if (zone && !(await owner.one('select 1 from pg_timezone_names where name = $1', [zone]))) throw new Error(`Unknown time zone "${zone}".`);
98
+ if (zone && !(await owner.one('select 1 from pg_timezone_names where name = $1', [zone]))) throw new Error(tx('glob.unknown_zone', { zone }));
98
99
  const currency = b.currency?.trim().toUpperCase() || null;
99
- if (currency && !/^[A-Z]{3}$/.test(currency)) throw new Error('Currency: a three-letter ISO 4217 code such as EUR or USD.');
100
+ if (currency && !/^[A-Z]{3}$/.test(currency)) throw new Error(tx('glob.currency_a_three_letter_iso'));
100
101
  await owner.query('update meta.app set time_zone = $2, time_zone_auto = $3, currency = $4 where id = $1', [appId, zone, b.time_zone_auto === 'true', currency]);
101
102
  }
102
103
 
@@ -143,75 +144,75 @@ export async function globalizationRoutes(app: FastifyInstance) {
143
144
  const all = [...new Set([a.language, ...langs])];
144
145
 
145
146
  const main = html`${appHeader(a, 'shared', s)}
146
- <p class="muted u-mt0"><a href="${BASE}/apps/${a.id}/shared">Shared Components</a> / Globalization</p>
147
+ <p class="muted u-mt0"><a href="${BASE}/apps/${a.id}/shared">${tx('glob.shared_components')}</a> / Globalization</p>
147
148
  <div class="columns">
148
- ${region('Languages', html`<ul class="checklist">
149
- <li>Primary language: <b>${langName(a.language)}</b></li>
150
- <li>Translated into: ${langs.length ? langs.map((l, i) => html`${i ? ', ' : ''}<b>${langName(l)}</b>`) : html`<span class="muted">none yet</span>`}</li>
151
- <li>Language derived from: <b>${{ browser: 'the browser', user: 'the user’s preference, then the browser', primary: 'always the primary language' }[a.language_from as string]}</b></li>
152
- <li>pgkiln’s own texts (sign-in, reports, messages) exist in ${BUILTIN_LANGUAGES.slice(0, -1).map(([, n]) => n).join(', ')} and ${BUILTIN_LANGUAGES.at(-1)?.[1]}; override any of them with a text message of the same name.</li>
149
+ ${region(tx('glob.languages'), html`<ul class="checklist">
150
+ <li>${tx.rich('glob.primary', { lang: html`<b>${langName(a.language)}</b>` })}</li>
151
+ <li>${tx.rich('glob.translated_into', { langs: langs.length ? html`${langs.map((l, i) => html`${i ? ', ' : ''}<b>${langName(l)}</b>`)}` : html`<span class="muted">${tx('glob.none_yet')}</span>` })}</li>
152
+ <li>${tx.rich('glob.derived', { from: html`<b>${tx.dyn(`glob.from.${a.language_from}`, String(a.language_from))}</b>` })}</li>
153
+ <li>${tx('glob.builtin', { list: BUILTIN_LANGUAGES.slice(0, -1).map(([, n]) => n).join(', '), last: BUILTIN_LANGUAGES.at(-1)?.[1] ?? '' })}</li>
153
154
  </ul>
154
- <p><a class="btn" href="${BASE}/apps/${a.id}/settings">Change languages in Settings</a></p>`)}
155
- ${lang ? region('Export and import', html`
156
- <p class="muted u-mt0">Give translators an XLIFF file (as in APEX) or a CSV file, then import it here. Empty targets are ignored on import.</p>
155
+ <p><a class="btn" href="${BASE}/apps/${a.id}/settings">${tx('glob.change_languages_in_settings')}</a></p>`)}
156
+ ${lang ? region(tx('glob.export_and_import'), html`
157
+ <p class="muted u-mt0">${tx('glob.give_translators_an_xliff_file')}</p>
157
158
  <div class="buttons">
158
159
  <a class="btn" href="${BASE}/apps/${a.id}/globalization/export?lang=${lang}&format=xliff">${raw('&#8595;')} XLIFF (${lang})</a>
159
160
  <a class="btn" href="${BASE}/apps/${a.id}/globalization/export?lang=${lang}&format=csv">${raw('&#8595;')} CSV (${lang})</a>
160
161
  </div>
161
162
  <form method="post" action="${BASE}/apps/${a.id}/globalization/import" class="u-mt1">${csrf(s)}
162
163
  <input type="hidden" name="lang" value="${lang}">
163
- <div class="field" data-wide><label class="label" for="f_file">File</label><input id="f_file" type="file" accept=".xlf,.xliff,.csv,.xml,text/csv,application/xml" data-fill="f_doc"></div>
164
- <div class="field" data-wide><label class="label" for="f_doc">…or paste XLIFF / CSV</label><textarea id="f_doc" name="doc" class="code" rows="4" required></textarea></div>
164
+ <div class="field" data-wide><label class="label" for="f_file">${tx('glob.file')}</label><input id="f_file" type="file" accept=".xlf,.xliff,.csv,.xml,text/csv,application/xml" data-fill="f_doc"></div>
165
+ <div class="field" data-wide><label class="label" for="f_doc">${tx('glob.or_paste_xliff_csv')}</label><textarea id="f_doc" name="doc" class="code" rows="4" required></textarea></div>
165
166
  <div class="buttons"><button class="btn btn-hot">Import into ${lang}</button></div>
166
167
  </form>`) : ''}
167
168
  </div>
168
169
  ${lang
169
- ? region(`Translate into ${langName(lang)}`, html`
170
- ${langs.length > 1 ? html`<nav class="chips" aria-label="Language">${langs.map((l) => html`<a class="chip" href="?lang=${l}"${l === lang ? raw(' aria-current="page"') : ''}>${langName(l)}</a> `)}</nav>` : ''}
171
- <p class="muted u-mt0">${done} of ${texts.length} texts translated. Leave a translation empty to show the ${a.language} text.</p>
170
+ ? region(tx('glob.translate_into', { lang: langName(lang) }), html`
171
+ ${langs.length > 1 ? html`<nav class="chips" aria-label="${tx('glob.language')}">${langs.map((l) => html`<a class="chip" href="?lang=${l}"${l === lang ? raw(' aria-current="page"') : ''}>${langName(l)}</a> `)}</nav>` : ''}
172
+ <p class="muted u-mt0">${tx('glob.progress', { done, total: texts.length, language: a.language })}</p>
172
173
  <form method="post" action="${BASE}/apps/${a.id}/globalization/save">${csrf(s)}
173
174
  <input type="hidden" name="lang" value="${lang}">
174
175
  <div class="table-wrap"><table class="report report-reflow translate-table">
175
- <thead><tr><th>${langName(a.language)}</th><th>${langName(lang)}</th><th>Used in</th></tr></thead>
176
+ <thead><tr><th>${langName(a.language)}</th><th>${langName(lang)}</th><th>${tx('glob.used_in')}</th></tr></thead>
176
177
  <tbody>${[...texts, ...extra.map((r: { source: string }) => ({ text: r.source, places: 'added by hand' }))].map((t, i) => html`<tr>
177
178
  <td data-label="${a.language}"><input type="hidden" name="s${i}" value="${t.text}"><span class="source-text">${t.text}</span></td>
178
179
  <td data-label="${lang}">${t.text.length > 80 || t.text.includes('\n')
179
- ? html`<textarea name="t${i}" rows="3" aria-label="Translation of: ${t.text.slice(0, 60)}" lang="${lang}">${map.get(t.text) ?? ''}</textarea>`
180
- : html`<input name="t${i}" value="${map.get(t.text) ?? ''}" aria-label="Translation of: ${t.text}" lang="${lang}">`}</td>
181
- <td data-label="Used in" class="muted">${t.places}</td></tr>`)}</tbody>
180
+ ? html`<textarea name="t${i}" rows="3" aria-label="${tx('glob.aria_translation', { text: t.text.slice(0, 60) })}" lang="${lang}">${map.get(t.text) ?? ''}</textarea>`
181
+ : html`<input name="t${i}" value="${map.get(t.text) ?? ''}" aria-label="${tx('glob.aria_translation', { text: t.text })}" lang="${lang}">`}</td>
182
+ <td data-label="${tx('glob.used_in')}" class="muted">${t.places}</td></tr>`)}</tbody>
182
183
  </table></div>
183
- <div class="buttons"><button class="btn btn-hot">Save translations</button></div>
184
+ <div class="buttons"><button class="btn btn-hot">${tx('glob.save_translations')}</button></div>
184
185
  </form>
185
- <h3>Add a text</h3>
186
- <p class="muted">For texts pgkiln derives itself, such as column headings made from column names (<code>hiredate</code> → “Hiredate”).</p>
186
+ <h3>${tx('glob.add_a_text')}</h3>
187
+ <p class="muted">${tx.code('glob.for_texts_pgkiln_derives_itself')}</p>
187
188
  <form method="post" action="${BASE}/apps/${a.id}/globalization/save" class="form-grid">${csrf(s)}
188
189
  <input type="hidden" name="lang" value="${lang}">
189
- ${input('s0', `Text (${a.language})`, '', { required: true })}
190
- ${input('t0', `Translation (${lang})`, '', { required: true })}
191
- <div class="buttons"><button class="btn">Add</button></div>
190
+ ${input('s0', tx('glob.text_lang', { lang: a.language }), '', { required: true })}
191
+ ${input('t0', tx('glob.translation_lang', { lang }), '', { required: true })}
192
+ <div class="buttons"><button class="btn">${tx('glob.add')}</button></div>
192
193
  </form>`)
193
- : region('Translate', html`<p>Add a translated language under <a href="${BASE}/apps/${a.id}/settings">Settings → Globalization</a> first (for example <code>nl</code>).</p>`)}
194
- ${region('Text messages', html`
195
- <p class="muted u-mt0">Use them in SQL with <code>meta.message('NAME', param0, …)</code> (placeholders <code>%0</code>…<code>%9</code>) and in texts as <code>&amp;APP_TEXT$NAME.</code>, like APEX_LANG.MESSAGE. A message named like a pgkiln text (e.g. <code>login.title</code>, <code>report.no_data</code>) replaces it. Missing languages fall back to the primary language.</p>
196
- <div class="table-wrap"><table class="report report-reflow"><thead><tr><th>Name</th><th>Language</th><th>Text</th><th></th></tr></thead><tbody>
194
+ : region(tx('glob.translate'), html`<p>${tx.rich('glob.add_lang_first', { link: html`<a href="${BASE}/apps/${a.id}/settings">${tx('glob.settings_globalization')}</a>`, example: html`<code>nl</code>` })}</p>`)}
195
+ ${region(tx('glob.text_messages'), html`
196
+ <p class="muted u-mt0">${tx.code('glob.use_them_in_sql_with')}</p>
197
+ <div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('glob.name')}</th><th>${tx('glob.language')}</th><th>${tx('glob.text')}</th><th></th></tr></thead><tbody>
197
198
  ${messages.rows.length
198
199
  ? messages.rows.map((m) => html`<tr>
199
- <td data-label="Name"><code>${m.name}</code></td><td data-label="Language">${m.language}</td>
200
- <td data-label="Text"><form method="post" action="${BASE}/apps/${a.id}/globalization/message" class="search u-m0 u-mwnone">${csrf(s)}
200
+ <td data-label="${tx('glob.name')}"><code>${m.name}</code></td><td data-label="${tx('glob.language')}">${m.language}</td>
201
+ <td data-label="${tx('glob.text')}"><form method="post" action="${BASE}/apps/${a.id}/globalization/message" class="search u-m0 u-mwnone">${csrf(s)}
201
202
  <input type="hidden" name="name" value="${m.name}"><input type="hidden" name="language" value="${m.language}">
202
- <input name="text" value="${m.text}" aria-label="Text of ${m.name} (${m.language})"><button class="btn">Save</button></form></td>
203
+ <input name="text" value="${m.text}" aria-label="${tx('glob.aria_text_of', { name: m.name, lang: m.language })}"><button class="btn">${tx('glob.save')}</button></form></td>
203
204
  <td data-label=""><form method="post" action="${BASE}/apps/${a.id}/globalization/message/delete">${csrf(s)}
204
- <input type="hidden" name="name" value="${m.name}"><input type="hidden" name="language" value="${m.language}"><button class="link-button" data-confirm="Delete ${m.name} (${m.language})?">Delete</button></form></td></tr>`)
205
- : html`<tr><td colspan="4" class="empty">No text messages.</td></tr>`}
205
+ <input type="hidden" name="name" value="${m.name}"><input type="hidden" name="language" value="${m.language}"><button class="link-button" data-confirm="${tx('glob.delete_confirm', { name: m.name, lang: m.language })}">${tx('glob.delete')}</button></form></td></tr>`)
206
+ : html`<tr><td colspan="4" class="empty">${tx('glob.no_text_messages')}</td></tr>`}
206
207
  </tbody></table></div>
207
- <h3>Add a text message</h3>
208
+ <h3>${tx('glob.add_a_text_message')}</h3>
208
209
  <form method="post" action="${BASE}/apps/${a.id}/globalization/message" class="form-grid">${csrf(s)}
209
- ${input('name', 'Name', '', { required: true, placeholder: 'e.g. GREETING or login.title' })}
210
- ${select('language', 'Language', a.language, all.map((l): [string, string] => [l, langName(l)]))}
211
- ${input('text', 'Text', '', { required: true, placeholder: 'Hello %0!' })}
212
- <div class="buttons"><button class="btn btn-hot">Add message</button></div>
210
+ ${input('name', tx('glob.name'), '', { required: true, placeholder: tx('glob.e_g_greeting_or_login') })}
211
+ ${select('language', tx('glob.language'), a.language, all.map((l): [string, string] => [l, langName(l)]))}
212
+ ${input('text', tx('glob.text'), '', { required: true, placeholder: tx('glob.hello_0') })}
213
+ <div class="buttons"><button class="btn btn-hot">${tx('glob.add_message')}</button></div>
213
214
  </form>`)}`;
214
- return send(reply, s, shell(s, `${a.name} globalization`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Shared Components', `${BASE}/apps/${a.id}/shared`], ['Globalization']], main));
215
+ return send(reply, s, shell(s, `${a.name} globalization`, [[tx('glob.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('glob.shared_components'), `${BASE}/apps/${a.id}/shared`], [tx('glob.globalization')]], main));
215
216
  });
216
217
 
217
218
  app.post(`${BASE}/apps/:id/globalization/save`, async (req: Req, reply) => {
@@ -267,7 +268,7 @@ export async function globalizationRoutes(app: FastifyInstance) {
267
268
  const n = await saveTranslations(a.id, lang, pairs.filter(([, t]) => t.trim() !== ''));
268
269
  flash(s, `${n} translation(s) imported into ${lang}.`);
269
270
  } catch (e) {
270
- flash(s, `Import failed: ${(e as Error).message}`, 'error');
271
+ flash(s, tx('glob.import_failed', { error: (e as Error).message }), 'error');
271
272
  }
272
273
  return back(reply, s, `${BASE}/apps/${req.params.id}/globalization?lang=${encodeURIComponent(lang)}`);
273
274
  });
@@ -283,9 +284,9 @@ export async function globalizationRoutes(app: FastifyInstance) {
283
284
  [req.params.id, b.name?.trim(), b.language?.trim().toLowerCase(), b.text ?? ''],
284
285
  );
285
286
  clearLocaleCache(Number(req.params.id));
286
- flash(s, 'Text message saved.');
287
+ flash(s, tx('glob.text_message_saved'));
287
288
  } catch (e) {
288
- flash(s, /text_message_name_check/.test((e as Error).message) ? 'Names start with a letter and contain letters, digits, _ . $ or -.' : (e as Error).message, 'error');
289
+ flash(s, /text_message_name_check/.test((e as Error).message) ? tx('glob.names_start_with_a_letter') : (e as Error).message, 'error');
289
290
  }
290
291
  return back(reply, s, `${BASE}/apps/${req.params.id}/globalization`);
291
292
  });
@@ -295,7 +296,7 @@ export async function globalizationRoutes(app: FastifyInstance) {
295
296
  if (!s) return;
296
297
  await owner.query('delete from meta.text_message where app_id = $1 and name = $2 and language = $3', [req.params.id, req.body?.name, req.body?.language]);
297
298
  clearLocaleCache(Number(req.params.id));
298
- flash(s, 'Text message deleted.');
299
+ flash(s, tx('glob.text_message_deleted'));
299
300
  return back(reply, s, `${BASE}/apps/${req.params.id}/globalization`);
300
301
  });
301
302
  }