mirror of
https://github.com/OliveTin/OliveTin
synced 2025-12-12 00:55:34 +00:00
feat: Add translations and language support
This commit is contained in:
@@ -12,43 +12,84 @@ import { createConnectTransport } from '@connectrpc/connect-web'
|
|||||||
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import router from './resources/vue/router.js'
|
import router from './resources/vue/router.js'
|
||||||
import App from './resources/vue/App.vue'
|
import App from './resources/vue/App.vue'
|
||||||
|
|
||||||
|
import combinedTranslations from '../lang/combined_output.json'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
initMarshaller
|
initMarshaller
|
||||||
} from './js/marshaller.js'
|
} from './js/marshaller.js'
|
||||||
|
|
||||||
import { checkWebsocketConnection } from './js/websocket.js'
|
import { checkWebsocketConnection } from './js/websocket.js'
|
||||||
|
|
||||||
function initClient () {
|
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) {
|
||||||
|
return navigator.languages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initClient () {
|
||||||
const transport = createConnectTransport({
|
const transport = createConnectTransport({
|
||||||
baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
|
baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
window.client = createClient(OliveTinApiService, transport)
|
window.client = createClient(OliveTinApiService, transport)
|
||||||
|
window.initResponse = await window.client.init({})
|
||||||
|
|
||||||
|
const i18nSettings = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: getSelectedLanguage(),
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: combinedTranslations.messages,
|
||||||
|
postTranslation: (translated) => {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
if (params.has('debug-translations')) {
|
||||||
|
return '____'
|
||||||
|
} else {
|
||||||
|
return translated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return i18nSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVue () {
|
function setupVue (i18nSettings) {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(i18nSettings)
|
||||||
|
|
||||||
|
// Make i18n instance accessible globally for language switching
|
||||||
|
window.i18n = i18nSettings.global
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
function main () {
|
async function main () {
|
||||||
initClient()
|
|
||||||
|
|
||||||
// Expose websocket connection function globally so App.vue can call it after successful init
|
|
||||||
window.checkWebsocketConnection = checkWebsocketConnection
|
window.checkWebsocketConnection = checkWebsocketConnection
|
||||||
|
|
||||||
setupVue()
|
const i18nSettings = await initClient()
|
||||||
|
|
||||||
|
setupVue(i18nSettings)
|
||||||
|
|
||||||
initMarshaller()
|
initMarshaller()
|
||||||
|
|
||||||
// window.addEventListener('EventConfigChanged', fetchGetDashboardComponents)
|
|
||||||
// window.addEventListener('EventEntityChanged', fetchGetDashboardComponents)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main() // call self
|
main() // call self
|
||||||
|
|||||||
73
frontend/package-lock.json
generated
73
frontend/package-lock.json
generated
@@ -17,9 +17,10 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.8.0",
|
"picocrank": "^1.8.1",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.1.12",
|
||||||
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -732,6 +733,50 @@
|
|||||||
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@intlify/core-base": {
|
||||||
|
"version": "11.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
|
||||||
|
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/message-compiler": "11.1.12",
|
||||||
|
"@intlify/shared": "11.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/message-compiler": {
|
||||||
|
"version": "11.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
|
||||||
|
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/shared": "11.1.12",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/shared": {
|
||||||
|
"version": "11.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
|
||||||
|
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -2362,9 +2407,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picocrank": {
|
"node_modules/picocrank": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.1.tgz",
|
||||||
"integrity": "sha512-YPGmXvw7vvjIcgrAe3io87kZDM+NUa+aiEYxk8CVqBzgI4koXeF+2VEGPHBwknZBBEbJfXsSdnxVwXrLKpWKfw==",
|
"integrity": "sha512-g3JIVY8W5EVDGG+tG83Z0vzhMnj+J/RYkt6/ssdzZofR+6EWfFqE1+DQiyqb6rzjNuj0Y3xy3bpIprtzY9SQ6Q==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugeicons/core-free-icons": "^1.0.16",
|
"@hugeicons/core-free-icons": "^1.0.16",
|
||||||
@@ -3307,6 +3352,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-i18n": {
|
||||||
|
"version": "11.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
|
||||||
|
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/core-base": "11.1.12",
|
||||||
|
"@intlify/shared": "11.1.12",
|
||||||
|
"@vue/devtools-api": "^6.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.6.3",
|
"version": "4.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
||||||
|
|||||||
@@ -30,9 +30,10 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.8.0",
|
"picocrank": "^1.8.1",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.1.12",
|
||||||
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<template #user-info>
|
<template #user-info>
|
||||||
<div class="flex-row user-info" style="gap: .5em;">
|
<div class="flex-row user-info" style="gap: .5em;">
|
||||||
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">Login</router-link></span>
|
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
|
||||||
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
|
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
|
||||||
<span id="username-text">{{ username }}</span>
|
<span id="username-text">{{ username }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -19,35 +19,32 @@
|
|||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<div id="layout">
|
<div id="layout">
|
||||||
<Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation && !initError" />
|
<Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation" />
|
||||||
|
|
||||||
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
||||||
<main title="Main content">
|
<main title="Main content">
|
||||||
<section v-if="initError" class="error-container error" style="text-align: center; padding: 2em;">
|
<router-view :key="$route.fullPath" />
|
||||||
<h2>Failed to Initialize OliveTin</h2>
|
|
||||||
<p><strong>Error Message:</strong> {{ initErrorMessage }}</p>
|
|
||||||
<p>Please check the your browser console first, and then the server logs for more details.</p>
|
|
||||||
<button @click="retryInit" class="bad">Retry</button>
|
|
||||||
</section>
|
|
||||||
<router-view v-else :key="$route.fullPath" />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer title="footer" v-if="showFooter && !initError">
|
<footer title="footer" v-if="showFooter">
|
||||||
<p>
|
<p>
|
||||||
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
|
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
|
||||||
OliveTin {{ currentVersion }}
|
OliveTin {{ currentVersion }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span>
|
<span>
|
||||||
<a href="https://docs.olivetin.app" target="_new">Documentation</a>
|
<a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">Raise an issue on
|
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
|
||||||
GitHub</a>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>{{ serverConnection }}</span>
|
<span>{{ t('connected') }}</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
||||||
@@ -55,10 +52,29 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog ref="languageDialog" class="language-dialog" @click="handleDialogClick">
|
||||||
|
<div class="dialog-content" @click.stop>
|
||||||
|
<h2>Select Language</h2>
|
||||||
|
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
|
||||||
|
<option v-for="(name, code) in availableLanguages" :key="code" :value="code">
|
||||||
|
{{ name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="browser-languages">
|
||||||
|
Browser languages:
|
||||||
|
<span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
|
||||||
|
<span v-else>Not available</span>
|
||||||
|
</p>
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<button @click="closeLanguageDialog">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
|
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
|
||||||
import Header from 'picocrank/vue/components/Header.vue';
|
import Header from 'picocrank/vue/components/Header.vue';
|
||||||
@@ -67,13 +83,16 @@ import { Menu01Icon } from '@hugeicons/core-free-icons'
|
|||||||
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
|
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
|
||||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||||
import logoUrl from '../../OliveTinLogo.png';
|
import logoUrl from '../../OliveTinLogo.png';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const sidebar = ref(null);
|
const sidebar = ref(null);
|
||||||
const username = ref('notset');
|
const username = ref('notset');
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
const serverConnection = ref('Connected');
|
const serverConnection = ref(true);
|
||||||
const currentVersion = ref('?');
|
const currentVersion = ref('?');
|
||||||
const bannerMessage = ref('');
|
const bannerMessage = ref('');
|
||||||
const bannerCss = ref('');
|
const bannerCss = ref('');
|
||||||
@@ -82,10 +101,46 @@ const showFooter = ref(true)
|
|||||||
const showNavigation = ref(true)
|
const showNavigation = ref(true)
|
||||||
const showLogs = ref(true)
|
const showLogs = ref(true)
|
||||||
const showDiagnostics = ref(true)
|
const showDiagnostics = ref(true)
|
||||||
const initError = ref(false)
|
|
||||||
const initErrorMessage = ref('')
|
|
||||||
const showLoginLink = ref(true)
|
const showLoginLink = ref(true)
|
||||||
|
|
||||||
|
const languageDialog = ref(null)
|
||||||
|
const browserLanguages = ref([])
|
||||||
|
|
||||||
|
const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-language') : null
|
||||||
|
const languagePreference = ref(initialLanguagePreference || 'auto')
|
||||||
|
const selectedLanguage = ref(languagePreference.value)
|
||||||
|
|
||||||
|
// Available languages with display names
|
||||||
|
const availableLanguages = {
|
||||||
|
'auto': 'Browser Language',
|
||||||
|
'en': 'English',
|
||||||
|
'de-DE': 'Deutsch',
|
||||||
|
'es-ES': 'Español',
|
||||||
|
'it-IT': 'Italiano',
|
||||||
|
'zh-Hans-CN': '简体中文'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed property to get current language display name
|
||||||
|
const currentLanguageName = computed(() => {
|
||||||
|
if (languagePreference.value === 'auto') {
|
||||||
|
return availableLanguages['auto']
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableLanguages[languagePreference.value] || languagePreference.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function getBrowserLanguage() {
|
||||||
|
if (navigator.languages && navigator.languages.length > 0) {
|
||||||
|
return navigator.languages[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.language) {
|
||||||
|
return navigator.language
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
if (sidebar.value && showNavigation.value) {
|
if (sidebar.value && showNavigation.value) {
|
||||||
sidebar.value.toggle()
|
sidebar.value.toggle()
|
||||||
@@ -93,101 +148,129 @@ function toggleSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateHeaderFromInit() {
|
function updateHeaderFromInit() {
|
||||||
if (window.initResponse) {
|
if (!window.initResponse) {
|
||||||
username.value = window.initResponse.authenticatedUser
|
return
|
||||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
}
|
||||||
currentVersion.value = window.initResponse.currentVersion
|
|
||||||
bannerMessage.value = window.initResponse.bannerMessage || ''
|
|
||||||
bannerCss.value = window.initResponse.bannerCss || ''
|
|
||||||
showFooter.value = window.initResponse.showFooter
|
|
||||||
showNavigation.value = window.initResponse.showNavigation
|
|
||||||
showLogs.value = window.initResponse.showLogList
|
|
||||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
|
||||||
|
|
||||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
username.value = window.initResponse.authenticatedUser
|
||||||
showLoginLink.value = false
|
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||||
}
|
currentVersion.value = window.initResponse.currentVersion
|
||||||
|
bannerMessage.value = window.initResponse.bannerMessage || ''
|
||||||
|
bannerCss.value = window.initResponse.bannerCss || ''
|
||||||
|
showFooter.value = window.initResponse.showFooter
|
||||||
|
showNavigation.value = window.initResponse.showNavigation
|
||||||
|
showLogs.value = window.initResponse.showLogList
|
||||||
|
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||||
|
|
||||||
|
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||||
|
showLoginLink.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebar()
|
||||||
|
|
||||||
|
if (window.checkWebsocketConnection) {
|
||||||
|
window.checkWebsocketConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initResponse.loginRequired) {
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSidebar() {
|
||||||
|
if (!sidebar.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof sidebar.value.clear === 'function') {
|
||||||
|
sidebar.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rootDashboard of initResponse.rootDashboards) {
|
||||||
|
sidebar.value.addNavigationLink({
|
||||||
|
id: rootDashboard,
|
||||||
|
name: rootDashboard,
|
||||||
|
title: rootDashboard,
|
||||||
|
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
||||||
|
icon: DashboardSquare01Icon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.value.addSeparator()
|
||||||
|
sidebar.value.addRouterLink('Entities', t('nav.entities'))
|
||||||
|
|
||||||
|
if (showLogs.value) {
|
||||||
|
sidebar.value.addRouterLink('Logs', t('nav.logs'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDiagnostics.value) {
|
||||||
|
sidebar.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLanguageDialog() {
|
||||||
|
selectedLanguage.value = languagePreference.value
|
||||||
|
|
||||||
|
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||||
|
browserLanguages.value = navigator.languages
|
||||||
|
} else {
|
||||||
|
browserLanguages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageDialog.value) {
|
||||||
|
languageDialog.value.showModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLanguageDialog() {
|
||||||
|
if (languageDialog.value) {
|
||||||
|
languageDialog.value.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeLanguage() {
|
||||||
|
if (!window.i18n || !selectedLanguage.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedLanguage.value === 'auto') {
|
||||||
|
localStorage.removeItem('olivetin-language')
|
||||||
|
languagePreference.value = 'auto'
|
||||||
|
window.i18n.locale.value = getBrowserLanguage()
|
||||||
|
} else {
|
||||||
|
window.i18n.locale.value = selectedLanguage.value
|
||||||
|
localStorage.setItem('olivetin-language', selectedLanguage.value)
|
||||||
|
languagePreference.value = selectedLanguage.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sidebar with new translations
|
||||||
|
if (sidebar.value) {
|
||||||
|
renderSidebar()
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLanguageDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDialogClick(event) {
|
||||||
|
// Close dialog when clicking on the backdrop
|
||||||
|
if (event.target === languageDialog.value) {
|
||||||
|
closeLanguageDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the function to window so other components can call it
|
|
||||||
window.updateHeaderFromInit = updateHeaderFromInit
|
window.updateHeaderFromInit = updateHeaderFromInit
|
||||||
|
|
||||||
async function requestInit() {
|
|
||||||
try {
|
|
||||||
const initResponse = await window.client.init({})
|
|
||||||
|
|
||||||
// Store init response first so the login view can read options (e.g., authLocalLogin)
|
|
||||||
window.initResponse = initResponse
|
|
||||||
|
|
||||||
// Check if login is required and redirect if so (after storing initResponse)
|
|
||||||
if (initResponse.loginRequired) {
|
|
||||||
router.push('/login')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
window.initError = false
|
|
||||||
window.initErrorMessage = ''
|
|
||||||
window.initCompleted = true
|
|
||||||
|
|
||||||
window.updateHeaderFromInit()
|
|
||||||
|
|
||||||
if (showNavigation.value && sidebar.value) {
|
|
||||||
for (const rootDashboard of initResponse.rootDashboards) {
|
|
||||||
sidebar.value.addNavigationLink({
|
|
||||||
id: rootDashboard,
|
|
||||||
name: rootDashboard,
|
|
||||||
title: rootDashboard,
|
|
||||||
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
|
||||||
icon: DashboardSquare01Icon,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebar.value.addSeparator()
|
|
||||||
sidebar.value.addRouterLink('Entities')
|
|
||||||
|
|
||||||
if (showLogs.value) {
|
|
||||||
sidebar.value.addRouterLink('Logs')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDiagnostics.value) {
|
|
||||||
sidebar.value.addRouterLink('Diagnostics')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLoaded.value = true;
|
|
||||||
initError.value = false;
|
|
||||||
|
|
||||||
// Only start websocket connection after successful init
|
|
||||||
if (window.checkWebsocketConnection) {
|
|
||||||
window.checkWebsocketConnection()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error initializing client", error)
|
|
||||||
initError.value = true
|
|
||||||
initErrorMessage.value = error.message || 'Failed to connect to OliveTin server'
|
|
||||||
window.initError = true
|
|
||||||
window.initErrorMessage = error.message || 'Failed to connect to OliveTin server'
|
|
||||||
window.initCompleted = false
|
|
||||||
serverConnection.value = 'Disconnected'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryInit() {
|
|
||||||
initError.value = false
|
|
||||||
initErrorMessage.value = ''
|
|
||||||
window.initError = false
|
|
||||||
window.initErrorMessage = ''
|
|
||||||
window.initCompleted = false
|
|
||||||
requestInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
serverConnection.value = 'Connected';
|
serverConnection.value = true;
|
||||||
// Initialize global state
|
updateHeaderFromInit()
|
||||||
window.initError = false
|
|
||||||
window.initErrorMessage = ''
|
// Initialize selected language from stored preference
|
||||||
window.initCompleted = false
|
selectedLanguage.value = languagePreference.value
|
||||||
requestInit()
|
|
||||||
|
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||||
|
browserLanguages.value = navigator.languages
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -204,4 +287,51 @@ onMounted(() => {
|
|||||||
.user-link:hover {
|
.user-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-dialog {
|
||||||
|
border: 1px solid var(--border-color, #ccc);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dialog::backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid var(--border-color, #ccc);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-languages {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--fg2, #555);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -105,7 +105,7 @@ function waitForInitAndLoadDashboard() {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
// Check if init has completed successfully
|
// Check if init has completed successfully
|
||||||
if (window.initCompleted && window.initResponse) {
|
if (window.initResponse) {
|
||||||
getDashboard()
|
getDashboard()
|
||||||
} else if (window.initError) {
|
} else if (window.initError) {
|
||||||
// Init failed, show error immediately
|
// Init failed, show error immediately
|
||||||
@@ -118,7 +118,7 @@ function waitForInitAndLoadDashboard() {
|
|||||||
} else {
|
} else {
|
||||||
// Init hasn't completed yet, poll for completion
|
// Init hasn't completed yet, poll for completion
|
||||||
checkInitInterval = setInterval(() => {
|
checkInitInterval = setInterval(() => {
|
||||||
if (window.initCompleted && window.initResponse) {
|
if (window.initResponse) {
|
||||||
clearInterval(checkInitInterval)
|
clearInterval(checkInitInterval)
|
||||||
checkInitInterval = null
|
checkInitInterval = null
|
||||||
getDashboard()
|
getDashboard()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Section title="Logs" :padding="false">
|
<Section :title="t('logs.title')" :padding="false">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<label class="input-with-icons">
|
<label class="input-with-icons">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor"
|
<path fill="currentColor"
|
||||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||||
</svg>
|
</svg>
|
||||||
<input placeholder="Filter current page" v-model="searchText" />
|
<input :placeholder="t('search-filter')" v-model="searchText" />
|
||||||
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
|
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor"
|
<path fill="currentColor"
|
||||||
@@ -16,26 +16,24 @@
|
|||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<p class = "padding">This is a list of logs from actions that have been executed. You can filter the list by action title.</p>
|
<p class = "padding">{{ t('logs.page-description') }}</p>
|
||||||
<div v-show="filteredLogs.length > 0">
|
<div v-show="filteredLogs.length > 0">
|
||||||
<table class="logs-table">
|
<table class="logs-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Timestamp</th>
|
<th>{{ t('logs.timestamp') }}</th>
|
||||||
<th>Action</th>
|
<th>{{ t('logs.action') }}</th>
|
||||||
<th>Metadata</th>
|
<th>{{ t('logs.metadata') }}</th>
|
||||||
<th>Status</th>
|
<th>{{ t('logs.status') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||||
<td>
|
|
||||||
<span class="icon" v-html="log.actionIcon"></span>
|
<span class="icon" v-html="log.actionIcon"></span>
|
||||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||||
{{ log.actionTitle }}
|
{{ log.actionTitle }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
|
||||||
<td class="tags">
|
<td class="tags">
|
||||||
<span class="annotation">
|
<span class="annotation">
|
||||||
<span class="annotation-key">User:</span>
|
<span class="annotation-key">User:</span>
|
||||||
@@ -59,8 +57,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="logs.length === 0" class="empty-state">
|
<div v-show="logs.length === 0" class="empty-state">
|
||||||
<p>There are no logs to display.</p>
|
<p>{{ t('logs.no-logs-to-display') }}</p>
|
||||||
<router-link to="/">Return to index</router-link>
|
<router-link to="/">{{ t('return-to-index') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
@@ -69,6 +67,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import Pagination from '../components/Pagination.vue'
|
import Pagination from '../components/Pagination.vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const logs = ref([])
|
const logs = ref([])
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
@@ -77,6 +76,8 @@ const currentPage = ref(1)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
if (!searchText.value) {
|
if (!searchText.value) {
|
||||||
return logs.value
|
return logs.value
|
||||||
|
|||||||
2
lang/Makefile
Normal file
2
lang/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
default:
|
||||||
|
go run main.go
|
||||||
36
lang/README.md
Normal file
36
lang/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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".
|
||||||
|
|
||||||
|
It would be most welcome to have human contributors who are native speakers improve these
|
||||||
|
translations, or add new ones.
|
||||||
|
|
||||||
|
## How to contribute
|
||||||
|
|
||||||
|
If a translation file does not exist for your locale, you can create one by copying
|
||||||
|
en.yaml and changing the locale code in the filename. You can view the language that
|
||||||
|
your browser reports by opening the "Select Language" dialog from the footer.
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Internally, OliveTin uses the vue-i18n library for translations. This does support
|
||||||
|
language pluralization and other advanced features. For docs, check the following;
|
||||||
|
|
||||||
|
https://vue-i18n.intlify.dev/guide/essentials/pluralization.html
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
request with your improvements.
|
||||||
|
|
||||||
|
As always, if you need any help, please feel free to raise an issue on GitHub or
|
||||||
|
jump into the Discord server for OliveTin.
|
||||||
1
lang/combined_output.json
Normal file
1
lang/combined_output.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"messages":{"de-DE":{"connected":"Verbunden","docs":"Dokumentation","login-button":"Login","logs.action":"Aktion","logs.metadata":"Metadaten","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.status":"Status","logs.timestamp":"Zeitstempel","logs.title":"Protokolle","nav.actions":"Aktionen","nav.diagnostics":"Diagnostik","nav.entities":"Entitäten","nav.logs":"Protokolle","raise-issue":"Ein Problem melden auf GitHub","return-to-index":"Zurück zur Startseite","search-filter":"Filter aktuelle Seite","welcome":"Willkommen bei OliveTin"},"en":{"connected":"Connected","docs":"Documentation","login-button":"Login","logs.action":"Action","logs.metadata":"Metadata","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. You can filter the list by action title.","logs.status":"Status","logs.timestamp":"Timestamp","logs.title":"Logs","nav.actions":"Actions","nav.diagnostics":"Diagnostics","nav.entities":"Entities","nav.logs":"Logs","raise-issue":"Raise an issue on GitHub","return-to-index":"Return to index","search-filter":"Filter current page","welcome":"Welcome to OliveTin"},"es-ES":{"connected":"Conectado","docs":"Documentación","login-button":"Iniciar sesión","logs.action":"Acción","logs.metadata":"Metadatos","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.status":"Status","logs.timestamp":"Timestamp","logs.title":"Registros","nav.actions":"Acciones","nav.diagnostics":"Diagnósticos","nav.entities":"Entidades","nav.logs":"Registros","raise-issue":"Reportar un problema en GitHub","return-to-index":"Volver a la página principal","search-filter":"Filtrar página actual","welcome":"Bienvenido a OliveTin"},"it-IT":{"connected":"Connesso","docs":"Documentazione","login-button":"Login","logs.action":"Azione","logs.metadata":"Metadati","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.status":"Status","logs.timestamp":"Timestamp","logs.title":"Registri","logs.user":"Utente","nav.actions":"Azioni","nav.diagnostics":"Diagnostica","nav.entities":"Entità","nav.logs":"Registri","raise-issue":"Segnala un problema su GitHub","return-to-index":"Torna alla pagina principale","search-filter":"Filtra la pagina corrente","welcome":"Benvenuto in OliveTin"},"zh-Hans-CN":{"connected":"已连接","docs":"文档","login-button":"登录","logs.action":"动作","logs.metadata":"元数据","logs.no-logs-to-display":"没有日志可显示。","logs.page-description":"这是一个动作执行日志列表。您可以按动作标题过滤列表。","logs.status":"状态","logs.timestamp":"时间戳","logs.title":"日志","nav.actions":"动作","nav.diagnostics":"诊断","nav.entities":"实体","nav.logs":"日志","raise-issue":"报告问题 on GitHub","return-to-index":"返回首页","search-filter":"过滤当前页面","welcome":"欢迎使用 OliveTin"}}}
|
||||||
20
lang/de-DE.yaml
Normal file
20
lang/de-DE.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
schemaVersion: 1
|
||||||
|
translations:
|
||||||
|
welcome: Willkommen bei OliveTin
|
||||||
|
nav.actions: Aktionen
|
||||||
|
nav.logs: Protokolle
|
||||||
|
nav.entities: Entitäten
|
||||||
|
nav.diagnostics: Diagnostik
|
||||||
|
connected: Verbunden
|
||||||
|
login-button: Login
|
||||||
|
raise-issue: Ein Problem melden auf GitHub
|
||||||
|
docs: Dokumentation
|
||||||
|
logs.title: Protokolle
|
||||||
|
logs.page-description: Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.
|
||||||
|
logs.timestamp: Zeitstempel
|
||||||
|
logs.action: Aktion
|
||||||
|
logs.metadata: Metadaten
|
||||||
|
logs.status: Status
|
||||||
|
logs.no-logs-to-display: Es gibt keine Protokolle zu anzeigen.
|
||||||
|
return-to-index: Zurück zur Startseite
|
||||||
|
search-filter: Filter aktuelle Seite
|
||||||
20
lang/en.yaml
Normal file
20
lang/en.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
schemaVersion: 1
|
||||||
|
translations:
|
||||||
|
welcome: Welcome to OliveTin
|
||||||
|
docs: Documentation
|
||||||
|
raise-issue: Raise an issue on GitHub
|
||||||
|
nav.actions: Actions
|
||||||
|
nav.logs: Logs
|
||||||
|
nav.entities: Entities
|
||||||
|
nav.diagnostics: Diagnostics
|
||||||
|
connected: Connected
|
||||||
|
login-button: Login
|
||||||
|
logs.title: Logs
|
||||||
|
logs.page-description: This is a list of logs from actions that have been executed. You can filter the list by action title.
|
||||||
|
logs.timestamp: Timestamp
|
||||||
|
logs.action: Action
|
||||||
|
logs.metadata: Metadata
|
||||||
|
logs.status: Status
|
||||||
|
logs.no-logs-to-display: There are no logs to display.
|
||||||
|
return-to-index: Return to index
|
||||||
|
search-filter: Filter current page
|
||||||
20
lang/es-ES.yaml
Normal file
20
lang/es-ES.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
schemaVersion: 1
|
||||||
|
translations:
|
||||||
|
welcome: Bienvenido a OliveTin
|
||||||
|
nav.actions: Acciones
|
||||||
|
nav.logs: Registros
|
||||||
|
nav.entities: Entidades
|
||||||
|
nav.diagnostics: Diagnósticos
|
||||||
|
connected: Conectado
|
||||||
|
login-button: Iniciar sesión
|
||||||
|
raise-issue: Reportar un problema en GitHub
|
||||||
|
docs: Documentación
|
||||||
|
logs.title: Registros
|
||||||
|
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.timestamp: Timestamp
|
||||||
|
logs.action: Acción
|
||||||
|
logs.metadata: Metadatos
|
||||||
|
logs.status: Status
|
||||||
|
logs.no-logs-to-display: No hay registros para mostrar.
|
||||||
|
return-to-index: Volver a la página principal
|
||||||
|
search-filter: Filtrar página actual
|
||||||
11
lang/go.mod
Normal file
11
lang/go.mod
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module github.com/OliveTin/OliveTin/langtool
|
||||||
|
|
||||||
|
go 1.24.9
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||||
20
lang/go.sum
Normal file
20
lang/go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
|
||||||
|
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
21
lang/it-IT.yaml
Normal file
21
lang/it-IT.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
schemaVersion: 1
|
||||||
|
translations:
|
||||||
|
welcome: Benvenuto in OliveTin
|
||||||
|
nav.actions: Azioni
|
||||||
|
nav.logs: Registri
|
||||||
|
nav.entities: Entità
|
||||||
|
nav.diagnostics: Diagnostica
|
||||||
|
docs: Documentazione
|
||||||
|
connected: Connesso
|
||||||
|
login-button: Login
|
||||||
|
raise-issue: Segnala un problema su GitHub
|
||||||
|
logs.title: Registri
|
||||||
|
logs.page-description: Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.
|
||||||
|
logs.timestamp: Timestamp
|
||||||
|
logs.action: Azione
|
||||||
|
logs.metadata: Metadati
|
||||||
|
logs.status: Status
|
||||||
|
logs.no-logs-to-display: Non ci sono registri da mostrare.
|
||||||
|
return-to-index: Torna alla pagina principale
|
||||||
|
search-filter: Filtra la pagina corrente
|
||||||
|
logs.user: Utente
|
||||||
125
lang/main.go
Normal file
125
lang/main.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/jamesread/golure/pkg/dirs"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LanguageFilev1 struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
Translations map[string]string `json:"translations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CombinedTranslationsOutput struct {
|
||||||
|
Messages map[string]map[string]string `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
combinedContent := getCombinedLanguageContent()
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(combinedContent)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error marshalling combined language content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile("combined_output.json", jsonData, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error saving combined language content to file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Combined language content saved to combined_output.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLanguageDir() string {
|
||||||
|
dirsToSearch := []string{
|
||||||
|
"../lang",
|
||||||
|
"../../../../lang/", // Relative to this file, for unit tests
|
||||||
|
"/app/lang/",
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, _ := dirs.GetFirstExistingDirectory("lang", dirsToSearch)
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCombinedLanguageContent() *CombinedTranslationsOutput {
|
||||||
|
output := &CombinedTranslationsOutput{
|
||||||
|
Messages: make(map[string]map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
languageDir := getLanguageDir()
|
||||||
|
|
||||||
|
files, err := os.ReadDir(languageDir)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error reading language directory %s: %v", languageDir, err)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range filterLanguageFiles(files) {
|
||||||
|
languageName := strings.Replace(file.Name(), ".yaml", "", 1)
|
||||||
|
|
||||||
|
fullPath := filepath.Join(languageDir, file.Name())
|
||||||
|
log.Infof("Loading language file: %s", fullPath)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error reading language file %s: %v", fullPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamlData LanguageFilev1
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(content, &yamlData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error reading language file %s: %v", fullPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Messages[languageName] = yamlData.Translations
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterLanguageFiles(files []os.DirEntry) []os.DirEntry {
|
||||||
|
ret := make([]os.DirEntry, 0)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(file.Name(), ".yaml") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAcceptLanguages(headerLanguage string) []string {
|
||||||
|
acceptLanguages := make([]string, 0)
|
||||||
|
|
||||||
|
for _, lang := range strings.Split(headerLanguage, ",") {
|
||||||
|
lang = strings.TrimSpace(lang)
|
||||||
|
|
||||||
|
acceptLanguages = append(acceptLanguages, lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acceptLanguages
|
||||||
|
}
|
||||||
20
lang/zh-Hans-CN.yaml
Normal file
20
lang/zh-Hans-CN.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
schemaVersion: 1
|
||||||
|
translations:
|
||||||
|
welcome: 欢迎使用 OliveTin
|
||||||
|
nav.actions: 动作
|
||||||
|
nav.logs: 日志
|
||||||
|
nav.entities: 实体
|
||||||
|
nav.diagnostics: 诊断
|
||||||
|
connected: 已连接
|
||||||
|
login-button: 登录
|
||||||
|
raise-issue: 报告问题 on GitHub
|
||||||
|
docs: 文档
|
||||||
|
logs.title: 日志
|
||||||
|
logs.page-description: 这是一个动作执行日志列表。您可以按动作标题过滤列表。
|
||||||
|
logs.timestamp: 时间戳
|
||||||
|
logs.action: 动作
|
||||||
|
logs.metadata: 元数据
|
||||||
|
logs.status: 状态
|
||||||
|
logs.no-logs-to-display: 没有日志可显示。
|
||||||
|
return-to-index: 返回首页
|
||||||
|
search-filter: 过滤当前页面
|
||||||
Reference in New Issue
Block a user