Переглянути джерело

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
 	$(MAKE) -wC lang
 
 
 generated-check: proto lang-generate
 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; }
 	test -z "$$untracked" || { printf 'Untracked generated files:\n%s\n' "$$untracked"; exit 1; }
 
 
 dist:
 dist:

+ 1 - 1
frontend/Makefile

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

+ 0 - 2
frontend/index.html

@@ -8,8 +8,6 @@
 
 
 		<title>OliveTin</title>
 		<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 = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
 
 
 		<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.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 { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { FitAddon } from '@xterm/addon-fit'
 import { WebLinksAddon } from '@xterm/addon-web-links'
 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 App from './resources/vue/App.vue'
 
 
 import { initWebsocket } from './js/websocket.js'
 import { initWebsocket } from './js/websocket.js'
+import { getSelectedLocale, loadInitialMessages } from './resources/vue/i18n.js'
 import { applyThemeStyles, getStoredThemePreference } from './resources/vue/utils/themeLoader.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 () {
 async function initClient () {
   const transport = createConnectTransport({
   const transport = createConnectTransport({
     baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
     baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
   })
   })
+  const locale = getSelectedLocale()
 
 
   window.client = createClient(OliveTinApiService, transport)
   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) {
   if (window.initResponse.enableCustomJs) {
     const script = document.createElement('script')
     const script = document.createElement('script')
@@ -72,9 +44,9 @@ async function initClient () {
 
 
   const i18nSettings = createI18n({
   const i18nSettings = createI18n({
     legacy: false,
     legacy: false,
-    locale: getSelectedLanguage(),
+    locale,
     fallbackLocale: 'en',
     fallbackLocale: 'en',
-    messages: combinedTranslations.messages,
+    messages,
     postTranslation: (translated) => {
     postTranslation: (translated) => {
       const params = new URLSearchParams(window.location.search)
       const params = new URLSearchParams(window.location.search)
 
 

+ 10 - 21
frontend/package-lock.json

@@ -13,25 +13,23 @@
 				"@connectrpc/connect-web": "^2.2.0",
 				"@connectrpc/connect-web": "^2.2.0",
 				"@hugeicons/core-free-icons": "^4.3.2",
 				"@hugeicons/core-free-icons": "^4.3.2",
 				"@hugeicons/vue": "^1.0.8",
 				"@hugeicons/vue": "^1.0.8",
-				"@vitejs/plugin-vue": "^6.0.8",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
 				"iconify-icon": "^3.0.2",
 				"picocrank": "^1.29.0",
 				"picocrank": "^1.29.0",
-				"unplugin-vue-components": "^32.1.0",
-				"vite": "^8.2.2",
 				"vue": "^3.5.42",
 				"vue": "^3.5.42",
 				"vue-i18n": "^11.4.10",
 				"vue-i18n": "^11.4.10",
 				"vue-router": "^5.3.1"
 				"vue-router": "^5.3.1"
 			},
 			},
 			"devDependencies": {
 			"devDependencies": {
+				"@vitejs/plugin-vue": "^6.0.8",
 				"eslint": "^9.39.5",
 				"eslint": "^9.39.5",
 				"eslint-plugin-vue": "^10.11.0",
 				"eslint-plugin-vue": "^10.11.0",
 				"neostandard": "^0.13.0",
 				"neostandard": "^0.13.0",
-				"process": "^0.11.10",
 				"stylelint": "^17.15.0",
 				"stylelint": "^17.15.0",
-				"stylelint-config-standard": "^40.0.0"
+				"stylelint-config-standard": "^40.0.0",
+				"vite": "^8.2.2"
 			},
 			},
 			"engines": {
 			"engines": {
 				"node": "^20.19.0 || >=22.12.0"
 				"node": "^20.19.0 || >=22.12.0"
@@ -5604,9 +5602,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/postcss": {
 		"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": [
 			"funding": [
 				{
 				{
 					"type": "opencollective",
 					"type": "opencollective",
@@ -5623,7 +5621,7 @@
 			],
 			],
 			"license": "MIT",
 			"license": "MIT",
 			"dependencies": {
 			"dependencies": {
-				"nanoid": "^3.3.17",
+				"nanoid": "^3.3.18",
 				"picocolors": "^1.1.1",
 				"picocolors": "^1.1.1",
 				"source-map-js": "^1.2.1"
 				"source-map-js": "^1.2.1"
 			},
 			},
@@ -5688,15 +5686,6 @@
 				"node": ">= 0.8.0"
 				"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": {
 		"node_modules/prop-types": {
 			"version": "15.8.1",
 			"version": "15.8.1",
 			"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 			"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7207,9 +7196,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/vite/node_modules/picomatch": {
 		"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",
 			"license": "MIT",
 			"engines": {
 			"engines": {
 				"node": ">=12"
 				"node": ">=12"

+ 7 - 14
frontend/package.json

@@ -3,42 +3,35 @@
 	"version": "1.0.0",
 	"version": "1.0.0",
 	"description": "The WebUI for OliveTin",
 	"description": "The WebUI for OliveTin",
 	"repository": "https://github.com/OliveTin/OliveTin",
 	"repository": "https://github.com/OliveTin/OliveTin",
-	"source": "index.html",
+	"type": "module",
 	"devDependencies": {
 	"devDependencies": {
+		"@vitejs/plugin-vue": "^6.0.8",
 		"eslint": "^9.39.5",
 		"eslint": "^9.39.5",
 		"eslint-plugin-vue": "^10.11.0",
 		"eslint-plugin-vue": "^10.11.0",
 		"neostandard": "^0.13.0",
 		"neostandard": "^0.13.0",
-		"process": "^0.11.10",
 		"stylelint": "^17.15.0",
 		"stylelint": "^17.15.0",
-		"stylelint-config-standard": "^40.0.0"
+		"stylelint-config-standard": "^40.0.0",
+		"vite": "^8.2.2"
 	},
 	},
 	"scripts": {
 	"scripts": {
+		"build": "vite build",
+		"dev": "vite",
 		"lint": "eslint main.js js/* resources/vue vite.config.mjs && stylelint \"style.css\" \"themes/**/*.css\"",
 		"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\"",
 		"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"
 		"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",
 	"license": "AGPL-3.0-only",
 	"dependencies": {
 	"dependencies": {
 		"@connectrpc/connect": "^2.2.0",
 		"@connectrpc/connect": "^2.2.0",
 		"@connectrpc/connect-web": "^2.2.0",
 		"@connectrpc/connect-web": "^2.2.0",
 		"@hugeicons/core-free-icons": "^4.3.2",
 		"@hugeicons/core-free-icons": "^4.3.2",
 		"@hugeicons/vue": "^1.0.8",
 		"@hugeicons/vue": "^1.0.8",
-		"@vitejs/plugin-vue": "^6.0.8",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.29.0",
 		"picocrank": "^1.29.0",
-		"unplugin-vue-components": "^32.1.0",
-		"vite": "^8.2.2",
 		"vue": "^3.5.42",
 		"vue": "^3.5.42",
 		"vue-i18n": "^11.4.10",
 		"vue-i18n": "^11.4.10",
 		"vue-router": "^5.3.1"
 		"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 { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
 import logoUrl from '../../OliveTinLogo.png'
 import logoUrl from '../../OliveTinLogo.png'
 import { useI18n } from 'vue-i18n'
 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 { searchIndexItems, clearSearchIndex, indexSystemNavigation, indexSearchHints, indexRootDashboardEntries } from './stores/searchIndex.js'
 import { applyThemeStyles } from './utils/themeLoader.js'
 import { applyThemeStyles } from './utils/themeLoader.js'
 const { t } = useI18n()
 const { t } = useI18n()
@@ -272,30 +272,6 @@ const headerLoginRoute = computed(() => {
   return showLoginLink.value ? { name: 'Login' } : null
   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 () {
 function toggleSidebar () {
   if (sidebar.value && showNavigation.value) {
   if (sidebar.value && showNavigation.value) {
     sidebar.value.toggle()
     sidebar.value.toggle()
@@ -518,7 +494,7 @@ function closeLanguageDialog () {
   }
   }
 }
 }
 
 
-function changeLanguage () {
+async function changeLanguage () {
   if (!window.i18n || !selectedLanguage.value) {
   if (!window.i18n || !selectedLanguage.value) {
     return
     return
   }
   }
@@ -526,9 +502,9 @@ function changeLanguage () {
   if (selectedLanguage.value === 'auto') {
   if (selectedLanguage.value === 'auto') {
     localStorage.removeItem('olivetin-language')
     localStorage.removeItem('olivetin-language')
     languagePreference.value = 'auto'
     languagePreference.value = 'auto'
-    window.i18n.locale.value = normalizeBrowserLanguage()
+    await activateLocale(window.i18n, resolveBrowserLocale())
   } else {
   } else {
-    window.i18n.locale.value = selectedLanguage.value
+    await activateLocale(window.i18n, selectedLanguage.value)
     localStorage.setItem('olivetin-language', selectedLanguage.value)
     localStorage.setItem('olivetin-language', selectedLanguage.value)
     languagePreference.value = 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 { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import vue from '@vitejs/plugin-vue'
-import Components from 'unplugin-vue-components/vite'
 
 
 export default defineConfig({
 export default defineConfig({
   resolve: {
   resolve: {
     dedupe: ['vue', 'vue-router']
     dedupe: ['vue', 'vue-router']
   },
   },
-  plugins: [
-    Components({
-      dirs: ['resources/vue/'],
-      extensions: ['vue'],
-      deep: true,
-      dts: false
-    }),
-    vue()
-  ],
+  plugins: [vue()],
   build: {
   build: {
     rolldownOptions: {
     rolldownOptions: {
       onLog (level, log, defaultHandler) {
       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
 Hey, thanks for reading this quick introduction to translations. The project founder
 only speaks two languages; English, and Bad English (!), so the initial translations
 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
 It would be most welcome to have human contributors who are native speakers improve these
 translations, or add new ones.
 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)
 [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
 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
 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.
 the YAML file and the developers will take care of it.
 
 
 ## Contributing improvements
 ## 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.
 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.
 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() {
 func main() {
 	combinedContent := getCombinedLanguageContent()
 	combinedContent := getCombinedLanguageContent()
-
 	sortedContent := sortTranslations(combinedContent)
 	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 {
 	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 {
 	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.
 // sortTranslations creates a new structure with sorted keys for deterministic output.