Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ jobs:
shell
sparse-checkout-cone-mode: true

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Run checks
env:
OMARCHY_PATH: ${{ github.workspace }}/.ci/omarchy
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ All notable changes to **Taskwarrior Time** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Spanish UI with system-locale detection and manual selection in About

## [1.0.2] - 2026-09-29

### Fixed
Expand Down Expand Up @@ -65,6 +71,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
версии следуют [Semantic Versioning](https://semver.org/lang/ru/).

## [Не выпущено]

### Добавлено

- Испанский интерфейс с автоопределением языка системы и ручным выбором во вкладке «О плагине»

## [1.0.2] - 2026-09-29

### Исправлено
Expand Down
18 changes: 14 additions & 4 deletions Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Panel {
property bool debugLogging: false
property string debugLogPath: ""
property string debugSessionId: ""
property string uiLanguage: "system" // system | ru | en
property string uiLanguage: "system" // system | ru | en | es
property var _logQueue: []
property bool _logFlushScheduled: false
readonly property string pluginVersion: "1.0.2"
Expand Down Expand Up @@ -1110,7 +1110,7 @@ Panel {

function setUiLanguage(lang) {
var next = String(lang || "system")
if (next !== "system" && next !== "ru" && next !== "en")
if (next !== "system" && next !== "ru" && next !== "en" && next !== "es")
next = "system"
root.uiLanguage = next
settingsSetProc.command = [
Expand Down Expand Up @@ -1555,7 +1555,7 @@ Panel {
root.debugLogging = !!data.debugLogging
root.debugLogPath = String(data.logPath || "")
root.debugSessionId = String(data.sessionId || "")
if (data.uiLanguage === "ru" || data.uiLanguage === "en" || data.uiLanguage === "system")
if (data.uiLanguage === "ru" || data.uiLanguage === "en" || data.uiLanguage === "es" || data.uiLanguage === "system")
root.uiLanguage = String(data.uiLanguage)
if (root.debugLogging)
root.dlog("ui.settings.loaded", {
Expand All @@ -1582,7 +1582,7 @@ Panel {
root.debugLogging = !!data.debugLogging
root.debugLogPath = String(data.logPath || "")
root.debugSessionId = String(data.sessionId || "")
if (data.uiLanguage === "ru" || data.uiLanguage === "en" || data.uiLanguage === "system")
if (data.uiLanguage === "ru" || data.uiLanguage === "en" || data.uiLanguage === "es" || data.uiLanguage === "system")
root.uiLanguage = String(data.uiLanguage)
// Banner already written by helper on enable; acknowledge from UI.
if (root.debugLogging)
Expand Down Expand Up @@ -4245,6 +4245,16 @@ Panel {
horizontalPadding: Style.space(8)
onClicked: root.setUiLanguage("en")
}
Button {
text: root.tr("uiLanguageEs")
selected: root.uiLanguage === "es"
foreground: root.foreground
fontFamily: root.fontFamily
fontSize: Style.font.caption
verticalPadding: Style.space(2)
horizontalPadding: Style.space(8)
onClicked: root.setUiLanguage("es")
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Anything you do here is stored in your normal Taskwarrior / Timewarrior data. Yo
- **Projects** view: browse projects, rename, clear project from tasks
- **Unsaved changes** dialog when closing a dirty editor (Save / Keep editing / Discard)
- Uniform **hotkeys**: `Ctrl+Enter` commit, `Esc` cancel, `Ctrl+Delete` destroy
- **i18n**: English and Russian UI (system language, or pick one in About)
- **i18n**: English, Russian, and Spanish UI (system language, or pick one in About)
- **About** tab with version, developer info, What’s new / Marketplace / Report a problem / GitHub, language switch, and debug logging

### Task list
Expand Down Expand Up @@ -180,11 +180,11 @@ Please do **not** paste secrets, tokens, or private task content.

## Development checks

From a clone of this repository:
From a clone of this repository (Node.js 22+ and Python 3 required for behavior tests):

```bash
./scripts/enable-git-hooks # once per clone — enables pre-push checks
./scripts/check # manifest validate + qmllint
./scripts/check # manifest validate + behavior tests + qmllint
```

`git push` runs the same checks via `.githooks/pre-push` and aborts if they fail. GitHub Actions runs `./scripts/check` on push and pull requests.
Expand Down
6 changes: 3 additions & 3 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
- Вкладка **Проекты**: просмотр, переименование, очистка проекта у задач
- Диалог **несохранённых изменений** при закрытии черновика (Сохранить / Продолжить / Отменить)
- Единые **горячие клавиши**: `Ctrl+Enter` — сохранить/добавить, `Esc` — отмена, `Ctrl+Delete` — удалить/сбросить
- **Локализация**: английский и русский (системный язык или выбор на вкладке «О плагине»)
- **Локализация**: английский, русский и испанский (системный язык или выбор на вкладке «О плагине»)
- Вкладка **О плагине**: версия, разработчик, Что нового / Маркетплейс / Сообщить о проблеме / GitHub, язык интерфейса и отладочный лог

### Список задач
Expand Down Expand Up @@ -180,11 +180,11 @@ omarchy plugin remove taskwarrior-time

## Проверки при разработке

В клоне этого репозитория:
В клоне этого репозитория (для поведенческих тестов нужны Node.js 22+ и Python 3):

```bash
./scripts/enable-git-hooks # один раз на клон — включает pre-push
./scripts/check # validate манифеста + qmllint
./scripts/check # validate манифеста + поведенческие тесты + qmllint
```

`git push` прогоняет те же проверки через `.githooks/pre-push` и отменяет push при ошибке. GitHub Actions запускает `./scripts/check` на push и pull request.
Expand Down
6 changes: 3 additions & 3 deletions bin/taskwarrior-time
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ def _write_private_text(path: Path, text: str) -> None:

def _normalize_ui_language(value: Any) -> str:
lang = str(value or "system").strip().lower()
if lang in ("system", "ru", "en"):
if lang in ("system", "ru", "en", "es"):
return lang
return "system"

Expand Down Expand Up @@ -1758,8 +1758,8 @@ def main() -> int:
p_ss.add_argument(
"--ui-language",
default=None,
choices=["system", "ru", "en"],
help="UI language: system, ru, or en",
choices=["system", "ru", "en", "es"],
help="UI language: system, ru, en, or es",
)

p_dl = sub.add_parser("debug-log")
Expand Down
176 changes: 159 additions & 17 deletions i18n.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
// Locale-aware strings for Taskwarrior Time (plugin id: taskwarrior-time).

function isRussian(localeName) {
function languageFromLocale(localeName) {
var candidates = []
if (localeName !== undefined && localeName !== null && String(localeName) !== "")
candidates.push(String(localeName))
var loc = null
try {
var loc = Qt.locale()
if (loc) {
if (loc.name) candidates.push(String(loc.name))
// Prefer the first UI language (e.g. "ru-RU", "ru") when set.
var ui = loc.uiLanguages
if (ui && ui.length > 0) candidates.push(String(ui[0]))
loc = Qt.locale()
if (loc && loc.uiLanguages) {
for (var i = 0; i < loc.uiLanguages.length; i++)
candidates.push(String(loc.uiLanguages[i]))
}
} catch (e) {}
if (loc && loc.name) candidates.push(String(loc.name))

for (var i = 0; i < candidates.length; i++) {
var raw = String(candidates[i] || "").toLowerCase().replace(/-/g, "_")
for (var j = 0; j < candidates.length; j++) {
var raw = String(candidates[j] || "").toLowerCase().replace(/-/g, "_")
if (!raw) continue
// "ru", "ru_RU", "ru_RU.UTF-8", "ru.utf8"
// "es", "es_ES", "es_ES.UTF-8", "es.utf8" (and other supported languages).
var lang = raw.split(".")[0].split("_")[0]
if (lang === "ru") return true
if (lang === "ru" || lang === "es" || lang === "en") return lang
}
return false
return "en"
}

function t(key, localeName, uiLanguage) {
var mode = String(uiLanguage || "system").toLowerCase()
var ru
if (mode === "ru") ru = true
else if (mode === "en") ru = false
else ru = isRussian(localeName)
var language = mode === "ru" || mode === "en" || mode === "es"
? mode : languageFromLocale(localeName)
var en = {
title: "Taskwarrior Time",
titleInProgress: "Taskwarrior Time",
Expand Down Expand Up @@ -170,6 +168,7 @@ function t(key, localeName, uiLanguage) {
uiLanguageSystem: "System",
uiLanguageRu: "Русский",
uiLanguageEn: "English",
uiLanguageEs: "Spanish",
}
var ruMap = {
title: "Taskwarrior Time",
Expand Down Expand Up @@ -311,7 +310,150 @@ function t(key, localeName, uiLanguage) {
uiLanguageSystem: "Системный",
uiLanguageRu: "Русский",
uiLanguageEn: "English",
uiLanguageEs: "Испанский",
}
var map = ru ? ruMap : en
var esMap = {
title: "Taskwarrior Time",
titleInProgress: "Taskwarrior Time",
projects: "Proyectos",
filterAll: "Todas",
filterActive: "Activas",
filterToday: "Hoy",
filterDone: "Completadas",
filterSpoiler: "Filtros",
filterClear: "Restablecer",
filterAny: "Cualquiera",
filterStatus: "Estado",
filterStatusOpen: "Abiertas",
filterStatusAll: "Cualquier estado",
filterProject: "Proyecto",
filterDue: "Vencimiento",
filterDueSoon: "Próximas",
filterTimer: "Temporizador",
filterTimerOn: "En marcha",
filterTimerOff: "Parado",
filterDeps: "Dependencias",
filterDepsClear: "Sin dependencias",
filterSearch: "Buscar",
filterSearchHint: "Descripción…",
groupBy: "Agrupar por",
groupNone: "Ninguno",
groupProject: "Proyecto",
groupPriority: "Prioridad",
groupDue: "Vencimiento",
groupStatus: "Estado",
addPlaceholder: "Nueva tarea…",
addSection: "Nueva tarea",
addDetails: "Campos de tarea",
addDetailsHide: "Ocultar campos",
advancedShow: "Programación, enlaces y tiempo",
advancedHide: "Ocultar programación, enlaces y tiempo",
startTimerOnCreate: "Iniciar temporizador al crear",
addTaskBtn: "Añadir tarea",
startDate: "Inicio",
endDate: "Fecha límite",
details: "Detalles",
detailsHint: "Notas adicionales…",
titleField: "Título",
editSectionPlan: "Planificación",
editSectionSchedule: "Fechas",
editSectionLinks: "Dependencias",
editSectionTime: "Tiempo",
priority: "Prioridad",
priorityNone: "—",
project: "Proyecto",
projectNone: "Sin proyecto",
allProjects: "Todos los proyectos",
add: "Añadir",
unsavedChanges: "Esta tarea tiene cambios sin guardar.",
unsavedSave: "Guardar",
unsavedContinue: "Seguir editando",
unsavedDiscard: "Descartar",
save: "Guardar",
cancel: "Cancelar",
deleteTask: "Eliminar",
confirmDelete: "¿Eliminar esta tarea?",
confirmClearProject: "¿Quitar este proyecto de todas las tareas?",
confirm: "Confirmar",
play: "Iniciar temporizador",
stop: "Detener temporizador",
todayTime: "Hoy",
adjustTime: "Ajustar tiempo",
adjustTimeHint: "30 min, 1 h, 1:30",
timeAdd: "+ Añadir",
timeRemove: "− Quitar",
done: "Hecha",
pending: "Pendiente",
waiting: "En espera",
waitingFor: "Esperando a",
waitingForHint: "¿Qué estás esperando?…",
outcome: "Resultado",
outcomeHint: "¿Cuál fue el resultado?…",
status: "Estado",
depends: "Depende de",
blocks: "Bloquea",
blocked: "Bloqueada",
blocking: "Bloquea",
overdue: "Vencida",
noTasks: "No hay tareas",
noProjects: "Aún no hay proyectos",
newProject: "Nuevo proyecto…",
rename: "Renombrar",
clearProject: "Quitar proyecto",
expand: "Detalles",
missingTaskwarrior: "Instala Taskwarrior (task)",
sectionNone: "Bandeja de entrada",
dueOverdue: "Vencidas",
dueToday: "Hoy",
dueTomorrow: "Mañana",
dueWeek: "Esta semana",
dueLater: "Más adelante",
dueNone: "Sin fecha límite",
priH: "alta",
priM: "media",
priL: "baja",
priNone: "Sin prioridad",
statusPending: "Pendiente",
statusCompleted: "Completada",
statusWaiting: "En espera",
tooltipTimer: "Temporizador",
tooltipOverdue: "%1 tareas vencidas",
tooltipPending: "%1 pendientes",
tooltipWaiting: "%1 en espera",
tooltipIdle: "No hay tareas abiertas",
tooltipDebugOn: "Registro de depuración activado",
viewTasks: "Tareas",
viewProjects: "Proyectos",
addDep: "Añadir dependencia",
noDepCandidates: "No hay otras tareas para vincular",
removeDep: "Quitar",
datesHint: "today, tomorrow, DD.MM.AAAA[ HH:MM:SS]",
dateApply: "Aplicar",
dateTime: "Hora",
dateClear: "Borrar",
about: "Acerca de",
aboutTitle: "Taskwarrior Time",
aboutVersion: "Versión",
aboutDeveloper: "Desarrollador",
aboutDeveloperName: "DataArchitectPro",
aboutGithub: "Abrir en GitHub",
aboutMarketplace: "Marketplace de Omarchy",
aboutChangelog: "Novedades",
aboutNewIssue: "Informar de un problema",
aboutLinks: "Ayuda y enlaces",
aboutClose: "Cerrar",
debugLog: "Registro de depuración",
debugLogOn: "Registro: ACTIVADO",
debugLogOff: "Registro: DESACTIVADO",
debugLogHint: "Escribe un registro de diagnóstico con versiones (plugin, Taskwarrior, Timewarrior y Omarchy) y eventos de la interfaz. Adjunta el archivo completo al informar de un problema.",
aboutOpenLogFolder: "Abrir carpeta de registros",
aboutClearLogs: "Borrar registros",
uiLanguage: "Idioma de la interfaz",
uiLanguageSystem: "Sistema",
uiLanguageRu: "Ruso",
uiLanguageEn: "Inglés",
uiLanguageEs: "Español",
}
var map = language === "es" ? esMap : (language === "ru" ? ruMap : en)
return map[key] !== undefined ? map[key] : (en[key] !== undefined ? en[key] : key)
}
17 changes: 17 additions & 0 deletions scripts/check
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ else
fi
fi

# --- behavior tests ---------------------------------------------------------
section "behavior tests"

if command -v node >/dev/null 2>&1; then
node --test tests/i18n.test.js || FAILED=1
else
echo "behavior tests: node is required for i18n.test.js" >&2
FAILED=1
fi

if command -v python3 >/dev/null 2>&1; then
python3 -m unittest discover -s tests -p 'test_*.py' || FAILED=1
else
echo "behavior tests: python3 is required for helper settings tests" >&2
FAILED=1
fi

# --- summary ----------------------------------------------------------------
section "summary"
if ((FAILED)); then
Expand Down
Loading