Appearance
Translations
Concept layer — frozen. The Translations generic. Nothing here is written by a normal playbook run; a project's own feature analysis is the live document and takes every edit. This layer names no project and links to none — the dependency runs one way, from an application to its concept.
Business Context
In a multi-language system, users and administrators need to manage application content in various languages efficiently. This feature allows storing, retrieving and modifying translations in a fully dynamic and business-configurable way. It supports custom translation keys that can be used for both static translations and dynamic entity descriptions, and it also supports backend-only translation needs such as notifications.
Business-Level Definition
Offer a scalable and flexible translation system that enables localized user experiences throughout the system, allowing dynamic updates to translation values and the ability to add, remove or deactivate languages as needed.
Requirements Definition
- Store arbitrary translation keys and values.
- Support any number of languages dynamically.
- Modify translation values and add new languages without deployment — this empowers business teams to manage translations independently.
- Enable dynamic runtime language switching.
- Enable the use of translation types to filter translations by static content or by specific entities.
- Allow translations to be organized by component, so that APIs can fetch only those relevant to a particular component.
Technical Context
User Stories / Use Cases
- As an administrator, I want to add, update or delete a translation for a specific key in multiple languages.
- As an administrator, I want to add, update or delete a language in the system.
- As a frontend application, I want to fetch all relevant translations for a given language.
- As a backend application, I want to use predefined translations to localize text for backend operations, such as notifications.
Functional Requirements
- Add, update or delete a translation key–value pair.
- Filter translations by entity attributes.
- Identify an incomplete translation map by comparing keys that are missing in certain languages.
- Support region codes — language plus region — and pick one spelling: the hyphenated BCP 47 form (
cs-CZ) or the underscore form many runtimes emit (cs_CZ). Validate it by pattern on every write and every query, and normalise nothing: a store that accepts both spellings has two locales for one language, and no read finds both. A project records which it chose. - A backend consumer — a notification, an export, a generated document — resolves the recipient's locale from data: a per-person preference (a field the Users & Groups record has to carry), then the scope's default, then the deployment default. There is no request to read a locale from. Without the preference field, "backend-side localisation" is the deployment default for everyone, which is what ships when the field is forgotten.
- Retire and replace, never edit, the keys that historical records resolve through — the display names of reference-data values, the labels an audit feed renders, enum labels. See Technical risks; the rule is stated here because it is a write-path rule, and the class of a key is decided when the key is created.
Non-Functional Requirements
- Search by locale, key and translation must return results in under 1 second — indexing must be used.
Domain Model & Data Attribute Table (DAT)
translations— the translation entity.
API Analysis (API-A)
See child page: Generic Translations API.
Logging & Monitoring
.info
- on creation, update and deletion of a translation.
.warn
- missing keys for a given locale.
Caching & Performance
- Delivery is two-tier whatever the project decides: the client receives one bundle per locale per component group with its start-up payload and holds it for the session; every render after that is a map lookup in the client. That client-side copy is the cache that matters and the longer staleness window, and no server-side invalidation shortens it.
- A server-side cache (in memory, or shared such as Redis) in front of the read is a recommendation, not a requirement: add it when the per-payload read is measured to matter, keyed per locale and component grouping — the grouping under Requirements Definition, not the tenant dimension — and invalidated on update and delete. Projects that serve one indexed read per payload and cache nothing on the server are within the concept; say so on the project page, with the trade.
Legal Context
The mechanism holds the project's own words about itself, so the questions below are about what those words carry, not about the store. None of them is answerable from the design.
- Does any string in the store carry legal weight? Consent wording, policy links, regulatory disclosures and statutory warnings are all just rows here. If yes: who signs a wording off per language, and is the translated version binding in that language or a courtesy rendering of one authoritative original? The two imply different review paths, and different behaviour when a locale is absent.
- Is the project obliged to offer a particular language at all? This concept treats the set of languages as data, which is a technical freedom, not a commercial one. Whether a language is a business choice or a procurement, consumer-protection or accessibility requirement is a question for the deployment.
- Can the project show what wording a person was shown, and when? Updates overwrite in place. If any obligation turns on proving what a user was asked to agree to, the store as described cannot answer it, and the project needs versioned wording or a record kept elsewhere. This is far cheaper to decide before the question is asked in anger.
- Does any third party see the text? A machine-translation service, a translation-memory vendor or an outsourced translator all mean the text leaves the project under some agreement. The related question is whether anything user-supplied can ever reach the store — if it can, every assumption in this chapter and the next one changes.
- Does the store hold personal data? By design it should not: interface wording identifies nobody. Nothing structural prevents a person's name being typed into a value, though, and this store's contents are usually served very widely.
Cybersecurity Considerations
The vocabulary is effectively public. At least part of it must be served before anyone is authenticated, because a sign-in surface needs its words before it knows who is reading. Everything in the store should therefore be treated as published text: an error message naming an internal system, a threshold or an unannounced feature discloses it to anyone who can reach that surface. This makes the grouping that decides which application receives which keys a disclosure boundary, not merely a payload-size optimisation — a key tagged for the pre-authentication surface is a key given to the internet.
Whoever may write a translation may inject content into every page. Stored values are rendered into pages and this concept does not sanitize them: escaping belongs to the rendering surface, and the stored value is arbitrary text validated only for shape. Write access is therefore a content-injection capability rather than a copy-editing one, and should be gated as such — read separated from write, with write held by few. A project that renders values as markup rather than as text has moved the store from "arbitrary text" to "arbitrary markup" and must say so explicitly, because that is a materially larger surface.
The editing path is part of the threat model. Where wording changes travel as reviewed changes to a deployment repository, the write surface is small, reviewed and offline. Where an administration screen edits wording at runtime, it is live and reachable with a permission. This is the security half of the same choice weighed under Risk Assessment: the agility a project wants and the write surface it accepts are one decision, not two.
Completeness checks are unbounded by nature. Comparing every key across every locale is a whole-store operation with no meaningful filter, so exposing it as an ordinary endpoint hands out a full scan behind a single permission. Bound it, schedule it, or both.
Risk Assessment
Business risks
- A wrong translation is worse than a missing one, because it is confident. A missing string reads as an unfinished product; a mistranslated action label can persuade someone to do the opposite of what they intended, and is far harder to notice from inside the project.
- An untranslated string in code is a silent defect. A literal in a rendered position appears in no completeness report, because a report can only compare keys that exist. The completeness measure is therefore structurally optimistic: it describes the vocabulary routed through the store, not the words the product shows. Unless "no literals in code" is enforced mechanically, it is a convention and the measure inherits its accuracy.
- How quickly wording can be corrected is the choice this concept exists to enable, and it is routinely lost. Runtime editing puts wording in the hands of the people who own it and turns a typo into a minutes-long fix, at the cost of a live write surface with no review step and no release trail. Changes through a release makes every change reviewed, dated and attributable, at the cost of a developer and a deployment for a single word. Most projects intend the first and ship the second; the failure is not choosing the second, it is not noticing that it was chosen.
Technical risks
- Fallback granularity. Falling back per response — using the default language only when a locale returns nothing at all — is simple and predictable, but treats a locale carrying one row as supported, so a half-seeded language leaves almost every key unresolved. Falling back per key never leaves a key unresolved, at the cost of screens that mix two languages silently. Neither is wrong; an unstated choice is, because the two fail in opposite directions.
- Row identity decides whether one key can have two answers. If a row's identity includes anything beyond the locale and the key — a grouping tag, a component list — two live rows can answer the same key in the same language. A flat key-to-text map cannot represent both, so one wins, and without a deterministic ordering which one is unspecified and can differ between environments.
- Staleness wherever it is cached. Invalidating on write reaches the server's copy only. A bundle handed to a client for the duration of a session is stale until that session ends, so a project caching on both sides has two staleness windows and the client's is the longer one.
- The widest blast radius of any configuration store. Every screen's first render depends on this read, so a failure stops the product rendering rather than degrading one feature. There is no meaningful degraded mode short of shipping a fallback bundle with the client — a property to state, not to mitigate inside the capability.
- Wording is not versioned — and one class of key has to be exempted from that. An update overwrites and the previous text is gone. For ordinary interface wording this is correct; it is the point of holding wording as data. It is wrong for wording that historical records resolve through: the display label of a reference-data value, the description rendered for an audit entry, the display name of a versioned artefact. Those records carry the key, not the words, so overwriting the row re-labels history and nothing in the affected record changes to show it. Such keys are retired and replaced rather than edited — a new key for the new wording, the old key left standing for the records that still resolve through it. Which class a key belongs to is a property of the key and has to be decided when it is created, because afterwards the first overwrite has already happened. See Lookup Tables for the referencing side of the same rule.
- Residual. Completeness across locales can be checked but not guaranteed: every new key is another chance to seed one language and forget another. No design removes this; it is managed by making the check cheap and by actually running it.
- A seed that stays loadable only through conflict-ignore is accumulating duplicates. An
ON CONFLICT DO NOTHINGon every insert is how an append-only seed keeps running; it is also how the same row comes to be inserted hundreds of times without anyone noticing. Harmless to the data, and a count worth taking occasionally, because the same habit is what lets a near-duplicate — same key, different tag — slip past as a new row.
Auditing, Reporting & Measurement
What should be recorded: which key changed, in which language, from what wording to what, when, and by whom. The "from what" matters more here than in most stores precisely because updates overwrite — without it, the previous wording is unrecoverable.
Where the trail lives depends on the editing path, and that is the trap. Where wording changes travel as reviewed changes to a deployment repository, the version-control history is the audit trail — dated, attributable, reviewed, and stronger than anything a row could carry. The corollary, once an application-side trail exists: rows written by a seed changeset produce no entries, by construction — nothing in that path is application code. The trail records decisions taken through the product; say so on the project page, or the first reader who opens the trail after a deployment and finds none of the newly seeded keys files it as a defect. It is also not a property of the store, so it disappears the day a runtime editing surface is built. A project adding that surface must add a trail with it, or the record quietly stops existing and nobody finds out until someone asks who changed a label.
Row-level markers answer "how", not "who", unless they are made to. Created- and updated-by columns stamped with a fixed marker record which path wrote a row, which is useful and is not attribution. If the acting person is wanted, the actor has to be threaded from the request through to the write; it is available there and commonly just not passed on.
Report completeness along the same axis the text is delivered on. Completeness — which keys are missing from which languages — is the reporting a project should expect to want, with one caveat worth designing for. If text is delivered in groups, a completeness report over the whole store answers the wrong question: a key present in one language for one application and in another language for a different one counts as globally complete while being broken for both.
The store cannot see rendering. It does not know how often a key failed to resolve, how often a locale fell back to the default, which languages users actually choose, or which keys are never requested and could be retired. Those need client-side telemetry, and a project that wants them should decide so deliberately rather than expect this capability to supply them. Nor does anything here report on words that were never keys at all — the untranslated-string risk above, which is the gap that makes every other measure read better than the truth.
Unsupported locale — fall back, and say so
A request for a locale the system does not carry returns the configured default locale rather than an error or an empty map. A client asking for a language nobody has translated yet should show something; failing the request degrades a whole screen over a missing translation.
- The response says the substitution happened, so a caller can tell a real answer from a fallback and is not left guessing why the text is in another language.
- The event is logged at info, not warning. It is expected behaviour, not a fault — a locale the system was never configured for is not a defect, and logging it as one trains readers to ignore warnings.
- Which locale is the default is a project decision. Name it in the project's own analysis.
This is distinct from a missing key within a supported locale. That is a genuine gap: it is reported through the inconsistencies read, and logged at warning. One is "we do not speak that language"; the other is "we speak it and forgot a word".
What the client does with a missing key is a third policy beside the two fallback granularities under Technical risks: a single-locale bundle with no second locale to fall to, and an unresolved key rendered as the key itself. Ugly, obvious, non-fatal — a control is never blank, and the defect names itself on screen. It is the cheapest policy and a defensible one; a project states it, and states whether any surface carries a hard-coded last-resort string instead (page titles usually do), because each such string is one the completeness report will never see.
Two reads, for two different callers
The concept defines two read paths. The client fetch every project needs; the administration listing exists when — and from the day — a runtime editing surface exists. Where wording changes travel as reviewed changes to a deployment repository (see Risk Assessment), the version-control diff of the seed tree is the administration view, with history the API listing could never give, and there is no caller for a second read. Declaring an editing surface without the listing leaves its callers to improvise; declaring the listing without an editing surface documents an API nobody uses.
| Administration listing | Client fetch | |
|---|---|---|
| Returns | every locale, grouped | one locale |
| Shape | full rows — identifiers, audit columns, active | flat key → string map |
| For | managing translations | rendering a screen |
| Why not one endpoint | an administrator needs to see what exists, including inactive rows; a client needs the smallest thing it can consume, and audit metadata it must ignore is a liability |
Both filter to live rows by default; the administration listing may opt into showing soft-deleted and inactive rows, since curating them is its job.
One trap follows from having only the client fetch. A delete endpoint addressed by row identifier is undrivable when no read returns identifiers — the only way to learn an id is to re-write the row and read it out of the response. Where the client fetch is the only read, the delete addresses the identity triple, or does not exist.
Known instances
None recorded in this copy. Add this project's instance here as it adopts the concept — one line naming the feature that implements it. This is the only place a concept may name a project, and the template ships it empty so a fork inherits no other project's entries.