From c41c25c9237bd198e70b20551e048dba9054f265 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Sat, 4 Apr 2026 09:50:27 +0200 Subject: [PATCH 1/5] feat(i18n): add pt-PT locale support and browser locale detection - add pt-PT locale files for frontend and Tauri - register pt-PT in Vue i18n - improve browser locale detection for pt-PT - keep compatibility with pt-BR and zh-TW expanded locales - validate translation keys against en.json - no missing or extra keys detected --- src-tauri/locales/pt-PT.json | 48 ++ src/helpers/subtitles/languages.ts | 39 +- src/i18n.ts | 55 +- src/locales/pt-PT.json | 970 +++++++++++++++++++++++++++++ 4 files changed, 1090 insertions(+), 22 deletions(-) create mode 100644 src-tauri/locales/pt-PT.json create mode 100644 src/locales/pt-PT.json diff --git a/src-tauri/locales/pt-PT.json b/src-tauri/locales/pt-PT.json new file mode 100644 index 00000000..26d37932 --- /dev/null +++ b/src-tauri/locales/pt-PT.json @@ -0,0 +1,48 @@ +{ + "tray": { + "quit": "Sair", + "showOrHide": "Mostrar/Ocultar janela", + "addToQueue": "Adicionar da área de transferência", + "downloadQueue": "Transferir fila", + "shortcuts": { + "ctrlShiftV": "Ctrl + Shift + V", + "ctrlShiftEnter": "Ctrl + Shift + Enter", + "altShiftV": "Alt + Shift + V", + "altShiftEnter": "Alt + Shift + Enter" + } + }, + "notifications": { + "queueAdded": { + "title": "Adicionado à fila", + "body": "{title}" + }, + "queueDownloading": { + "title": "A transferir a fila", + "body": "A transferir {n} item | A transferir {n} itens" + }, + "queueFinished": { + "title": "Fila concluída", + "body": "Transferência de {n} item concluída | Transferência de {n} itens concluída" + }, + "videoFinished": { + "title": "Transferência concluída", + "body": "{title}" + }, + "playlistFinished": { + "title": "Playlist concluída", + "body": "{title}" + }, + "videoReady": { + "title": "Pronto a transferir", + "body": "{title}" + }, + "playlistReady": { + "title": "Playlist pronta a transferir", + "body": "{title} | {title} ({n} item) | {title} ({n} itens)" + }, + "downloadFailed": { + "title": "Falha na transferência", + "body": "{title}\n{message}" + } + } +} \ No newline at end of file diff --git a/src/helpers/subtitles/languages.ts b/src/helpers/subtitles/languages.ts index 1d3346a6..84e32bb7 100644 --- a/src/helpers/subtitles/languages.ts +++ b/src/helpers/subtitles/languages.ts @@ -7,48 +7,59 @@ export interface SubtitleLanguageOption { } const codes = ISO6391.getAllCodes(); -const expanded_codes: Record = { + +const expandedCodes: Record = { + 'pt-PT': { code: 'pt-PT', englishName: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)' }, 'pt-BR': { code: 'pt-BR', englishName: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)' }, 'zh-TW': { code: 'zh-TW', englishName: 'Traditional Chinese (Taiwan)', nativeName: '繁體中文(台灣)' }, }; -export const languageOptions: SubtitleLanguageOption[] = [...codes, ...Object.keys(expanded_codes)] +export const languageOptions: SubtitleLanguageOption[] = [...codes, ...Object.keys(expandedCodes)] .map((code) => { - if (expanded_codes[code]) return expanded_codes[code]; - - const englishName = ISO6391.getName(code); - const nativeName = ISO6391.getNativeName(code); + if (expandedCodes[code]) return expandedCodes[code]; return { code, - englishName, - nativeName, + englishName: ISO6391.getName(code), + nativeName: ISO6391.getNativeName(code), }; }) .sort((a, b) => a.englishName.localeCompare(b.englishName)); export const languageOptionsLookup = new Map( - languageOptions.map(option => [option.code, option] as const), + languageOptions.map((option) => [option.code, option] as const), ); +function normalizeLocale(candidate: string): string { + const parts = candidate.replace(/_/g, '-').split('-'); + const language = parts[0]?.toLowerCase(); + const region = parts[1]?.toUpperCase(); + + return region ? `${language}-${region}` : language; +} + export function detectBrowserLanguageCodes(): string[] { let candidates = navigator?.languages ?? []; - if (candidates.length === 0) { + if (candidates.length === 0 && navigator?.language) { candidates = [navigator.language]; } const normalized = new Set(); for (const candidate of candidates ?? []) { - if (!candidate) { - continue; + if (!candidate) continue; + + const locale = normalizeLocale(candidate); + + if (expandedCodes[locale]) { + normalized.add(locale); } - const isoCode = candidate.split('-')[0]?.toLowerCase(); + const isoCode = locale.split('-')[0]; if (isoCode && ISO6391.validate(isoCode)) { normalized.add(isoCode); } } return Array.from(normalized); -} +} \ No newline at end of file diff --git a/src/i18n.ts b/src/i18n.ts index 14982cc2..5a184b8e 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -8,27 +8,65 @@ import de from './locales/de.json'; import nb from './locales/nb.json'; import ru from './locales/ru.json'; import tr from './locales/tr.json'; +import ptPT from './locales/pt-PT.json'; import ptBR from './locales/pt-BR.json'; import zhTW from './locales/zh-TW.json'; import { detectBrowserLanguageCodes } from './helpers/subtitles/languages.ts'; -export const availableLocales: Record = { - 'en': true, 'es': true, 'nl': true, 'it': true, 'fr': true, 'de': true, 'nb': true, 'ru': true, 'tr': true, 'pt-BR': true, 'zh-TW': true, +export const availableLocales = { + en: true, + es: true, + nl: true, + it: true, + fr: true, + de: true, + nb: true, + ru: true, + tr: true, + 'pt-PT': true, + 'pt-BR': true, + 'zh-TW': true, } as const; type MessageSchema = typeof en; export type Locale = keyof typeof availableLocales; -export function getDefaultLocale() { - const browserLocale = detectBrowserLanguageCodes()[0]; - if (availableLocales[browserLocale]) { - return browserLocale; +const localeAliases: Record = { + pt: 'pt-PT', + 'pt-PT': 'pt-PT', + 'pt-BR': 'pt-BR', + zh: 'zh-TW', + 'zh-Hant': 'zh-TW', + 'zh-TW': 'zh-TW', + no: 'nb', + 'nb-NO': 'nb', +}; + +export function getDefaultLocale(): Locale { + for (const code of detectBrowserLanguageCodes()) { + if (code in availableLocales) { + return code as Locale; + } + + if (code in localeAliases) { + return localeAliases[code]; + } + + const baseCode = code.split('-')[0]; + if (baseCode in availableLocales) { + return baseCode as Locale; + } + + if (baseCode in localeAliases) { + return localeAliases[baseCode]; + } } + return 'en'; } export const i18n: I18n = createI18n<[MessageSchema], Locale>({ - locale: 'en', + locale: getDefaultLocale(), legacy: false, globalInjection: false, fallbackLocale: 'en', @@ -42,7 +80,8 @@ export const i18n: I18n = createI18n<[MessageSchema], Locale>({ nb, ru, tr, + 'pt-PT': ptPT, 'pt-BR': ptBR, 'zh-TW': zhTW, }, -}); +}); \ No newline at end of file diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json new file mode 100644 index 00000000..402a425f --- /dev/null +++ b/src/locales/pt-PT.json @@ -0,0 +1,970 @@ +{ + "common": { + "download": "Transferir", + "save": "Guardar", + "later": "Mais tarde", + "load": "Carregar", + "install": "Instalar", + "percentage": "{percent}%", + "add": "Adicionar", + "back": "Voltar", + "continue": "Continuar", + "retry": "Tentar novamente", + "enable": "Ativar", + "pleaseWait": "Aguarda…", + "loading": "A carregar", + "emptySearch": "Nenhum resultado encontrado.", + "parenthesis": "({content})", + "divide": "{left} / {right}", + "video": "vídeo", + "unknown": "desconhecido", + "confirm": "Confirmar", + "best": "Melhor", + "resume": "Retomar" + }, + "layout": { + "header": { + "placeholder": "Insere um URL de vídeo/playlist para adicionar à fila", + "nav": { + "settings": "Definições da aplicação" + } + }, + "footer": { + "progress": { + "downloading": "A transferir a fila - {done} de {total} concluídos.", + "completed": "Transferência concluída – {n} item transferido. | Transferência concluída – {n} itens transferidos.", + "ready": "Pronto a transferir! {n} item na fila. | Pronto a transferir! {n} itens na fila." + }, + "nav": { + "downloadLocation": "Localizações", + "login": { + "screenReader": { + "on": "Editar autenticação (sessão iniciada)", + "off": "Editar autenticação (sessão não iniciada)" + }, + "tooltip": { + "on": "Editar autenticação", + "off": "Iniciar sessão" + } + }, + "subtitles": { + "screenReader": { + "on": "Editar legendas (ativadas)", + "off": "Editar legendas (desativadas)" + }, + "tooltip": "Legendas" + } + }, + "format": { + "trackSelect": { + "screenReader": "Selecionar faixa para todos os vídeos" + }, + "formatSelect": { + "noFormats": "Sem formatos", + "placeholder": "Selecionar formato", + "screenReader": "Selecionar formato para todos os vídeos" + } + }, + "queue": { + "empty": "Fila vazia", + "resume": "Retomar tudo", + "pause": "Pausar tudo", + "clearSuccessful": "Limpar concluídos", + "clearErrored": "Limpar com erro", + "clearPending": "Limpar pendentes", + "cancelDownloading": "Cancelar transferência", + "more": "Mais ações da fila" + } + } + }, + "auth": { + "title": "Autenticação", + "cookies": { + "legend": "Cookies", + "legendBadge": "Recomendado", + "legendLabel": "Utiliza sessões iniciadas a partir de um ficheiro de cookies ou importa-as do teu navegador.", + "file": { + "label": "Carregar cookies de um ficheiro cookies.txt", + "hint": "Para exportares os teus cookies para um ficheiro, {link}", + "hintLink": "consulta o guia." + }, + "browser": { + "label": "Carregar automaticamente cookies a partir de um navegador:", + "placeholder": "Não carregar cookies de um navegador", + "hint": "Alguns navegadores podem não suportar o carregamento de cookies. Consulta a {link}", + "hintLink": "lista de navegadores compatíveis." + } + }, + "credentials": { + "title": "Métodos avançados", + "description": "Utiliza métodos como autenticação básica, bearer tokens, cabeçalhos, palavra-passe do vídeo, etc.", + "labels": { + "username": "Nome de utilizador", + "password": "Palavra-passe", + "token": "Token", + "headers": "Cabeçalhos" + }, + "basicAuth": { + "legend": "Autenticação básica", + "legendLabel": "Utilizada em sites com autenticação simples." + }, + "videoPassword": { + "legend": "Palavra-passe do vídeo", + "legendLabel": "Fornece uma palavra-passe para aceder a vídeos protegidos." + }, + "bearerToken": { + "legend": "Bearer token", + "legendLabel": "Fornece um bearer token para aceder a recursos privados." + }, + "customHeaders": { + "legend": "Cabeçalhos personalizados", + "legendBadge": "Avançado", + "legendLabel": "Um por linha: CAMPO:VALOR", + "placeholder": "X-Api-Key:…" + } + }, + "init": { + "title": "Armazenamento de segredos não ativado", + "description": "Para utilizares métodos de autenticação avançados, o armazenamento de segredos tem de ser ativado.", + "hint": "O armazenamento de segredos utiliza o gestor de credenciais do teu sistema. Isto pode exigir uma palavra-passe.", + "error": { + "title": "Não foi possível ativar o armazenamento de segredos", + "description": "Ocorreu um erro ao ativar o armazenamento de segredos. Tenta novamente.", + "hint": "Erro: {error}", + "unknown": "Erro desconhecido" + }, + "toasts": { + "error": "Não foi possível inicializar o armazenamento de segredos: {error}" + } + }, + "toasts": { + "error": "Não foi possível guardar as definições de autenticação. {error}", + "saved": "Definições de autenticação guardadas!" + } + }, + "media": { + "view": { + "tabs": { + "metadata": "Metadados", + "logs": "Registos" + }, + "thumbnailAlt": "Miniatura de {title}", + "metadata": { + "uploader": "Autor:", + "extractor": "Extrator:", + "link": "Link:", + "chapters": "Capítulos", + "description": "Descrição" + }, + "stats": { + "title": "Estatísticas do vídeo", + "views": "Visualizações", + "likes": "Gostos", + "dislikes": "Não gosto", + "comments": "Comentários", + "rating": "Classificação", + "duration": "Duração" + }, + "logs": { + "failure": "Falha", + "errors": { + "count": "Erros ({count})", + "empty": "Nenhum erro encontrado." + }, + "warnings": { + "count": "Avisos ({count})", + "empty": "Nenhum aviso encontrado." + }, + "all": { + "title": "Todos os registos", + "empty": "Nenhum registo encontrado." + }, + "toasts": { + "reported": "O erro foi reportado.", + "error": "Não foi possível reportar o erro." + } + } + }, + "preferences": { + "badges": { + "override": "Substituir" + }, + "labels": { + "quality": "Escolhe as preferências de formato, codec e faixas.", + "network": "Configura proxy, mascaramento de cliente e cabeçalhos de pedido. Substitui as predefinições da aplicação para esta transferência.", + "output": "Configura contentor, conversão e modelos de nome de ficheiro. Substitui as predefinições da aplicação para esta transferência.", + "headersOverride": "Um por linha: CAMPO:VALOR. Substitui os cabeçalhos globais para esta transferência." + }, + "hints": { + "format": "Escolhe a qualidade de vídeo ou áudio. Normalmente, maior qualidade significa ficheiros maiores e transferências mais longas.", + "encodings": "Escolhe os codecs preferidos. Útil para compatibilidade de reprodução ou para manter a qualidade original.", + "tracks": "Escolhe variantes de idioma ou canal quando disponíveis, por exemplo áudio dobrado." + }, + "tabs": { + "quality": "Qualidade", + "network": "Rede" + } + }, + "steps": { + "fetch": { + "progress": "{percentage}% — {done} de {total}", + "progressLabel": "A obter metadados…" + }, + "configure": { + "format": { + "trackSelect": { + "screenReader": "Selecionar faixa a transferir" + }, + "formatSelect": { + "noFormats": "Sem formatos", + "placeholder": "Selecionar formato", + "screenReader": "Selecionar formato da faixa" + } + }, + "encodings": { + "audioSelect": { + "noFormats": "Sem codificações de áudio", + "placeholder": "Codec de áudio", + "screenReader": "Selecionar codificação de áudio" + }, + "videoSelect": { + "noFormats": "Sem codificações de vídeo", + "placeholder": "Codec de vídeo", + "screenReader": "Selecionar codificação de vídeo" + } + }, + "tracks": { + "audioSelect": { + "noFormats": "Sem faixas de áudio", + "placeholder": "Faixa de áudio", + "screenReader": "Selecionar faixa de áudio" + }, + "videoSelect": { + "noFormats": "Sem faixas de vídeo", + "placeholder": "Faixa de vídeo", + "screenReader": "Selecionar faixa de vídeo" + } + }, + "metadata": { + "duration": "Duração: {duration}", + "size": "Tamanho: ", + "sizeInfo": "Quantidade de dados que será transferida.\nNão é o tamanho final do ficheiro.", + "items": "Itens: {amount} {failedCount}", + "failedCount": "({amount} com falha)" + }, + "trackTypes": { + "both": "Vídeo + Áudio", + "audio": "Áudio", + "video": "Vídeo" + } + }, + "download": { + "progress": "A transferir {category}… — {percentage}%", + "progressList": "A transferir… {percentage}% — {done} de {total}", + "indeterminate": "{status}…", + "category": { + "video": "vídeo", + "audio": "áudio", + "subtitles": "legendas", + "thumbnail": "miniatura", + "metadata": "metadados", + "other": "outro" + }, + "status": { + "initializing": "A inicializar", + "downloading": "A transferir", + "merging": "A combinar", + "finalizing": "A limpar" + }, + "metadata": { + "eta": "ETA: {eta}", + "speed": "Velocidade: {speed}" + } + }, + "paused": { + "progress": "Transferência em pausa — {percentage}%", + "progressList": "Transferência em pausa — {percentage}% — {done} de {total}", + "indeterminate": "Transferência em pausa", + "metadata": { + "eta": "Restante: {eta}", + "etaItems": "Resta {n} item | Restam {n} itens" + } + }, + "done": { + "complete": "Transferência concluída — 100%", + "showFolder": "Mostrar na pasta", + "open": "Abrir ficheiro", + "openFirst": "Abrir o primeiro ficheiro", + "undeterminedLocation": "Não foi possível determinar a localização da transferência para este item." + }, + "error": { + "errorPrefix": "Erro - {message}", + "report": "Reportar", + "showFull": "Ver registos", + "signIn": "Iniciar sessão" + } + }, + "card": { + "actions": { + "remove": "Remover", + "externalUrl": "Visitar URL externo", + "retry": "Tentar novamente", + "download": "Transferir individualmente", + "preferences": "Preferências de transferência", + "pause": "Pausar transferência", + "resume": "Retomar transferência", + "metadata": "Mostrar metadados" + }, + "toasts": { + "retry": "A tentar novamente a transferência.", + "retryError": "Não foi possível voltar a tentar a transferência." + } + } + }, + "subtitles": { + "title": "Legendas", + "options": { + "enable": { + "legend": "Transferência de legendas", + "legendLabel": "Escolhe como as legendas devem ser obtidas ao transferir multimédia.", + "label": "Ativar legendas", + "hint": "Transfere ficheiros de legendas juntamente com o teu conteúdo multimédia." + }, + "autoGenerated": { + "label": "Incluir legendas geradas automaticamente", + "hint": "Transfere legendas geradas automaticamente além das fornecidas pelo criador." + }, + "format": { + "legend": "Formato preferido", + "legendLabel": "Seleciona o teu formato de legendas preferido. Se o formato não estiver disponível, será utilizado o melhor formato seguinte.", + "label": "Formato" + }, + "embed": { + "label": "Adicionar faixa de legendas:", + "hint": "Adiciona as legendas ao ficheiro de vídeo. Funciona com os formatos mp4, mkv e webm." + }, + "languages": { + "legend": "Idiomas", + "legendLabel": "Escolhe os idiomas das legendas a transferir.", + "downloadAll": "Transferir todas as legendas disponíveis.", + "search": "Pesquisar idiomas", + "searchPlaceholder": "Pesquisar por nome do idioma", + "suggested": "Sugeridos para ti", + "all": "Todos os idiomas", + "noResult": "Nenhum idioma corresponde a \"{query}\".", + "selected": "Selecionados:", + "allSelected": "Todas as legendas disponíveis", + "searchHint": "Dica: utiliza a caixa de pesquisa para filtrar rapidamente a lista. Os idiomas selecionados mantêm-se destacados." + } + }, + "toasts": { + "saved": "Definições de legendas guardadas!", + "error": "Não foi possível guardar as definições de legendas. {error}" + }, + "formats": { + "srt": "SubRip (.srt)", + "vtt": "WebVTT (.vtt)", + "ass": "Advanced SubStation Alpha (.ass)", + "ttml": "Timed Text Markup (.ttml)", + "json3": "JSON (.json)" + } + }, + "location": { + "title": "Localização da transferência", + "downloadDir": { + "label": "Localização da transferência", + "hint": "Clica na caixa para escolher onde os vídeos serão transferidos." + }, + "directory": { + "legend": "Organizar por", + "legendLabel": "Seleciona como as transferências são organizadas em pastas.", + "videoDir": { + "label": "Localização da transferência:", + "hint": "Substitui a localização global da transferência ao transferir vídeos." + }, + "audioDir": { + "label": "Localização da transferência:", + "hint": "Substitui a localização global da transferência ao transferir áudio." + }, + "directoryPreset": { + "label": "Organizar transferências por:", + "exampleLabel": "Exemplo de saída:", + "options": { + "none": "Sem subpastas", + "playlist": "Playlist", + "channel": "Canal", + "channelPlaylist": "Canal → playlist", + "yearMonth": "Ano / mês", + "artistAlbum": "Artista → álbum", + "custom": "Personalizado" + }, + "examples": { + "playlist": "A Minha Playlist/", + "channel": "O Meu Canal/", + "channelPlaylist": "O Meu Canal/A Minha Playlist/", + "yearMonth": "2025/03/", + "artistAlbum": "Nome do Artista/Nome do Álbum/" + } + }, + "outputFormat": { + "label": "Formato da pasta:" + } + }, + "filename": { + "legend": "Formato do nome de ficheiro", + "legendLabel": "Seleciona o nome que as transferências terão.", + "formatPreset": { + "label": "Predefinição de nome de ficheiro:", + "exampleLabel": "Exemplo de saída:", + "options": { + "titleQuality": "Título + qualidade", + "titleOnly": "Só título", + "titleQualityPlaylist": "Playlist + título + qualidade", + "custom": "Personalizado" + }, + "examples": { + "video": { + "titleQuality": "O Meu Vídeo-(1080p30).mp4", + "titleOnly": "O Meu Vídeo.mp4", + "titleQualityPlaylist": "03-O Meu Vídeo-(1080p30).mkv" + }, + "audio": { + "titleQuality": "O Meu Áudio-(320k).mp3", + "titleOnly": "O Meu Áudio.mp3", + "titleQualityPlaylist": "03-O Meu Áudio-(320k).mp3" + } + }, + "restrictFilenames": { + "label": "Restringir nomes de ficheiro:", + "hint": "Substitui espaços por sublinhados e remove caracteres especiais como \"&\" dos nomes de ficheiro." + } + }, + "outputFormat": { + "label": "Formato do nome de ficheiro:" + } + }, + "toasts": { + "saved": "Localizações guardadas!", + "error": "Não foi possível guardar as localizações." + } + }, + "settings": { + "title": "Definições", + "reset": { + "label": "Repor", + "confirm": "Clica novamente para repor" + }, + "performance": { + "legend": "Desempenho", + "legendLabel": "Configura quantos recursos do sistema são utilizados.", + "maxConcurrency": { + "label": "Máximo de tarefas em simultâneo:" + }, + "splitPlaylistThreshold": { + "label": "Separar playlists em vídeos individuais quando:", + "options": { + "never": "Nunca separar", + "lessThan": "Menos de {number} vídeos" + } + }, + "autoLoadSize": { + "label": "Carregar automaticamente o tamanho do vídeo:" + } + }, + "input": { + "legend": "Entrada", + "legendLabel": "Escolhe como inicias transferências com o OVD.", + "autoFillClipboard": { + "label": "Preencher automaticamente links copiados:", + "hint": "Deteta links que copiaste para que possas transferi-los com um clique." + }, + "preferVideoInMixedLinks": { + "label": "Preferir vídeo único em links de playlists:", + "hint": "Quando um link contém um vídeo e uma playlist, transfere apenas o vídeo em vez da playlist completa." + }, + "globalShortcuts": { + "label": "Ativar atalhos:", + "add_hint": "Adicionar vídeos à fila com:", + "download_hint": "Transferir a fila com:" + }, + "authentication": { + "label": "Iniciar sessão para transferir vídeos privados:", + "link": "Configurar autenticação" + } + }, + "output": { + "legend": "Saída", + "legendLabel": "Configura que tipo de ficheiros são transferidos.", + "addThumbnail": { + "label": "Adicionar imagem de capa:", + "hint": "Incorpora a miniatura no ficheiro para que os leitores multimédia possam mostrar a capa." + }, + "addMetadata": { + "label": "Adicionar título e informação do artista:", + "hint": "Guarda metadados como título, autor/artista e descrição dentro do ficheiro." + }, + "partialDownload": { + "legend": "Transferência parcial", + "hint": "Define timestamps ou capítulos para transferir apenas algumas partes.", + "use": "Utilizar", + "useTimes": "Timestamps", + "useChapter": "Capítulos", + "chapter": "Capítulo", + "startAt": "Começar em", + "stopAt": "Parar em", + "invalidHint": "Timestamps inválidos serão ignorados.", + "allChapters": "Todos os capítulos", + "errors": { + "invalid": "Introduz uma hora válida.", + "beforeStart": "A hora de fim tem de ser posterior à hora de início.", + "exceedsDuration": "O timestamp não pode exceder a duração do vídeo." + } + }, + "location": { + "label": "Localização da transferência e nomes de ficheiro:", + "link": "Configurar localizações de transferência" + }, + "subtitles": { + "label": "Adicionar legendas:", + "link": "Configurar legendas" + }, + "policy": { + "label": "Modo de conversão:", + "hint": "Se o ficheiro transferido ainda não corresponder ao formato selecionado, poderá ser necessário convertê-lo. Ignorar conversão: mais rápido, pode ignorar o formato escolhido. Converter se possível: tenta alterar o contentor sem recodificar. Converter se necessário: utiliza sempre o formato selecionado, mas é mais lento e usa mais CPU.", + "options": { + "never": "Ignorar conversão", + "remuxOnly": "Converter se possível (recomendado)", + "allowReencode": "Converter se necessário (mais lento)" + } + }, + "tabs": { + "audio": { + "screenreader": "Definições de áudio", + "label": "Áudio", + "format": { + "label": "Formato de áudio:", + "hint": "Escolhe o formato de áudio preferido. O modo de conversão decide se o ficheiro será realmente convertido.", + "options": { + "mp3": "MP3 (recomendado)", + "m4a": "M4A", + "ogg": "OGG", + "aac": "AAC", + "opus": "Opus", + "wav": "WAV", + "flac": "FLAC" + } + } + }, + "video": { + "screenreader": "Definições de vídeo", + "label": "Vídeo", + "container": { + "label": "Formato de vídeo:", + "hint": "Escolhe o formato de vídeo preferido. O modo de conversão decide se o ficheiro será realmente convertido.", + "options": { + "mp4": "MP4", + "mkv": "MKV" + } + } + } + } + }, + "appearance": { + "legend": "Aspeto", + "legendLabel": "Configura o aspeto e a experiência da aplicação.", + "theme": { + "label": "Tema:", + "options": { + "system": "Sistema ({theme})", + "light": "Claro", + "dark": "Escuro" + } + }, + "language": { + "label": "Idioma:", + "options": { + "system": "Sistema ({language})" + } + }, + "expandedOptions": { + "label": "Seletor adicional:", + "hint": "Escolhe qual o seletor extra mostrado antes da transferência.", + "options": { + "none": "Nenhum", + "encodings": "Codificações", + "tracks": "Faixas" + } + } + }, + "network": { + "legend": "Rede", + "legendLabel": "Configura como a aplicação se liga à internet.", + "enableProxy": { + "label": "Ligar através de proxy:" + }, + "proxy": { + "label": "Configuração do proxy:" + }, + "impersonate": { + "label": "Mascarar cliente:", + "hint": "Faz a aplicação identificar-se como um sistema operativo ou navegador específico, para reduzir o fingerprinting.", + "options": { + "none": "Não mascarar", + "any": "Mascarar como cliente aleatório" + } + } + }, + "sponsorBlock": { + "legend": "SponsorBlock", + "legendLabel": "Remover ou marcar partes de um vídeo.", + "removeParts": { + "label": "Partes a remover", + "placeholder": "Seleciona 1 ou mais partes para remover do vídeo." + }, + "markParts": { + "label": "Partes a marcar", + "placeholder": "Seleciona 1 ou mais partes para marcar com um capítulo." + }, + "apiUrl": { + "label": "URL da API" + }, + "parts": { + "sponsor": { + "label": "Patrocínio", + "description": "Promoção paga de produto ou serviço pelo criador ou por terceiros." + }, + "selfpromo": { + "label": "Autopromoção / Não pago", + "description": "Promoção do próprio criador a merchandising, serviços ou canais (não paga)." + }, + "interaction": { + "label": "Lembrete de interação", + "description": "Lembretes para pôr gosto, subscrever, seguir ou interagir com o criador." + }, + "intro": { + "label": "Introdução / Interlúdio", + "description": "Animação de introdução, interlúdio ou pausa sem conteúdo no início." + }, + "outro": { + "label": "Final / End cards / Créditos", + "description": "End cards, créditos ou segmento de encerramento perto do fim." + }, + "hook": { + "label": "Teaser / Saudações", + "description": "Antevisões narradas do vídeo seguinte, saudações e despedidas." + }, + "music_offtopic": { + "label": "Secção sem música", + "description": "Partes de fala ou distrações em vídeos com música." + }, + "preview": { + "label": "Pré-visualização / Recapitulação", + "description": "Pré-visualização do conteúdo seguinte ou recapitulação de segmentos anteriores." + }, + "filler": { + "label": "Enchimento / Desvios", + "description": "Desvios, piadas ou cenas de enchimento não necessárias para compreender o conteúdo principal." + }, + "chapter": { + "label": "Capítulo", + "description": "Marca um capítulo/secção do vídeo (utilizado com o tipo de ação de capítulo)." + } + } + }, + "update": { + "legend": "Atualizações", + "legendLabel": "Escolhe que partes da aplicação devem ser atualizadas automaticamente.", + "updateBinaries": { + "label": "Atualizar automaticamente ferramentas:", + "hint": "O OVD utiliza ferramentas como yt-dlp e ffmpeg para transferir vídeos. Ativa isto para as manter atualizadas." + }, + "updateApp": { + "label": "Atualizar automaticamente a aplicação:", + "hint": "Ativa esta opção para receber novas funcionalidades e correções logo que seja lançada uma nova versão." + } + }, + "system": { + "legend": "Sistema", + "legendLabel": "Configura como esta aplicação se integra no sistema.", + "trayEnabled": { + "label": { + "mac": "Mostrar ícone na barra de menus", + "generic": "Mostrar ícone na área de notificação" + }, + "hint": { + "mac": "Mostra um ícone na barra de menus para acesso rápido.", + "generic": "Mostra um ícone na área de notificação para acesso rápido." + } + }, + "minimiseToTray": { + "label": "Minimizar para a área de notificação ao fechar", + "hint": "Ao fechar a janela, a aplicação é escondida na área de notificação em vez de sair." + }, + "autoStart": { + "label": "Iniciar ao iniciar sessão", + "hint": "Inicia o Open Video Downloader automaticamente quando iniciares sessão." + }, + "autoStartMinimised": { + "label": { + "mac": "Iniciar oculto ao iniciar sessão", + "windows": "Iniciar minimizado na área de notificação ao iniciar sessão", + "generic": "Iniciar oculto ao iniciar sessão (não recomendado)" + }, + "hint": { + "mac": "Quando a aplicação iniciar com a sessão, não abrirá nenhuma janela. Utiliza a barra de menus ou o ícone na Dock para a abrir.", + "windows": "Quando a aplicação iniciar com a sessão, ficará na área de notificação sem abrir nenhuma janela.", + "generic": "Quando a aplicação iniciar com a sessão, não abrirá nenhuma janela." + } + } + }, + "notifications": { + "legend": "Notificações", + "legendLabel": "Configura quando as notificações são enviadas.", + "behavior": { + "label": "Mostrar notificações:", + "options": { + "always": "Sempre", + "onBackground": "Quando estiver em segundo plano", + "never": "Nunca" + }, + "hint": "Dica: as notificações podem ser úteis ao utilizar a aplicação com atalhos." + }, + "disabled": { + "label": "Ativar/desativar notificações específicas", + "disabledScreenReader": "Esta opção está desativada.", + "kinds": { + "queueAdded": "Fila adicionada", + "queueDownloading": "Fila em transferência", + "queueFinished": "Fila concluída", + "videoFinished": "Vídeo concluído", + "playlistFinished": "Playlist concluída", + "downloadFailed": "Transferência falhada", + "videoReady": "Vídeo pronto", + "playlistReady": "Playlist pronta" + } + } + }, + "toasts": { + "saved": "Definições guardadas!", + "reset": "Definições repostas para os valores predefinidos." + } + }, + "updater": { + "available": "Atualização disponível!", + "install": { + "title": "Instalar atualização", + "subtitle": "A aplicação será reiniciada." + }, + "downloading": "A transferir atualização…" + }, + "install": { + "title": { + "installing": "A instalar ferramentas necessárias…", + "failed": "Falha ao instalar algumas ferramentas necessárias.", + "installed": "Ferramentas necessárias instaladas." + }, + "subtitle": { + "installing": "Estamos a preparar tudo.", + "failed": "A aplicação pode não funcionar como esperado. Reporta este problema.", + "installed": "Pronto a transferir!" + } + }, + "components": { + "base": { + "emptyState": { + "title": "A tua fila está vazia", + "subtitle": "Adiciona alguns vídeos para começar." + }, + "dragState": { + "title": "Larga para adicionar à fila", + "subtitle": "Adiciona à fila arrastando vídeos." + }, + "fileInput": { + "select": "Selecionar ficheiro", + "placeholder": "Seleciona um ficheiro…", + "invalid": "Ficheiro inválido.", + "clear": "Limpar ficheiro", + "recents": { + "clear": "Limpar ficheiros recentes", + "open": "Abrir seleções recentes", + "empty": "Sem seleções recentes" + } + }, + "folderInput": { + "select": "Selecionar pasta", + "placeholder": "Seleciona uma pasta…", + "invalid": "Pasta inválida.", + "clear": "Limpar pasta", + "recents": { + "clear": "Limpar pastas recentes", + "open": "Abrir seleções recentes", + "empty": "Sem seleções recentes" + } + }, + "secretInput": { + "hide": "Ocultar", + "show": "Mostrar", + "clear": "Limpar {label}" + }, + "toolCard": { + "failed": "Falha ao instalar esta ferramenta. Expande para mais informações." + } + } + }, + "errors": { + "runner": { + "internal": { + "shortMessage": "Erro interno", + "message": "Ocorreu um erro interno ao processar este item." + }, + "signInRequired": { + "shortMessage": "Início de sessão necessário", + "message": "Tens de iniciar sessão para veres este conteúdo." + }, + "membersOnly": { + "shortMessage": "Só para membros", + "message": "Este vídeo só está disponível para membros do canal." + }, + "geoBlocked": { + "shortMessage": "Bloqueado por região", + "message": "Este conteúdo não está disponível na tua região." + }, + "accessForbidden403": { + "shortMessage": "Acesso proibido", + "message": "Não tens permissão para aceder a este recurso (HTTP 403)." + }, + "notFound404": { + "shortMessage": "Não encontrado", + "message": "O vídeo ou a playlist pedidos não foram encontrados (HTTP 404)." + }, + "server5xx": { + "shortMessage": "Erro do servidor", + "message": "Ocorreu um problema temporário no servidor. Tenta novamente mais tarde." + }, + "rateLimited429": { + "shortMessage": "Limite excedido", + "message": "Foram enviados demasiados pedidos. Aguarda e tenta novamente." + }, + "requestedFormatUnavailable": { + "shortMessage": "Formato indisponível", + "message": "O formato selecionado não está disponível para este vídeo." + }, + "unableToExtractInitialData": { + "shortMessage": "Falha na extração de dados", + "message": "Não foi possível extrair os dados iniciais necessários do vídeo." + }, + "unableToExtractIdOrInfo": { + "shortMessage": "Erro de metadados", + "message": "Falha ao extrair a informação do vídeo ou do autor." + }, + "nsigExtractionFailed": { + "shortMessage": "Falha na assinatura", + "message": "Falha ao extrair ou descodificar a assinatura do vídeo." + }, + "embedOnly": { + "shortMessage": "Vídeo apenas incorporado", + "message": "Este vídeo só pode ser acedido através de um leitor incorporado." + }, + "urlUnsupported": { + "shortMessage": "URL inválido", + "message": "O link fornecido não é suportado nem reconhecido." + }, + "unknownUrlType": { + "shortMessage": "Tipo de URL desconhecido", + "message": "O URL fornecido utiliza um protocolo não suportado." + }, + "networkNameResolutionFailed": { + "shortMessage": "Erro de rede", + "message": "Não foi possível resolver o nome do anfitrião. Verifica a tua ligação à internet." + }, + "unableToConnectToProxy": { + "shortMessage": "Falha na ligação ao proxy", + "message": "Não foi possível ligar ao proxy. Verifica as definições do proxy." + }, + "signInRequiredForBotDetection": { + "shortMessage": "Início de sessão necessário", + "message": "Inicia sessão para confirmares que não és um bot." + }, + "playlistPrivateOrMissing": { + "shortMessage": "Playlist indisponível", + "message": "Esta playlist é privada ou já não existe." + }, + "videoNotFoundOrPrivate": { + "shortMessage": "Vídeo indisponível", + "message": "Este vídeo foi removido ou definido como privado." + }, + "channelUnavailable": { + "shortMessage": "Canal indisponível", + "message": "O canal que aloja este conteúdo já não está acessível." + }, + "ffmpegNotFound": { + "shortMessage": "ffmpeg não encontrado", + "message": "A transcodificação para mp3 ou mp4 requer o ffmpeg." + }, + "ffmpegFailed": { + "shortMessage": "ffmpeg falhou", + "message": "O FFmpeg encontrou um erro durante o processamento." + }, + "cannotAllocateMemory": { + "shortMessage": "Erro de memória", + "message": "O FFmpeg ficou sem memória ao processar o vídeo." + }, + "permissionDenied": { + "shortMessage": "Permissão negada", + "message": "A aplicação não tem permissão para aceder a este ficheiro ou pasta." + }, + "noWritePermission": { + "shortMessage": "Não é possível escrever", + "message": "O caminho de destino não tem permissão de escrita." + }, + "windowsFileMissing": { + "shortMessage": "Ficheiro em falta", + "message": "O ficheiro referido não foi encontrado (WinError 2)." + }, + "blockedGeneric": { + "shortMessage": "Vídeo bloqueado", + "message": "Este vídeo está bloqueado e não pode ser transferido." + }, + "copyrightClaimed": { + "shortMessage": "Bloqueio por direitos de autor", + "message": "O vídeo foi bloqueado devido a uma reclamação de direitos de autor." + }, + "notPremieredYet": { + "shortMessage": "Ainda não estreou", + "message": "Este vídeo não estará disponível até a estreia começar." + }, + "livestreamNotStarted": { + "shortMessage": "Transmissão ainda não começou", + "message": "A transmissão em direto ainda não começou. Volta a verificar mais tarde." + }, + "sslVerifyFailed": { + "shortMessage": "Falha SSL", + "message": "Não foi possível verificar a ligação segura. Verifica os teus certificados ou a data/hora." + }, + "sponsorBlockUnreachable": { + "shortMessage": "SponsorBlock indisponível", + "message": "Não foi possível ligar à API do SponsorBlock para obter os dados dos segmentos." + }, + "downloadRetryingFragment": { + "shortMessage": "A tentar novamente o fragmento", + "message": "A transferência de um fragmento falhou e será repetida automaticamente." + }, + "unknown": { + "shortMessage": "Erro desconhecido", + "message": "Ocorreu um erro desconhecido ao processar este item." + } + } + }, + "about": { + "title": "Acerca de", + "credit": "Feito com ❤ por jely2002 e colaboradores", + "poweredBy": "Baseado em código aberto:", + "viewLicenses": "Ver licenças", + "version": "Versão: {version}", + "links": { + "github": "GitHub", + "wiki": "Wiki", + "reportABug": "Reportar um erro" + } + } +} \ No newline at end of file From 18531f89df908491a50be9aad051a7901ee1112c Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Sun, 5 Apr 2026 14:35:59 +0200 Subject: [PATCH 2/5] fix(i18n): remove incompatible explicit vue-i18n type annotation - remove explicit I18n annotation from src/i18n.ts - let createI18n infer the correct locale-aware type - keep pt-PT, pt-BR and zh-TW locale support intact - fix CI build/lint failure caused by vue-i18n type mismatch --- src/i18n.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 5a184b8e..c1bd68c3 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,4 +1,4 @@ -import { createI18n, I18n } from 'vue-i18n'; +import { createI18n } from 'vue-i18n'; import en from './locales/en.json'; import es from './locales/es.json'; import nl from './locales/nl.json'; @@ -65,7 +65,7 @@ export function getDefaultLocale(): Locale { return 'en'; } -export const i18n: I18n = createI18n<[MessageSchema], Locale>({ +export const i18n = createI18n<[MessageSchema], Locale>({ locale: getDefaultLocale(), legacy: false, globalInjection: false, @@ -84,4 +84,4 @@ export const i18n: I18n = createI18n<[MessageSchema], Locale>({ 'pt-BR': ptBR, 'zh-TW': zhTW, }, -}); \ No newline at end of file +}); From f31c5dc517d9df4983886c58312a08d95552bc2e Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Sun, 5 Apr 2026 15:04:07 +0200 Subject: [PATCH 3/5] fix(i18n): add pt-PT locale support and resolve locale typing issues - add pt-PT locale support - fix Vue i18n locale typing - update browser locale detection - resolve related lint issues --- src/helpers/subtitles/languages.ts | 2 +- src/i18n.ts | 44 +++++++++++++++--------------- src/main.ts | 8 ++++-- src/stores/settings.ts | 8 ++++-- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/helpers/subtitles/languages.ts b/src/helpers/subtitles/languages.ts index 84e32bb7..1aa7b87c 100644 --- a/src/helpers/subtitles/languages.ts +++ b/src/helpers/subtitles/languages.ts @@ -62,4 +62,4 @@ export function detectBrowserLanguageCodes(): string[] { } return Array.from(normalized); -} \ No newline at end of file +} diff --git a/src/i18n.ts b/src/i18n.ts index c1bd68c3..68565135 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -14,15 +14,15 @@ import zhTW from './locales/zh-TW.json'; import { detectBrowserLanguageCodes } from './helpers/subtitles/languages.ts'; export const availableLocales = { - en: true, - es: true, - nl: true, - it: true, - fr: true, - de: true, - nb: true, - ru: true, - tr: true, + 'en': true, + 'es': true, + 'nl': true, + 'it': true, + 'fr': true, + 'de': true, + 'nb': true, + 'ru': true, + 'tr': true, 'pt-PT': true, 'pt-BR': true, 'zh-TW': true, @@ -32,13 +32,13 @@ type MessageSchema = typeof en; export type Locale = keyof typeof availableLocales; const localeAliases: Record = { - pt: 'pt-PT', + 'pt': 'pt-PT', 'pt-PT': 'pt-PT', 'pt-BR': 'pt-BR', - zh: 'zh-TW', + 'zh': 'zh-TW', 'zh-Hant': 'zh-TW', 'zh-TW': 'zh-TW', - no: 'nb', + 'no': 'nb', 'nb-NO': 'nb', }; @@ -71,17 +71,17 @@ export const i18n = createI18n<[MessageSchema], Locale>({ globalInjection: false, fallbackLocale: 'en', messages: { - en, - es, - nl, - it, - fr, - de, - nb, - ru, - tr, + 'en': en, + 'es': es, + 'nl': nl, + 'it': it, + 'fr': fr, + 'de': de, + 'nb': nb, + 'ru': ru, + 'tr': tr, 'pt-PT': ptPT, 'pt-BR': ptBR, 'zh-TW': zhTW, }, -}); +}); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7f94b5a2..7dec4867 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { createApp, Ref } from 'vue'; +import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import './app.css'; @@ -75,12 +75,14 @@ async function initStores(): Promise { if (settings.appearance.theme !== 'system') { applyTheme(settings.appearance.theme); } - const locale = i18n.global.locale as Ref; + + const locale = i18n.global.locale; locale.value = settings.appearance.language === 'system' ? getDefaultLocale() : settings.appearance.language; + document.documentElement.setAttribute('lang', locale.value); } catch (e) { console.error(`Unable to load settings: ${e}`); } -} +} \ No newline at end of file diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 24470d84..c4f008c5 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { Ref, ref } from 'vue'; +import { ref } from 'vue'; import { invoke } from '@tauri-apps/api/core'; import { getDefaultLocale, i18n } from '../i18n'; import { defaultSettings } from '../tauri/types/config.ts'; @@ -36,8 +36,10 @@ export const useSettingsStore = defineStore('settings', () => { function applySettings(cfg: Settings) { Object.assign(settings.value, cfg); if (cfg.appearance.language) { - const locale = i18n.global.locale as Ref; - locale.value = cfg.appearance.language === 'system' ? getDefaultLocale() : cfg.appearance.language; + const locale = i18n.global.locale; + locale.value = cfg.appearance.language === 'system' + ? getDefaultLocale() + : cfg.appearance.language; document.documentElement.setAttribute('lang', locale.value); } } From 5adba0cd1e37ac88d672254d90b0c580558266df Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Sun, 5 Apr 2026 16:15:48 +0200 Subject: [PATCH 4/5] fix(i18n): resolve locale assignment and remaining lint errors - assign Vue i18n locale directly instead of using .value - fix regional locale helper lint issue - add missing trailing newlines - resolve remaining TypeScript and lint failures --- src/helpers/subtitles/languages.ts | 2 +- src/main.ts | 2 +- src/stores/settings.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/subtitles/languages.ts b/src/helpers/subtitles/languages.ts index 1aa7b87c..c7ea5c93 100644 --- a/src/helpers/subtitles/languages.ts +++ b/src/helpers/subtitles/languages.ts @@ -27,7 +27,7 @@ export const languageOptions: SubtitleLanguageOption[] = [...codes, ...Object.ke .sort((a, b) => a.englishName.localeCompare(b.englishName)); export const languageOptionsLookup = new Map( - languageOptions.map((option) => [option.code, option] as const), + languageOptions.map(option => [option.code, option] as const), ); function normalizeLocale(candidate: string): string { diff --git a/src/main.ts b/src/main.ts index 7dec4867..fb8ad866 100644 --- a/src/main.ts +++ b/src/main.ts @@ -85,4 +85,4 @@ async function initStores(): Promise { } catch (e) { console.error(`Unable to load settings: ${e}`); } -} \ No newline at end of file +} diff --git a/src/stores/settings.ts b/src/stores/settings.ts index c4f008c5..81f1ea8a 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -36,11 +36,11 @@ export const useSettingsStore = defineStore('settings', () => { function applySettings(cfg: Settings) { Object.assign(settings.value, cfg); if (cfg.appearance.language) { - const locale = i18n.global.locale; - locale.value = cfg.appearance.language === 'system' + i18n.global.locale = cfg.appearance.language === 'system' ? getDefaultLocale() : cfg.appearance.language; - document.documentElement.setAttribute('lang', locale.value); + + document.documentElement.setAttribute('lang', i18n.global.locale); } } From b3f55d9ee5244999d9b850eb7698928d0c001ba7 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Sun, 5 Apr 2026 18:51:46 +0200 Subject: [PATCH 5/5] fix(i18n): resolve locale assignment typing in app and settings store - fix locale assignment in src/main.ts - fix locale assignment in src/stores/settings.ts - use typed Locale value before assigning to vue-i18n - remove remaining invalid locale.value usage --- src/main.ts | 10 +++++----- src/stores/settings.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main.ts b/src/main.ts index fb8ad866..bb874080 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,7 @@ import { binaryHandlers } from '../tests/utils/mocks/binaryHandlers'; import { updateHandlers } from '../tests/utils/mocks/updateHandlers'; import { invoke } from '@tauri-apps/api/core'; import { strongholdHandlers } from '../tests/utils/mocks/strongholdHandlers'; -import { getDefaultLocale, i18n } from './i18n'; +import { getDefaultLocale, i18n, type Locale } from './i18n'; import { createSentryPiniaPlugin } from '@sentry/vue'; import { createSentry } from './sentry.ts'; import { startWindowWatcher } from './tauri/window.ts'; @@ -76,12 +76,12 @@ async function initStores(): Promise { applyTheme(settings.appearance.theme); } - const locale = i18n.global.locale; - locale.value = settings.appearance.language === 'system' + const currentLocale: Locale = settings.appearance.language === 'system' ? getDefaultLocale() - : settings.appearance.language; + : settings.appearance.language as Locale; - document.documentElement.setAttribute('lang', locale.value); + i18n.global.locale = currentLocale; + document.documentElement.setAttribute('lang', currentLocale); } catch (e) { console.error(`Unable to load settings: ${e}`); } diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 81f1ea8a..9671814a 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; import { invoke } from '@tauri-apps/api/core'; -import { getDefaultLocale, i18n } from '../i18n'; +import { getDefaultLocale, i18n, type Locale } from '../i18n'; import { defaultSettings } from '../tauri/types/config.ts'; import { Settings } from '../tauri/types/config.ts'; @@ -35,12 +35,14 @@ export const useSettingsStore = defineStore('settings', () => { function applySettings(cfg: Settings) { Object.assign(settings.value, cfg); + if (cfg.appearance.language) { - i18n.global.locale = cfg.appearance.language === 'system' + const currentLocale: Locale = cfg.appearance.language === 'system' ? getDefaultLocale() - : cfg.appearance.language; + : cfg.appearance.language as Locale; - document.documentElement.setAttribute('lang', i18n.global.locale); + i18n.global.locale = currentLocale; + document.documentElement.setAttribute('lang', currentLocale); } }