Skip to content

UI Text ​

Reword any interface string for your institution — in English, Spanish or French — with no rebuild and no deploy.

Administration → UI Text (/admin/ui-text), gated on ADMIN.UI_TEXT (migration 225).

What this is for ​

The interface ships with roughly 8,900 strings. Institutions disagree about wording: one calls it a bulletin, another a catalog; one says Advisor, another Faculty Mentor. Rather than fork the frontend, any shipped string can be overridden here.

You can edit wording. You cannot add a string or remove one — see What you cannot do.

Draft, publish, history ​

Editing writes to a draft. Nothing you type is visible to users until you publish.

ActionEffect
Edit a stringSaved to draft/
Publishdraft/ becomes live/, and the previous live/ is snapshotted into history/<stamp>/
Discard draftThrows away unpublished edits
ExportDownloads the overlay as JSON

The history snapshots are also this feature's audit trail — each publish records its version, timestamp and who published it.

Not stored in the database ​

This is the part that surprises people. The overrides are plain JSON files under LOCALE_STORAGE_PATH, one directory per deployment:

locales/
  draft/{en,es,fr}.json     what an administrator is working on
  draft/_source.json        generated: key -> shipped English at the time of the edit
  live/{en,es,fr}.json      what is served
  live/_source.json
  live/_version.json        {"version", "published_at", "published_by"}
  history/<stamp>/          a snapshot of live/ at each publish

Files rather than rows is what makes "per institution" work without a tenant column: /sdasis uses ./locales, the demo uses ./locales-demo, and /records uses ./locales-records.

In production this must be a bind mount

LOCALE_STORAGE_PATH has to point at a bind-mounted directory. If it points inside the container's own filesystem, every published override is lost the next time the container is recreated — which happens on any deploy.

The overlay is sparse (only the keys you changed) and flat ({"auth.login.title": "..."}), so a release that rewords other strings or adds new ones still reaches your deployment without conflict.

What you cannot do ​

Add a key. The write endpoint refuses any key that is not in the embedded shipped catalog: "…is not a key in this version of the interface". The usual cause is an overlay entry that survived a release which renamed or removed the key.

Remove a key. There is no delete. Clearing an override restores the shipped wording — which is different from setting it to an empty string, and an empty string is rejected with "Text is required. Clear the override to restore the shipped wording."

That restriction is visible in the route table itself: there is no POST-to-create and no DELETE on a key.

Validation, and the one silent failure it prevents ​

An override is checked against the shipped English, for every locale — English is what names the parameters the t() call actually passes.

Placeholders must be a subset. Dropping {count} from "Showing {count} results" is a legitimate rewording. Inventing {total} is not, because nothing passes it and it would render as the literal text {total}. The error names what is available.

Plural branch counts must match exactly.

This is the rule worth understanding

vue-i18n reads a top-level | as a plural separator. So rewriting "Yes" as Yes | No renders "Yes" forever, and nothing anywhere reports a problem — not the browser console, not the server log. Dropping the pipe from a two-branch plural is the mirror image: the singular form renders for every count.

The validator rejects both:

A '|' separates plural forms, so only the text before it would ever show. Write {'|'} if you meant a literal pipe.

Write {'|'} for a literal pipe. The same escape covers {'{2}'} and {'@'}.

Linked messages (@:key) are rejected outright. No shipped string uses one, and allowing it would let an override pull in the text of an unrelated key.

Length is capped per override. The longest shipped string is 433 bytes; the cap leaves room for a much wordier rewrite without turning the overlay into a blob store.

Keys with a dot in them ​

The overlay is flat rather than nested because 21 shipped keys contain a literal dot, for example:

admin.academicPolicies.rules["DEGREE.TOTAL_CREDITS"].label

Nesting one of those correctly requires the shipped key tree, which the backend does not have. The frontend does, and unflattens there. If you are editing the exported JSON by hand, keep it flat.

What happens at release time ​

deploy.ps1 never rewrites your overlay. It reports overrides that a release has:

  • orphaned — the key no longer exists, so the override does nothing
  • out of date — the shipped English changed after your override was written (tracked via live/_source.json), so your wording may no longer say the right thing

deploy.ps1 -PruneLocaleOverrides drops the orphaned ones, behind a history snapshot.

The login page ​

The published overlay is served from a public endpoint (GET /core/ui-text/live), because the login page has to render your institution's wording before any session exists. It returns published interface strings only — no catalog, no personal data. That is strictly less than the branding endpoint beside it, which gives out the institution's name, logos and colours.

Endpoints ​

MethodEndpointPurpose
GET/core/ui-text/catalogThe shipped strings, to edit against
GET/core/ui-text/draftThe working draft
PUT/core/ui-text/draftWrite draft edits
POST/core/ui-text/draft/discardThrow away the draft
POST/core/ui-text/publishDraft becomes live; previous live snapshotted
GET/core/ui-text/exportDownload the overlay
GET/core/ui-text/livePublic — the published overlay

The catalog comes from a go:embed of a generated copy of the frontend's locale files, because the catalog in the browser is compiled message AST rather than strings. Developers regenerate it with npm run i18n:emit-catalog; npm run build fails if it is stale.

  • Lookup Tables — reference-data display values, which are translatable separately via display_value_es / display_value_fr on the ten lookup tables that carry those columns
  • Core API — the ui-text and email-templates endpoints. Email template wording is edited through its own endpoint, not through this overlay

Student Information System Documentation