chore: Better fixups on lang tool and support

This commit is contained in:
jamesread
2025-11-11 23:24:44 +00:00
parent 7c66170ef5
commit b97dd23abb
10 changed files with 224 additions and 17 deletions

View File

@@ -84,6 +84,7 @@ import { UserCircle02Icon } from '@hugeicons/core-free-icons'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
import logoUrl from '../../OliveTinLogo.png';
import { useI18n } from 'vue-i18n';
import combinedTranslations from '../../../lang/combined_output.json';
const { t, locale } = useI18n();
@@ -129,13 +130,25 @@ const currentLanguageName = computed(() => {
return availableLanguages[languagePreference.value] || languagePreference.value
})
function getBrowserLanguage() {
if (navigator.languages && navigator.languages.length > 0) {
return navigator.languages[0]
}
function normalizeBrowserLanguage() {
const available = Object.keys(combinedTranslations.messages || {})
if (navigator.language) {
return navigator.language
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'
@@ -235,7 +248,7 @@ function changeLanguage() {
if (selectedLanguage.value === 'auto') {
localStorage.removeItem('olivetin-language')
languagePreference.value = 'auto'
window.i18n.locale.value = getBrowserLanguage()
window.i18n.locale.value = normalizeBrowserLanguage()
} else {
window.i18n.locale.value = selectedLanguage.value
localStorage.setItem('olivetin-language', selectedLanguage.value)

View File

@@ -7,7 +7,7 @@
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>
<input :placeholder="t('search-filter')" v-model="searchText" />
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
<button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />

View File

@@ -16,7 +16,7 @@ your browser reports by opening the "Select Language" dialog from the footer.
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
[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.
@@ -24,7 +24,7 @@ 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
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

View File

@@ -1 +1,150 @@
{"messages":{"de-DE":{"connected":"Verbunden","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.blocked":"Blockiert","logs.completed":"Abgeschlossen","logs.exit-code":"Ausführungscode","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.timed-out":"Zeitüberschreitung","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","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.blocked":"Blocked","logs.completed":"Completed","logs.exit-code":"Exit code","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.timed-out":"Timed out","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","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.blocked":"Bloqueado","logs.completed":"Completado","logs.exit-code":"Código de salida","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.timed-out":"Tiempo agotado","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","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.blocked":"Bloccato","logs.completed":"Completato","logs.exit-code":"Codice di uscita","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.timed-out":"Tempo scaduto","logs.timestamp":"Timestamp","logs.title":"Registri","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":"文档","language-dialog.browser-languages":"浏览器语言","language-dialog.close":"关闭","language-dialog.not-available":"不可用","language-dialog.title":"选择语言","login-button":"登录","logs.action":"动作","logs.blocked":"阻塞","logs.completed":"完成","logs.exit-code":"退出代码","logs.metadata":"元数据","logs.no-logs-to-display":"没有日志可显示。","logs.page-description":"这是一个动作执行日志列表。您可以按动作标题过滤列表。","logs.status":"状态","logs.timed-out":"超时","logs.timestamp":"时间戳","logs.title":"日志","nav.actions":"动作","nav.diagnostics":"诊断","nav.entities":"实体","nav.logs":"日志","raise-issue":"在 GitHub 上报告问题","return-to-index":"返回首页","search-filter":"过滤当前页面","welcome":"欢迎使用 OliveTin"}}}
{
"_comment": "This file is generated. Please re-generate this file using 'make' when you update a translation.",
"messages": {
"de-DE": {
"connected": "Verbunden",
"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.blocked": "Blockiert",
"logs.clear-filter": "Suchfilter löschen",
"logs.completed": "Abgeschlossen",
"logs.exit-code": "Ausführungscode",
"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.timed-out": "Zeitüberschreitung",
"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",
"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.blocked": "Blocked",
"logs.clear-filter": "Clear search filter",
"logs.completed": "Completed",
"logs.exit-code": "Exit code",
"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.timed-out": "Timed out",
"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",
"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.blocked": "Bloqueado",
"logs.clear-filter": "Limpiar filtro de búsqueda",
"logs.completed": "Completado",
"logs.exit-code": "Código de salida",
"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": "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",
"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",
"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.blocked": "Bloccato",
"logs.clear-filter": "Cancella filtro di ricerca",
"logs.completed": "Completato",
"logs.exit-code": "Codice di uscita",
"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": "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",
"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": "文档",
"language-dialog.browser-languages": "浏览器语言",
"language-dialog.close": "关闭",
"language-dialog.not-available": "不可用",
"language-dialog.title": "选择语言",
"login-button": "登录",
"logs.action": "动作",
"logs.blocked": "阻塞",
"logs.clear-filter": "清除搜索筛选器",
"logs.completed": "完成",
"logs.exit-code": "退出代码",
"logs.metadata": "元数据",
"logs.no-logs-to-display": "没有日志可显示。",
"logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
"logs.status": "状态",
"logs.timed-out": "超时",
"logs.timestamp": "时间戳",
"logs.title": "日志",
"nav.actions": "动作",
"nav.diagnostics": "诊断",
"nav.entities": "实体",
"nav.logs": "日志",
"raise-issue": "在 GitHub 上报告问题",
"return-to-index": "返回首页",
"search-filter": "过滤当前页面",
"welcome": "欢迎使用 OliveTin"
}
}
}

View File

@@ -20,6 +20,7 @@ translations:
logs.blocked: Blockiert
logs.exit-code: Ausführungscode
logs.completed: Abgeschlossen
logs.clear-filter: Suchfilter löschen
return-to-index: Zurück zur Startseite
search-filter: Filter aktuelle Seite
language-dialog.title: Sprache auswählen

View File

@@ -20,6 +20,7 @@ translations:
logs.blocked: Blocked
logs.exit-code: Exit code
logs.completed: Completed
logs.clear-filter: Clear search filter
return-to-index: Return to index
search-filter: Filter current page
language-dialog.title: Select Language

View File

@@ -11,15 +11,16 @@ translations:
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.timestamp: Marca de tiempo
logs.action: Acción
logs.metadata: Metadatos
logs.status: Status
logs.status: Estado
logs.no-logs-to-display: No hay registros para mostrar.
logs.timed-out: Tiempo agotado
logs.blocked: Bloqueado
logs.exit-code: Código de salida
logs.completed: Completado
logs.clear-filter: Limpiar filtro de búsqueda
return-to-index: Volver a la página principal
search-filter: Filtrar página actual
language-dialog.title: Seleccionar idioma

View File

@@ -11,15 +11,16 @@ translations:
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.timestamp: Date e ora
logs.action: Azione
logs.metadata: Metadati
logs.status: Status
logs.status: Stato
logs.no-logs-to-display: Non ci sono registri da mostrare.
logs.timed-out: Tempo scaduto
logs.blocked: Bloccato
logs.exit-code: Codice di uscita
logs.completed: Completato
logs.clear-filter: Cancella filtro di ricerca
return-to-index: Torna alla pagina principale
search-filter: Filtra la pagina corrente
language-dialog.title: Seleziona lingua

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
@@ -18,13 +19,16 @@ type LanguageFilev1 struct {
}
type CombinedTranslationsOutput struct {
Comment string `json:"_comment"`
Messages map[string]map[string]string `json:"messages"`
}
func main() {
combinedContent := getCombinedLanguageContent()
jsonData, err := json.Marshal(combinedContent)
sortedContent := sortTranslations(combinedContent)
jsonData, err := json.MarshalIndent(sortedContent, "", " ")
if err != nil {
log.Fatalf("Error marshalling combined language content: %v", err)
@@ -39,6 +43,41 @@ func main() {
log.Infof("Combined language content saved to combined_output.json")
}
// sortTranslations creates a new structure with sorted keys for deterministic output.
func sortTranslations(input *CombinedTranslationsOutput) *CombinedTranslationsOutput {
sorted := &CombinedTranslationsOutput{
Comment: input.Comment,
Messages: make(map[string]map[string]string),
}
// Sort language names
langNames := make([]string, 0, len(input.Messages))
for langName := range input.Messages {
langNames = append(langNames, langName)
}
sort.Strings(langNames)
// For each language, sort the translation keys
for _, langName := range langNames {
translations := input.Messages[langName]
sortedTranslations := make(map[string]string)
keys := make([]string, 0, len(translations))
for key := range translations {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
sortedTranslations[key] = translations[key]
}
sorted.Messages[langName] = sortedTranslations
}
return sorted
}
func getLanguageDir() string {
dirsToSearch := []string{
"../lang",
@@ -53,6 +92,7 @@ func getLanguageDir() string {
func getCombinedLanguageContent() *CombinedTranslationsOutput {
output := &CombinedTranslationsOutput{
Comment: "This file is generated. Please re-generate this file using 'make' when you update a translation.",
Messages: make(map[string]map[string]string),
}

View File

@@ -26,3 +26,4 @@ translations:
logs.blocked: 阻塞
logs.exit-code: 退出代码
logs.completed: 完成
logs.clear-filter: 清除搜索筛选器