Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
6812826
Add CameraX and ZXing for the staff ISBN scanner
fabiodalez-dev Oct 13, 2026
554b853
Add the data layer for staff management, offline catalogue and loans …
fabiodalez-dev Oct 13, 2026
16e6677
Answer from the offline catalogue when the server is unreachable
fabiodalez-dev Oct 13, 2026
ae65a4e
Add the loans calendar subscription and the staff entry to Profile
fabiodalez-dev Oct 13, 2026
e8cb1e7
Add the staff area: catalogue management and the ISBN scanner
fabiodalez-dev Oct 13, 2026
8eaffdf
Release 1.7.0 (versionCode 20): staff catalogue, offline catalogue, l…
fabiodalez-dev Oct 13, 2026
8074242
Treat the calendar feed link as a secret
fabiodalez-dev Oct 13, 2026
38a6579
docs: STATUS records the 1.7.0 verification
fabiodalez-dev Oct 13, 2026
6c67828
Let author and publisher edits clear a field
fabiodalez-dev Oct 13, 2026
a837565
Keep the staff book list paging after a reload
fabiodalez-dev Oct 13, 2026
34e6d49
Require a level with a shelf and stay on the record when the cover fails
fabiodalez-dev Oct 13, 2026
ff81814
Harden the offline catalogue search and sync
fabiodalez-dev Oct 13, 2026
3db98c5
Revoke the staff role only on the API's own forbidden error
fabiodalez-dev Oct 13, 2026
a7e7f98
Follow device connectivity and never show raw network errors
fabiodalez-dev Oct 13, 2026
399ea83
Fix count wording and the calendar feed description
fabiodalez-dev Oct 13, 2026
f571720
Show the refreshed role in the Profile header
fabiodalez-dev Oct 13, 2026
73c5786
Wait up to 60 s for a server response
fabiodalez-dev Oct 13, 2026
c38d990
Add device tests for the staff area, scanner, offline mode and calendar
fabiodalez-dev Oct 13, 2026
fbe4e31
Add a live end-to-end smoke test against a real Pinakes instance
fabiodalez-dev Oct 13, 2026
18a9f58
Hide gender and tax code when the library hides them
fabiodalez-dev Oct 13, 2026
db5a2d4
Give authors and publishers a page with an info card and their books
fabiodalez-dev Oct 13, 2026
4ccf9ce
Fit the loan calendar on phones
fabiodalez-dev Oct 13, 2026
fd2a099
Check hidden profile fields in the live smoke test
fabiodalez-dev Oct 13, 2026
97225a8
Never save an author or publisher form that failed to load
fabiodalez-dev Oct 13, 2026
a89ee35
Collect connectivity once in the catalogue search
fabiodalez-dev Oct 13, 2026
b7969dd
Keep raw network errors out of the Profile snackbars
fabiodalez-dev Oct 13, 2026
aea2f9f
Add one copy from the duplicate dialog when editing
fabiodalez-dev Oct 13, 2026
d030981
Restart the scan suppression window when a sheet closes
fabiodalez-dev Oct 13, 2026
e2e4b5f
Update the offline loans and wishlist after each change
fabiodalez-dev Oct 13, 2026
2b977f3
Confirm a loan request or wishlist add before refreshing the offline …
fabiodalez-dev Oct 13, 2026
2585002
Format dates in the language picked in the app
fabiodalez-dev Oct 13, 2026
dceb7c1
Mention reservations and loan start dates in the calendar sheet
fabiodalez-dev Oct 13, 2026
e6c9a60
Add the Mobile API 1.7.0 staff data layer for circulation, books and …
fabiodalez-dev Oct 13, 2026
f1ad1a3
Run the circulation desk from the staff area
fabiodalez-dev Oct 13, 2026
9b788bc
Send an Idempotency-Key with every new loan and reservation
fabiodalez-dev Oct 13, 2026
bf13e1c
Cover the circulation desk with on-device flows
fabiodalez-dev Oct 13, 2026
ae03e82
Manage copies, series and duplicates from the staff area
fabiodalez-dev Oct 13, 2026
3a44e5e
Import catalogue files and fill in covers from the staff area
fabiodalez-dev Oct 13, 2026
16a3ca9
Manage donation offers and archives natively
fabiodalez-dev Oct 14, 2026
98704f1
Document 1.8.0 and extend the live smoke to the staff phase 2 areas
fabiodalez-dev Oct 14, 2026
7e8080f
Follow the backend's content-tool precisions
fabiodalez-dev Oct 14, 2026
9e7e6c0
Attach eBooks and audiobooks from the book editor
fabiodalez-dev Oct 14, 2026
126b167
Make the live smoke's offer step self-contained and record the green run
fabiodalez-dev Oct 14, 2026
eca52e6
Write staff counts so they read correctly with one item
fabiodalez-dev Oct 14, 2026
80d7173
Fix paging, batching, uploads and failed loads in the staff screens
fabiodalez-dev Oct 14, 2026
b8db4c6
Manage periodicals, book clubs and FRBR works from the staff area
fabiodalez-dev Oct 14, 2026
9be8401
Extend the live smoke to the Tier 2 bridges and document them
fabiodalez-dev Oct 14, 2026
e6c10f9
Send the app language to the server with every request
fabiodalez-dev Oct 14, 2026
adf1d67
Keep the My Library tab labels on one line
fabiodalez-dev Oct 14, 2026
c4a4dc7
Show the staff area in the README
fabiodalez-dev Oct 14, 2026
d8f9b3a
Call the operators' section the admin area
fabiodalez-dev Oct 14, 2026
8722971
Keep the bottom navigation on the book page
fabiodalez-dev Oct 14, 2026
1b967c4
Keep every character a reader types into the scan code fields
fabiodalez-dev Oct 14, 2026
60c5e4a
Say on the book page that only borrowers can review it
fabiodalez-dev Oct 14, 2026
e52bdfb
Open every new session on Home
fabiodalez-dev Oct 14, 2026
37222aa
Read the issue add-on when the periodicals desk scans with the camera
fabiodalez-dev Oct 14, 2026
07e4313
Keep a workflow state's other flags when the club workflow is saved
fabiodalez-dev Oct 14, 2026
4da43cc
Let the periodicals desk wait for the issue add-on before looking up …
fabiodalez-dev Oct 14, 2026
ea339a9
Release 1.8.0 (versionCode 22) and start the camera-denied test from …
fabiodalez-dev Oct 14, 2026
0f40061
Do not reopen the issue just handled while the magazine is still unde…
fabiodalez-dev Oct 14, 2026
e4ce70e
Skip the camera-denied test on a device left with the camera granted
fabiodalez-dev Oct 14, 2026
3ede07d
Keep a held masthead alive while its own frames keep arriving
fabiodalez-dev Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,61 @@ Login, home, catalog, book detail and calendar captures use the 2026 interface o
</tr>
</table>

### Admin area

Admin and staff accounts get a native staff area when the instance runs Mobile API 1.7.0: cataloguing with ISBN scanning, the whole circulation desk and the content of the Desiderata, Archives, Periodicals, Book Club and FRBR plugins. Captured on Android 15 at 360 dp against a local instance with demo data.

#### Circulation desk

<table>
<tr>
<td align="center"><img src="docs/screenshots/staff/circulation-desk.png" width="200" alt="Circulation desk with counters"><br><sub>Circulation desk</sub></td>
<td align="center"><img src="docs/screenshots/staff/loans-overdue.png" width="200" alt="Overdue loans list"><br><sub>Loans by status</sub></td>
<td align="center"><img src="docs/screenshots/staff/loan-detail-overdue.png" width="200" alt="Overdue loan with return, edit, reminder and receipt actions"><br><sub>Loan actions</sub></td>
<td align="center"><img src="docs/screenshots/staff/loan-detail-request.png" width="200" alt="Pending request with approve and reject"><br><sub>Approve a request</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/staff/new-loan.png" width="200" alt="New loan form with borrower and book search"><br><sub>New loan</sub></td>
<td align="center"><img src="docs/screenshots/staff/reservations.png" width="200" alt="Reservations grouped by title with queue position"><br><sub>Reservations queue</sub></td>
<td align="center"><img src="docs/screenshots/staff/copy-scanner.png" width="200" alt="Copy label scanner"><br><sub>Scan a copy label</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-hub.png" width="200" alt="Admin area hub"><br><sub>Admin area</sub></td>
</tr>
</table>

#### Cataloguing

<table>
<tr>
<td align="center"><img src="docs/screenshots/staff/isbn-preview.png" width="200" alt="ISBN lookup preview from the server scrapers"><br><sub>Scan an ISBN</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-books.png" width="200" alt="Staff book list"><br><sub>Books</sub></td>
<td align="center"><img src="docs/screenshots/staff/book-edit.png" width="200" alt="Book editor with cover sources"><br><sub>Edit a record</sub></td>
<td align="center"><img src="docs/screenshots/staff/book-copies.png" width="200" alt="Copies, location and labels in the book editor"><br><sub>Copies and labels</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/staff/author-detail.png" width="200" alt="Author page with books and record details"><br><sub>Author page</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-authors.png" width="200" alt="Author search"><br><sub>Authors</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-genres.png" width="200" alt="Genre tree"><br><sub>Genres</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-series.png" width="200" alt="Series search"><br><sub>Series</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/staff/series-detail.png" width="200" alt="Series detail with its volumes"><br><sub>Series detail</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-merges.png" width="200" alt="Merge duplicate authors, publishers and genres"><br><sub>Merge duplicates</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-imports.png" width="200" alt="CSV and LibraryThing import"><br><sub>Import a file</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-covers.png" width="200" alt="Missing covers and enrichment"><br><sub>Covers and enrichment</sub></td>
</tr>
</table>

#### Plugin content

<table>
<tr>
<td align="center"><img src="docs/screenshots/staff/staff-desiderata.png" width="200" alt="Donation offers by status"><br><sub>Donation offers</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-archives.png" width="200" alt="Archival units with publishing filter"><br><sub>Archives</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-periodicals.png" width="200" alt="Periodicals desk with issue barcode lookup"><br><sub>Periodicals desk</sub></td>
<td align="center"><img src="docs/screenshots/staff/staff-hub-2.png" width="200" alt="Staff hub plugin section"><br><sub>Staff tools</sub></td>
</tr>
</table>

## Features

| Area | What you get |
Expand All @@ -73,6 +128,10 @@ Login, home, catalog, book detail and calendar captures use the 2026 interface o
| **Profile** | Edit profile, change password, device list, theme switcher, language switcher, logout |
| **Notifications** | Loan due/overdue, reservation ready, book available |
| **Themes** | 2026 warm neutrals, Geist + Fraunces, complete 3D covers and paired theme colours. Light by default; pick light/dark/system in Profile |
| **Admin area** | For admin and staff accounts: cataloguing (ISBN/EAN scanner with batch mode, manual records, copies with their own codes, series, merge duplicates, label PDFs, CSV and LibraryThing import, covers and enrichment, authors, publishers, genres, shelves) and plugin content (donation offers, archives, periodicals desk, book clubs, FRBR works, digital files) |
| **Circulation desk** | Counters, loans by status, new loan with borrower search and copy scanning, approve/reject, pickup, return with copy outcome, renew, bulk extend, reminders, receipt PDF, reservations queue. Hidden while offline |
| **Offline catalogue** | Full catalogue mirrored on the device with delta sync; search, book pages, My Library and Wishlist work offline, while loan and reservation actions stay hidden until the connection is back |
| **Loans in your calendar** | A private, revocable calendar link with your loans (start to due date) and reservations, for Google Calendar, Apple Calendar, Outlook and others |
| **Languages** | Italian, English, French, German, following the device locale or an in-app choice |

## Tech stack
Expand All @@ -97,6 +156,7 @@ Login, home, catalog, book detail and calendar captures use the 2026 interface o
./gradlew assembleDebug # debug APK → app/build/outputs/apk/debug/app-debug.apk
./gradlew lintDebug # static analysis
./gradlew testDebugUnitTest
adb shell pm revoke com.pinakes.app android.permission.CAMERA # the camera-denied test needs it denied
./gradlew connectedDebugAndroidTest # UI regression tests, with a running emulator
./gradlew assembleRelease # R8 + resource shrinking, unsigned without release credentials
```
Expand Down
28 changes: 27 additions & 1 deletion STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Pinakes Android — Build Status

## 1.8.0 (versionCode 22): circulation desk, book tools and plugin content for staff

Requires Mobile API 1.7.0. Every area is gated on its own `/health` flag (`staff_circulation`, `staff_copies`, `staff_series`, `staff_merges`, `staff_labels`, `staff_imports`, `staff_covers`, `staff_desiderata`, `staff_archives`) plus the staff role and `staff_catalog`; a server that answers `false` or omits a flag keeps the 1.7.0 staff area for that part. The 403 `forbidden` contract is unchanged: every new call goes through the shared `staffCall`, which drops the stored role.

- **Circulation desk** (hidden, never disabled, while the device is offline; an open desk screen closes when the connection drops): counters of the day, loans by bucket (requests, to pick up, scheduled, on loan, overdue, history) with search and multi-select bulk extend/recall, loan detail with the server's actions (approve, reject with reason, confirm/cancel pickup, return with the copy's outcome and a validated penalty, renew, edit user and dates, reminder, receipt PDF read in the app or shared through the FileProvider, receipt by email), new loan and new reservation (borrower search with eligibility, book search or a scanned copy code, per-user availability calendar, hand over now), reservations grouped by title (edit dates, cancel with reason, reactivate), borrower page. Creates carry an `Idempotency-Key` (one per submission, kept across the automatic retry after a dropped connection or a 409 `concurrent_retry`, discarded after success or `idempotency_conflict`).
- **Copy scanner**: the same CameraX + ZXing pipeline reading CODE 128 and CODE 39 inventory labels (and EAN-13); manual code entry always visible; the sheet shows the copy and its open loan, then returns it with an outcome, lends it, or opens the book or the loan.
- **Book tools**: per-copy list in the book editor (state, notes, delete with the server's refusal reasons, a copy with its own inventory code), book and copy label PDFs and the label settings sheet; series tree with search, details, rename (an existing name asks before merging), merge, delete, parent work, book numbers, removal and multi-select assignment; duplicate merges (suggested author groups or manual pick, publishers, genres); CSV and LibraryThing imports picked with the system document picker and run chunk by chunk from the server's progress (out-of-order recovery, busy retry, resumable after process death, pause/resume/cancel, error lines, past imports with their error CSV); missing covers fetched one book at a time with progress and the rate limit honoured; server-side cover sync and enrichment batches on the long-timeout client behind a confirmation; automatic enrichment switch.
- **Plugin content**: Desiderata offers by status (accept, reject, receive into the wanted title or a picked record, delete) and wanted titles marked received; Archives units (published or not) created and edited with the plugin's rules checked client-side, publish switch, authorities with roles, cover and document uploads, delete; authority records list and editor.
- Local verification: 377 debug unit tests (new: tolerant 1.7.0 parsing, loan actions, penalty validation, copy codes and export file names, staff areas × flags × connectivity, idempotency keys, series tree and series/label forms, import chunk rules, archive/authority forms and offer actions), zero lint errors (111 warnings, all pre-existing), debug and R8 release builds.
- Device tests: 92 instrumented tests on Android 15 (`staffe2e` with `FakePinakes`). New flows: return as damaged with penalty, invalid penalty not sent, borrower page, 409 message, new loan with user search and Idempotency-Key, ineligible borrower, return by typed copy code, unknown code, reservation cancel and create, circulation hidden offline and restored, desk screen closing offline, no section without flags; copy edit, refused delete, copy with code, taken code; series number and rename clash merge; suggested author merge; import resume with out-of-order recovery and an upload through the real system document picker; covers loop and enrichment switch; desiderata receive (linked and into a picked record) and closed offer; archive publish keeping every field and create after validation; a retried merge already done; digital upload through the system picker and its absence without the flag; review regressions (bulk recall in batches with Select all, reservation paging, an authority that failed to load cannot be saved, a failed duplicate list with Retry, an import that ended elsewhere is not resumable, exports deleted on sign out); Tier 2 (a typed issue code received, a masthead's issues and a new year, a club created and a member's role changed, a workflow edited, a work created with an expression and a linked book, no Tier 2 rows without their flags).
- Live smoke (`tools/e2e/live_smoke.py`) gains: loan created by API with an Idempotency-Key and its replay then returned in the app by typing the copy code, a copy with an explicit code from the book editor, a series created in the app, a two-row CSV import followed to completion, a free Desiderata offer received into a picked record, an unpublished archive unit absent from the public list. Each phase 2 step is skipped while its flag is off; cleanup returns loans the run left open and deletes only the books, series, units and offers the run created. Tier 2 steps: an unknown code at the periodicals desk (read-only, the bridge has no deletes), a book club and an FRBR work created in the app (deleted in cleanup). Run on 2026-10-14 against the local server (Mobile API 1.7.0 branch, every staff flag on) on an Android 15 emulator: 19 passed, 0 skipped, 0 failed.
- **Tier 2** (each behind its own flag: `staff_periodicals`, `staff_bookclub`, `staff_frbr`, `staff_digital_library`): periodicals desk (scan or type an arriving issue's barcode, then receive or claim it; mastheads with years, issue states, new years and issues, subscriptions); book club management (create, settings, reading workflow with reorderable states and voting/reading/archived marks, members' role and membership state, adding a borrower, delete); FRBR works (search, create, edit, delete, expressions, link and unlink books); eBook and audiobook upload from the book editor (streamed, long-timeout client). The periodicals bridge has no article routes, so articles stay on the website.

## 1.7.0 (versionCode 20): staff catalogue, offline catalogue, loans calendar

Requires Mobile API 1.6.0 (Pinakes 0.8.5). Every new entry point is gated on the matching `/health` feature, so an older server keeps showing exactly the 1.6.2 app.

- **Staff area** (`staff_catalog` + role `admin`/`staff`, refreshed from login and `GET /me`; a 403 on any staff call drops the stored role and leaves the screen): Profile → Staff area, and a scan button on the Catalog tab. Books (list with search, edit, delete with the active-circulation refusal), a shared create/edit form for records with or without an ISBN (identifiers validated live, authors/publishers pickers with "add new", genre/subgenre, shelf/level with automatic position, cover from URL / photo / picked image, copies), authors, publishers, genres (tree, find-or-create) and shelves/levels. Edits are true patches: only changed fields are sent.
- **ISBN scanner**: CameraX + ZXing EAN-13 (no Play services), checksum validation, two identical reads before a lookup, 3 s suppression, batch mode with remembered copies, shelf/level and optional auto-confirm, preview / duplicate ("add copies", "open existing") / not-found sheets. Manual ISBN/EAN entry is always visible (emulators, denied permission). CAMERA is requested only there, with rationale and a settings fallback; `camera.any` is optional.
- **Offline catalogue**: Room v2 holds the whole catalogue, kept current by `GET /catalog/sync` (delta with a server watermark, forced full resync every 7 days or when the server asks); loans, reservations and wishlist are written through on every fetch. Without a connection, search, book pages, Library and Wishlist answer from the cache with an offline banner. Loan and reservation actions are hidden offline (never shown disabled) and cached availability is shown only as "last known on <sync time>". The cache is excluded from backups.
- **Loans calendar**: Profile → Loans in your calendar (`calendar_feed`): create the private ICS link, copy it (primary, Google Calendar can only subscribe from its website), open it as `webcal://` with an HTTPS fallback, regenerate (old link stops) or turn it off.
- Local verification: 283 debug unit tests (new: ISBN/EAN codec, scan debouncer, staff access, staff form and write bodies, sync merge, offline policy, calendar links, tolerant 1.6.0 parsing, Room v2 catalogue/library DAOs), zero lint errors (111 warnings, mostly newer-dependency notices), debug and R8 release builds.
- Device tests (re-runnable): `app/src/androidTest/java/com/pinakes/app/staffe2e/` drives the real app (MainActivity, real Hilt graph, ViewModels, Room) against `FakePinakes`, a MockWebServer on the device loopback answering JSON fixtures from `app/src/androidTest/assets/fixtures/` and recording every request. Covered: staff entry only for admin/staff with `staff_catalog` (hidden for standard users, for the flag off and after a server-side downgrade, header included); scanner manual ISBN → preview → add → counter, duplicate → add copies, not found → manual form with the code, invalid code rejected, camera denied keeps manual entry (the camera prompt is denied through UiAutomator); manual form title required and shelf-without-level both send nothing, edit sends only the changed key (recorded PUT body); offline search banner with cached results and no availability or loan actions, offline detail "last known" wording without loan buttons, Library/Wishlist offline empty state with no raw exception, going offline after an online load hides the loan button and coming back restores it (real airplane mode); calendar create → masked link, copy flagged sensitive, regenerate and turn off both confirm; gender and tax code absent from the profile editor when the library hides them (present when shown) and never asked at registration; author detail with the padded info card and the author's books (`author_id` sent); loan-calendar month (7 weekday columns, every day cell inside the screen and inside a 320dp column, month navigation) and the birth-date picker within a 360dp screen. Run on a disposable emulator (it replaces the app's stored session and cache): `ANDROID_SERIAL=emulator-5554 ./gradlew connectedDebugAndroidTest` (one class: add `-Pandroid.testInstrumentationRunnerArguments.class=com.pinakes.app.staffe2e.ScannerFlowTest`).
- Live smoke (real app, real server): `tools/e2e/live-smoke.sh [--install]` with `PINAKES_E2E_URL` (as the device reaches it, e.g. `http://10.0.2.2:8081`), `PINAKES_E2E_EMAIL`, `PINAKES_E2E_PASSWORD` (admin or staff), optional `PINAKES_E2E_HOST_URL`, `PINAKES_E2E_ISBN` (default 9788845292613, must not be catalogued) and `ANDROID_SERIAL`. It clears the app, onboards, signs in, opens the staff hub, scan-adds an ISBN with 2 copies, adds a copy through the duplicate sheet, creates a record without ISBN, sets its shelf/level (skipped when the instance has none) and checks nothing else changed, searches and opens it in airplane mode, creates the calendar link and fetches the feed, and (with `PINAKES_E2E_DB_NAME/USER/PASSWORD/SOCKET` or `_HOST/_PORT`) hides gender and tax code in the settings table, checks they vanish from `/me` and from the profile editor, then restores the previous values; every step is checked through the API. Cleanup removes only what the run created (books, a calendar link that did not exist before, device tokens issued during the run) and clears the app. Texts follow the device language through `i18n/*.json`; `tools/e2e/ui.py` is the adb/uiautomator driver (`python3 tools/e2e/ui.py dump`).
- Not covered by automation: real camera decoding and photo capture (emulators have no barcode in view).

## 2026 restyle (2026-10-08)

The app now uses the 2026 web design: Geist / Fraunces, warm neutrals, a server-ready `ThemePalette`, complete 3D book covers, a searchable home hero with a cover fan, catalog grid/list views, dot status pills, grouped availability/actions and one card per exposed digital asset. Shared tokens apply to all existing account and plugin screens. The five-section bottom navigation is retained. Article lists and details now show server-resolved covers, subtitles, published online resources and access conditions, with a website action for the existing staff management flows. Native collections require the additive Mobile API 1.5.0 server update; existing circulation and authentication routes are retained.
Expand Down Expand Up @@ -58,7 +84,7 @@ It follows the **device locale** by default and offers an **in-app language
switcher** in Profile (System default / Italiano / English / Français / Deutsch) via
`AppCompatDelegate.setApplicationLocales(...)`, persisted across restarts (`autoStoreLocales`).

- **Single source of truth = JSON.** Translations live in `i18n/{en,it,fr,de}.json` (622 keys each,
- **Single source of truth = JSON.** Translations live in `i18n/{en,it,fr,de}.json` (815 keys each,
en = default/source). A Gradle task (`GenerateI18nResTask`) generates `res/values*/strings.xml` from
those JSONs at build time, so the app uses standard Android string resources but the editable source
stays JSON — syncable with the web app's `locale/*.json`.
Expand Down
Loading
Loading