Pārlūkot izejas kodu

perf: modernize frontend tooling and loading (#1121)

James Read 1 dienu atpakaļ
vecāks
revīzija
2f4361bb0f

+ 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:

+ 0 - 25
frontend/.eslintrc.json

@@ -1,25 +0,0 @@
-{
-    "env": {
-        "browser": true,
-        "es2021": true
-    },
-    "extends": [
-        "plugin:vue/recommended",
-        "standard"
-    ],
-    "parser": "vue-eslint-parser",
-    "parserOptions": {
-        "ecmaVersion": 12,
-        "sourceType": "module",
-        "parser": {
-            "js": "espree"
-        }
-    },
-    "rules": {
-        "vue/multi-word-component-names": "off",
-        "vue/require-default-prop": "off",
-        "vue/no-v-html": "warn",
-        "no-tabs": "off",
-        "no-mixed-spaces-and-tabs": "off"
-    }
-}

+ 1 - 1
frontend/Makefile

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

+ 31 - 0
frontend/eslint.config.mjs

@@ -0,0 +1,31 @@
+import neostandard from 'neostandard'
+import pluginVue from 'eslint-plugin-vue'
+
+export default [
+	{
+		ignores: [
+			'dist/**',
+			'node_modules/**',
+			'resources/scripts/gen/**'
+		]
+	},
+	...neostandard({
+		env: ['browser'],
+		noJsx: true
+	}),
+	...pluginVue.configs['flat/recommended'],
+	{
+		files: ['**/*.{js,mjs,vue}'],
+		languageOptions: {
+			ecmaVersion: 'latest',
+			sourceType: 'module'
+		},
+		rules: {
+			'@stylistic/no-tabs': 'off',
+			'@stylistic/no-mixed-spaces-and-tabs': 'off',
+			'vue/multi-word-component-names': 'off',
+			'vue/require-default-prop': 'off',
+			'vue/no-v-html': 'warn'
+		}
+	}
+]

+ 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)
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 575 - 169
frontend/package-lock.json


+ 9 - 15
frontend/package.json

@@ -3,41 +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",
-		"process": "^0.11.10",
+		"neostandard": "^0.13.0",
 		"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",
-		"standard": "^17.1.2",
-		"unplugin-vue-components": "^32.1.0",
-		"vite": "^8.2.2",
 		"vue": "^3.5.42",
 		"vue-i18n": "^11.4.10",
 		"vue-router": "^5.3.1"

+ 23 - 23
frontend/resources/vue/ActionButton.vue

@@ -273,9 +273,9 @@ function constructFromJson (json) {
   if (bindingId.value) {
     rateLimits[bindingId.value] = rateLimitExpires.value
     setBindingExecutionState(
-	  bindingId.value,
-	  !!json.hasRunningInstance,
-	  !!json.hasQueuedInstance
+      bindingId.value,
+      !!json.hasRunningInstance,
+      !!json.hasQueuedInstance
     )
   }
   updateRateLimitStatus()
@@ -310,8 +310,8 @@ function updateRateLimitStatus () {
     isRateLimited.value = false
     rateLimitMessage.value = ''
     if (rateLimitInterval.value) {
-	  clearInterval(rateLimitInterval.value)
-	  rateLimitInterval.value = null
+      clearInterval(rateLimitInterval.value)
+      rateLimitInterval.value = null
     }
     return
   }
@@ -325,8 +325,8 @@ function updateRateLimitStatus () {
     rateLimitMessage.value = ''
     rateLimitExpires.value = 0
     if (rateLimitInterval.value) {
-	  clearInterval(rateLimitInterval.value)
-	  rateLimitInterval.value = null
+      clearInterval(rateLimitInterval.value)
+      rateLimitInterval.value = null
     }
   } else {
     // Still rate limited
@@ -336,9 +336,9 @@ function updateRateLimitStatus () {
 
     // Set up interval to update every second
     if (!rateLimitInterval.value) {
-	  rateLimitInterval.value = setInterval(() => {
+      rateLimitInterval.value = setInterval(() => {
         updateRateLimitStatus()
-	  }, 1000)
+      }, 1000)
     }
   }
 }
@@ -360,12 +360,12 @@ async function handleClick () {
     const bindingId = props.actionData.bindingId
     const prefilled = props.prefilledArguments || {}
     if (Object.keys(prefilled).length > 0) {
-	  router.push({
+      router.push({
         path: `/actionBinding/${bindingId}/argumentForm`,
         state: { prefilledArguments: prefilled }
-	  })
+      })
     } else {
-	  router.push(`/actionBinding/${bindingId}/argumentForm`)
+      router.push(`/actionBinding/${bindingId}/argumentForm`)
     }
   } else {
     await startAction()
@@ -439,7 +439,7 @@ async function startAction (actionArgs) {
   stopButtonResultWatch = watch(
     () => buttonResults[startActionArgs.uniqueTrackingId],
     (newResult, oldResult) => {
-	  onLogEntryChanged(newResult)
+      onLogEntryChanged(newResult)
     }
   )
 
@@ -450,11 +450,11 @@ async function startAction (actionArgs) {
     const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
 
     if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
-	  router.push(`/logs/${trackingId}`)
+      router.push(`/logs/${trackingId}`)
     }
 
     if (!connectionState.connected) {
-	  await pollExecutionUntilDone(trackingId)
+      await pollExecutionUntilDone(trackingId)
     }
   } catch (err) {
     stopWatchingButtonResult()
@@ -554,14 +554,14 @@ onMounted(() => {
   watch(
     rateLimits,
     () => {
-	  const id = bindingId.value
-	  if (id && rateLimits[id] !== undefined) {
+      const id = bindingId.value
+      if (id && rateLimits[id] !== undefined) {
         const newExpires = rateLimits[id]
         if (newExpires !== rateLimitExpires.value) {
-		  rateLimitExpires.value = newExpires
-		  updateRateLimitStatus()
+          rateLimitExpires.value = newExpires
+          updateRateLimitStatus()
         }
-	  }
+      }
     },
     { deep: true }
   )
@@ -570,9 +570,9 @@ onMounted(() => {
   watch(
     () => pendingBindingFlash[bindingId.value],
     (pending) => {
-	  if (pending) {
+      if (pending) {
         consumeAndFlashPendingResult()
-	  }
+      }
     },
     { immediate: true }
   )
@@ -593,7 +593,7 @@ watch(
   (newData) => {
     updateFromJson(newData)
     if (newData?.icon !== undefined) {
-	  glyph.value = newData.icon ?? ''
+      glyph.value = newData.icon ?? ''
     }
   },
   { deep: true }

+ 18 - 31
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 { ensureLocaleMessages, resolveBrowserLocale } from './i18n.js'
 import { searchIndexItems, clearSearchIndex, indexSystemNavigation, indexSearchHints, indexRootDashboardEntries } from './stores/searchIndex.js'
 import { applyThemeStyles } from './utils/themeLoader.js'
 const { t } = useI18n()
@@ -220,6 +220,7 @@ const browserLanguages = ref([])
 const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-language') : null
 const languagePreference = ref(initialLanguagePreference || 'auto')
 const selectedLanguage = ref(languagePreference.value)
+let latestLanguageChange = 0
 
 const themeDialog = ref(null)
 const availableThemes = ref([])
@@ -272,30 +273,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,19 +495,29 @@ function closeLanguageDialog () {
   }
 }
 
-function changeLanguage () {
+async function changeLanguage () {
   if (!window.i18n || !selectedLanguage.value) {
     return
   }
 
-  if (selectedLanguage.value === 'auto') {
+  const languageChange = ++latestLanguageChange
+  const requestedPreference = selectedLanguage.value
+  const requestedLocale = requestedPreference === 'auto' ? resolveBrowserLocale() : requestedPreference
+
+  await ensureLocaleMessages(window.i18n, requestedLocale)
+
+  if (languageChange !== latestLanguageChange) {
+    return
+  }
+
+  window.i18n.locale.value = requestedLocale
+
+  if (requestedPreference === 'auto') {
     localStorage.removeItem('olivetin-language')
     languagePreference.value = 'auto'
-    window.i18n.locale.value = normalizeBrowserLanguage()
   } else {
-    window.i18n.locale.value = selectedLanguage.value
-    localStorage.setItem('olivetin-language', selectedLanguage.value)
-    languagePreference.value = selectedLanguage.value
+    localStorage.setItem('olivetin-language', requestedPreference)
+    languagePreference.value = requestedPreference
   }
 
   // Update navigation with new translations

+ 29 - 29
frontend/resources/vue/components/actionIconGlyphHelpers.mjs

@@ -1,38 +1,38 @@
 const fallbackNamedHtmlEntities = {
-	amp: '&',
-	apos: "'",
-	darr: '\u2193',
-	gt: '>',
-	laquo: '\u00ab',
-	larr: '\u2190',
-	nbsp: '\u00a0',
-	quot: '"',
-	raquo: '\u00bb',
-	rarr: '\u2192',
-	uarr: '\u2191',
+  amp: '&',
+  apos: "'",
+  darr: '\u2193',
+  gt: '>',
+  laquo: '\u00ab',
+  larr: '\u2190',
+  nbsp: '\u00a0',
+  quot: '"',
+  raquo: '\u00bb',
+  rarr: '\u2192',
+  uarr: '\u2191',
 }
 
-export function decodeHtmlEntities(text) {
-	if (typeof document !== 'undefined') {
-		const textarea = document.createElement('textarea')
-		textarea.innerHTML = text
+export function decodeHtmlEntities (text) {
+  if (typeof document !== 'undefined') {
+    const textarea = document.createElement('textarea')
+    textarea.innerHTML = text
 
-		return textarea.value
-	}
+    return textarea.value
+  }
 
-	return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
-		const codePoint = Number.parseInt(hex, 16)
-		return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
-	}).replace(/&#(\d+);?/g, (_, decimal) => {
-		const codePoint = Number.parseInt(decimal, 10)
-		return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
-	}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
-		return fallbackNamedHtmlEntities[name] ?? entity
-	})
+  return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
+    const codePoint = Number.parseInt(hex, 16)
+    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
+  }).replace(/&#(\d+);?/g, (_, decimal) => {
+    const codePoint = Number.parseInt(decimal, 10)
+    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
+  }).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
+    return fallbackNamedHtmlEntities[name] ?? entity
+  })
 }
 
-export function glyphLooksLikeHtml(text) {
-	const trimmedText = text.trim()
+export function glyphLooksLikeHtml (text) {
+  const trimmedText = text.trim()
 
-	return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
+  return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
 }

+ 7 - 7
frontend/resources/vue/components/actionIconGlyphHelpers.test.mjs

@@ -3,18 +3,18 @@ import assert from 'node:assert/strict'
 import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
 
 test('decodeHtmlEntities decodes named entity icons as plain glyph text', () => {
-	assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
-	assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
-	assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
+  assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
+  assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
+  assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
 })
 
 test('decoded named entity icons are not treated as HTML markup', () => {
-	const decodedGlyph = decodeHtmlEntities('&rarr;')
+  const decodedGlyph = decodeHtmlEntities('&rarr;')
 
-	assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
+  assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
 })
 
 test('decodeHtmlEntities keeps existing numeric entity icon support', () => {
-	assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
-	assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
+  assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
+  assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
 })

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

@@ -0,0 +1,53 @@
+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 ensureLocaleMessages (i18n, locale) {
+  if (!i18n.availableLocales.includes(locale)) {
+    i18n.setLocaleMessage(locale, await loadLocaleMessages(locale))
+  }
+}

+ 1 - 1
frontend/resources/vue/utils/choiceChecklistHelpers.test.mjs

@@ -13,7 +13,7 @@ const choices = [
   { title: 'Photos', value: 'photos' }
 ]
 
-	test('parseChecklistValue parses JSON-encoded values', () => {
+test('parseChecklistValue parses JSON-encoded values', () => {
   assert.deepEqual(parseChecklistValue('["documents","photos"]'), ['documents', 'photos'])
   assert.deepEqual(parseChecklistValue('["kitchen,bedroom","hallway"]'), ['kitchen,bedroom', 'hallway'])
   assert.deepEqual(parseChecklistValue(''), [])

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

@@ -0,0 +1,23 @@
+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 parts = lowerCandidate.split('-')
+
+    for (let length = parts.length - 1; length > 0; length--) {
+      const prefix = `${parts.slice(0, length).join('-')}-`
+      const match = availableLocales.find(locale => locale.toLowerCase().startsWith(prefix))
+
+      if (match) {
+        return match
+      }
+    }
+  }
+
+  return 'en'
+}

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

@@ -0,0 +1,22 @@
+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 preserves script subtags during fallback', () => {
+  assert.equal(selectBrowserLocale(availableLocales, ['zh-Hant-HK']), 'zh-Hant-TW')
+})
+
+test('selectBrowserLocale uses English when no locale matches', () => {
+  assert.equal(selectBrowserLocale(availableLocales, ['fr-FR']), 'en')
+})

+ 18 - 18
frontend/resources/vue/utils/prefilledArguments.test.mjs

@@ -4,31 +4,31 @@ import assert from 'node:assert/strict'
 import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from './prefilledArguments.js'
 
 test('readPrefilledArgumentsFromNavigation returns navigation state values', () => {
-	assert.deepEqual(
-		readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
-		{ ansible_host: '10.0.0.1' }
-	)
+  assert.deepEqual(
+    readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
+    { ansible_host: '10.0.0.1' }
+  )
 })
 
 test('readPrefilledArgumentsFromNavigation returns empty object when state is absent', () => {
-	assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
-	assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
+  assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
+  assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
 })
 
 test('getInitialArgumentValue prefers navigation state over query params', () => {
-	assert.equal(
-		getInitialArgumentValue(
-			'ansible_host',
-			{ ansible_host: '10.0.0.1' },
-			'?ansible_host=10.0.0.2'
-		),
-		'10.0.0.1'
-	)
+  assert.equal(
+    getInitialArgumentValue(
+      'ansible_host',
+      { ansible_host: '10.0.0.1' },
+      '?ansible_host=10.0.0.2'
+    ),
+    '10.0.0.1'
+  )
 })
 
 test('getInitialArgumentValue falls back to query params when state is absent', () => {
-	assert.equal(
-		getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
-		'10.0.0.2'
-	)
+  assert.equal(
+    getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
+    '10.0.0.2'
+  )
 })

+ 7 - 7
frontend/resources/vue/views/ExecutionView.vue

@@ -355,10 +355,10 @@ async function fetchExecutionResult (executionTrackingIdParam) {
   } catch (err) {
     // Check if it's a "not found" error (404 or similar)
     if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
-	  notFound.value = true
-	  errorMessage.value = err.message || 'The execution could not be found in the system.'
+      notFound.value = true
+      errorMessage.value = err.message || 'The execution could not be found in the system.'
     } else {
-	  renderError(err)
+      renderError(err)
     }
     throw err
   }
@@ -434,7 +434,7 @@ async function renderExecutionResult (res) {
   if (terminal) {
     await terminal.reset()
     await terminal.write(res.logEntry.output, () => {
-	  terminal.fit()
+      terminal.fit()
     })
   }
 }
@@ -488,11 +488,11 @@ onMounted(() => {
   watch(
     () => buttonResults[props.executionTrackingId],
     (newResult, oldResult) => {
-	  if (newResult) {
+      if (newResult) {
         renderExecutionResult({
-		  logEntry: newResult
+          logEntry: newResult
         })
-	  }
+      }
     }
   )
 })

+ 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.

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels