diff --git a/.github/ISSUE_TEMPLATE/translation_fix.yml b/.github/ISSUE_TEMPLATE/translation_fix.yml
new file mode 100644
index 00000000..95a389a6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/translation_fix.yml
@@ -0,0 +1,63 @@
+name: Translation improvement
+description: Suggest a correction or better wording for interface text in a specific language (no Git required).
+title: "[i18n] "
+labels: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for helping improve translations. Before submitting, skim **Rules for translators** in `docs/CONTRIBUTING_TRANSLATIONS.md` in this repository (placeholders, context, what not to change).
+
+ - type: dropdown
+ id: language
+ attributes:
+ label: Language
+ description: Locale code for the translation you are improving.
+ options:
+ - nl (Nederlands)
+ - de (Deutsch)
+ - fr (Français)
+ - it (Italiano)
+ - fi (Suomi)
+ - es (Español)
+ - no (Norsk)
+ - ar (العربية)
+ - he (עברית)
+ - en (English)
+ validations:
+ required: true
+
+ - type: input
+ id: location
+ attributes:
+ label: Where did you see this?
+ description: Page name, menu path, or URL path (e.g. Dashboard, Settings, /login).
+ validations:
+ required: true
+
+ - type: textarea
+ id: current_text
+ attributes:
+ label: Current text (as shown in the app)
+ description: Copy the exact wording from the UI in the language you selected.
+ validations:
+ required: true
+
+ - type: textarea
+ id: suggested_text
+ attributes:
+ label: Suggested text
+ description: Your improved translation. Keep any %(name)s-style placeholders identical to the current text if present.
+ validations:
+ required: true
+
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes (optional)
+ description: Why this reads better, grammar fix, formal vs informal tone, screenshot description, etc.
+
+ - type: markdown
+ attributes:
+ value: |
+ **Optional:** Attach a screenshot in a follow-up comment if the form does not allow images.
diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml
new file mode 100644
index 00000000..96e0ba20
--- /dev/null
+++ b/.github/workflows/crowdin-sync.yml
@@ -0,0 +1,34 @@
+# Manual Crowdin sync: uploads English source .po, downloads translations, opens a PR.
+# Prerequisites: repo secrets CROWDIN_PROJECT_ID and CROWDIN_PERSONAL_TOKEN (see docs/CONTRIBUTING_TRANSLATIONS.md).
+name: Crowdin sync
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ crowdin:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Crowdin push and pull
+ uses: crowdin/github-action@v2
+ with:
+ upload_sources: true
+ # Set to true once to seed Crowdin with existing translations/…/messages.po, then set back to false.
+ upload_translations: false
+ download_translations: true
+ localization_branch_name: i18n/crowdin
+ create_pull_request: true
+ pull_request_title: "chore(i18n): Crowdin translations"
+ pull_request_body: "Automated sync from Crowdin. Review placeholders and `no` vs `nb` paths before merge."
+ commit_message: "chore(i18n): sync Crowdin translations"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3aff8da..ba3e3b47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,16 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
-- **Quote create returned HTTP 500 after save (#583)** — The quote was persisted, but the redirect to the quote detail page crashed because the view expected `requires_approval` and `can_be_sent`, and templates could set `approval_level`, while the `Quote` model had no matching fields. Added `requires_approval` and `approval_level` columns, a `can_be_sent` property aligned with send rules, wired the create-form checkbox, fixed the approval banner branch to use `approval_status == 'not_required'`, migration `145_add_quotes_requires_approval`, and regression coverage in `tests/test_routes/test_quotes_web.py`.
+- **Quote create returned HTTP 500 after save (#583)** — The quote was saved, but the redirect to the quote detail page crashed when **Valid until** was set: the template compared `valid_until` to `now()`, and `now` was never defined in the Jinja context. The expired badge now uses `Quote.is_expired` (same rule, app timezone). Regression coverage in `tests/test_routes/test_quotes_web.py` posts `valid_until` so the view path is exercised.
+- **Desktop app navigation guard** — `will-navigate` no longer mis-classifies `file:` loads (opaque `"null"` origin) as external navigation. Allowed in-app protocols include `file:`, `about:`, and `devtools:`; `http:` / `https:` are still blocked from the embedded window.
+- **Desktop offline UI (bundle)** — Shared helpers load before dependent modules; timesheet period and time-off request lists expose **Delete** where allowed (with `currentUserProfile.id` for ownership); approve/reject controls read approval state from `state.currentUserProfile`; API client includes `deleteTimesheetPeriod` and `deleteTimeOffRequest`.
### Added
-- **Quote line item reorder (Issue #584)** — Non-null `quote_items.position` (migration `146_add_quote_item_position`); `Quote.items` is ordered by `position`, then `id`. Create, edit, duplicate, bulk duplicate, API item payloads, and quote-template apply assign positions from the submitted row order. **Create quote** and **edit quote** line tables include **Move up** / **Move down** per row so items can be reordered without deleting and re-entering lines; PDFs and detail views follow the saved order.
+- **Quote line item reorder (Issue #584)** — Non-null `quote_items.position` (migration `146_add_quote_item_position`); `Quote.items` is ordered by `position`, then `id`. Create, edit, duplicate, bulk duplicate, API item payloads, and quote-template apply assign positions from the submitted row order. **Create quote** and **edit quote** forms include per-row **Move up** / **Move down** controls on **Quote line items**, **Costs**, and **Extra goods** so rows can be reordered without deleting and re-entering data; PDFs and detail views follow the saved order. New translatable UI strings: **Order**, **Move up**, **Move down** (run `pybabel extract` / `update` per [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md)).
- **Offline queue replay** — Queued requests now store method, headers, and body in a replay-safe form (serializable for localStorage). POST/PUT requests replayed when back online send the same body and method. Legacy queue items (with `options` only) are still replayed via fallback.
- **Inventory API scopes** — New scopes `read:inventory` and `write:inventory` for inventory-only API access. Existing `read:projects` and `write:projects` still grant the same inventory access for backward compatibility.
- **Client portal reports: date range and CSV export** — Reports support optional `days` query param (1–365, default 30). Add `?format=csv` to download a CSV of the same report (summary, hours by project, time by date). Export uses the same access control as the reports page.
- **Jira webhook verification** — When a webhook secret is configured in the Jira integration (Connection Settings → Webhook Secret), incoming webhooks are verified using HMAC-SHA256 of the request body. Supported headers: `X-Hub-Signature-256`, `X-Atlassian-Webhook-Signature`, `X-Hub-Signature`. Requests with missing or invalid signature are rejected. If no secret is set, behavior is unchanged (all webhooks accepted).
+- **Crowdin integration (maintainers)** — Root [`crowdin.yml`](crowdin.yml) maps `translations/en/LC_MESSAGES/messages.po` to per-locale `messages.po` paths (with `nb` → `no` for Norwegian). Manual [`.github/workflows/crowdin-sync.yml`](.github/workflows/crowdin-sync.yml) uploads sources and downloads translations when `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` are set. [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) includes a Crowdin setup section; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) and contributor docs cross-link it.
### Changed
+- **Documentation (translations)** — Added [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for contributors without Git (issue template, optional spreadsheet or hosted platform, maintainer workflow). Root [CONTRIBUTING.md](CONTRIBUTING.md) links to it; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) defers the enabled locale list to `app/config.py` (`LANGUAGES`) and points translators at the new guide.
- **Factur-X / PDF/A-3 invoice PDFs (export and email)** — Download and email attachments use the same embed-and-normalize path. Embedded CII uses Associated File relationship **Data** and MIME **text/xml**. PDF/A-3 normalization embeds sRGB via `app/resources/icc/` (override with `INVOICE_SRGB_ICC_PATH`). Added `app/utils/invoice_pdf_postprocess.py` and tests; [PEPPOL e-Invoicing](docs/admin/configuration/PEPPOL_EINVOICING.md) updated (veraPDF note, pytest command).
- **Documentation sync** — CODEBASE_AUDIT.md: marked gaps 2.3–2.7 and 2.9 as fixed; added “Implemented 2026-03-16” summary. CLIENT_FEATURES_IMPLEMENTATION_STATUS: report date range and CSV export noted as implemented. INCOMPLETE_IMPLEMENTATIONS_ANALYSIS: added “Verified 2026-03-16” for webhook verification, issues permissions, search API, offline queue.
- **Activity feed API date params** — `/api/activity` now returns 400 with a clear message when `start_date` or `end_date` are invalid (e.g. not ISO 8601). Invalid dates on the web route `/activity` are logged and the filter is skipped (no 500).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2fe68d3d..e70ec9f8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,6 +5,7 @@ Thank you for your interest in contributing to TimeTracker. This page gives you
## How to Contribute
- **Report bugs** — Use the [GitHub issue tracker](https://github.com/drytrix/TimeTracker/issues). Include steps to reproduce, expected vs actual behavior, and your environment (OS, deployment method, version).
+- **Improve translations (no Git)** — Use the **Translation improvement** issue template, or read [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for spreadsheet, maintainer workflow, and optional Crowdin setup ([`crowdin.yml`](crowdin.yml), **Actions → Crowdin sync**).
- **Suggest features** — Open a [feature request](https://github.com/drytrix/TimeTracker/issues/new?template=feature_request.md) with a clear description and use case.
- **Submit code** — Fork the repo, create a branch, make your changes, add tests, and open a pull request. Follow the [full contributing guidelines](docs/development/CONTRIBUTING.md) for setup, coding standards, and PR process.
diff --git a/app/templates/quotes/_edit_quote_form_scripts.html b/app/templates/quotes/_edit_quote_form_scripts.html
index a0cfd248..53e64d8d 100644
--- a/app/templates/quotes/_edit_quote_form_scripts.html
+++ b/app/templates/quotes/_edit_quote_form_scripts.html
@@ -10,6 +10,54 @@ document.addEventListener('DOMContentLoaded', function() {
const stockItems = {{ stock_items_json | safe if stock_items_json else '[]' }};
const warehouses = {{ warehouses_json | safe if warehouses_json else '[]' }};
+ function reorderButtonsHtml() {
+ return (
+ '
' +
+ '' +
+ '
'
+ );
+ }
+
+ function refreshReorderStates(parent, rowSelector) {
+ if (!parent) return;
+ const rows = parent.querySelectorAll(rowSelector);
+ rows.forEach(function(r, i) {
+ const up = r.querySelector('.quote-line-move-up');
+ const down = r.querySelector('.quote-line-move-down');
+ if (up) up.disabled = i === 0;
+ if (down) down.disabled = i === rows.length - 1;
+ });
+ }
+
+ function moveRowInSection(row, delta) {
+ const parent = row.parentElement;
+ if (!parent) return;
+ var sel = null;
+ if (row.classList.contains('quote-item-row')) sel = '.quote-item-row';
+ else if (row.classList.contains('quote-expense-row')) sel = '.quote-expense-row';
+ else if (row.classList.contains('quote-good-row')) sel = '.quote-good-row';
+ else return;
+ const rows = Array.from(parent.querySelectorAll(sel));
+ const idx = rows.indexOf(row);
+ if (idx < 0) return;
+ const newIdx = idx + delta;
+ if (newIdx < 0 || newIdx >= rows.length) return;
+ const ref = rows[newIdx];
+ if (delta < 0) parent.insertBefore(row, ref);
+ else parent.insertBefore(row, ref.nextSibling);
+ refreshReorderStates(parent, sel);
+ calculateTotals();
+ }
+
+ function wireReorderButtons(row) {
+ row.querySelector('.quote-line-move-up')?.addEventListener('click', function() {
+ moveRowInSection(row, -1);
+ });
+ row.querySelector('.quote-line-move-down')?.addEventListener('click', function() {
+ moveRowInSection(row, 1);
+ });
+ }
+
function stockOptionsHtml(selectedId) {
let h = '';
if (stockItems && Array.isArray(stockItems)) {
@@ -40,7 +88,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (!stockCols || !descWrap) return;
if (stockMode) {
stockCols.classList.remove('hidden');
- descWrap.classList.remove('md:col-span-6');
+ descWrap.classList.remove('md:col-span-5');
descWrap.classList.add('md:col-span-2');
} else {
stockCols.classList.add('hidden');
@@ -53,7 +101,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (ss) ss.value = '';
if (ws) ws.value = '';
descWrap.classList.remove('md:col-span-2');
- descWrap.classList.add('md:col-span-6');
+ descWrap.classList.add('md:col-span-5');
}
}
@@ -94,12 +142,15 @@ document.addEventListener('DOMContentLoaded', function() {
}
row.querySelector('.remove-item')?.addEventListener('click', function() {
row.remove();
+ if (itemsContainer) refreshReorderStates(itemsContainer, '.quote-item-row');
calculateTotals();
});
+ wireReorderButtons(row);
row.querySelectorAll('[data-calc-trigger]').forEach(function(el) {
el.addEventListener('input', calculateTotals);
});
applyItemRowLayout(row);
+ if (itemsContainer) refreshReorderStates(itemsContainer, '.quote-item-row');
}
function addItemRow() {
@@ -109,14 +160,15 @@ document.addEventListener('DOMContentLoaded', function() {
'' +
'' +
'' +
+ reorderButtonsHtml() +
'
`;
diff --git a/docs/CONTRIBUTING_TRANSLATIONS.md b/docs/CONTRIBUTING_TRANSLATIONS.md
new file mode 100644
index 00000000..4eebe88b
--- /dev/null
+++ b/docs/CONTRIBUTING_TRANSLATIONS.md
@@ -0,0 +1,98 @@
+# Contributing translations (no Git required)
+
+This project uses **GNU gettext** `.po` files under `translations//LC_MESSAGES/messages.po`, compiled at app startup (see [TRANSLATION_SYSTEM.md](TRANSLATION_SYSTEM.md)). You do **not** need Git or developer tools to suggest fixes.
+
+## How we accept help (channels)
+
+Maintainers should pick what fits workload and community size. The **default** for this repository is **A**; scale up to **B** or **C** when needed.
+
+| Channel | Best for | Git? |
+|--------|-----------|------|
+| **A. GitHub issue (recommended default)** | Wrong/missing wording, one string or a small batch | No — use the “Translation improvement” template when creating an issue |
+| **B. Spreadsheet or form** | Many rows at once, non-GitHub users | No — maintainer copies suggestions into `.po` |
+| **C. Hosted translation platform** | Ongoing community, many languages, history and glossaries | No for translators; maintainer connects repo or uploads `.po` |
+
+### A. GitHub issue (primary)
+
+1. Open a new issue and choose **Translation improvement**.
+2. Fill in language, where you saw the text, current UI text, and your suggested wording.
+3. A maintainer updates the correct `messages.po` and merges the change.
+
+No repository access is required.
+
+### B. Spreadsheet or form (optional)
+
+Use when contributors cannot or will not use GitHub:
+
+1. Maintainer shares a table with columns such as: **Language code**, **Screen or page**, **Text as shown now**, **Should be (your suggestion)**, **Notes**.
+2. Contributors only edit the suggestion column.
+3. Maintainer applies changes to the `.po` files and validates placeholders (see below).
+
+### C. Hosted platform (optional, higher volume)
+
+Examples: [Weblate](https://weblate.org/) (open source, can be self-hosted), [Crowdin](https://crowdin.com/), [POEditor](https://poeditor.com/), [Transifex](https://www.transifex.com/). Translators work in the browser; integration or export/import keeps `.po` in sync with the codebase. Setup is maintainer-owned.
+
+#### Crowdin setup (maintainers)
+
+This repo includes a root [`crowdin.yml`](../crowdin.yml) that maps **source** `translations/en/LC_MESSAGES/messages.po` to **translations** under `translations//LC_MESSAGES/messages.po`, with **`nb` → `no`** so Norwegian matches `app/config.py` (`no`, not `nb`). You may still have a legacy `translations/nb/` tree locally; prefer **`no`** in Crowdin and in config so you do not maintain two Norwegian copies.
+
+1. **Create a Crowdin account and project** at [crowdin.com](https://crowdin.com/) → **Create project**.
+2. **Source language:** English. Treat the resource as **Gettext PO** (`.po`).
+3. **Target languages:** Add every locale you ship: `nl`, `de`, `fr`, `it`, `fi`, `es`, `no`, `ar`, `he` (match `LANGUAGES` in `app/config.py`). For Norwegian, add Norwegian (Bokmål) in Crowdin; the `crowdin.yml` mapping writes files into `translations/no/`.
+4. **Sync with this repository (pick one):**
+ - **GitHub Action:** In the GitHub repo, add Actions secrets `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` (Crowdin project **Details** shows the numeric project ID; **Account Settings → API** creates the token with project access, typically Manager). Run **Crowdin sync** from the **Actions** tab → **Run workflow**. For a **one-time** import of existing `.po` files into Crowdin’s translation memory, temporarily set `upload_translations: true` in [.github/workflows/crowdin-sync.yml](../.github/workflows/crowdin-sync.yml), run it once, then set it back to `false`.
+ - **Crowdin’s GitHub integration:** Crowdin → **Integrations → GitHub** → connect the repo and branch; point it at the same `crowdin.yml` so Crowdin can open PRs when translations are updated.
+ - **Crowdin CLI:** Install the [Crowdin CLI](https://crowdin.github.io/crowdin-cli/), export the same env vars, run `crowdin upload sources` (and optionally `crowdin upload translations` once) from the repository root.
+5. **When developers add or change `_()` strings:** Run `pybabel extract` / `pybabel update` locally (see [TRANSLATION_SYSTEM.md](TRANSLATION_SYSTEM.md)), commit if you version those files, then upload sources to Crowdin again.
+6. **Landing translations:** Approve in Crowdin if you use review, then download (workflow or integration PR), merge, and run the app so `.mo` files rebuild.
+
+Translators only need a Crowdin account; they do not use git.
+
+### Other options (reference)
+
+- **Poedit:** Maintainer can zip `translations//LC_MESSAGES/messages.po` for a trusted translator; they edit in [Poedit](https://poedit.net/) and send the file back. Avoid two people editing the same locale in parallel without coordination.
+- **GitHub web editor on `.po` files:** Possible for experts only; easy to break quoting or plural blocks.
+
+## Rules for translators
+
+Follow these so your suggestion can be applied without breaking the app:
+
+1. **Do not change English source keys.** In `.po` files those are `msgid` lines. In an issue or spreadsheet you describe what you see; maintainers map it to the file. Never invent a new English “key” string.
+2. **Preserve placeholders exactly.** If the UI shows `Hello, %(username)s` or similar, your translation must include the same placeholders (same names, same `%(name)s`-style segments). Same for `%s`, `%d`, or other format tokens.
+3. **Plurals:** Some strings have one vs many forms. If you are unsure, describe the case in **Notes** and a maintainer will set `msgstr[0]` / `msgstr[1]` correctly in the `.po` file.
+4. **Context matters.** Say which **page**, **button**, or **dialog** the text appears on, and attach a **screenshot** if possible. One English phrase can appear in multiple places with different meanings.
+5. **Length and tone:** Short labels (buttons, nav) should stay compact. Full sentences can be more natural in your language than literal word-for-word English.
+
+**Supported locale codes** (see `app/config.py` `LANGUAGES`): `en`, `nl`, `de`, `fr`, `it`, `fi`, `es`, `no`, `ar`, `he`.
+
+## Maintainer workflow
+
+Designate at least one person responsible for translation intake (issues, spreadsheet, or platform export).
+
+### Applying contributor suggestions
+
+1. Identify the locale file: `translations//LC_MESSAGES/messages.po`.
+2. Find the entry (by `msgid` / English source or grep for the current `msgstr`).
+3. Update `msgstr` (and plural `msgstr[n]` if needed). Remove `#, fuzzy` if you are sure the translation is correct (fuzzy entries may be ignored at compile time depending on setup).
+4. Restart the app or trigger your usual deploy so `.mo` is regenerated (see [TRANSLATION_SYSTEM.md](TRANSLATION_SYSTEM.md) — compilation runs on startup via `app/utils/i18n.py`).
+
+### After new UI strings ship in code
+
+When developers add or change translatable strings:
+
+```bash
+pybabel extract -F babel.cfg -o messages.pot .
+pybabel update -i messages.pot -d translations
+```
+
+Then fill new empty entries in each `messages.po`, run the app, and smoke-test critical screens in a few locales.
+
+### Verification checklist
+
+- [ ] Placeholders in `msgstr` match the `msgid` / source string.
+- [ ] `.po` file is valid UTF-8 and parses (Poedit or `msgfmt --check`).
+- [ ] UI checked in the target language for overflow or clipping on small screens (especially for short buttons).
+
+## See also
+
+- Technical overview: [TRANSLATION_SYSTEM.md](TRANSLATION_SYSTEM.md)
diff --git a/docs/TRANSLATION_SYSTEM.md b/docs/TRANSLATION_SYSTEM.md
index 674e5fbe..03f796e3 100644
--- a/docs/TRANSLATION_SYSTEM.md
+++ b/docs/TRANSLATION_SYSTEM.md
@@ -2,7 +2,7 @@
## Overview
-TimeTracker includes a comprehensive internationalization (i18n) system powered by Flask-Babel. The application supports 6 languages out of the box:
+TimeTracker includes a comprehensive internationalization (i18n) system powered by Flask-Babel. Enabled locales are defined in `app/config.py` (`LANGUAGES`), for example:
- **English** (en) - Default
- **Dutch** (nl - Nederlands)
@@ -10,6 +10,7 @@ TimeTracker includes a comprehensive internationalization (i18n) system powered
- **French** (fr - Français)
- **Italian** (it - Italiano)
- **Finnish** (fi - Suomi)
+- **Spanish** (es), **Norwegian** (no), **Arabic** (ar), **Hebrew** (he), and others as configured
## User Experience
@@ -38,7 +39,7 @@ Language preference is persisted:
### Translation Files
-Translation files are located in `translations/` directory:
+Translation files are located in `translations/` directory (see `app/config.py` for the full list of enabled locales; additional `.po` files may exist for locales not yet wired in config).
```
translations/
@@ -47,7 +48,9 @@ translations/
├── de/LC_MESSAGES/messages.po # German
├── fr/LC_MESSAGES/messages.po # French
├── it/LC_MESSAGES/messages.po # Italian
-└── fi/LC_MESSAGES/messages.po # Finnish
+├── fi/LC_MESSAGES/messages.po # Finnish
+├── es/LC_MESSAGES/messages.po # Spanish
+└── ... # Other locales as configured
```
### Configuration
@@ -252,7 +255,7 @@ Potential improvements:
1. Add more languages (Spanish, Portuguese, Japanese, etc.)
2. Right-to-left (RTL) language support (Arabic, Hebrew)
-3. User-contributed translations via Crowdin or similar
+3. User-contributed translations via [CONTRIBUTING_TRANSLATIONS.md](CONTRIBUTING_TRANSLATIONS.md) (issues, spreadsheet, or a hosted platform such as Weblate/Crowdin)
4. Automatic language detection improvement
5. Translation coverage reporting
@@ -261,13 +264,14 @@ Potential improvements:
For questions or issues with translations:
1. Check this documentation
-2. Review `app/__init__.py` locale selector
-3. Inspect browser network requests to `/i18n/set-language`
-4. Check application logs for translation compilation errors
+2. **Contributors without Git:** see [CONTRIBUTING_TRANSLATIONS.md](CONTRIBUTING_TRANSLATIONS.md) (issue template, spreadsheet option, maintainer workflow, and optional [Crowdin](https://crowdin.com/) using root [`crowdin.yml`](../crowdin.yml) and the **Crowdin sync** GitHub Action)
+3. Review `app/__init__.py` locale selector
+4. Inspect browser network requests to `/i18n/set-language`
+5. Check application logs for translation compilation errors
---
-**Last Updated**: 2025-10-07
+**Last Updated**: 2026-04-15
**Flask-Babel Version**: 4.0.0
**Babel Version**: 2.14.0
diff --git a/docs/admin/configuration/DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md b/docs/admin/configuration/DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md
index 03c71181..83050f08 100644
--- a/docs/admin/configuration/DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md
+++ b/docs/admin/configuration/DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md
@@ -143,6 +143,16 @@ If you're hitting path length limits:
- Cleaner, more reliable than npm install
- Build script already uses this
+## App window stuck on loading or shows blank content
+
+If the installer or executable starts but the main window never leaves the loading state, shows a blank page, or behaves as if navigation is stuck (often reported on Windows 11):
+
+1. **Update to the latest build** from the project releases or rebuild from the current `develop` branch. Older builds could mishandle `file:` URL navigation in Electron or ship an incomplete renderer bundle.
+2. **Rebuild the renderer** when building from source: from the `desktop` folder run `npm install` then `npm run build:renderer`, then `npm run build:win` (or your usual build command). The packaged app expects an up-to-date `src/renderer/js/bundle.js`.
+3. **Confirm the server URL** on the login screen and try again after a full quit and restart.
+
+If the problem persists after a clean rebuild, open an issue with your app version, Windows build, and any DevTools console output (run `npm run dev` for a local build with DevTools).
+
## Additional Resources
- [npm Troubleshooting Guide](https://docs.npmjs.com/common-errors)
diff --git a/docs/development/CONTRIBUTING.md b/docs/development/CONTRIBUTING.md
index 9178e139..76aade51 100644
--- a/docs/development/CONTRIBUTING.md
+++ b/docs/development/CONTRIBUTING.md
@@ -50,6 +50,10 @@ This project and everyone participating in it is governed by our [Code of Conduc
- Push to the branch (`git push origin feature/amazing-feature`)
- Open a Pull Request
+### Translations (no Git required)
+
+Contributors who only want to fix wording can use the **Translation improvement** GitHub issue template or follow [CONTRIBUTING_TRANSLATIONS.md](../CONTRIBUTING_TRANSLATIONS.md) (spreadsheet option, maintainer workflow, optional Crowdin using [`crowdin.yml`](../../crowdin.yml) and the **Crowdin sync** workflow). Developers adding new `_('...')` strings should run `pybabel extract` / `update` as described there.
+
## Development Setup
### Prerequisites
diff --git a/docs/development/FRONTEND_QUALITY_GATES.md b/docs/development/FRONTEND_QUALITY_GATES.md
index 10579f27..0dac6acb 100644
--- a/docs/development/FRONTEND_QUALITY_GATES.md
+++ b/docs/development/FRONTEND_QUALITY_GATES.md
@@ -58,6 +58,6 @@ This document tracks frontend modernization phases and how to run quality checks
|------|-----------|
| Web base layout | `app/templates/base.html`, `app/static/base-init.js`, `app/static/pwa-enhancements.js` |
| Web search/IDs | `app/templates/*/list.html` (unique filter search IDs), `app/static/enhanced-search.js` |
-| Desktop renderer | `desktop/src/renderer/js/app.js`, `desktop/src/renderer/js/state.js`, `desktop/src/renderer/js/ui/notifications.js`, `desktop/src/renderer/js/bundle.js` |
+| Desktop renderer | `desktop/src/renderer/js/app.js` (esbuild entry; import shared modules such as `utils/helpers.js` from here), `desktop/src/renderer/js/state.js`, `desktop/src/renderer/js/ui/notifications.js`, `desktop/src/renderer/js/bundle.js` (run `npm run build:renderer` after renderer changes) |
| Mobile finance | `mobile/lib/presentation/screens/finance_workforce_screen.dart`, `mobile/lib/presentation/providers/finance_workforce_providers.dart` |
| Mobile home | `mobile/lib/presentation/screens/home_screen.dart` (IndexedStack for tabs) |
diff --git a/setup.py b/setup.py
index d609413a..9a280421 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ from setuptools import setup, find_packages
setup(
name='timetracker',
- version='5.3.0',
+ version='5.3.1',
packages=find_packages(),
include_package_data=True,
package_data={
diff --git a/tests/test_routes/test_quotes_web.py b/tests/test_routes/test_quotes_web.py
index 07a9615f..66081243 100644
--- a/tests/test_routes/test_quotes_web.py
+++ b/tests/test_routes/test_quotes_web.py
@@ -14,6 +14,8 @@ def test_create_quote_redirect_then_view_returns_200(admin_authenticated_client,
"title": "Regression Quote 583",
"tax_rate": "0",
"currency_code": "EUR",
+ # Triggers Valid until block + expired badge on view (issue #583 used undefined now()).
+ "valid_until": "2020-01-01",
},
follow_redirects=False,
)