Manual Installation (Local Development)
Prerequisites
- Node.js — the app itself (NestJS 12) boots on v20.19+, but the backend test runner (Vitest 5)
refuses to start below v22.12, so use v22.12+ to actually run
npm test. - PostgreSQL — the repo ships
docker-compose.dev.ymlto start one locally (also Redis and Mailpit), or pointDATABASE_URLat your own instance - npm
Steps
-
Clone the project:
git clone https://github.com/invoicerr-app/invoicerr.gitcd invoicerr -
Backend setup:
cd backendnpm installnpx prisma generatenpm run startPDF generation (every document type shares one renderer — quotes, invoices, credit notes, expenses, received invoices, purchase orders, goods receipts) needs a Chromium/Chrome binary. Outside the Docker image nothing provides one automatically, so run
npx playwright-core install chromiumonce to fetch a matching build — or pointCHROMIUM_EXECUTABLE_PATHat a browser you already have (seebackend/.env.example). -
Frontend setup, in a new terminal:
cd frontendnpm installnpm run start -
Open in your browser:
- Frontend:
http://localhost:5173 - API:
http://localhost:3000
- Frontend:
Running end-to-end tests (Cypress)
-
Start the backend and frontend with the test environment variables:
cd backend && npm run start:test &cd frontend && npm run start:test &Make sure you have a
.env.testfile in each directory. -
In another terminal, run Cypress:
cd e2enpm installnpm run e2e:open # or npm run e2e:run
In CI, the GitHub Actions workflow runs these steps automatically.
Time limits
Cypress has no per-test timeout of its own, so the suite adds two:
- A test that runs longer than 180 seconds fails at its next command with a "time limit" error
(
e2e/cypress/support/test-time-limit.ts). - In CI,
e2e/scripts/run-specs.shruns each spec in its own Cypress process under a hard limit of 15 minutes (SPEC_TIMEOUT_MINto change it). A spec whose browser stops answering is killed and reported as failed, and the remaining specs still run. You can use it locally too:scripts/run-specs.sh cypress/e2e/05-clients.cy.ts,cypress/e2e/09-settings.cy.ts --browser firefox.
Right-to-left (RTL) layout
The frontend mirrors its layout for right-to-left locales instead of only translating the text.
- Which locales are RTL is an explicit list,
RTL_LOCALESinfrontend/src/lib/i18n.ts(ar,he,fa,ur). It is never guessed from the script of the active catalog, so adding a locale whose code is not on that list (even one written right-to-left) keeps the layout left-to-right until the list is updated.isRtlLocale()in the same file is what every other RTL decision (the document's owndir, which edge the sidebar docks to, which icons flip) reads. - Tailwind classes are logical, not physical. Use
ms-/me-instead ofml-/mr-,ps-/pe-instead ofpl-/pr-,start-/end-instead ofleft-/right-,text-start/text-endinstead oftext-left/text-right, androunded-s/rounded-e(or the four-cornerrounded-ss/rounded-se/rounded-es/rounded-ee) instead of the physical corner utilities. A handful of places stay physical on purpose and are not a gap to fix:- a numeric table/list column (an amount, a quantity, a rate) keeps
text-right, since issue #559 treats number alignment as a deliberate exception rather than something that should track reading direction; components/ui/sidebar.tsx's own dock-side geometry (SidebarRail, the container'sleft-0/right-0, itsborder-l/border-r) andcomponents/ui/sheet.tsx's matching side-conditional block are keyed to the component's ownsideprop ("left" | "right"), an independent concept from text direction.components/sidebar.tsxis what makes the app's own sidebar RTL-aware, by choosingside="right"for an RTL locale instead of asking the primitive to guess fromdir;components/ui/input-otp.tsx's digit-box borders stay physical because a one-time code is a numeric identifier, always read left-to-right regardless of UI language.
- a numeric table/list column (an amount, a quantity, a rate) keeps
- A value that must stay left-to-right inside RTL text (a monetary amount, a date, an IBAN, a
VAT/SIRET/other identifier) is wrapped with
dir="ltr", either directly on the element, or through the shared<LtrValue>component (components/ui/ltr-value.tsx) for inline amounts. Plainunicode-bidi: isolateis not enough on its own: it keeps the value's own character order stable but still lets the bidi algorithm place the whole run, and decide where a leading minus sign or a currency code lands, based on the surrounding RTL text. - Directional icons (a chevron, an arrow, a back button) flip with the
rtl:Tailwind variant, for exampleclassName="... rtl:rotate-180", the same patterncomponents/ui/calendar.tsxalready used for its own previous/next chevrons before issue #559.
Testing RTL locally
No Arabic, Hebrew, Persian or Urdu translation exists yet. frontend/src/lib/i18n.ts registers a
dev-only locale, rtltest, that copies the English catalog under a right-to-left code, built only
when import.meta.env.DEV is true, so it never reaches a vite build output. To try it:
- Run the frontend in dev mode (
npm run devornpm run start:test). - In the browser console or devtools storage panel, set
localStorage.i18nextLng = "rtltest"and reload the page (changing the key alone does not re-render a mounted page). - The document switches to
dir="rtl", the sidebar docks to the right, and every value wrapped indir="ltr"keeps reading left-to-right.
e2e/cypress/e2e/116-rtl-layout.cy.ts drives the same locale through Cypress, the same way
73-account-page.cy.ts drives a real language switch: writing i18nextLng to localStorage, then
a real cy.reload().
Once an Arabic, Hebrew, Persian or Urdu catalog is complete enough to list in
SUPPORTED_LANGUAGES, no other change is needed here: its code is already in RTL_LOCALES, and
the mechanism above already reads from that list.