Internationalization & Localization
How the Sindh IT Portal — Facilitation Desk (SITP) is built trilingual (English master → Urdu → Sindhi), right-to-left native, dual-calendar aware, and glossary-consistent across every surface — public site, docs, notifications, official letters, and AI output.
| Field | Value |
|---|---|
| Doc ID | 09 |
| Status | Draft |
| Owner | S&ITD / MAAHIR |
| Languages | EN (master) · UR · SD |
| Applies to modules | A (PUB) primary · cross-cutting: B (TKT), E (AI), G (NOT), J (KB), L (DOC), M (MTG), N (SUG), O (TRN), P (OFC) |
| Related docs | /specs/en/15-tech-architecture/ §15, /specs/en/16-branding-design-system/, /specs/en/10-ux-sitemap-flows/, /specs/en/07-ai-ocr-spec/, /specs/en/13-test-strategy/, _glossary.md, _context.md §2 & §7 |
1. Scope & How to Read This Document
This document is the authoritative specification for everything multilingual in SITP. It covers:
- The language strategy — EN as master, UR and SD as parallel translations (§2).
- Locale storage and resolution — per user, per org, per session (§3).
- String externalization — no hardcoded strings anywhere (§4).
- Translation-file workflow — per-locale JSON/MDX catalogs (§5).
- AI-assisted translation pipeline + human native review (§6, with workflow diagram).
- Glossary-driven term consistency (§7).
- RTL handling — CSS logical properties, direction switching, icon/layout mirroring (§8).
- Typography & fonts — Inter/Poppins, Noto Nastaliq Urdu, Noto Naskh Arabic (§9).
- Dual Gregorian + Hijri calendar (§10).
- Number, date, currency, and phone formatting (§11).
- Content fallback — missing translation → EN with visible marker (§12).
- SEO: hreflang + sitemap per locale (§13).
- Accessibility per locale — WCAG 2.1 AA across EN/UR/SD (§14).
- Translation QA & review gates (§15).
- Official terminology disambiguation — Department vs Ministry, and other locked distinctions (§16).
- Functional requirements, NFRs, user stories, test cases (§17–§20).
The high-level platform treatment of i18n lives in /specs/en/15-tech-architecture/ §15; this document expands that section into a full, build-ready specification. The trilingual term dictionary in _glossary.md is the single source of approved translations — this document governs how those translations are applied, not what they are.
2. Language Strategy & Locale Model
2.1 The three locales
| Code | Language | Script | Direction | Role | Native name |
|---|---|---|---|---|---|
en |
English | Latin | LTR | Master / source of truth | English |
ur |
Urdu | Arabic (Nastaliq) | RTL | Parallel translation | اردو |
sd |
Sindhi | Arabic (Naskh, extended) | RTL | Parallel translation | سنڌي |
English is the authoring locale. Every UI string, KB article, circular, SOP, notification template, official letter template, and email/SMS/WhatsApp template is authored in English first and then produced as a faithful parallel translation in Urdu and Sindhi. No content ships in UR or SD without an EN master. The only exception is citizen-submitted free text, which is stored verbatim in whatever language the submitter wrote it in, and translated on demand by the AI Translation capability (see §6).
This mirrors the documentation set itself: each doc folder contains {en,ur,sd}.md, EN master (see _conventions.md §2). The same discipline applies to product content.
2.2 Locale identifiers
Internally, locale is represented by the ISO 639-1 code (en, ur, sd) without region subtags, because the variety targeted is the Pakistan provincial standard for each. Where a third-party library requires a region subtag (e.g., en-PK, ur-PK, sd-PK), the subtag PK is appended for that interaction only; the canonical stored value remains the bare two-letter code.
2.3 Script and direction association
Direction (ltr or rtl) is a function of locale, never a separate user setting. Selecting ur or sd flips the entire layout to RTL; selecting en flips it back to LTR. There is no "English in RTL" or "Urdu in LTR" mode. This keeps CSS, layout, and component logic simple and predictable, and matches user expectation.
3. Locale Storage & Resolution
3.1 Where locale is stored
Locale is stored in three places, in resolution order:
| # | Store | Scope | Set by | Notes |
|---|---|---|---|---|
| 1 | URL path prefix (/en/, /ur/, /sd/) |
Per request / per page | User navigation, locale switcher | Canonical and shareable. Always present on public pages. |
| 2 | usr_user.locale column |
Per user | User, on registration or via profile/preference center | Default for authenticated sessions when no URL prefix is present. |
| 3 | org_organization.default_locale column |
Per organization (company or department) | Org Admin | Fallback when a user has no explicit locale (e.g., newly invited staff). |
Anonymous visitors get locale from (a) explicit URL prefix, else (b) Accept-Language header negotiated against [en, ur, sd], else (c) en (the master). The resolved locale is persisted to a first-party cookie (sitp_locale) so subsequent visits are sticky, but the URL prefix always wins — this keeps deep links shareable and unambiguous.
3.2 How locale propagates
Once resolved at the edge, locale is propagated everywhere it is needed, with no re-prompting of the user:
- JWT / OIDC token: locale is a claim, so every authenticated API call carries it.
- Job payloads: every BullMQ job (notifications, exports, AI calls) includes
locale, so a notification rendered for a Urdu-locale user is rendered in Urdu even if the job is processed minutes later by a worker that never saw the original request. - Request context: the NestJS
LocaleInterceptor(see/specs/en/15-tech-architecture/§4) attaches locale to the async request context; every service reads it from there rather than from a parameter passed through every call. - Templates: notification, letter, and document templates receive locale and render the correct variant.
- Search: Meilisearch queries carry the user's locale so same-language matches rank higher (see
/specs/en/15-tech-architecture/§8). - AI calls: every AI request to the FastAPI service carries
locale+target_localesso translation, summary, and draft-reply outputs land in the right language.
3.3 Locale switching
A visible locale switcher is present in the header of every page (public site, docs, portal app, mobile PWA). Switching locale:
- Rewrites the current URL to the target locale prefix.
- Updates the
sitp_localecookie. - If authenticated, persists the new locale to
usr_user.locale(so it sticks across devices). - Re-renders the page in the new locale and direction without a full reload where possible (Next.js App Router supports this natively).
The switcher never drops the user's place: the same route, the same ticket, the same KB article — just in the other language.
3.4 Per-org default for staff
Government staff (officers, section heads, watchers) typically work in their department's default locale, which may differ from a citizen's preferred locale. When an officer views a ticket filed by a Urdu-locale company, the system chrome (menus, buttons, status labels) renders in the officer's locale, while the ticket content (title, description, messages) is shown in the author's original locale with an on-demand "translate" affordance (see §6). This separation prevents a Sindhi-speaking officer from being forced to read Urdu chrome, and vice versa, while preserving the integrity of the original record.
4. String Externalization (No Hardcoded Strings)
4.1 The rule
No user-facing string is ever hardcoded in source code. Every label, button, tooltip, error message, status name, email subject, SMS body, placeholder, aria-label, toast, empty-state, and validation message lives in a message catalog keyed by a stable identifier. Components reference the key; the runtime resolves the key to the active locale's string.
This applies to:
- The Next.js portal (UI strings).
- The Docusaurus docs site (sidebar labels, navbar, footer, theme strings — Docusaurus's own i18n).
- NestJS API responses (validation errors, status enums rendered to users, job-result messages).
- Notification templates (email, SMS, WhatsApp, in-app).
- Official document templates (letters, certificates).
- Generated PDF/Excel exports (headers, column labels, footers).
4.2 Enforcement
Hardcoding is prevented by a combination of:
- Lint rule (
no-hardcoded-strings): an ESLint custom rule that flags any string literal passed to a JSX child,placeholder,aria-label,title, oraltattribute unless it is wrapped in thet()translation function. Exceptions are explicitly allowlisted (e.g., pure display-only constants like ticket ID formats). - CI gate: the lint rule runs in CI; a violation fails the build.
- Code review checklist: reviewers confirm new strings are keyed.
- Key audit: a weekly job reports any catalog key referenced in code but missing from one or more locale files (see §12 for fallback behavior and §15 for QA).
4.3 Interpolation and pluralization
Strings support ICU MessageFormat for:
- Interpolation:
Hello, {name}→السلام علیکم، {name}. - Pluralization:
{count, plural, =0 {No tickets} one {# ticket} other {# tickets}}. - Selection (gender/formality where needed): rare in this product; reserved for salutations in official letters.
Plural rules differ across locales (English has one/other; Arabic-script locales have zero/one/two/few/many/other). The message-format library handles this; translators supply all required plural forms per key.
5. Translation-File Workflow
5.1 Catalog structure
UI strings live in per-locale message catalogs, organized by feature surface so files stay reviewable:
locales/
en/
common.json
auth.json
tickets.json
notifications.json
letters.json
dashboard.json
errors.json
...
ur/
common.json
auth.json
...
sd/
common.json
auth.json
...
Each catalog is a flat JSON object keyed by stable dotted identifiers:
{
"tickets.status.new": "New",
"tickets.status.resolved": "Resolved",
"tickets.emptyState.title": "No tickets yet",
"tickets.emptyState.body": "File your first ticket to get started."
}
Keys are namespaced by feature (tickets., auth., letters., …) and never renamed once shipped — renaming a key breaks translations in flight. Obsolete keys are deprecated, not deleted, until no code references them and all three locales have dropped them.
5.2 Long-form content (MDX)
Long-form content — KB articles, circulars, SOPs, the docs site pages themselves — is authored as MDX per locale rather than JSON, because the content mixes prose, components, and media. The same file-and-folder convention as the docs set applies: each content item is a folder with en.mdx, ur.mdx, sd.mdx. Front matter carries the locale, last-translated timestamp, source EN revision it was translated from, and review status (see §6).
5.3 Per-locale JSON/MDX parity
For every feature release:
- EN catalogs and MDX are updated.
- A diff is generated: new keys, changed keys, deleted keys.
- UR and SD catalogs are updated (AI-assisted first, human-reviewed second — see §6).
- A parity check runs in CI: every key present in
en/*.jsonmust be present inur/*.jsonandsd/*.json. Missing keys fail the build (forcing an explicit decision: translate now, or intentionally mark as fallback-to-EN with a tracked debt item).
5.4 Status tracking per content item
Each translatable artifact (UI key set per feature, KB article, SOP, letter template) carries a translation status:
| Status | Meaning |
|---|---|
source-only |
EN authored; UR/SD pending. |
ai-draft |
AI translation generated; awaiting human review. |
in-review |
Assigned to a native reviewer; not yet approved. |
approved |
Native reviewer signed off; safe to publish. |
published |
Live in the locale. |
stale |
EN source changed since last approval; re-translation needed. |
Status is stored per locale per artifact and surfaced in the translation dashboard (see §15).
6. AI-Assisted Translation Pipeline + Human Native Review
6.1 Pipeline overview
Source content authored in EN is translated into UR and SD through a hybrid pipeline: AI does the first pass (fast, cheap, consistent), a native human reviewer does the final pass (correctness, register, cultural fit, terminology). Nothing publishes in UR or SD on AI output alone. This is the same AI Translation capability described in /specs/en/07-ai-ocr-spec/ and /specs/en/15-tech-architecture/ §6.3 #6, constrained by the glossary.
6.2 Workflow diagram
6.3 Written description of the workflow
Source content is authored in English (UI catalog entry, KB MDX file, letter template, or notification template). A differ compares the new EN revision against the previously-translated EN revision and emits three buckets: new strings (never translated), changed strings (EN edited since last translation — marked stale), and deleted strings (drop from the locale catalog).
For new and changed items, the glossary constraints are loaded from the terminology database (itself derived from _glossary.md — see §7) and injected into the AI call as forced translations and forbidden mappings. The AI translation pass runs per target locale (ur, then sd) via the pluggable LLM (see /specs/en/15-tech-architecture/ §6.1), producing a draft that already respects approved terminology.
The draft lands in a per-locale review queue. A native human reviewer (Urdu-native for UR, Sindhi-native for SD — never the same person for both, never a non-native speaker) opens the draft alongside the EN source and the glossary, and checks four things: (1) correctness — does it say what the EN says; (2) register — is the formality right for a government-facing product; (3) terminology — are glossary terms used exactly as approved, with the Department-vs-Ministry distinction (§16) intact; (4) RTL fit — does the string read naturally when rendered right-to-left in the target font, with no broken bi-directional mixing. The reviewer either approves or edits inline; edits are tracked as a diff against the AI draft.
On approval, the string is published to the locale catalog and becomes live. Reviewer corrections are fed back into the AI tuning set (a curated corpus of EN→UR and EN→SD pairs with reviewer-accepted outputs), which improves future AI first-pass quality and reduces reviewer load over time. Items that are not yet translated fall back to English with a visible marker in the UI (see §12), so the product is never blocked on translation — but the gap is tracked and closed in the next review cycle.
6.4 On-demand translation of user content
The same pipeline, minus the human gate, runs on demand for user-submitted free text — ticket descriptions, chat messages, MoM uploads, inbound email/SMS/WhatsApp replies. An officer viewing a Urdu ticket in an English-locale session clicks "Translate"; the AI returns an EN rendering inline, clearly labelled as machine translation, with the original preserved. The original is the record; the translation is a convenience. For official documents (resolution certificates, letters), the human-reviewed catalog translations are used, never raw AI output.
6.5 Reviewer roster
Reviewers are engaged against a native-language requirement: Urdu reviewers are Urdu-native (typically based in Sindh, familiar with Pakistani government register); Sindhi reviewers are Sindhi-native. Reviewers are accredited through the Training & Certification module (O) with an exam-gated "Native Reviewer" certification before handling live content — mirroring the staff certification pattern. At least two reviewers per locale are maintained for redundancy and cross-checking on sensitive content (official letters, legal/RTI wording).
7. Glossary-Driven Term Consistency
7.1 The glossary as contract
_glossary.md is the authoritative trilingual term dictionary for SITP. Every government, legal, ticketing, role, technical, and acronym term has an approved EN/UR/SD rendering. Translators, reviewers, and the AI translation pipeline must use these renderings exactly; deviation is a defect caught in QA (see §15).
7.2 From markdown to a queryable terminology DB
The markdown glossary is the human-readable source. A build step compiles it into a terminology database (a JSON map keyed by English term, with ur, sd, definition, and notes fields) that:
- The AI translation pipeline loads as constraints (forced translations for known terms; forbidden mappings for common errors).
- The QA linter checks finished translations against (flag any place where "Department" was rendered as
وزارت— see §16). - The reviewer workbench displays inline so the reviewer sees the approved term beside the draft.
- The docs build uses to verify UR/SD doc translations match.
7.3 Glossary growth
When a document or translation introduces a term not in the glossary, the convention (see _conventions.md §2) is to add it to the glossary first, then use the approved translation everywhere. The glossary is therefore a living artifact; changes to it are versioned and broadcast to all active reviewers so they re-check in-flight translations against the new term.
7.4 Consistency checks in CI
A terminology linter runs in CI over every locale catalog and MDX file:
- Detects non-approved renderings of glossary terms.
- Detects inconsistent translations of the same EN term across files (the same English word translated two different ways in two catalogs).
- Detects forbidden mappings (e.g.,
Department→وزارت). - Reports findings as build warnings (for soft inconsistencies) or build failures (for forbidden mappings).
8. RTL Handling
8.1 Direction as a layout primitive
Urdu and Sindhi render right-to-left. RTL is not a CSS afterthought; it is a first-class layout dimension handled by three mechanisms working together.
8.2 Mechanism 1 — the dir attribute
The document <html> element carries dir="ltr" for en and dir="rtl" for ur and sd. Docusaurus sets this automatically from the locale; the Next.js portal sets it from the resolved locale on the root layout. The dir attribute cascades to every descendant, so the entire component tree flips with no per-component work. The lang attribute is set in parallel (lang="en", lang="ur", lang="sd") for screen readers and search engines (see §14).
8.3 Mechanism 2 — CSS logical properties
All layout CSS uses logical properties instead of physical ones, so a single rule serves both directions:
| Physical (forbidden) | Logical (used) |
|---|---|
margin-left |
margin-inline-start |
margin-right |
margin-inline-end |
padding-left / padding-right |
padding-inline-start / padding-inline-end |
left / right (positioning) |
inset-inline-start / inset-inline-end |
float: left |
restructure with flex/grid + inline-start |
text-align: left |
text-align: start |
border-left |
border-inline-start |
TailwindCSS (locked stack) supports logical properties via its ps-/pe-/ms-/me-/start-/end- utilities and the rtl:/ltr: variant modifiers for the rare case where a literal mirror is needed. The team standardizes on the logical utilities; physical pl-/pr-/ml-/mr- are flagged by lint unless explicitly justified.
8.4 Mechanism 3 — icon and layout mirroring
Directional icons mirror in RTL: a "back" arrow that points left in LTR points right in RTL; a "next" chevron flips; a sidebar pinned to the left in LTR pins to the right in RTL. This is handled by:
- An
<Icon dir="auto" />wrapper that flips horizontal-only icons when the inherited direction is RTL. Icons that are genuinely non-directional (play, expand, list-bullet) are markeddata-no-mirrorand stay as-is. - Layout-level mirroring via flexbox
flex-row(which respectsdirautomatically) rather than absolute positioning. - Explicit
rtl:Tailwind variants for the handful of cases that need literal mirror (e.g., a custom avatar overlap).
8.5 Bi-directional text mixing
When Urdu or Sindhi text contains embedded Latin (ticket IDs like SITP-2026-LBR-000123, acronyms like SECP, numbers, URLs), the Unicode Bidirectional Algorithm handles rendering. To prevent spillover errors at boundaries, the team wraps embedded LTR runs in <bdi> and uses dir="auto" on user-submitted content fields so the browser picks direction from the first strong directional character. Ticket IDs, CNICs, and NTNs are always rendered LTR even inside RTL prose, using an explicit <span dir="ltr"> wrapper.
8.6 What RTL is not
RTL is not a separate design or a separate component tree. There is one design system (see /specs/en/16-branding-design-system/) rendered in two directions. There are no Component.ltr.tsx and Component.rtl.tsx files. The few exceptions (an icon that must not mirror, a hero illustration with embedded directional meaning) are flagged and reviewed individually.
9. Typography & Fonts
9.1 Locked font stack
Per _context.md §7:
| Locale | Primary font | Fallback |
|---|---|---|
en |
Inter (UI) / Poppins (display/headings) | system sans-serif |
ur |
Noto Nastaliq Urdu | Jameel Noori Nastaleeq (system), sans-serif |
sd |
Noto Naskh Arabic (Sindhi-extended glyph coverage) | system Arabic, sans-serif |
9.2 Loading strategy
Fonts are self-hosted (not loaded from Google Fonts at runtime, for privacy and performance) and subset by locale:
- Only the active locale's font family loads on a given page. A visitor on
/ur/...does not download Inter; a visitor on/en/...does not download Noto Nastaliq Urdu. - Each font is split into unicode-range subsets and served as
woff2withfont-display: swapso text renders immediately in the fallback and swaps to the web font once available. - Preload hints are emitted for the critical above-the-fold subset only.
9.3 Line-height and vertical rhythm
Nastaliq (Urdu) and Naskh (Sindhi) scripts need taller line-height than Latin text — Nastaliq in particular has a large vertical extent (descenders and stacked diacritics). The design system ships per-locale line-height and letter-spacing tokens: EN tokens default to ~1.5 line-height; UR tokens default to ~2.0; SD tokens default to ~1.8. These are applied via CSS variables keyed off the lang attribute, not per-component overrides.
9.4 Font sizes and readability
Urdu and Sindhi render physically smaller than Latin at the same font-size due to script geometry. The design system applies a per-locale font-size scaling factor (UR/SD body text is ~10–15% larger than EN at the same visual hierarchy level) so visual readability is constant across languages.
10. Dual Calendar System (Gregorian + Hijri)
10.1 Government convention
Per _context.md §2, SITP displays both Gregorian and Hijri dates throughout — the convention for Pakistan government correspondence. Wherever a date is shown to a user (ticket created-on, SLA due date, MoM meeting date, official letter date, notification timestamp), both calendars appear, with Gregorian primary and Hijri derived beside or beneath it.
10.2 Storage vs. presentation
- Storage: every timestamp is stored as UTC Gregorian (
DATETIME(6)in MariaDB — see/specs/en/15-tech-architecture/§5.3). Hijri is never stored as the source of truth; it is always derived. - Presentation: a locale-aware formatter renders the stored UTC instant into (a) Gregorian in the user's locale format, and (b) Hijri via the Umm al-Qura or equivalent astronomical conversion, adjusted for Pakistan's moon-sighting where official dates diverge (handled by a maintained override table for declared public holidays).
- Time zone: display is converted to
Asia/Karachi(PKT, UTC+5) at presentation.
10.3 Date pickers
Every date picker in the portal (SLA due-date override, meeting scheduler, report date-range filter, holiday calendar editor) offers dual input: the user picks a Gregorian date and the corresponding Hijri date is shown beside it (and vice versa — picking a Hijri date back-computes the Gregorian). Both values travel to the backend; the Gregorian is authoritative.
10.4 Holiday calendar
The Sindh public holiday calendar (used for SLA pauses per _context.md §5) carries both Gregorian and Hijri dates for each holiday, because Islamic holidays are Hijri-defined (Eid, Ashura, Eid Milad-un-Nabi) while civil holidays are Gregorian-defined (Independence Day, Labour Day). The SLA engine consumes the Gregorian normalized form.
11. Number, Date, Currency & Phone Formatting
All formatting goes through a single locale-aware formatter (built on Intl with locale set from the resolved locale). No format string is constructed by hand.
| Concern | EN format | UR format | SD format | Notes |
|---|---|---|---|---|
| Gregorian date | 17 Jul 2026 |
۱۷ جولائی ۲۰۲۶ |
۱۷ جولاءِ ۲۰۲۶ |
Locale-aware month names; Eastern Arabic numerals in UR/SD by default (configurable — see §11.1). |
| Time | 2:30 PM |
۲:۳۰ بجہ دوپہر |
۲:۳۰ بجہُ دوپھر |
12-hour with locale AM/PM string; PKT. |
| Integer | 1,234 |
۱٬۲۳۴ |
۱٬۲۳۴ |
Grouping separator locale-aware. |
| Currency (PKR) | Rs 15,000 / ₨ 15,000 |
روپے ۱۵٬۰۰۰ |
روپيا ۱۵٬۰۰۰ |
Always PKR; stored as integer paisa (see /specs/en/15-tech-architecture/ §5.3). |
| Phone (PK) | +92 300 1234567 |
+۹۲ ۳۰۰ ۱۲۳۴۵۶۷ |
+۹۲ ۳۰۰ ۱۲۳۴۵۶۷ |
E.164 stored; displayed with PK grouping. |
| Percentage | 45% |
۴۵٪ |
۴۵٪ |
Locale numeral + locale percent sign. |
| Ticket ID | SITP-2026-LBR-000123 |
SITP-2026-LBR-000123 |
SITP-2026-LBR-000123 |
Always Latin numerals, always LTR — IDs are identifiers, not prose. |
11.1 Numeral policy
Pakistan commonly uses Eastern Arabic numerals (۰۱۲۳۴۵۶۷۸۹) in Urdu and Sindhi prose, but Latin numerals in technical/data contexts. SITP's default is Eastern Arabic in prose for UR/SD, Latin in data-dense UI (tables, dashboards, ticket IDs, SLA countdowns) for legibility and consistency with the analytics exports. The numeral variant is a per-locale token configurable by the Super Admin, so a department that prefers all-Latin can switch.
11.2 Phone formatting
Phone numbers are stored in E.164 (+92...), validated at input, and rendered through a PK-aware formatter. CNIC and NTN formats follow their official patterns (CNIC: XXXXX-XXXXXXX-X; NTN: 7 digits) and are always rendered LTR even in RTL locales, wrapped in <span dir="ltr">.
11.3 Currency
All currency is PKR (Pakistani Rupee). The portal does not handle foreign currency natively; the IT-industry beneficiary context is domestic. Currency is stored as integer minor units (paisa) and formatted at presentation.
12. Content Fallback Strategy
12.1 The rule
If a translation is missing in the active locale, the system falls back to English and shows a visible marker so the gap is never silent. Silent fallback (showing English with no indication) is forbidden because it hides translation debt from users and reviewers.
12.2 The visible marker
A missing-key resolution renders as:
- The English string, in a subtle visual treatment (a thin dotted underline, or a small "EN" badge in the corner, configurable by the user's accessibility settings).
- A console warning in dev/staging (never in production console output that end users would see, but logged to Sentry for tracking).
- A row in the translation debt dashboard (see §15) listing the key, the locale, and where it is used.
12.3 Fallback chain
active-locale string → (missing) → English string → (missing) → the key itself in [brackets]
The final [key.itself] state is a hard failure caught in CI (§5.3) and should never reach production. The middle state (English fallback with marker) is the acceptable interim; the goal is zero of either in a released locale.
12.4 Long-form content
For MDX content (KB articles, SOPs), if a locale's translation is stale or missing, the page renders the English version with a banner at the top: "This article is not yet available in [Urdu/Sindhi]. Showing the English original." The banner carries a feedback link so users can request prioritization, and the gap is logged.
13. SEO: hreflang & Sitemap per Locale
13.1 hreflang
Every public page emits <link rel="alternate"> tags for each locale, plus x-default:
<link rel="alternate" hreflang="en" href="https://sindhitportal.maahir.io/en/..." />
<link rel="alternate" hreflang="ur" href="https://sindhitportal.maahir.io/ur/..." />
<link rel="alternate" hreflang="sd" href="https://sindhitportal.maahir.io/sd/..." />
<link rel="alternate" hreflang="x-default" href="https://sindhitportal.maahir.io/en/..." />
x-default points to the EN master. The locale-prefixed URL is canonical (see §3.1); there is no un-prefixed locale to avoid duplicate-content ambiguity. hreflang is emitted by both the Next.js portal (per route) and the Docusaurus docs site (built-in i18n support).
13.2 Sitemap
A per-locale sitemap index is generated at build time:
/sitemap.xml → index listing the per-locale sitemaps
/sitemap-en.xml → all EN public URLs
/sitemap-ur.xml → all UR public URLs
/sitemap-sd.xml → all SD public URLs
Each per-locale sitemap lists only URLs in that locale's prefix, with lastmod reflecting the content's last translation approval (not the EN source edit), so search engines see when the UR or SD version actually changed.
13.3 Localized metadata
<title>, <meta description>, Open Graph, and Twitter Card tags are all sourced from the locale catalog — never hardcoded, never auto-translated by the SEO layer. Each public page's metadata is a reviewed translation. OG images carry localized text overlays where the design system supports it (see /specs/en/16-branding-design-system/).
13.4 robots and crawl budget
The /ur/ and /sd/ trees are fully crawlable (not blocked); they are first-class content. The search engine's crawl budget is monitored in observability; if a locale is consistently under-indexed, the cause (slow render, missing hreflang, thin content) is investigated and fixed.
14. Accessibility per Locale
WCAG 2.1 AA conformance (locked in _context.md §2) applies to all three locales equally. Accessibility is not a single-locale concern.
14.1 Language attribution
- Every page sets
<html lang="..." dir="...">correctly. - Mixed-language runs are wrapped in
<span lang="...">so a screen reader switches voice mid-sentence (e.g., a Sindhi paragraph quoting an English acronym). - User-submitted content uses
lang="auto"so the screen reader picks up the language.
14.2 RTL screen reader support
- All interactive elements have visible focus indicators that work in both directions (focus ring on the correct side).
- Reading order in the DOM matches the visual order in RTL (no CSS tricks that reorder content for sighted users but break screen-reader flow).
- Landmark roles (
nav,main,aside,footer) are direction-agnostic and announced consistently.
14.3 Color contrast and typography
- The brand palette (Ajrak-inspired indigo/maroon/black — see
_context.md§7) is tested at WCAG AA contrast in both LTR and RTL layouts. - The per-locale font scaling (§9.4) and line-height tokens (§9.3) are validated for legibility at the AA minimum body-text size.
- Long Urdu/Sindhi words are allowed to break anywhere with
overflow-wrap: anywhereto prevent horizontal scroll on narrow viewports.
14.4 Keyboard navigation
Tab order follows the visual order in both LTR and RTL. Keyboard shortcuts that imply direction (e.g., Alt+Left for back) are mirrored in RTL (Alt+Right for back) or made direction-neutral where possible. Skip-to-content links work in both directions.
14.5 Assistive technology testing
Each release includes an assistive-tech sweep across NVDA + Firefox and VoiceOver + Safari in all three locales, with particular attention to Nastaliq and Naskh pronunciation. Findings are tracked to closure before the locale is marked published.
15. Translation QA & Review Gates
15.1 The QA pipeline
Translation quality is enforced at four gates:
| Gate | When | What runs | Failure action |
|---|---|---|---|
| G1 — Lint | On every commit | No-hardcoded-strings rule; logical-properties-only rule; glossary terminology check. | Build fails. |
| G2 — Parity | On every PR touching locales | Every EN key present in UR and SD; no orphan keys. | Build fails (or explicit debt item opened). |
| G3 — Native review | Before publish | Native human reviewer signs off per §6. | Item stays in in-review. |
| G4 — Visual sweep | Before release | LTR and RTL screenshots of every changed screen; bi-di mixing check; font-render check. | Release blocked. |
15.2 The translation debt dashboard
An internal dashboard (Super Admin and Department Admin visible) reports:
- Count of fallback-to-EN events in production per locale per week.
- Count of
staletranslations (EN changed since last approval). - Count of
source-onlyandai-draftitems in each review queue. - Reviewer throughput (items approved per reviewer per week).
- Top fallback keys by frequency (so the most user-visible gaps are prioritized).
15.3 Linguistic QA sampling
Beyond per-item review, a periodic linguistic QA sweep samples published content per locale and checks for: terminology drift since the glossary was last updated, register consistency, tone across the product, and cultural appropriateness. Findings feed back into the glossary and the AI tuning set.
15.4 Visual regression in both directions
The visual regression suite (part of /specs/en/13-test-strategy/) runs every story in both dir="ltr" and dir="rtl". A change that looks right in EN but breaks layout in UR or SD fails the suite.
16. Official Terminology & Disambiguation
16.1 The Department vs Ministry distinction
The most consequential terminology rule in SITP, locked in _glossary.md (top critical-distinction note):
S&ITD is a provincial Department — Urdu
محکمہ/ Sindhiمحڪمو. It is NOT a Ministry — Urduوزارت/ Sindhiوزارت. Translators must never render "Department" as "وزارت" or vice-versa.
This matters because Pakistan's federal ministries and provincial departments are constitutionally distinct; misnaming S&ITD as a "Ministry" in an official letter or public page is a factual and political error, not a stylistic one. The AI translation pipeline is given this as a hard forbidden mapping; the QA linter (§7.4, §15 G1) fails any translation that produces وزارت for "Department" or محکمہ for "Ministry".
16.2 Other locked distinctions
| Term | Correct | Wrong | Why it matters |
|---|---|---|---|
| Department (provincial) | محکمہ / محڪمو | وزارت / وزارت | Provincial vs federal (§16.1). |
| Notification (gazette) | نوٹیفکیشن / نوٽيفڪيشن | اطلاع / اطلاع | A binding legal order vs a system alert (glossary §1 vs §7). |
| Notification (system alert) | اطلاع / اطلاع | نوٹیفکیشن / نوٽيفڪيشن | Same word, opposite mapping — context determines which. |
| Complaint | شکایت / شڪايت | other renderings | Standard citizen-facing term. |
| Authorized Representative | مجاز نمائندہ / مجاز نمائندو | نمائندہ alone | "Authorized" carries the legal empowerment; dropping it weakens the term. |
| Tracking ID | ٹریکنگ آئی ڈی / ٽريڪنگ آءِ ڊي | other renderings | Public-facing identifier; consistency aids look-up. |
16.3 Never-translated terms
Some terms are kept in their original form across all three locales, by glossary rule:
- Acronyms:
SITP,S&ITD,SACM,NADRA,SECP,FBR,SRB,PSEB,NITB,SLA,FRT,TAT,CSAT,RBAC,2FA,OTP,PII,OCR,LLM,RAG,MoM,TRI,PKI,IVR,RTI,WCAG,RTL,CII,CERT-PK,PPP,ADP,SIFC,MoU. - System identifiers: ticket IDs (
SITP-2026-LBR-000123), CNIC, NTN, STRN, SECP incorporation numbers. - Product/system names that are proper nouns:
e-Office,Keycloak,Meilisearch,MinIO,Metabase,Mailjet,WhatsApp.
Translators and the AI pipeline are told to leave these as-is; attempting to "translate" NADRA into Urdu script is a defect.
16.4 Transliteration fallback
Where the glossary marks a term (معیاری ترجمہ درکار) ("standard translation needed"), the term is transliterated into native script and flagged for future glossary refinement. These items appear in the debt dashboard (§15.2) so the glossary owners close them.
17. Functional Requirements
| ID | Requirement | MoSCoW |
|---|---|---|
FR-PUB-101 |
The system shall support exactly three locales: en (master, LTR), ur (RTL), sd (RTL). |
[Must] |
FR-PUB-102 |
The system shall resolve locale from (in order) URL prefix, user preference, org default, Accept-Language, and finally en. |
[Must] |
FR-PUB-103 |
The system shall propagate the resolved locale through the JWT, every BullMQ job payload, the NestJS request context, every template render, every search query, and every AI call. | [Must] |
FR-PUB-104 |
No user-facing string shall be hardcoded in source code; all strings shall be sourced from per-locale message catalogs. | [Must] |
FR-PUB-105 |
The system shall provide a per-locale message catalog (JSON) for UI strings and per-locale MDX for long-form content. | [Must] |
FR-PUB-106 |
The system shall provide a locale switcher on every page that rewrites the URL prefix, updates the cookie, persists to the user record, and preserves the user's current location. | [Must] |
FR-PUB-107 |
The system shall render Urdu and Sindhi locales right-to-left via the dir attribute and CSS logical properties. |
[Must] |
FR-PUB-108 |
The system shall mirror directional icons in RTL and provide a data-no-mirror escape hatch for non-directional icons. |
[Must] |
FR-PUB-109 |
The system shall load only the active locale's font family (Inter/Poppins for EN, Noto Nastaliq Urdu for UR, Noto Naskh Arabic for SD), self-hosted, subset, woff2. | [Must] |
FR-PUB-110 |
The system shall display every user-facing date in both Gregorian and Hijri, with Gregorian as the stored source of truth. | [Must] |
FR-PUB-111 |
The system shall provide dual-input date pickers that show Gregorian and Hijri side by side. | [Should] |
FR-PUB-112 |
The system shall format numbers, dates, currency, and phone numbers through a single locale-aware formatter (Intl-based). |
[Must] |
FR-PUB-113 |
Currency shall always be PKR, stored as integer paisa, formatted at presentation. | [Must] |
FR-PUB-114 |
Ticket IDs, CNICs, NTNs, and acronyms shall always render in Latin script and LTR direction, even within RTL prose. | [Must] |
FR-PUB-115 |
On a missing translation in the active locale, the system shall fall back to the English string with a visible marker. | [Must] |
FR-PUB-116 |
Silent fallback (English shown with no marker) shall not occur. | [Must] |
FR-PUB-117 |
Every public page shall emit hreflang alternate links for en, ur, sd, and x-default (pointing to EN). |
[Must] |
FR-PUB-118 |
The system shall generate a per-locale sitemap (sitemap-en.xml, sitemap-ur.xml, sitemap-sd.xml) under a sitemap index. |
[Must] |
FR-PUB-119 |
The system shall apply glossary-approved translations as forced constraints in the AI translation pipeline. | [Must] |
FR-PUB-120 |
The system shall refuse to publish any UR/SD content that has not passed native human review. | [Must] |
FR-PUB-121 |
The system shall track translation status (source-only, ai-draft, in-review, approved, published, stale) per locale per artifact. |
[Must] |
FR-PUB-122 |
The system shall provide an on-demand "translate" affordance for user-submitted content (ticket text, messages, MoM), clearly labelled as machine translation, with the original preserved. | [Should] |
FR-PUB-123 |
The system shall render the Sindh public holiday calendar with both Gregorian and Hijri dates, consumed by the SLA engine. | [Must] |
FR-PUB-124 |
The system shall provide a translation debt dashboard reporting fallback events, stale items, and reviewer throughput. | [Should] |
FR-PUB-125 |
The CI pipeline shall fail on (a) hardcoded strings, (b) catalog key-parity gaps, (c) forbidden glossary mappings (e.g., Department → وزارت). | [Must] |
FR-PUB-126 |
The system shall set <html lang="..." dir="..."> correctly on every page. |
[Must] |
FR-PUB-127 |
The visual regression suite shall run every screen story in both LTR and RTL. | [Must] |
FR-PUB-128 |
The system shall maintain at least two accredited native reviewers per locale for redundancy. | [Should] |
FR-PUB-129 |
Numeral rendering (Eastern Arabic vs Latin in UR/SD prose) shall be configurable per locale by the Super Admin. | [Could] |
18. Non-Functional Requirements
| ID | Requirement | MoSCoW |
|---|---|---|
NFR-PUB-101 |
Locale resolution shall complete in under 5 ms at the edge (cookie/URL read); no DB call on the hot path. | [Must] |
NFR-PUB-102 |
The active locale's font payload (woff2, subset) shall be under 200 KB on first paint. | [Must] |
NFR-PUB-103 |
Switching locale on a page shall not trigger a full page reload where the framework supports it (Next.js App Router native). | [Should] |
NFR-PUB-104 |
RTL and LTR layouts shall use the same component tree (no duplicated components per direction). | [Must] |
NFR-PUB-105 |
The product shall conform to WCAG 2.1 AA in all three locales, verified per release. | [Must] |
NFR-PUB-106 |
Machine translation latency (on-demand user-content translation) shall be under 3 seconds p95 for content under 2,000 characters. | [Should] |
NFR-PUB-107 |
The translation debt dashboard shall refresh at least daily. | [Should] |
NFR-PUB-108 |
Hijri conversion shall match Pakistan official Hijri dates (moon-sighting overrides) for declared public holidays. | [Must] |
NFR-PUB-109 |
All three locales shall remain continuously crawlable (no locale blocked in robots.txt). |
[Must] |
NFR-PUB-110 |
Catalog key stability: no in-use translation key shall be renamed without a deprecation window. | [Must] |
19. User Stories
US-PUB-101 — Switch locale without losing place [Must]
As a Company Representative I want to switch the portal from English to Urdu mid-session So that I can read content in my preferred language without losing the ticket or page I was viewing.
Acceptance Criteria (Gherkin)
Scenario: Locale switch preserves location
Given a Company Representative is viewing ticket SITP-2026-LBR-000123 in the English locale
When they click the locale switcher and select Urdu
Then the URL rewrites to the /ur/ prefix for the same ticket
And the page re-renders in Urdu with right-to-left direction
And the same ticket SITP-2026-LBR-000123 remains on screen
And the sitp_locale cookie is set to "ur"
And their user record's locale is persisted as "ur"
US-PUB-102 — See dates in both Gregorian and Hijri [Must]
As an Officer I want every date on a ticket to show both Gregorian and Hijri So that my official correspondence matches government convention.
Acceptance Criteria (Gherkin)
Scenario: Dual calendar display
Given an Officer is viewing a ticket with created-at 2026-07-17 14:30 PKT
Then the created-at timestamp shows "17 Jul 2026" (Gregorian)
And beside or beneath it shows the corresponding Hijri date
And both are rendered in the Officer's locale numeral format
US-PUB-103 — Translate an inbound Urdu ticket on demand [Should]
As an Officer with English locale I want to click "Translate" on a ticket filed in Urdu So that I can understand it without leaving my English session.
Acceptance Criteria (Gherkin)
Scenario: On-demand machine translation
Given an Officer with locale "en" is viewing a ticket whose description was authored in Urdu
When they click the "Translate" affordance
Then the system returns an English rendering via the AI translation capability
And the rendering is clearly labelled "Machine translation"
And the original Urdu text is preserved as the record
And an audit row is written recording the translation call
US-PUB-104 — Never see silent English fallback [Must]
As a Sindhi-locale user I want to know when a piece of content is showing in English because the Sindhi version is missing So that I am not confused by an unexpected language switch.
Acceptance Criteria (Gherkin)
Scenario: Visible fallback marker
Given the Sindhi locale catalog is missing the key "tickets.newBadge.tooltip"
When a Sindhi-locale user views the ticket list
Then the tooltip shows the English string
And the tooltip is rendered with the fallback marker (dotted underline or EN badge)
And the missing key is logged to the translation debt dashboard
US-PUB-105 — Reviewer approves an AI translation draft [Must]
As a Native Urdu Reviewer I want to review the AI-generated Urdu draft of a newly authored KB article So that only correct, glossary-consistent Urdu reaches the public site.
Acceptance Criteria (Gherkin)
Scenario: Native review gate
Given a KB article was authored in English and the AI translation pass produced an Urdu draft
And the draft's status is "ai-draft"
When the reviewer opens the review workbench
Then they see the English source, the Urdu draft, and the glossary-approved terms side by side
And they can edit the draft inline
And on approval the status moves to "approved" and then "published"
And their edits are saved to the AI tuning corpus
20. Test Cases
| ID | Covers | Scenario |
|---|---|---|
TC-PUB-101-01 |
FR-PUB-102 |
Visit /ur/tickets/... anonymously; assert locale resolves to ur, dir=rtl, cookie set. |
TC-PUB-104-01 |
FR-PUB-104 |
Run lint over a PR that introduces a hardcoded JSX string; assert build fails. |
TC-PUB-105-01 |
FR-PUB-105 |
Add key tickets.foo to en/tickets.json only; assert CI parity check fails for missing ur/sd. |
TC-PUB-107-01 |
FR-PUB-107 |
Render the ticket list story in ur; assert <html dir="rtl"> and logical-property padding on the leading edge. |
TC-PUB-108-01 |
FR-PUB-108 |
Render a "back" icon button in ur; assert the icon is mirrored vs en. |
TC-PUB-110-01 |
FR-PUB-110 |
Render a ticket created-at in ur; assert both Gregorian and Hijri strings appear. |
TC-PUB-114-01 |
FR-PUB-114 |
Render a ticket ID inside an Urdu paragraph; assert it is wrapped dir="ltr" and Latin. |
TC-PUB-115-01 |
FR-PUB-115 |
Delete key x from sd/common.json; render the page in Sindhi; assert English fallback with marker. |
TC-PUB-119-01 |
FR-PUB-119 |
AI-translate an EN string containing "Department"; assert output uses محڪمو (SD), never وزارت. |
TC-PUB-120-01 |
FR-PUB-120 |
Attempt to publish an ai-draft UR item without review; assert publish is refused. |
TC-PUB-125-01 |
FR-PUB-125 |
Submit a PR whose UR catalog renders "Department" as وزارت; assert CI forbidden-mapping check fails the build. |
TC-PUB-127-01 |
FR-PUB-127 |
Run the visual regression suite for a changed tickets-page story in both ltr and rtl; assert both pass. |
21. Open Questions / TBD
| # | Item | Status |
|---|---|---|
| 1 | Numeral default in UR/SD prose: Eastern Arabic by default, or Latin? Proposal: Eastern Arabic in prose, Latin in data-dense UI, configurable. | Proposal · TBD sign-off |
| 2 | Hijri conversion source: Umm al-Qura algorithm vs Pakistan Ruet-e-Hilal Committee moon-sighting overrides for declared holidays. Proposal: algorithm + override table maintained by ops. | TBD |
| 3 | Whether to add en-PK / ur-PK / sd-PK region subtags in hreflang for finer SEO targeting, or stay with bare en/ur/sd. |
TBD |
| 4 | Number of accredited native reviewers per locale at launch (minimum 2 proposed in FR-PUB-128). |
TBD staffing |
| 5 | Whether the on-demand machine translation of user content should be persisted (cached) or always re-generated. | TBD |
| 6 | Self-hosted font subsetting tooling (pyftsubset vs fonttools pipeline) — confirmed at build setup. | TBD tooling |
| 7 | Whether citizen-submitted free text in a fourth language (e.g., Pashto, Balochi) is in scope for V1 or deferred. | Deferred (V2 signal) |
End of document.