Skip to Content
FoundationsInternationalization

Internationalization

The Mukoko ecosystem serves users across Africa, a continent with over 2,000 languages. Our internationalization strategy is built into the design system at the foundational level.

Why Noto Sans

Noto Sans was chosen as the primary font for a specific reason: it is one of the few font families with comprehensive coverage of African language scripts and diacritics.

Languages like Yoruba, Igbo, Hausa, and Swahili use Latin script with diacritical marks that many popular fonts (Inter, SF Pro, Roboto) do not render correctly. Noto Sans handles:

  • Yoruba: underdots (ẹ, ọ, ṣ), tone marks (à, á, è, é)
  • Igbo: underdots (ị, ọ, ụ), combining marks
  • Hausa: hooked characters (ɓ, ɗ, ƙ)
  • Shona: circumflex accents (â, ê), extended vowels
  • Swahili: standard Latin script (well-supported)
  • Amharic/Tigrinya: Ge’ez script (via Noto Sans Ethiopic)
  • Arabic: via Noto Sans Arabic (RTL support)

Font stack

--font-sans: "Noto Sans", sans-serif; /* Body text */ --font-serif: "Noto Serif", serif; /* Headings */ --font-mono: "JetBrains Mono", monospace; /* Code */

Load these via next/font/google for optimal performance with font subsetting.

Text direction

LTR (left-to-right)

Most African languages use LTR script. The default dir="ltr" on the <html> element handles this.

RTL (right-to-left)

For Arabic, the Mukoko registry includes a DirectionProvider component that wraps Radix UI’s direction context:

import { DirectionProvider } from "@/components/ui/direction" <DirectionProvider dir="rtl"> <YourApp /> </DirectionProvider>

When building RTL-aware layouts:

  • Use logical properties (ms-4 instead of ml-4, ps-4 instead of pl-4)
  • Use start/end instead of left/right (text-start, justify-end)
  • Radix UI components handle RTL arrow key navigation automatically

Number formatting

African regions use different number formats. Use the Intl.NumberFormat API:

// South Africa (ZAR) new Intl.NumberFormat("en-ZA", { style: "currency", currency: "ZAR", }).format(1234.56) // → "R 1 234,56" // Nigeria (NGN) new Intl.NumberFormat("en-NG", { style: "currency", currency: "NGN", }).format(1234.56) // → "₦1,234.56" // Zimbabwe (USD, ZWL) new Intl.NumberFormat("en-ZW", { style: "currency", currency: "USD", }).format(1234.56) // → "US$1,234.56"

Never hardcode currency symbols or decimal separators.

Date formatting

Date formats vary across African regions. Always use Intl.DateTimeFormat:

// Short date — adapts to locale new Intl.DateTimeFormat("en-ZW", { dateStyle: "medium", }).format(new Date()) // → "2 Apr 2026" // With time new Intl.DateTimeFormat("en-ZA", { dateStyle: "long", timeStyle: "short", }).format(new Date()) // → "2 April 2026 at 14:30"

Relative time

Use Intl.RelativeTimeFormat for “3 hours ago”, “in 2 days”:

const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }) rtf.format(-1, "day") // → "yesterday" rtf.format(3, "hour") // → "in 3 hours"

Content considerations

  • Do not assume English — labels, error messages, and UI text should be externalizable
  • Avoid idioms — “hit the ground running” does not translate; prefer direct language
  • Support variable text length — translated strings may be 30-50% longer than English; design layouts that flex
  • Respect local naming conventions — names may include prefixes, suffixes, or structures unfamiliar to Western systems; use a single “Full name” field rather than forcing first/last name separation
Last updated on