Design system/Fundamentals
Voice and writing
Text is part of the interface, not something poured on at the end. Flowra speaks in a friendly, short, precise way, and every string people see goes through one path: tr().
In codelib/tr.tslib/locales/
Voice
We use the informal you, like a colleague sitting next to you. Sentences are short, verbs are clear, and we don't promise anything that isn't built.
Do
"We'll let you know when it's ready."
Don't
"This feature will be made available in future releases."
Do
"No one with this username was found."
Don't
"Error 404: User not found."
Buttons are verbs
A button's label says what happens when you press it: "Create action", "Save", "Yes, delete". "OK" and "Confirm" on their own say nothing. In a delete dialog, the primary button names the action itself, so someone who only reads the button still knows what it does.
No internal words
People don't care about IDs, feature flags, APIs, or database table names. A server error is translated into a sentence that says what happened and what to do now. The technical reason belongs in the audit log, for whoever asks for it.
Do
"The payment didn't go through. Please try again later."
Don't
"Gateway has not been implemented yet."
Punctuation
- The em dash never appears anywhere in user-facing text, in any language. Use a period, comma, semicolon, or colon instead.
- Quotations and section names go inside Persian quotation marks «».
- The product name is «فلورا» in Persian text and Flowra in the Brand kit and Latin-script text: one word with a capital F.
- Numbers are written with numLocale() so each language gets its own digits and separators.
Strings in code
Every Persian string people see is written inside tr() and has an entry in each language's catalog. Parameterized sentences use the asterisk pattern so word order stays free in every language. The result of tr() is never stored in a module-level constant, because it's built only once, in whatever language is active at that moment; use trMap for tables and trList for lists.
Values stored in the database stay in Persian and are translated at display time, not at write time.
tr("دعوت «*» رد شد.").replace("*", name)
// a module-level list: rebuilt when the language changes
const STATUSES = trList(() => [tr("باز"), tr("انجام شد")]);Error messages in three parts
An error message says what happened, why if it helps the person, and what to do now. It sits under the relevant field, not at the top of the form.
Do
“Your password needs at least 8 characters.”
Don't
“Invalid input.”
Do
“That username is taken. Try another one.”
Don't
“Error: duplicate.”
Do
"The payment didn't go through. Please try again later."
Don't
“Error 503 from gateway.”
Length and shape
- Button labels are one to three words, verb first: “Create action”, “Upgrade to Prime”.
- A field label is a noun: “Email”, “Amount”. When it floats up it's 11px, and a full sentence gets cut off.
- A toast is one sentence with no button, and stays no longer than 3.6 seconds.
- An empty state sentence has two parts: what isn't there, and how to create the first one.
- A confirmation dialog states the consequence, and its button names the action itself: “Yes, delete”.
Do
“You don't have any projects yet.”
Don't
“There are no items to display.”
Do
“Saved.”
Don't
“The operation was completed successfully.”
Related pages