diff --git a/frontend/main.js b/frontend/main.js index 66cbd78..654aa79 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -12,43 +12,84 @@ import { createConnectTransport } from '@connectrpc/connect-web' import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb' import { createApp } from 'vue' +import { createI18n } from 'vue-i18n' + import router from './resources/vue/router.js' import App from './resources/vue/App.vue' +import combinedTranslations from '../lang/combined_output.json' + import { initMarshaller } from './js/marshaller.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({ baseUrl: window.location.protocol + '//' + window.location.host + '/api/' - }) 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) app.use(router) + app.use(i18nSettings) + + // Make i18n instance accessible globally for language switching + window.i18n = i18nSettings.global + app.mount('#app') } -function main () { - initClient() - - // Expose websocket connection function globally so App.vue can call it after successful init +async function main () { window.checkWebsocketConnection = checkWebsocketConnection - setupVue() + const i18nSettings = await initClient() + + setupVue(i18nSettings) initMarshaller() - -// window.addEventListener('EventConfigChanged', fetchGetDashboardComponents) -// window.addEventListener('EventEntityChanged', fetchGetDashboardComponents) } main() // call self diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a8a3291..a987d7f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,9 +17,10 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.8.0", + "picocrank": "^1.8.1", "unplugin-vue-components": "^30.0.0", "vite": "^7.1.12", + "vue-i18n": "^11.1.12", "vue-router": "^4.6.3" }, "devDependencies": { @@ -732,6 +733,50 @@ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "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": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2362,9 +2407,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.0.tgz", - "integrity": "sha512-YPGmXvw7vvjIcgrAe3io87kZDM+NUa+aiEYxk8CVqBzgI4koXeF+2VEGPHBwknZBBEbJfXsSdnxVwXrLKpWKfw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.1.tgz", + "integrity": "sha512-g3JIVY8W5EVDGG+tG83Z0vzhMnj+J/RYkt6/ssdzZofR+6EWfFqE1+DQiyqb6rzjNuj0Y3xy3bpIprtzY9SQ6Q==", "license": "ISC", "dependencies": { "@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": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6e86726..b697736 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,9 +30,10 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.8.0", + "picocrank": "^1.8.1", "unplugin-vue-components": "^30.0.0", "vite": "^7.1.12", + "vue-i18n": "^11.1.12", "vue-router": "^4.6.3" } } diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index fa555b5..309f20a 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -8,7 +8,7 @@ @@ -204,4 +287,51 @@ onMounted(() => { .user-link:hover { 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; +} \ No newline at end of file diff --git a/frontend/resources/vue/Dashboard.vue b/frontend/resources/vue/Dashboard.vue index 0c36c7e..438d111 100644 --- a/frontend/resources/vue/Dashboard.vue +++ b/frontend/resources/vue/Dashboard.vue @@ -105,7 +105,7 @@ function waitForInitAndLoadDashboard() { }, 1000) // Check if init has completed successfully - if (window.initCompleted && window.initResponse) { + if (window.initResponse) { getDashboard() } else if (window.initError) { // Init failed, show error immediately @@ -118,7 +118,7 @@ function waitForInitAndLoadDashboard() { } else { // Init hasn't completed yet, poll for completion checkInitInterval = setInterval(() => { - if (window.initCompleted && window.initResponse) { + if (window.initResponse) { clearInterval(checkInitInterval) checkInitInterval = null getDashboard() diff --git a/frontend/resources/vue/views/LogsListView.vue b/frontend/resources/vue/views/LogsListView.vue index 1753a89..8aa98c2 100644 --- a/frontend/resources/vue/views/LogsListView.vue +++ b/frontend/resources/vue/views/LogsListView.vue @@ -1,12 +1,12 @@ @@ -69,6 +67,7 @@ import { ref, computed, onMounted } from 'vue' import Pagination from '../components/Pagination.vue' import Section from 'picocrank/vue/components/Section.vue' +import { useI18n } from 'vue-i18n' const logs = ref([]) const searchText = ref('') @@ -77,6 +76,8 @@ const currentPage = ref(1) const loading = ref(false) const totalCount = ref(0) +const { t } = useI18n() + const filteredLogs = computed(() => { if (!searchText.value) { return logs.value diff --git a/lang/Makefile b/lang/Makefile new file mode 100644 index 0000000..2c60965 --- /dev/null +++ b/lang/Makefile @@ -0,0 +1,2 @@ +default: + go run main.go \ No newline at end of file diff --git a/lang/README.md b/lang/README.md new file mode 100644 index 0000000..bc18b86 --- /dev/null +++ b/lang/README.md @@ -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. diff --git a/lang/combined_output.json b/lang/combined_output.json new file mode 100644 index 0000000..e7cd1df --- /dev/null +++ b/lang/combined_output.json @@ -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"}}} \ No newline at end of file diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml new file mode 100644 index 0000000..ad7ec6e --- /dev/null +++ b/lang/de-DE.yaml @@ -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 \ No newline at end of file diff --git a/lang/en.yaml b/lang/en.yaml new file mode 100644 index 0000000..2e80a56 --- /dev/null +++ b/lang/en.yaml @@ -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 \ No newline at end of file diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml new file mode 100644 index 0000000..72b28a4 --- /dev/null +++ b/lang/es-ES.yaml @@ -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 \ No newline at end of file diff --git a/lang/go.mod b/lang/go.mod new file mode 100644 index 0000000..5501db6 --- /dev/null +++ b/lang/go.mod @@ -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 diff --git a/lang/go.sum b/lang/go.sum new file mode 100644 index 0000000..60ed9cd --- /dev/null +++ b/lang/go.sum @@ -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= diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml new file mode 100644 index 0000000..8b128f7 --- /dev/null +++ b/lang/it-IT.yaml @@ -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 \ No newline at end of file diff --git a/lang/main.go b/lang/main.go new file mode 100644 index 0000000..7b54844 --- /dev/null +++ b/lang/main.go @@ -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 +} diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml new file mode 100644 index 0000000..9af429a --- /dev/null +++ b/lang/zh-Hans-CN.yaml @@ -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: 过滤当前页面 \ No newline at end of file