Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/docs/LOCALIZATION.md
diff options
context:
space:
mode:
authorLeitwolf112026-10-03 18:18:50 +0300
committerLeitwolf112026-10-03 18:18:50 +0300
commitba237e5ce9f50cd9e586697417c1d636e3cd129c (patch)
tree7e67ce6c748af16b6c4ca03a51a9502fac5009f7 /docs/LOCALIZATION.md
parente9eb2812a47b075c82a138db7f43cc08cccf072a (diff)
downloadprogmaweb-docs/api-and-web-reference.tar.gz
Document the architecture, the account API, localization and limitationsdocs/api-and-web-reference
The repository had a README and two operations notes. This adds the documents a contributor or operator needs and corrects what had gone stale. New, under docs/: - README.md: index of the documentation. - ARCHITECTURE.md: the two processes, the three hosts, the host-aware routing rules, the account flow and the trust boundaries. - ACCOUNT-API.md: every endpoint with its request, responses and errors, the field rules, the Google flow outcome by outcome, cookies, limits and configuration. The responses were checked against a locally running API. - LOCALIZATION.md: the four languages, how a page picks one, the cookie, how Hebrew text is laid out, and how to add a language. - DEVELOPMENT.md: requirements, local setup, checks, tests, what each CI workflow enforces, the audit exception and the DocFX project. - LIMITATIONS.md: what is missing or temporary, with the consequence of each: the in-memory account store, unverified addresses, no mail, no password change, the ignored returnTo parameter, the legacy ViGet status route, and that the API answers 500 to everything while the Google settings are empty. Updated: - README.md: languages, a documentation section, the local API command with its port, and the DocFX check. - ops/README.md: restarting the API deletes every account, how to release a frontend-only change without that restart, and lessons from past releases. - ops/mail/README.md: a status note that the API does not send mail yet. - The pull request template names all four languages. No source file changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/LOCALIZATION.md')
-rw-r--r--docs/LOCALIZATION.md106
1 files changed, 106 insertions, 0 deletions
diff --git a/docs/LOCALIZATION.md b/docs/LOCALIZATION.md
new file mode 100644
index 0000000..7ba409d
--- /dev/null
+++ b/docs/LOCALIZATION.md
@@ -0,0 +1,106 @@
+<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
+<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 -->
+
+# Localization
+
+Every page of Progmaweb is published in four languages. All of them live in `apps/web/lib/localization.ts`.
+
+| Locale | Language | Text direction |
+| --- | --- | --- |
+| `en-US` | English | left to right |
+| `de-DE` | German | left to right |
+| `ru-RU` | Russian | left to right |
+| `he-IL` | Hebrew | right to left |
+
+A page has one URL in every language. There are no language-specific paths and no `hreflang` alternatives, because a
+language is a preference of the visitor, not a second address of the page.
+
+## How a page picks its language
+
+For each request the server decides in this order (`apps/web/lib/locale.server.ts`):
+
+1. **The visitor's choice.** A valid `progmasoft_locale` cookie always wins.
+2. **The browser.** Without that cookie the `Accept-Language` header is negotiated: the published language with the
+ highest quality wins, and the order of the header breaks ties. Only the primary language is compared, so `de-AT`
+ selects German. `iw`, the former code for Hebrew, is accepted.
+3. **The fallback.** A browser that prefers no published language gets `en-US`.
+
+Examples of the negotiation:
+
+| `Accept-Language` | Page language |
+| --- | --- |
+| `he-IL,he;q=0.9,en;q=0.8` | Hebrew |
+| `de-AT,de;q=0.9` | German |
+| `tr-TR,tr;q=0.9,de;q=0.5` | German, the first preference that is published |
+| `tr-TR,tr;q=0.9` | English, the fallback |
+| `en;q=0.3,ru;q=0.9` | Russian; quality decides, not position |
+| absent | English |
+
+The header comes from the browser and is not trusted: only its first 1024 characters are read, and a malformed or
+zero quality never selects a language.
+
+## How a visitor changes the language
+
+The language menu in the header calls the server action `selectLocale` (`apps/web/lib/locale.actions.ts`). The action
+stores the cookie, and Next.js renders the current route again and applies the result in place. Nothing is reloaded:
+scroll position, theme and form input stay as they are.
+
+A link can carry the same choice as `?lang=<locale>`. The proxy stores the cookie and redirects to the URL without the
+parameter, so a preference never becomes a second indexed address. The menu falls back to this link when the action
+cannot reach the server.
+
+A value that is not a published locale is ignored in both paths.
+
+## The language cookie
+
+| Attribute | Value |
+| --- | --- |
+| Name | `progmasoft_locale` |
+| Domain | `.progmasoft.com` on production hosts, so the choice follows the visitor to the account and ViGet hosts; host-only elsewhere |
+| Lifetime | one year |
+| Flags | `HttpOnly`, `SameSite=Lax`, and `Secure` over TLS |
+
+Both paths write the cookie through `localeCookieOptions`. Two writers with different attributes would leave two
+cookies behind, one shadowing the other.
+
+## Hebrew and text direction
+
+Hebrew does not mirror the page. The layout, the navigation and code keep their left-to-right arrangement; only text
+follows the language.
+
+- The root element carries `data-text-direction="rtl"`, not `dir="rtl"`.
+- Blocks of prose (headings and paragraphs) run right to left and align to the right.
+- Short pieces such as links, buttons and list items take the direction of their own first letter, so a product name
+ on its own still reads left to right.
+- Code and form inputs stay left to right.
+- The onward arrow comes from `forwardArrow(locale)` and points left in Hebrew.
+- A left-to-right mark (`‎`) follows `Visual X#` in Hebrew strings. Without it the `#` would jump to the other
+ side of the name inside right-to-left text.
+
+The rules are at the end of `apps/web/app/globals.css`.
+
+## Message tree
+
+`messages` holds one tree per locale with identical shape. Pages and components read it through
+`getMessages(locale)`; there is no lookup by string key and no fallback from one language to another, so a missing
+message is a type error or a failing test rather than an English sentence on a German page.
+
+The tests in `apps/web/tests/localization.test.mjs` enforce that
+
+- every locale has exactly the keys and array lengths of `en-US`, with no empty string;
+- every locale has a name in its own language for the menu;
+- negotiation, cookie attributes and text direction behave as described above.
+
+## Adding a language
+
+1. Add the locale to `supportedLocales`, its own name to `localeNames`, and its primary language to
+ `localeByLanguage`.
+2. If the language is written right to left, extend `textDirection`.
+3. Add a complete message tree. Copy the `en-US` tree and translate every string; keep product names and technical
+ identifiers such as `Visual.XSharp.kts` unchanged.
+4. Extend the tests: the list of published locales and a negotiation case for the new language.
+5. Run `pnpm check`, `pnpm test`, `pnpm lint` and `pnpm build`, then read every page in the new language in a
+ browser, in both themes and at a narrow width.
+
+Have a native speaker read the text before it is published. The Hebrew text has not had that review yet; see
+[Known limitations](LIMITATIONS.md).