Преглед изворни кода

perf: optimize frontend build and initial loading

Remove redundant component scanning, lazy-load terminal CSS and locale
messages, and clean obsolete frontend package metadata. Generate one JSON
file per locale so initial loads only fetch English and the selected locale.

Co-authored-by: Cursor <cursoragent@cursor.com>
jamesread пре 12 часа
родитељ
комит
91f7764de2

+ 2 - 2
Makefile

@@ -48,8 +48,8 @@ lang-generate:
 	$(MAKE) -wC lang
 
 generated-check: proto lang-generate
-	git diff --exit-code -- service/gen frontend/resources/scripts/gen lang/combined_output.json
-	@untracked="$$(git ls-files --others --exclude-standard -- service/gen frontend/resources/scripts/gen lang/combined_output.json)"; \
+	git diff --exit-code -- service/gen frontend/resources/scripts/gen lang/generated
+	@untracked="$$(git ls-files --others --exclude-standard -- service/gen frontend/resources/scripts/gen lang/generated)"; \
 	test -z "$$untracked" || { printf 'Untracked generated files:\n%s\n' "$$untracked"; exit 1; }
 
 dist:

+ 1 - 1
frontend/Makefile

@@ -18,7 +18,7 @@ deps:
 	npm ci
 
 build:
-	npx vite build
+	npm run build
 
 dist: deps clean build
 

+ 0 - 2
frontend/index.html

@@ -8,8 +8,6 @@
 
 		<title>OliveTin</title>
 
-		<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
-
 		<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
 
 		<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />

+ 1 - 0
frontend/js/OutputTerminal.js

@@ -1,3 +1,4 @@
+import '@xterm/xterm/css/xterm.css'
 import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { WebLinksAddon } from '@xterm/addon-web-links'

+ 9 - 37
frontend/main.js

@@ -18,49 +18,21 @@ import router from './resources/vue/router.js'
 import App from './resources/vue/App.vue'
 
 import { initWebsocket } from './js/websocket.js'
+import { getSelectedLocale, loadInitialMessages } from './resources/vue/i18n.js'
 import { applyThemeStyles, getStoredThemePreference } from './resources/vue/utils/themeLoader.js'
-import combinedTranslations from '../lang/combined_output.json'
-
-function getSelectedLanguage () {
-  const storedLanguage = localStorage.getItem('olivetin-language')
-
-  if (storedLanguage && storedLanguage !== 'auto') {
-    return storedLanguage
-  }
-
-  if (storedLanguage === 'auto') {
-    localStorage.removeItem('olivetin-language')
-  }
-
-  if (navigator.languages && navigator.languages.length > 0) {
-    const available = Object.keys(combinedTranslations.messages || {})
-
-    for (const candidate of navigator.languages) {
-      const lowerCandidate = candidate.toLowerCase()
-      const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
-
-      if (exact) {
-        return exact
-      }
-
-      const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
-
-      if (prefix) {
-        return prefix
-      }
-    }
-  }
-
-  return 'en'
-}
 
 async function initClient () {
   const transport = createConnectTransport({
     baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
   })
+  const locale = getSelectedLocale()
 
   window.client = createClient(OliveTinApiService, transport)
-  window.initResponse = await window.client.init({})
+  const [initResponse, messages] = await Promise.all([
+    window.client.init({}),
+    loadInitialMessages(locale)
+  ])
+  window.initResponse = initResponse
 
   if (window.initResponse.enableCustomJs) {
     const script = document.createElement('script')
@@ -72,9 +44,9 @@ async function initClient () {
 
   const i18nSettings = createI18n({
     legacy: false,
-    locale: getSelectedLanguage(),
+    locale,
     fallbackLocale: 'en',
-    messages: combinedTranslations.messages,
+    messages,
     postTranslation: (translated) => {
       const params = new URLSearchParams(window.location.search)
 

+ 10 - 21
frontend/package-lock.json

@@ -13,25 +13,23 @@
 				"@connectrpc/connect-web": "^2.2.0",
 				"@hugeicons/core-free-icons": "^4.3.2",
 				"@hugeicons/vue": "^1.0.8",
-				"@vitejs/plugin-vue": "^6.0.8",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
 				"picocrank": "^1.29.0",
-				"unplugin-vue-components": "^32.1.0",
-				"vite": "^8.2.2",
 				"vue": "^3.5.42",
 				"vue-i18n": "^11.4.10",
 				"vue-router": "^5.3.1"
 			},
 			"devDependencies": {
+				"@vitejs/plugin-vue": "^6.0.8",
 				"eslint": "^9.39.5",
 				"eslint-plugin-vue": "^10.11.0",
 				"neostandard": "^0.13.0",
-				"process": "^0.11.10",
 				"stylelint": "^17.15.0",
-				"stylelint-config-standard": "^40.0.0"
+				"stylelint-config-standard": "^40.0.0",
+				"vite": "^8.2.2"
 			},
 			"engines": {
 				"node": "^20.19.0 || >=22.12.0"
@@ -5604,9 +5602,9 @@
 			}
 		},
 		"node_modules/postcss": {
-			"version": "8.5.26",
-			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
-			"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+			"version": "8.5.28",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+			"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
 			"funding": [
 				{
 					"type": "opencollective",
@@ -5623,7 +5621,7 @@
 			],
 			"license": "MIT",
 			"dependencies": {
-				"nanoid": "^3.3.17",
+				"nanoid": "^3.3.18",
 				"picocolors": "^1.1.1",
 				"source-map-js": "^1.2.1"
 			},
@@ -5688,15 +5686,6 @@
 				"node": ">= 0.8.0"
 			}
 		},
-		"node_modules/process": {
-			"version": "0.11.10",
-			"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
-			"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
-			"dev": true,
-			"engines": {
-				"node": ">= 0.6.0"
-			}
-		},
 		"node_modules/prop-types": {
 			"version": "15.8.1",
 			"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7207,9 +7196,9 @@
 			}
 		},
 		"node_modules/vite/node_modules/picomatch": {
-			"version": "4.0.5",
-			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
-			"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+			"version": "4.0.7",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+			"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
 			"license": "MIT",
 			"engines": {
 				"node": ">=12"

+ 7 - 14
frontend/package.json

@@ -3,42 +3,35 @@
 	"version": "1.0.0",
 	"description": "The WebUI for OliveTin",
 	"repository": "https://github.com/OliveTin/OliveTin",
-	"source": "index.html",
+	"type": "module",
 	"devDependencies": {
+		"@vitejs/plugin-vue": "^6.0.8",
 		"eslint": "^9.39.5",
 		"eslint-plugin-vue": "^10.11.0",
 		"neostandard": "^0.13.0",
-		"process": "^0.11.10",
 		"stylelint": "^17.15.0",
-		"stylelint-config-standard": "^40.0.0"
+		"stylelint-config-standard": "^40.0.0",
+		"vite": "^8.2.2"
 	},
 	"scripts": {
+		"build": "vite build",
+		"dev": "vite",
 		"lint": "eslint main.js js/* resources/vue vite.config.mjs && stylelint \"style.css\" \"themes/**/*.css\"",
 		"lint:fix": "eslint --fix main.js js/* resources/vue vite.config.mjs && stylelint --fix \"style.css\" \"themes/**/*.css\"",
+		"preview": "vite preview",
 		"test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs resources/vue/stores/*.test.mjs"
 	},
-	"author": "",
-	"parcelIgnore": [
-		"theme.css",
-		"OliveTinLogo.png",
-		"OliveTinLogo-57px.png",
-		"OliveTinLogo-120px.png",
-		"OliveTinLogo-180px.png"
-	],
 	"license": "AGPL-3.0-only",
 	"dependencies": {
 		"@connectrpc/connect": "^2.2.0",
 		"@connectrpc/connect-web": "^2.2.0",
 		"@hugeicons/core-free-icons": "^4.3.2",
 		"@hugeicons/vue": "^1.0.8",
-		"@vitejs/plugin-vue": "^6.0.8",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.29.0",
-		"unplugin-vue-components": "^32.1.0",
-		"vite": "^8.2.2",
 		"vue": "^3.5.42",
 		"vue-i18n": "^11.4.10",
 		"vue-router": "^5.3.1"

+ 4 - 28
frontend/resources/vue/App.vue

@@ -188,7 +188,7 @@ import { connectEventStreamIfNeeded } from '../../js/websocket.js'
 import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
 import logoUrl from '../../OliveTinLogo.png'
 import { useI18n } from 'vue-i18n'
-import combinedTranslations from '../../../lang/combined_output.json'
+import { activateLocale, resolveBrowserLocale } from './i18n.js'
 import { searchIndexItems, clearSearchIndex, indexSystemNavigation, indexSearchHints, indexRootDashboardEntries } from './stores/searchIndex.js'
 import { applyThemeStyles } from './utils/themeLoader.js'
 const { t } = useI18n()
@@ -272,30 +272,6 @@ const headerLoginRoute = computed(() => {
   return showLoginLink.value ? { name: 'Login' } : null
 })
 
-function normalizeBrowserLanguage () {
-  const available = Object.keys(combinedTranslations.messages || {})
-
-  if (navigator.languages && navigator.languages.length > 0) {
-    for (const candidate of navigator.languages) {
-      const lowerCandidate = candidate.toLowerCase()
-
-      // Try exact match (case-insensitive)
-      const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
-      if (exact) {
-        return exact
-      }
-
-      // Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
-      const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
-      if (prefix) {
-        return prefix
-      }
-    }
-  }
-
-  return 'en'
-}
-
 function toggleSidebar () {
   if (sidebar.value && showNavigation.value) {
     sidebar.value.toggle()
@@ -518,7 +494,7 @@ function closeLanguageDialog () {
   }
 }
 
-function changeLanguage () {
+async function changeLanguage () {
   if (!window.i18n || !selectedLanguage.value) {
     return
   }
@@ -526,9 +502,9 @@ function changeLanguage () {
   if (selectedLanguage.value === 'auto') {
     localStorage.removeItem('olivetin-language')
     languagePreference.value = 'auto'
-    window.i18n.locale.value = normalizeBrowserLanguage()
+    await activateLocale(window.i18n, resolveBrowserLocale())
   } else {
-    window.i18n.locale.value = selectedLanguage.value
+    await activateLocale(window.i18n, selectedLanguage.value)
     localStorage.setItem('olivetin-language', selectedLanguage.value)
     languagePreference.value = selectedLanguage.value
   }

+ 55 - 0
frontend/resources/vue/i18n.js

@@ -0,0 +1,55 @@
+import { selectBrowserLocale } from './utils/localeSelection.js'
+
+const localePathPrefix = '../../../lang/generated/'
+const localeModules = import.meta.glob('../../../lang/generated/*.json', {
+  import: 'default'
+})
+
+export const availableLocales = Object.keys(localeModules)
+  .map(path => path.slice(localePathPrefix.length, -'.json'.length))
+  .sort()
+
+export function resolveBrowserLocale (browserLanguages = navigator.languages) {
+  return selectBrowserLocale(availableLocales, browserLanguages)
+}
+
+export function getSelectedLocale () {
+  const storedLanguage = localStorage.getItem('olivetin-language')
+
+  if (storedLanguage && storedLanguage !== 'auto' && availableLocales.includes(storedLanguage)) {
+    return storedLanguage
+  }
+
+  if (storedLanguage === 'auto') {
+    localStorage.removeItem('olivetin-language')
+  }
+
+  return resolveBrowserLocale()
+}
+
+export async function loadLocaleMessages (locale) {
+  const loadLocale = localeModules[`${localePathPrefix}${locale}.json`]
+
+  if (!loadLocale) {
+    throw new Error(`Unsupported locale: ${locale}`)
+  }
+
+  return loadLocale()
+}
+
+export async function loadInitialMessages (locale) {
+  const locales = locale === 'en' ? ['en'] : ['en', locale]
+  const loadedMessages = await Promise.all(locales.map(async currentLocale => {
+    return [currentLocale, await loadLocaleMessages(currentLocale)]
+  }))
+
+  return Object.fromEntries(loadedMessages)
+}
+
+export async function activateLocale (i18n, locale) {
+  if (!i18n.availableLocales.includes(locale)) {
+    i18n.setLocaleMessage(locale, await loadLocaleMessages(locale))
+  }
+
+  i18n.locale.value = locale
+}

+ 19 - 0
frontend/resources/vue/utils/localeSelection.js

@@ -0,0 +1,19 @@
+export function selectBrowserLocale (availableLocales, browserLanguages) {
+  for (const candidate of browserLanguages || []) {
+    const lowerCandidate = candidate.toLowerCase()
+    const exact = availableLocales.find(locale => locale.toLowerCase() === lowerCandidate)
+
+    if (exact) {
+      return exact
+    }
+
+    const language = lowerCandidate.split('-')[0]
+    const prefix = availableLocales.find(locale => locale.toLowerCase().startsWith(`${language}-`))
+
+    if (prefix) {
+      return prefix
+    }
+  }
+
+  return 'en'
+}

+ 18 - 0
frontend/resources/vue/utils/localeSelection.test.mjs

@@ -0,0 +1,18 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import { selectBrowserLocale } from './localeSelection.js'
+
+const availableLocales = ['de-DE', 'en', 'es-ES', 'zh-Hans-CN', 'zh-Hant-TW']
+
+test('selectBrowserLocale prefers an exact locale match', () => {
+  assert.equal(selectBrowserLocale(availableLocales, ['es-ES']), 'es-ES')
+})
+
+test('selectBrowserLocale falls back to a matching language', () => {
+  assert.equal(selectBrowserLocale(availableLocales, ['de-AT']), 'de-DE')
+})
+
+test('selectBrowserLocale uses English when no locale matches', () => {
+  assert.equal(selectBrowserLocale(availableLocales, ['fr-FR']), 'en')
+})

+ 1 - 10
frontend/vite.config.mjs

@@ -1,20 +1,11 @@
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
-import Components from 'unplugin-vue-components/vite'
 
 export default defineConfig({
   resolve: {
     dedupe: ['vue', 'vue-router']
   },
-  plugins: [
-    Components({
-      dirs: ['resources/vue/'],
-      extensions: ['vue'],
-      deep: true,
-      dts: false
-    }),
-    vue()
-  ],
+  plugins: [vue()],
   build: {
     rolldownOptions: {
       onLog (level, log, defaultHandler) {

+ 7 - 6
lang/README.md

@@ -1,6 +1,6 @@
 Hey, thanks for reading this quick introduction to translations. The project founder
 only speaks two languages; English, and Bad English (!), so the initial translations
-have all been AI-generated. It is assumed that "something is better than nothing". 
+have all been AI-generated. It is assumed that "something is better than nothing".
 
 It would be most welcome to have human contributors who are native speakers improve these
 translations, or add new ones.
@@ -18,19 +18,20 @@ language pluralization and other advanced features. For docs, check the followin
 
 [Vue i18n Pluralization Guide](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html)
 
-The translation files are in YAML format. Each file contains key-value pairs. 
+The translation files are in YAML format. Each file contains key-value pairs.
 
-OliveTin developers then "process" these files into JSON format used for the app.
+OliveTin developers then process these files into per-locale JSON files under
+`lang/generated/`, which lets the app load only the languages it needs.
 
-If you are able, it would be appreciated if you run `make` in the language directory 
+If you are able, it would be appreciated if you run `make` in the language directory
 to process your language file before submitting a PR. This will ensure that the JSON
 file is up-to-date. If you don't understand how to do this, don't worry; just submit
 the YAML file and the developers will take care of it.
 
 ## Contributing improvements
 
-Please check out the file `CONTRIBUTING.md` for instructions on how to submit a pull 
+Please check out the file `CONTRIBUTING.md` for instructions on how to submit a pull
 request with your improvements.
 
-As always, if you need any help, please feel free to raise an issue on GitHub or 
+As always, if you need any help, please feel free to raise an issue on GitHub or
 jump into the Discord server for OliveTin.

+ 0 - 557
lang/combined_output.json

@@ -1,557 +0,0 @@
-{
-    "_comment": "This file is generated. Please re-generate this file using 'make' when you update a translation.",
-    "messages": {
-        "de-DE": {
-            "connected": "Verbunden",
-            "diagnostics.browser-info": "Browser-Informationen",
-            "diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
-            "diagnostics.config-issue-action": "Aktion",
-            "diagnostics.config-issue-argument": "Argument",
-            "diagnostics.config-issue-code": "Code",
-            "diagnostics.config-issue-config-file": "Konfigurationsdatei",
-            "diagnostics.config-issue-detail": "Detail",
-            "diagnostics.config-issue-message": "Meldung",
-            "diagnostics.config-issue-severity": "Schweregrad",
-            "diagnostics.config-issue-source": "Quelle",
-            "diagnostics.config-issues": "Konfigurationsprobleme",
-            "diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.",
-            "diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.",
-            "diagnostics.copied": "Kopiert!",
-            "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
-            "diagnostics.found-config": "Konfiguration gefunden",
-            "diagnostics.found-key": "Schlüssel gefunden",
-            "diagnostics.generate-browser-info": "Browser-Informationen erstellen",
-            "diagnostics.generate-server-diagnostics": "Server-Diagnostik erstellen",
-            "diagnostics.get-support": "Unterstützung erhalten",
-            "diagnostics.get-support-description": "Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.",
-            "diagnostics.server-diagnostics": "Server-Diagnostik",
-            "diagnostics.server-diagnostics-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.",
-            "diagnostics.server-diagnostics-docs": "Server-Diagnostik-Dokumentation",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "Unbekannt",
-            "diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData",
-            "diagnostics.where-to-find-help": "Wo Sie Hilfe finden",
-            "disconnected": "Getrennt",
-            "disconnected-banner-announcement": "Events-Websocket getrennt.",
-            "disconnected-banner-link-text": "Events-Websocket getrennt",
-            "disconnected-banner-suffix": " seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}.",
-            "disconnected-banner-suffix-reconnecting": " seit {disconnectedSince}. Verbindungsversuch…",
-            "docs": "Dokumentation",
-            "language-dialog.browser-languages": "Browser-Sprachen",
-            "language-dialog.close": "Schließen",
-            "language-dialog.not-available": "Nicht verfügbar",
-            "language-dialog.title": "Sprache auswählen",
-            "login-button": "Login",
-            "logs.action": "Aktion",
-            "logs.action-group-limits": "gleichzeitig: {concurrent}, Warteschlangengröße: {queueSize}",
-            "logs.back-to-list": "Zurück zur Liste",
-            "logs.blocked": "Blockiert",
-            "logs.calendar": "Kalender",
-            "logs.calendar-title": "Protokoll-Kalender",
-            "logs.clear-date-filter": "Datumsfilter löschen",
-            "logs.clear-filter": "Suchfilter löschen",
-            "logs.completed": "Abgeschlossen",
-            "logs.execution-id": "Ausführungs-ID",
-            "logs.exit-code": "Ausführungscode",
-            "logs.filter-error": "Filterausdruck konnte nicht angewendet werden.",
-            "logs.filter-help-examples": "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus.",
-            "logs.filter-help-intro": "Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or.",
-            "logs.filter-help-title": "Filtersyntax",
-            "logs.filter-placeholder": "Protokolle filtern (z. B. !Update oder Status != Completed)",
-            "logs.metadata": "Metadaten",
-            "logs.no-logs-for-filter": "Keine Protokolle entsprechen dem aktuellen Filter.",
-            "logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.",
-            "logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.",
-            "logs.queue": "Warteschlange",
-            "logs.queue-action-details": "Aktionsdetails",
-            "logs.queue-default-group": "Standard",
-            "logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.",
-            "logs.queue-entity": "Entität",
-            "logs.queue-group-active": "{active} aktiv (max. {max})",
-            "logs.queue-group-active-unlimited": "{active} aktiv",
-            "logs.queue-page-description": "Aktive und wartende Ausführungen, nach Aktionsgruppe gruppiert. Einträge ohne Berechtigung werden ausgeblendet.",
-            "logs.queue-position": "#{position}",
-            "logs.queue-running": "Läuft",
-            "logs.queue-title": "Ausführungswarteschlange",
-            "logs.queue-waiting": "Wartend",
-            "logs.status": "Status",
-            "logs.timed-out": "Zeitüberschreitung",
-            "logs.timestamp": "Zeitstempel",
-            "logs.title": "Protokolle",
-            "nav.actions": "Aktionen",
-            "nav.diagnostics": "Diagnostik",
-            "nav.entities": "Entitäten",
-            "nav.logs": "Protokolle",
-            "nav.system": "System",
-            "raise-issue": "Ein Problem melden auf GitHub",
-            "reconnecting": "Verbinde erneut…",
-            "return-to-index": "Zurück zur Startseite",
-            "search-filter": "Filter aktuelle Seite",
-            "theme-dialog.close": "Schließen",
-            "theme-dialog.default": "Standard-Design",
-            "theme-dialog.title": "Design auswählen",
-            "welcome": "Willkommen bei OliveTin"
-        },
-        "en": {
-            "connected": "Connected",
-            "diagnostics.browser-info": "Browser Info",
-            "diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
-            "diagnostics.config-issue-action": "Action",
-            "diagnostics.config-issue-argument": "Argument",
-            "diagnostics.config-issue-code": "Code",
-            "diagnostics.config-issue-config-file": "Config file",
-            "diagnostics.config-issue-detail": "Detail",
-            "diagnostics.config-issue-message": "Message",
-            "diagnostics.config-issue-severity": "Severity",
-            "diagnostics.config-issue-source": "Source",
-            "diagnostics.config-issues": "Configuration issues",
-            "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.",
-            "diagnostics.config-issues-none": "No configuration issues detected.",
-            "diagnostics.copied": "Copied!",
-            "diagnostics.copy-to-clipboard": "Copy to Clipboard",
-            "diagnostics.found-config": "Found Config",
-            "diagnostics.found-key": "Found Key",
-            "diagnostics.generate-browser-info": "Generate Browser Info",
-            "diagnostics.generate-server-diagnostics": "Generate Server Diagnostics",
-            "diagnostics.get-support": "Get support",
-            "diagnostics.get-support-description": "If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page.",
-            "diagnostics.server-diagnostics": "Server Diagnostics",
-            "diagnostics.server-diagnostics-description": "This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.",
-            "diagnostics.server-diagnostics-docs": "Server Diagnostics Documentation",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "Unknown",
-            "diagnostics.useragent-data-error": "Error retrieving userAgentData",
-            "diagnostics.where-to-find-help": "Where to find help",
-            "disconnected": "Disconnected",
-            "disconnected-banner-announcement": "Events websocket disconnected.",
-            "disconnected-banner-link-text": "Events websocket disconnected",
-            "disconnected-banner-suffix": " since {disconnectedSince}. Trying reconnect in {reconnectIn}.",
-            "disconnected-banner-suffix-reconnecting": " since {disconnectedSince}. Trying reconnect…",
-            "docs": "Documentation",
-            "language-dialog.browser-languages": "Browser languages",
-            "language-dialog.close": "Close",
-            "language-dialog.not-available": "Not available",
-            "language-dialog.title": "Select Language",
-            "login-button": "Login",
-            "logs.action": "Action",
-            "logs.action-group-limits": "concurrent: {concurrent}, queue size: {queueSize}",
-            "logs.back-to-list": "Back to List",
-            "logs.blocked": "Blocked",
-            "logs.calendar": "Calendar",
-            "logs.calendar-title": "Logs Calendar",
-            "logs.clear-date-filter": "Clear date filter",
-            "logs.clear-filter": "Clear search filter",
-            "logs.completed": "Completed",
-            "logs.execution-id": "Execution ID",
-            "logs.exit-code": "Exit code",
-            "logs.filter-error": "Could not apply filter expression.",
-            "logs.filter-help-examples": "Examples: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "Fields: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operators: ==, !=, contains. Prefix ! on a single word excludes matching entries.",
-            "logs.filter-help-intro": "Filters run on the server over entries you are allowed to view. Combine terms with and / or.",
-            "logs.filter-help-title": "Filter syntax",
-            "logs.filter-placeholder": "Filter logs (e.g. !Update or Status != Completed)",
-            "logs.metadata": "Metadata",
-            "logs.no-logs-for-filter": "No logs match the current filter.",
-            "logs.no-logs-to-display": "There are no logs to display.",
-            "logs.page-description": "This is a list of logs from actions that have been executed. Use the filter box for search terms and expressions.",
-            "logs.queue": "Queue",
-            "logs.queue-action-details": "Action Details",
-            "logs.queue-default-group": "Default",
-            "logs.queue-empty": "There are no active or waiting executions right now.",
-            "logs.queue-entity": "Entity",
-            "logs.queue-group-active": "{active} active (max {max})",
-            "logs.queue-group-active-unlimited": "{active} active",
-            "logs.queue-page-description": "Active and waiting executions grouped by action group. Entries you are not permitted to view are hidden.",
-            "logs.queue-position": "#{position}",
-            "logs.queue-running": "Running",
-            "logs.queue-title": "Execution Queue",
-            "logs.queue-waiting": "Waiting",
-            "logs.status": "Status",
-            "logs.timed-out": "Timed out",
-            "logs.timestamp": "Timestamp",
-            "logs.title": "Logs",
-            "nav.actions": "Actions",
-            "nav.diagnostics": "Diagnostics",
-            "nav.entities": "Entities",
-            "nav.logs": "Logs",
-            "nav.system": "System",
-            "raise-issue": "Raise an issue on GitHub",
-            "reconnecting": "Reconnecting…",
-            "return-to-index": "Return to index",
-            "search-filter": "Filter current page",
-            "theme-dialog.close": "Close",
-            "theme-dialog.default": "Default Theme",
-            "theme-dialog.title": "Select Theme",
-            "welcome": "Welcome to OliveTin"
-        },
-        "es-ES": {
-            "connected": "Conectado",
-            "diagnostics.browser-info": "Información del navegador",
-            "diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
-            "diagnostics.config-issue-action": "Acción",
-            "diagnostics.config-issue-argument": "Argumento",
-            "diagnostics.config-issue-code": "Código",
-            "diagnostics.config-issue-config-file": "Archivo de configuración",
-            "diagnostics.config-issue-detail": "Detalle",
-            "diagnostics.config-issue-message": "Mensaje",
-            "diagnostics.config-issue-severity": "Severidad",
-            "diagnostics.config-issue-source": "Origen",
-            "diagnostics.config-issues": "Problemas de configuración",
-            "diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.",
-            "diagnostics.config-issues-none": "No se detectaron problemas de configuración.",
-            "diagnostics.copied": "¡Copiado!",
-            "diagnostics.copy-to-clipboard": "Copiar al portapapeles",
-            "diagnostics.found-config": "Configuración encontrada",
-            "diagnostics.found-key": "Clave encontrada",
-            "diagnostics.generate-browser-info": "Generar información del navegador",
-            "diagnostics.generate-server-diagnostics": "Generar diagnóstico del servidor",
-            "diagnostics.get-support": "Obtener soporte",
-            "diagnostics.get-support-description": "Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.",
-            "diagnostics.server-diagnostics": "Diagnóstico del servidor",
-            "diagnostics.server-diagnostics-description": "Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.",
-            "diagnostics.server-diagnostics-docs": "Documentación de diagnóstico del servidor",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "Desconocido",
-            "diagnostics.useragent-data-error": "Error al recuperar userAgentData",
-            "diagnostics.where-to-find-help": "Dónde encontrar ayuda",
-            "disconnected": "Desconectado",
-            "disconnected-banner-announcement": "Websocket de eventos desconectado.",
-            "disconnected-banner-link-text": "Websocket de eventos desconectado",
-            "disconnected-banner-suffix": " desde {disconnectedSince}. Reintentando conexión en {reconnectIn}.",
-            "disconnected-banner-suffix-reconnecting": " desde {disconnectedSince}. Reintentando conexión…",
-            "docs": "Documentación",
-            "language-dialog.browser-languages": "Idiomas del navegador",
-            "language-dialog.close": "Cerrar",
-            "language-dialog.not-available": "No disponible",
-            "language-dialog.title": "Seleccionar idioma",
-            "login-button": "Iniciar sesión",
-            "logs.action": "Acción",
-            "logs.action-group-limits": "simultáneas: {concurrent}, tamaño de cola: {queueSize}",
-            "logs.back-to-list": "Volver a la Lista",
-            "logs.blocked": "Bloqueado",
-            "logs.calendar": "Calendario",
-            "logs.calendar-title": "Calendario de Registros",
-            "logs.clear-date-filter": "Limpiar filtro de fecha",
-            "logs.clear-filter": "Limpiar filtro de búsqueda",
-            "logs.completed": "Completado",
-            "logs.execution-id": "ID de ejecución",
-            "logs.exit-code": "Código de salida",
-            "logs.filter-error": "No se pudo aplicar la expresión del filtro.",
-            "logs.filter-help-examples": "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes.",
-            "logs.filter-help-intro": "Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or.",
-            "logs.filter-help-title": "Sintaxis del filtro",
-            "logs.filter-placeholder": "Filtrar registros (p. ej. !Update o Status != Completed)",
-            "logs.metadata": "Metadatos",
-            "logs.no-logs-for-filter": "Ningún registro coincide con el filtro actual.",
-            "logs.no-logs-to-display": "No hay registros para mostrar.",
-            "logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.",
-            "logs.queue": "Cola",
-            "logs.queue-action-details": "Detalles de la acción",
-            "logs.queue-default-group": "Predeterminado",
-            "logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.",
-            "logs.queue-entity": "Entidad",
-            "logs.queue-group-active": "{active} activas (máx. {max})",
-            "logs.queue-group-active-unlimited": "{active} activas",
-            "logs.queue-page-description": "Ejecuciones activas y en espera agrupadas por grupo de acciones. Las entradas que no puede ver se ocultan.",
-            "logs.queue-position": "#{position}",
-            "logs.queue-running": "En ejecución",
-            "logs.queue-title": "Cola de ejecución",
-            "logs.queue-waiting": "En espera",
-            "logs.status": "Estado",
-            "logs.timed-out": "Tiempo agotado",
-            "logs.timestamp": "Marca de tiempo",
-            "logs.title": "Registros",
-            "nav.actions": "Acciones",
-            "nav.diagnostics": "Diagnósticos",
-            "nav.entities": "Entidades",
-            "nav.logs": "Registros",
-            "nav.system": "Sistema",
-            "raise-issue": "Reportar un problema en GitHub",
-            "reconnecting": "Reconectando…",
-            "return-to-index": "Volver a la página principal",
-            "search-filter": "Filtrar página actual",
-            "theme-dialog.close": "Cerrar",
-            "theme-dialog.default": "Tema Predeterminado",
-            "theme-dialog.title": "Seleccionar tema",
-            "welcome": "Bienvenido a OliveTin"
-        },
-        "it-IT": {
-            "connected": "Connesso",
-            "diagnostics.browser-info": "Informazioni del browser",
-            "diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
-            "diagnostics.config-issue-action": "Azione",
-            "diagnostics.config-issue-argument": "Argomento",
-            "diagnostics.config-issue-code": "Codice",
-            "diagnostics.config-issue-config-file": "File di configurazione",
-            "diagnostics.config-issue-detail": "Dettaglio",
-            "diagnostics.config-issue-message": "Messaggio",
-            "diagnostics.config-issue-severity": "Gravità",
-            "diagnostics.config-issue-source": "Origine",
-            "diagnostics.config-issues": "Problemi di configurazione",
-            "diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.",
-            "diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.",
-            "diagnostics.copied": "Copiato!",
-            "diagnostics.copy-to-clipboard": "Copia negli appunti",
-            "diagnostics.found-config": "Configurazione trovata",
-            "diagnostics.found-key": "Chiave trovata",
-            "diagnostics.generate-browser-info": "Genera informazioni del browser",
-            "diagnostics.generate-server-diagnostics": "Genera diagnostica del server",
-            "diagnostics.get-support": "Ottenere supporto",
-            "diagnostics.get-support-description": "Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.",
-            "diagnostics.server-diagnostics": "Diagnostica del server",
-            "diagnostics.server-diagnostics-description": "Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.",
-            "diagnostics.server-diagnostics-docs": "Documentazione diagnostica del server",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "Sconosciuto",
-            "diagnostics.useragent-data-error": "Errore nel recupero di userAgentData",
-            "diagnostics.where-to-find-help": "Dove trovare aiuto",
-            "disconnected": "Disconnesso",
-            "disconnected-banner-announcement": "Websocket eventi disconnesso.",
-            "disconnected-banner-link-text": "Websocket eventi disconnesso",
-            "disconnected-banner-suffix": " dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}.",
-            "disconnected-banner-suffix-reconnecting": " dalle {disconnectedSince}. Tentativo di connessione…",
-            "docs": "Documentazione",
-            "language-dialog.browser-languages": "Lingue del browser",
-            "language-dialog.close": "Chiudi",
-            "language-dialog.not-available": "Non disponibile",
-            "language-dialog.title": "Seleziona lingua",
-            "login-button": "Login",
-            "logs.action": "Azione",
-            "logs.action-group-limits": "simultanei: {concurrent}, dimensione coda: {queueSize}",
-            "logs.back-to-list": "Torna all'Elenco",
-            "logs.blocked": "Bloccato",
-            "logs.calendar": "Calendario",
-            "logs.calendar-title": "Calendario dei Registri",
-            "logs.clear-date-filter": "Cancella filtro data",
-            "logs.clear-filter": "Cancella filtro di ricerca",
-            "logs.completed": "Completato",
-            "logs.execution-id": "ID esecuzione",
-            "logs.exit-code": "Codice di uscita",
-            "logs.filter-error": "Impossibile applicare l'espressione di filtro.",
-            "logs.filter-help-examples": "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti.",
-            "logs.filter-help-intro": "I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or.",
-            "logs.filter-help-title": "Sintassi del filtro",
-            "logs.filter-placeholder": "Filtra i registri (es. !Update o Status != Completed)",
-            "logs.metadata": "Metadati",
-            "logs.no-logs-for-filter": "Nessun registro corrisponde al filtro corrente.",
-            "logs.no-logs-to-display": "Non ci sono registri da mostrare.",
-            "logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.",
-            "logs.queue": "Coda",
-            "logs.queue-action-details": "Dettagli azione",
-            "logs.queue-default-group": "Predefinito",
-            "logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.",
-            "logs.queue-entity": "Entità",
-            "logs.queue-group-active": "{active} attive (max {max})",
-            "logs.queue-group-active-unlimited": "{active} attive",
-            "logs.queue-page-description": "Esecuzioni attive e in attesa raggruppate per gruppo di azioni. Le voci non autorizzate sono nascoste.",
-            "logs.queue-position": "#{position}",
-            "logs.queue-running": "In esecuzione",
-            "logs.queue-title": "Coda di esecuzione",
-            "logs.queue-waiting": "In attesa",
-            "logs.status": "Stato",
-            "logs.timed-out": "Tempo scaduto",
-            "logs.timestamp": "Date e ora",
-            "logs.title": "Registri",
-            "nav.actions": "Azioni",
-            "nav.diagnostics": "Diagnostica",
-            "nav.entities": "Entità",
-            "nav.logs": "Registri",
-            "nav.system": "Sistema",
-            "raise-issue": "Segnala un problema su GitHub",
-            "reconnecting": "Riconnessione…",
-            "return-to-index": "Torna alla pagina principale",
-            "search-filter": "Filtra la pagina corrente",
-            "theme-dialog.close": "Chiudi",
-            "theme-dialog.default": "Tema Predefinito",
-            "theme-dialog.title": "Seleziona tema",
-            "welcome": "Benvenuto in OliveTin"
-        },
-        "zh-Hans-CN": {
-            "connected": "已连接",
-            "diagnostics.browser-info": "浏览器信息",
-            "diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
-            "diagnostics.config-issue-action": "操作",
-            "diagnostics.config-issue-argument": "参数",
-            "diagnostics.config-issue-code": "代码",
-            "diagnostics.config-issue-config-file": "配置文件",
-            "diagnostics.config-issue-detail": "详情",
-            "diagnostics.config-issue-message": "消息",
-            "diagnostics.config-issue-severity": "严重程度",
-            "diagnostics.config-issue-source": "来源",
-            "diagnostics.config-issues": "配置问题",
-            "diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。",
-            "diagnostics.config-issues-none": "未检测到配置问题。",
-            "diagnostics.copied": "已复制!",
-            "diagnostics.copy-to-clipboard": "复制到剪贴板",
-            "diagnostics.found-config": "找到配置",
-            "diagnostics.found-key": "找到密钥",
-            "diagnostics.generate-browser-info": "生成浏览器信息",
-            "diagnostics.generate-server-diagnostics": "生成服务器诊断",
-            "diagnostics.get-support": "获取支持",
-            "diagnostics.get-support-description": "如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。",
-            "diagnostics.server-diagnostics": "服务器诊断",
-            "diagnostics.server-diagnostics-description": "此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。",
-            "diagnostics.server-diagnostics-docs": "服务器诊断文档",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "未知",
-            "diagnostics.useragent-data-error": "检索 userAgentData 时出错",
-            "diagnostics.where-to-find-help": "在哪里找到帮助",
-            "disconnected": "已断开连接",
-            "disconnected-banner-announcement": "事件 WebSocket 已断开。",
-            "disconnected-banner-link-text": "事件 WebSocket 已断开",
-            "disconnected-banner-suffix": "自 {disconnectedSince}。{reconnectIn} 后尝试重连。",
-            "disconnected-banner-suffix-reconnecting": "自 {disconnectedSince}。正在尝试重连…",
-            "docs": "文档",
-            "language-dialog.browser-languages": "浏览器语言",
-            "language-dialog.close": "关闭",
-            "language-dialog.not-available": "不可用",
-            "language-dialog.title": "选择语言",
-            "login-button": "登录",
-            "logs.action": "动作",
-            "logs.action-group-limits": "并发:{concurrent},队列大小:{queueSize}",
-            "logs.back-to-list": "返回列表",
-            "logs.blocked": "阻塞",
-            "logs.calendar": "日历",
-            "logs.calendar-title": "日志日历",
-            "logs.clear-date-filter": "清除日期筛选器",
-            "logs.clear-filter": "清除搜索筛选器",
-            "logs.completed": "完成",
-            "logs.execution-id": "执行 ID",
-            "logs.exit-code": "退出代码",
-            "logs.filter-error": "无法应用过滤表达式。",
-            "logs.filter-help-examples": "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。",
-            "logs.filter-help-intro": "过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。",
-            "logs.filter-help-title": "过滤语法",
-            "logs.filter-placeholder": "过滤日志(例如 !Update 或 Status != Completed)",
-            "logs.metadata": "元数据",
-            "logs.no-logs-for-filter": "没有符合当前过滤条件的日志。",
-            "logs.no-logs-to-display": "没有日志可显示。",
-            "logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
-            "logs.queue": "队列",
-            "logs.queue-action-details": "动作详细信息",
-            "logs.queue-default-group": "默认",
-            "logs.queue-empty": "当前没有正在运行或等待中的执行。",
-            "logs.queue-entity": "实体",
-            "logs.queue-group-active": "{active} 个活动(上限 {max})",
-            "logs.queue-group-active-unlimited": "{active} 个活动",
-            "logs.queue-page-description": "按动作组分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。",
-            "logs.queue-position": "第 {position} 位",
-            "logs.queue-running": "运行中",
-            "logs.queue-title": "执行队列",
-            "logs.queue-waiting": "等待中",
-            "logs.status": "状态",
-            "logs.timed-out": "超时",
-            "logs.timestamp": "时间戳",
-            "logs.title": "日志",
-            "nav.actions": "动作",
-            "nav.diagnostics": "诊断",
-            "nav.entities": "实体",
-            "nav.logs": "日志",
-            "nav.system": "系统",
-            "raise-issue": "在 GitHub 上报告问题",
-            "reconnecting": "正在重新连接…",
-            "return-to-index": "返回首页",
-            "search-filter": "过滤当前页面",
-            "theme-dialog.close": "关闭",
-            "theme-dialog.default": "默认主题",
-            "theme-dialog.title": "选择主题",
-            "welcome": "欢迎使用 OliveTin"
-        },
-        "zh-Hant-TW": {
-            "connected": "已連線",
-            "diagnostics.browser-info": "瀏覽器資訊",
-            "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。",
-            "diagnostics.config-issue-action": "動作",
-            "diagnostics.config-issue-argument": "參數",
-            "diagnostics.config-issue-code": "代碼",
-            "diagnostics.config-issue-config-file": "設定檔",
-            "diagnostics.config-issue-detail": "詳細資料",
-            "diagnostics.config-issue-message": "訊息",
-            "diagnostics.config-issue-severity": "嚴重程度",
-            "diagnostics.config-issue-source": "來源",
-            "diagnostics.config-issues": "設定問題",
-            "diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。",
-            "diagnostics.config-issues-none": "未偵測到設定問題。",
-            "diagnostics.copied": "已複製!",
-            "diagnostics.copy-to-clipboard": "複製到剪貼簿",
-            "diagnostics.found-config": "找到設定檔 (Config)",
-            "diagnostics.found-key": "找到金鑰 (Key)",
-            "diagnostics.generate-browser-info": "產生瀏覽器資訊",
-            "diagnostics.generate-server-diagnostics": "產生伺服器診斷資訊",
-            "diagnostics.get-support": "尋求技術支援",
-            "diagnostics.get-support-description": "如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。",
-            "diagnostics.server-diagnostics": "伺服器診斷",
-            "diagnostics.server-diagnostics-description": "此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。",
-            "diagnostics.server-diagnostics-docs": "伺服器診斷官方文件",
-            "diagnostics.ssh": "SSH",
-            "diagnostics.unknown": "未知",
-            "diagnostics.useragent-data-error": "取得 userAgentData 時發生錯誤",
-            "diagnostics.where-to-find-help": "哪裡可以尋求協助",
-            "disconnected": "已斷線",
-            "disconnected-banner-announcement": "系統事件 WebSocket 已斷線。",
-            "disconnected-banner-link-text": "系統事件 WebSocket 已斷線",
-            "disconnected-banner-suffix": " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。",
-            "disconnected-banner-suffix-reconnecting": " 自 {disconnectedSince} 起。嘗試重新連線中…",
-            "docs": "官方文件",
-            "language-dialog.browser-languages": "瀏覽器語言",
-            "language-dialog.close": "關閉",
-            "language-dialog.not-available": "無法使用",
-            "language-dialog.title": "選擇語言",
-            "login-button": "登入",
-            "logs.action": "動作",
-            "logs.action-group-limits": "並行數:{concurrent},佇列大小:{queueSize}",
-            "logs.back-to-list": "返回清單",
-            "logs.blocked": "已阻擋",
-            "logs.calendar": "日曆",
-            "logs.calendar-title": "日誌日曆",
-            "logs.clear-date-filter": "清除日期過濾條件",
-            "logs.clear-filter": "清除搜尋過濾條件",
-            "logs.completed": "已完成",
-            "logs.execution-id": "執行 ID",
-            "logs.exit-code": "結束代碼",
-            "logs.filter-error": "無法套用過濾運算式。",
-            "logs.filter-help-examples": "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
-            "logs.filter-help-fields": "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。",
-            "logs.filter-help-intro": "過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。",
-            "logs.filter-help-title": "過濾語法",
-            "logs.filter-placeholder": "過濾日誌 (例如 !Update 或 Status != Completed)",
-            "logs.metadata": "中繼資料",
-            "logs.no-logs-for-filter": "沒有符合目前過濾條件的日誌。",
-            "logs.no-logs-to-display": "目前沒有可顯示的日誌。",
-            "logs.page-description": "這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。",
-            "logs.queue": "佇列",
-            "logs.queue-action-details": "動作詳細資訊",
-            "logs.queue-default-group": "預設",
-            "logs.queue-empty": "目前沒有使用中或等待中的執行項目。",
-            "logs.queue-entity": "實體",
-            "logs.queue-group-active": "{active} 個使用中 (最大值 {max})",
-            "logs.queue-group-active-unlimited": "{active} 個使用中",
-            "logs.queue-page-description": "依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。",
-            "logs.queue-position": "#{position}",
-            "logs.queue-running": "執行中",
-            "logs.queue-title": "執行佇列",
-            "logs.queue-waiting": "等待中",
-            "logs.status": "狀態",
-            "logs.timed-out": "執行逾時",
-            "logs.timestamp": "時間戳記",
-            "logs.title": "日誌",
-            "nav.actions": "動作",
-            "nav.diagnostics": "診斷",
-            "nav.entities": "實體",
-            "nav.logs": "日誌",
-            "nav.system": "系統",
-            "raise-issue": "在 GitHub 上建立 Issue",
-            "reconnecting": "重新連線中…",
-            "return-to-index": "返回首頁",
-            "search-filter": "過濾目前頁面",
-            "theme-dialog.close": "關閉",
-            "theme-dialog.default": "預設佈景主題",
-            "theme-dialog.title": "選擇佈景主題",
-            "welcome": "歡迎使用 OliveTin"
-        }
-    }
-}

+ 92 - 0
lang/generated/de-DE.json

@@ -0,0 +1,92 @@
+{
+    "connected": "Verbunden",
+    "diagnostics.browser-info": "Browser-Informationen",
+    "diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
+    "diagnostics.config-issue-action": "Aktion",
+    "diagnostics.config-issue-argument": "Argument",
+    "diagnostics.config-issue-code": "Code",
+    "diagnostics.config-issue-config-file": "Konfigurationsdatei",
+    "diagnostics.config-issue-detail": "Detail",
+    "diagnostics.config-issue-message": "Meldung",
+    "diagnostics.config-issue-severity": "Schweregrad",
+    "diagnostics.config-issue-source": "Quelle",
+    "diagnostics.config-issues": "Konfigurationsprobleme",
+    "diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.",
+    "diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.",
+    "diagnostics.copied": "Kopiert!",
+    "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
+    "diagnostics.found-config": "Konfiguration gefunden",
+    "diagnostics.found-key": "Schlüssel gefunden",
+    "diagnostics.generate-browser-info": "Browser-Informationen erstellen",
+    "diagnostics.generate-server-diagnostics": "Server-Diagnostik erstellen",
+    "diagnostics.get-support": "Unterstützung erhalten",
+    "diagnostics.get-support-description": "Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.",
+    "diagnostics.server-diagnostics": "Server-Diagnostik",
+    "diagnostics.server-diagnostics-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.",
+    "diagnostics.server-diagnostics-docs": "Server-Diagnostik-Dokumentation",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "Unbekannt",
+    "diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData",
+    "diagnostics.where-to-find-help": "Wo Sie Hilfe finden",
+    "disconnected": "Getrennt",
+    "disconnected-banner-announcement": "Events-Websocket getrennt.",
+    "disconnected-banner-link-text": "Events-Websocket getrennt",
+    "disconnected-banner-suffix": " seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}.",
+    "disconnected-banner-suffix-reconnecting": " seit {disconnectedSince}. Verbindungsversuch…",
+    "docs": "Dokumentation",
+    "language-dialog.browser-languages": "Browser-Sprachen",
+    "language-dialog.close": "Schließen",
+    "language-dialog.not-available": "Nicht verfügbar",
+    "language-dialog.title": "Sprache auswählen",
+    "login-button": "Login",
+    "logs.action": "Aktion",
+    "logs.action-group-limits": "gleichzeitig: {concurrent}, Warteschlangengröße: {queueSize}",
+    "logs.back-to-list": "Zurück zur Liste",
+    "logs.blocked": "Blockiert",
+    "logs.calendar": "Kalender",
+    "logs.calendar-title": "Protokoll-Kalender",
+    "logs.clear-date-filter": "Datumsfilter löschen",
+    "logs.clear-filter": "Suchfilter löschen",
+    "logs.completed": "Abgeschlossen",
+    "logs.execution-id": "Ausführungs-ID",
+    "logs.exit-code": "Ausführungscode",
+    "logs.filter-error": "Filterausdruck konnte nicht angewendet werden.",
+    "logs.filter-help-examples": "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus.",
+    "logs.filter-help-intro": "Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or.",
+    "logs.filter-help-title": "Filtersyntax",
+    "logs.filter-placeholder": "Protokolle filtern (z. B. !Update oder Status != Completed)",
+    "logs.metadata": "Metadaten",
+    "logs.no-logs-for-filter": "Keine Protokolle entsprechen dem aktuellen Filter.",
+    "logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.",
+    "logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.",
+    "logs.queue": "Warteschlange",
+    "logs.queue-action-details": "Aktionsdetails",
+    "logs.queue-default-group": "Standard",
+    "logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.",
+    "logs.queue-entity": "Entität",
+    "logs.queue-group-active": "{active} aktiv (max. {max})",
+    "logs.queue-group-active-unlimited": "{active} aktiv",
+    "logs.queue-page-description": "Aktive und wartende Ausführungen, nach Aktionsgruppe gruppiert. Einträge ohne Berechtigung werden ausgeblendet.",
+    "logs.queue-position": "#{position}",
+    "logs.queue-running": "Läuft",
+    "logs.queue-title": "Ausführungswarteschlange",
+    "logs.queue-waiting": "Wartend",
+    "logs.status": "Status",
+    "logs.timed-out": "Zeitüberschreitung",
+    "logs.timestamp": "Zeitstempel",
+    "logs.title": "Protokolle",
+    "nav.actions": "Aktionen",
+    "nav.diagnostics": "Diagnostik",
+    "nav.entities": "Entitäten",
+    "nav.logs": "Protokolle",
+    "nav.system": "System",
+    "raise-issue": "Ein Problem melden auf GitHub",
+    "reconnecting": "Verbinde erneut…",
+    "return-to-index": "Zurück zur Startseite",
+    "search-filter": "Filter aktuelle Seite",
+    "theme-dialog.close": "Schließen",
+    "theme-dialog.default": "Standard-Design",
+    "theme-dialog.title": "Design auswählen",
+    "welcome": "Willkommen bei OliveTin"
+}

+ 92 - 0
lang/generated/en.json

@@ -0,0 +1,92 @@
+{
+    "connected": "Connected",
+    "diagnostics.browser-info": "Browser Info",
+    "diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
+    "diagnostics.config-issue-action": "Action",
+    "diagnostics.config-issue-argument": "Argument",
+    "diagnostics.config-issue-code": "Code",
+    "diagnostics.config-issue-config-file": "Config file",
+    "diagnostics.config-issue-detail": "Detail",
+    "diagnostics.config-issue-message": "Message",
+    "diagnostics.config-issue-severity": "Severity",
+    "diagnostics.config-issue-source": "Source",
+    "diagnostics.config-issues": "Configuration issues",
+    "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.",
+    "diagnostics.config-issues-none": "No configuration issues detected.",
+    "diagnostics.copied": "Copied!",
+    "diagnostics.copy-to-clipboard": "Copy to Clipboard",
+    "diagnostics.found-config": "Found Config",
+    "diagnostics.found-key": "Found Key",
+    "diagnostics.generate-browser-info": "Generate Browser Info",
+    "diagnostics.generate-server-diagnostics": "Generate Server Diagnostics",
+    "diagnostics.get-support": "Get support",
+    "diagnostics.get-support-description": "If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page.",
+    "diagnostics.server-diagnostics": "Server Diagnostics",
+    "diagnostics.server-diagnostics-description": "This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.",
+    "diagnostics.server-diagnostics-docs": "Server Diagnostics Documentation",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "Unknown",
+    "diagnostics.useragent-data-error": "Error retrieving userAgentData",
+    "diagnostics.where-to-find-help": "Where to find help",
+    "disconnected": "Disconnected",
+    "disconnected-banner-announcement": "Events websocket disconnected.",
+    "disconnected-banner-link-text": "Events websocket disconnected",
+    "disconnected-banner-suffix": " since {disconnectedSince}. Trying reconnect in {reconnectIn}.",
+    "disconnected-banner-suffix-reconnecting": " since {disconnectedSince}. Trying reconnect…",
+    "docs": "Documentation",
+    "language-dialog.browser-languages": "Browser languages",
+    "language-dialog.close": "Close",
+    "language-dialog.not-available": "Not available",
+    "language-dialog.title": "Select Language",
+    "login-button": "Login",
+    "logs.action": "Action",
+    "logs.action-group-limits": "concurrent: {concurrent}, queue size: {queueSize}",
+    "logs.back-to-list": "Back to List",
+    "logs.blocked": "Blocked",
+    "logs.calendar": "Calendar",
+    "logs.calendar-title": "Logs Calendar",
+    "logs.clear-date-filter": "Clear date filter",
+    "logs.clear-filter": "Clear search filter",
+    "logs.completed": "Completed",
+    "logs.execution-id": "Execution ID",
+    "logs.exit-code": "Exit code",
+    "logs.filter-error": "Could not apply filter expression.",
+    "logs.filter-help-examples": "Examples: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "Fields: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operators: ==, !=, contains. Prefix ! on a single word excludes matching entries.",
+    "logs.filter-help-intro": "Filters run on the server over entries you are allowed to view. Combine terms with and / or.",
+    "logs.filter-help-title": "Filter syntax",
+    "logs.filter-placeholder": "Filter logs (e.g. !Update or Status != Completed)",
+    "logs.metadata": "Metadata",
+    "logs.no-logs-for-filter": "No logs match the current filter.",
+    "logs.no-logs-to-display": "There are no logs to display.",
+    "logs.page-description": "This is a list of logs from actions that have been executed. Use the filter box for search terms and expressions.",
+    "logs.queue": "Queue",
+    "logs.queue-action-details": "Action Details",
+    "logs.queue-default-group": "Default",
+    "logs.queue-empty": "There are no active or waiting executions right now.",
+    "logs.queue-entity": "Entity",
+    "logs.queue-group-active": "{active} active (max {max})",
+    "logs.queue-group-active-unlimited": "{active} active",
+    "logs.queue-page-description": "Active and waiting executions grouped by action group. Entries you are not permitted to view are hidden.",
+    "logs.queue-position": "#{position}",
+    "logs.queue-running": "Running",
+    "logs.queue-title": "Execution Queue",
+    "logs.queue-waiting": "Waiting",
+    "logs.status": "Status",
+    "logs.timed-out": "Timed out",
+    "logs.timestamp": "Timestamp",
+    "logs.title": "Logs",
+    "nav.actions": "Actions",
+    "nav.diagnostics": "Diagnostics",
+    "nav.entities": "Entities",
+    "nav.logs": "Logs",
+    "nav.system": "System",
+    "raise-issue": "Raise an issue on GitHub",
+    "reconnecting": "Reconnecting…",
+    "return-to-index": "Return to index",
+    "search-filter": "Filter current page",
+    "theme-dialog.close": "Close",
+    "theme-dialog.default": "Default Theme",
+    "theme-dialog.title": "Select Theme",
+    "welcome": "Welcome to OliveTin"
+}

+ 92 - 0
lang/generated/es-ES.json

@@ -0,0 +1,92 @@
+{
+    "connected": "Conectado",
+    "diagnostics.browser-info": "Información del navegador",
+    "diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
+    "diagnostics.config-issue-action": "Acción",
+    "diagnostics.config-issue-argument": "Argumento",
+    "diagnostics.config-issue-code": "Código",
+    "diagnostics.config-issue-config-file": "Archivo de configuración",
+    "diagnostics.config-issue-detail": "Detalle",
+    "diagnostics.config-issue-message": "Mensaje",
+    "diagnostics.config-issue-severity": "Severidad",
+    "diagnostics.config-issue-source": "Origen",
+    "diagnostics.config-issues": "Problemas de configuración",
+    "diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.",
+    "diagnostics.config-issues-none": "No se detectaron problemas de configuración.",
+    "diagnostics.copied": "¡Copiado!",
+    "diagnostics.copy-to-clipboard": "Copiar al portapapeles",
+    "diagnostics.found-config": "Configuración encontrada",
+    "diagnostics.found-key": "Clave encontrada",
+    "diagnostics.generate-browser-info": "Generar información del navegador",
+    "diagnostics.generate-server-diagnostics": "Generar diagnóstico del servidor",
+    "diagnostics.get-support": "Obtener soporte",
+    "diagnostics.get-support-description": "Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.",
+    "diagnostics.server-diagnostics": "Diagnóstico del servidor",
+    "diagnostics.server-diagnostics-description": "Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.",
+    "diagnostics.server-diagnostics-docs": "Documentación de diagnóstico del servidor",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "Desconocido",
+    "diagnostics.useragent-data-error": "Error al recuperar userAgentData",
+    "diagnostics.where-to-find-help": "Dónde encontrar ayuda",
+    "disconnected": "Desconectado",
+    "disconnected-banner-announcement": "Websocket de eventos desconectado.",
+    "disconnected-banner-link-text": "Websocket de eventos desconectado",
+    "disconnected-banner-suffix": " desde {disconnectedSince}. Reintentando conexión en {reconnectIn}.",
+    "disconnected-banner-suffix-reconnecting": " desde {disconnectedSince}. Reintentando conexión…",
+    "docs": "Documentación",
+    "language-dialog.browser-languages": "Idiomas del navegador",
+    "language-dialog.close": "Cerrar",
+    "language-dialog.not-available": "No disponible",
+    "language-dialog.title": "Seleccionar idioma",
+    "login-button": "Iniciar sesión",
+    "logs.action": "Acción",
+    "logs.action-group-limits": "simultáneas: {concurrent}, tamaño de cola: {queueSize}",
+    "logs.back-to-list": "Volver a la Lista",
+    "logs.blocked": "Bloqueado",
+    "logs.calendar": "Calendario",
+    "logs.calendar-title": "Calendario de Registros",
+    "logs.clear-date-filter": "Limpiar filtro de fecha",
+    "logs.clear-filter": "Limpiar filtro de búsqueda",
+    "logs.completed": "Completado",
+    "logs.execution-id": "ID de ejecución",
+    "logs.exit-code": "Código de salida",
+    "logs.filter-error": "No se pudo aplicar la expresión del filtro.",
+    "logs.filter-help-examples": "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes.",
+    "logs.filter-help-intro": "Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or.",
+    "logs.filter-help-title": "Sintaxis del filtro",
+    "logs.filter-placeholder": "Filtrar registros (p. ej. !Update o Status != Completed)",
+    "logs.metadata": "Metadatos",
+    "logs.no-logs-for-filter": "Ningún registro coincide con el filtro actual.",
+    "logs.no-logs-to-display": "No hay registros para mostrar.",
+    "logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.",
+    "logs.queue": "Cola",
+    "logs.queue-action-details": "Detalles de la acción",
+    "logs.queue-default-group": "Predeterminado",
+    "logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.",
+    "logs.queue-entity": "Entidad",
+    "logs.queue-group-active": "{active} activas (máx. {max})",
+    "logs.queue-group-active-unlimited": "{active} activas",
+    "logs.queue-page-description": "Ejecuciones activas y en espera agrupadas por grupo de acciones. Las entradas que no puede ver se ocultan.",
+    "logs.queue-position": "#{position}",
+    "logs.queue-running": "En ejecución",
+    "logs.queue-title": "Cola de ejecución",
+    "logs.queue-waiting": "En espera",
+    "logs.status": "Estado",
+    "logs.timed-out": "Tiempo agotado",
+    "logs.timestamp": "Marca de tiempo",
+    "logs.title": "Registros",
+    "nav.actions": "Acciones",
+    "nav.diagnostics": "Diagnósticos",
+    "nav.entities": "Entidades",
+    "nav.logs": "Registros",
+    "nav.system": "Sistema",
+    "raise-issue": "Reportar un problema en GitHub",
+    "reconnecting": "Reconectando…",
+    "return-to-index": "Volver a la página principal",
+    "search-filter": "Filtrar página actual",
+    "theme-dialog.close": "Cerrar",
+    "theme-dialog.default": "Tema Predeterminado",
+    "theme-dialog.title": "Seleccionar tema",
+    "welcome": "Bienvenido a OliveTin"
+}

+ 92 - 0
lang/generated/it-IT.json

@@ -0,0 +1,92 @@
+{
+    "connected": "Connesso",
+    "diagnostics.browser-info": "Informazioni del browser",
+    "diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
+    "diagnostics.config-issue-action": "Azione",
+    "diagnostics.config-issue-argument": "Argomento",
+    "diagnostics.config-issue-code": "Codice",
+    "diagnostics.config-issue-config-file": "File di configurazione",
+    "diagnostics.config-issue-detail": "Dettaglio",
+    "diagnostics.config-issue-message": "Messaggio",
+    "diagnostics.config-issue-severity": "Gravità",
+    "diagnostics.config-issue-source": "Origine",
+    "diagnostics.config-issues": "Problemi di configurazione",
+    "diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.",
+    "diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.",
+    "diagnostics.copied": "Copiato!",
+    "diagnostics.copy-to-clipboard": "Copia negli appunti",
+    "diagnostics.found-config": "Configurazione trovata",
+    "diagnostics.found-key": "Chiave trovata",
+    "diagnostics.generate-browser-info": "Genera informazioni del browser",
+    "diagnostics.generate-server-diagnostics": "Genera diagnostica del server",
+    "diagnostics.get-support": "Ottenere supporto",
+    "diagnostics.get-support-description": "Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.",
+    "diagnostics.server-diagnostics": "Diagnostica del server",
+    "diagnostics.server-diagnostics-description": "Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.",
+    "diagnostics.server-diagnostics-docs": "Documentazione diagnostica del server",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "Sconosciuto",
+    "diagnostics.useragent-data-error": "Errore nel recupero di userAgentData",
+    "diagnostics.where-to-find-help": "Dove trovare aiuto",
+    "disconnected": "Disconnesso",
+    "disconnected-banner-announcement": "Websocket eventi disconnesso.",
+    "disconnected-banner-link-text": "Websocket eventi disconnesso",
+    "disconnected-banner-suffix": " dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}.",
+    "disconnected-banner-suffix-reconnecting": " dalle {disconnectedSince}. Tentativo di connessione…",
+    "docs": "Documentazione",
+    "language-dialog.browser-languages": "Lingue del browser",
+    "language-dialog.close": "Chiudi",
+    "language-dialog.not-available": "Non disponibile",
+    "language-dialog.title": "Seleziona lingua",
+    "login-button": "Login",
+    "logs.action": "Azione",
+    "logs.action-group-limits": "simultanei: {concurrent}, dimensione coda: {queueSize}",
+    "logs.back-to-list": "Torna all'Elenco",
+    "logs.blocked": "Bloccato",
+    "logs.calendar": "Calendario",
+    "logs.calendar-title": "Calendario dei Registri",
+    "logs.clear-date-filter": "Cancella filtro data",
+    "logs.clear-filter": "Cancella filtro di ricerca",
+    "logs.completed": "Completato",
+    "logs.execution-id": "ID esecuzione",
+    "logs.exit-code": "Codice di uscita",
+    "logs.filter-error": "Impossibile applicare l'espressione di filtro.",
+    "logs.filter-help-examples": "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti.",
+    "logs.filter-help-intro": "I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or.",
+    "logs.filter-help-title": "Sintassi del filtro",
+    "logs.filter-placeholder": "Filtra i registri (es. !Update o Status != Completed)",
+    "logs.metadata": "Metadati",
+    "logs.no-logs-for-filter": "Nessun registro corrisponde al filtro corrente.",
+    "logs.no-logs-to-display": "Non ci sono registri da mostrare.",
+    "logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.",
+    "logs.queue": "Coda",
+    "logs.queue-action-details": "Dettagli azione",
+    "logs.queue-default-group": "Predefinito",
+    "logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.",
+    "logs.queue-entity": "Entità",
+    "logs.queue-group-active": "{active} attive (max {max})",
+    "logs.queue-group-active-unlimited": "{active} attive",
+    "logs.queue-page-description": "Esecuzioni attive e in attesa raggruppate per gruppo di azioni. Le voci non autorizzate sono nascoste.",
+    "logs.queue-position": "#{position}",
+    "logs.queue-running": "In esecuzione",
+    "logs.queue-title": "Coda di esecuzione",
+    "logs.queue-waiting": "In attesa",
+    "logs.status": "Stato",
+    "logs.timed-out": "Tempo scaduto",
+    "logs.timestamp": "Date e ora",
+    "logs.title": "Registri",
+    "nav.actions": "Azioni",
+    "nav.diagnostics": "Diagnostica",
+    "nav.entities": "Entità",
+    "nav.logs": "Registri",
+    "nav.system": "Sistema",
+    "raise-issue": "Segnala un problema su GitHub",
+    "reconnecting": "Riconnessione…",
+    "return-to-index": "Torna alla pagina principale",
+    "search-filter": "Filtra la pagina corrente",
+    "theme-dialog.close": "Chiudi",
+    "theme-dialog.default": "Tema Predefinito",
+    "theme-dialog.title": "Seleziona tema",
+    "welcome": "Benvenuto in OliveTin"
+}

+ 92 - 0
lang/generated/zh-Hans-CN.json

@@ -0,0 +1,92 @@
+{
+    "connected": "已连接",
+    "diagnostics.browser-info": "浏览器信息",
+    "diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
+    "diagnostics.config-issue-action": "操作",
+    "diagnostics.config-issue-argument": "参数",
+    "diagnostics.config-issue-code": "代码",
+    "diagnostics.config-issue-config-file": "配置文件",
+    "diagnostics.config-issue-detail": "详情",
+    "diagnostics.config-issue-message": "消息",
+    "diagnostics.config-issue-severity": "严重程度",
+    "diagnostics.config-issue-source": "来源",
+    "diagnostics.config-issues": "配置问题",
+    "diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。",
+    "diagnostics.config-issues-none": "未检测到配置问题。",
+    "diagnostics.copied": "已复制!",
+    "diagnostics.copy-to-clipboard": "复制到剪贴板",
+    "diagnostics.found-config": "找到配置",
+    "diagnostics.found-key": "找到密钥",
+    "diagnostics.generate-browser-info": "生成浏览器信息",
+    "diagnostics.generate-server-diagnostics": "生成服务器诊断",
+    "diagnostics.get-support": "获取支持",
+    "diagnostics.get-support-description": "如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。",
+    "diagnostics.server-diagnostics": "服务器诊断",
+    "diagnostics.server-diagnostics-description": "此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。",
+    "diagnostics.server-diagnostics-docs": "服务器诊断文档",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "未知",
+    "diagnostics.useragent-data-error": "检索 userAgentData 时出错",
+    "diagnostics.where-to-find-help": "在哪里找到帮助",
+    "disconnected": "已断开连接",
+    "disconnected-banner-announcement": "事件 WebSocket 已断开。",
+    "disconnected-banner-link-text": "事件 WebSocket 已断开",
+    "disconnected-banner-suffix": "自 {disconnectedSince}。{reconnectIn} 后尝试重连。",
+    "disconnected-banner-suffix-reconnecting": "自 {disconnectedSince}。正在尝试重连…",
+    "docs": "文档",
+    "language-dialog.browser-languages": "浏览器语言",
+    "language-dialog.close": "关闭",
+    "language-dialog.not-available": "不可用",
+    "language-dialog.title": "选择语言",
+    "login-button": "登录",
+    "logs.action": "动作",
+    "logs.action-group-limits": "并发:{concurrent},队列大小:{queueSize}",
+    "logs.back-to-list": "返回列表",
+    "logs.blocked": "阻塞",
+    "logs.calendar": "日历",
+    "logs.calendar-title": "日志日历",
+    "logs.clear-date-filter": "清除日期筛选器",
+    "logs.clear-filter": "清除搜索筛选器",
+    "logs.completed": "完成",
+    "logs.execution-id": "执行 ID",
+    "logs.exit-code": "退出代码",
+    "logs.filter-error": "无法应用过滤表达式。",
+    "logs.filter-help-examples": "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。",
+    "logs.filter-help-intro": "过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。",
+    "logs.filter-help-title": "过滤语法",
+    "logs.filter-placeholder": "过滤日志(例如 !Update 或 Status != Completed)",
+    "logs.metadata": "元数据",
+    "logs.no-logs-for-filter": "没有符合当前过滤条件的日志。",
+    "logs.no-logs-to-display": "没有日志可显示。",
+    "logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
+    "logs.queue": "队列",
+    "logs.queue-action-details": "动作详细信息",
+    "logs.queue-default-group": "默认",
+    "logs.queue-empty": "当前没有正在运行或等待中的执行。",
+    "logs.queue-entity": "实体",
+    "logs.queue-group-active": "{active} 个活动(上限 {max})",
+    "logs.queue-group-active-unlimited": "{active} 个活动",
+    "logs.queue-page-description": "按动作组分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。",
+    "logs.queue-position": "第 {position} 位",
+    "logs.queue-running": "运行中",
+    "logs.queue-title": "执行队列",
+    "logs.queue-waiting": "等待中",
+    "logs.status": "状态",
+    "logs.timed-out": "超时",
+    "logs.timestamp": "时间戳",
+    "logs.title": "日志",
+    "nav.actions": "动作",
+    "nav.diagnostics": "诊断",
+    "nav.entities": "实体",
+    "nav.logs": "日志",
+    "nav.system": "系统",
+    "raise-issue": "在 GitHub 上报告问题",
+    "reconnecting": "正在重新连接…",
+    "return-to-index": "返回首页",
+    "search-filter": "过滤当前页面",
+    "theme-dialog.close": "关闭",
+    "theme-dialog.default": "默认主题",
+    "theme-dialog.title": "选择主题",
+    "welcome": "欢迎使用 OliveTin"
+}

+ 92 - 0
lang/generated/zh-Hant-TW.json

@@ -0,0 +1,92 @@
+{
+    "connected": "已連線",
+    "diagnostics.browser-info": "瀏覽器資訊",
+    "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。",
+    "diagnostics.config-issue-action": "動作",
+    "diagnostics.config-issue-argument": "參數",
+    "diagnostics.config-issue-code": "代碼",
+    "diagnostics.config-issue-config-file": "設定檔",
+    "diagnostics.config-issue-detail": "詳細資料",
+    "diagnostics.config-issue-message": "訊息",
+    "diagnostics.config-issue-severity": "嚴重程度",
+    "diagnostics.config-issue-source": "來源",
+    "diagnostics.config-issues": "設定問題",
+    "diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。",
+    "diagnostics.config-issues-none": "未偵測到設定問題。",
+    "diagnostics.copied": "已複製!",
+    "diagnostics.copy-to-clipboard": "複製到剪貼簿",
+    "diagnostics.found-config": "找到設定檔 (Config)",
+    "diagnostics.found-key": "找到金鑰 (Key)",
+    "diagnostics.generate-browser-info": "產生瀏覽器資訊",
+    "diagnostics.generate-server-diagnostics": "產生伺服器診斷資訊",
+    "diagnostics.get-support": "尋求技術支援",
+    "diagnostics.get-support-description": "如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。",
+    "diagnostics.server-diagnostics": "伺服器診斷",
+    "diagnostics.server-diagnostics-description": "此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。",
+    "diagnostics.server-diagnostics-docs": "伺服器診斷官方文件",
+    "diagnostics.ssh": "SSH",
+    "diagnostics.unknown": "未知",
+    "diagnostics.useragent-data-error": "取得 userAgentData 時發生錯誤",
+    "diagnostics.where-to-find-help": "哪裡可以尋求協助",
+    "disconnected": "已斷線",
+    "disconnected-banner-announcement": "系統事件 WebSocket 已斷線。",
+    "disconnected-banner-link-text": "系統事件 WebSocket 已斷線",
+    "disconnected-banner-suffix": " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。",
+    "disconnected-banner-suffix-reconnecting": " 自 {disconnectedSince} 起。嘗試重新連線中…",
+    "docs": "官方文件",
+    "language-dialog.browser-languages": "瀏覽器語言",
+    "language-dialog.close": "關閉",
+    "language-dialog.not-available": "無法使用",
+    "language-dialog.title": "選擇語言",
+    "login-button": "登入",
+    "logs.action": "動作",
+    "logs.action-group-limits": "並行數:{concurrent},佇列大小:{queueSize}",
+    "logs.back-to-list": "返回清單",
+    "logs.blocked": "已阻擋",
+    "logs.calendar": "日曆",
+    "logs.calendar-title": "日誌日曆",
+    "logs.clear-date-filter": "清除日期過濾條件",
+    "logs.clear-filter": "清除搜尋過濾條件",
+    "logs.completed": "已完成",
+    "logs.execution-id": "執行 ID",
+    "logs.exit-code": "結束代碼",
+    "logs.filter-error": "無法套用過濾運算式。",
+    "logs.filter-help-examples": "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
+    "logs.filter-help-fields": "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。",
+    "logs.filter-help-intro": "過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。",
+    "logs.filter-help-title": "過濾語法",
+    "logs.filter-placeholder": "過濾日誌 (例如 !Update 或 Status != Completed)",
+    "logs.metadata": "中繼資料",
+    "logs.no-logs-for-filter": "沒有符合目前過濾條件的日誌。",
+    "logs.no-logs-to-display": "目前沒有可顯示的日誌。",
+    "logs.page-description": "這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。",
+    "logs.queue": "佇列",
+    "logs.queue-action-details": "動作詳細資訊",
+    "logs.queue-default-group": "預設",
+    "logs.queue-empty": "目前沒有使用中或等待中的執行項目。",
+    "logs.queue-entity": "實體",
+    "logs.queue-group-active": "{active} 個使用中 (最大值 {max})",
+    "logs.queue-group-active-unlimited": "{active} 個使用中",
+    "logs.queue-page-description": "依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。",
+    "logs.queue-position": "#{position}",
+    "logs.queue-running": "執行中",
+    "logs.queue-title": "執行佇列",
+    "logs.queue-waiting": "等待中",
+    "logs.status": "狀態",
+    "logs.timed-out": "執行逾時",
+    "logs.timestamp": "時間戳記",
+    "logs.title": "日誌",
+    "nav.actions": "動作",
+    "nav.diagnostics": "診斷",
+    "nav.entities": "實體",
+    "nav.logs": "日誌",
+    "nav.system": "系統",
+    "raise-issue": "在 GitHub 上建立 Issue",
+    "reconnecting": "重新連線中…",
+    "return-to-index": "返回首頁",
+    "search-filter": "過濾目前頁面",
+    "theme-dialog.close": "關閉",
+    "theme-dialog.default": "預設佈景主題",
+    "theme-dialog.title": "選擇佈景主題",
+    "welcome": "歡迎使用 OliveTin"
+}

+ 31 - 7
lang/main.go

@@ -25,23 +25,47 @@ type CombinedTranslationsOutput struct {
 
 func main() {
 	combinedContent := getCombinedLanguageContent()
-
 	sortedContent := sortTranslations(combinedContent)
 
-	jsonData, err := json.MarshalIndent(sortedContent, "", "    ")
+	err := writeTranslationFiles(sortedContent.Messages)
+	if err != nil {
+		log.Fatalf("Error writing language content: %v", err)
+	}
+}
+
+func writeTranslationFiles(messages map[string]map[string]string) error {
+	err := os.RemoveAll("generated")
+	if err != nil {
+		return err
+	}
 
+	err = os.Mkdir("generated", 0o755)
 	if err != nil {
-		log.Fatalf("Error marshalling combined language content: %v", err)
+		return err
 	}
 
-	jsonData = append(jsonData, '\n')
-	err = os.WriteFile("combined_output.json", jsonData, 0644)
+	for languageName, translations := range messages {
+		err = writeTranslationFile(languageName, translations)
+		if err != nil {
+			return err
+		}
+	}
+
+	log.Infof("Language content saved to generated/")
 
+	return nil
+}
+
+func writeTranslationFile(languageName string, translations map[string]string) error {
+	jsonData, err := json.MarshalIndent(translations, "", "    ")
 	if err != nil {
-		log.Fatalf("Error saving combined language content to file: %v", err)
+		return err
 	}
 
-	log.Infof("Combined language content saved to combined_output.json")
+	jsonData = append(jsonData, '\n')
+	filename := filepath.Join("generated", languageName+".json")
+
+	return os.WriteFile(filename, jsonData, 0o644)
 }
 
 // sortTranslations creates a new structure with sorted keys for deterministic output.