Internationalization

Text you pass to a component is yours. The few strings a component writes itself arei18n.T messages, and one line in gsx.toml routes all of them through your translator.

Get started

Add ui/i18n/translate.go beside the vendored ui/i18n/i18n.go and register it, using your module path:

[renderers]
"example.com/app/ui/i18n.T" = "example.com/app/ui/i18n.Translate"

Write the translator. It receives the render context, so the request's locale is in reach:

package i18n

import "context"

// Translate renders every T. The English text is the message id; an
// unknown one renders as English. Replace lookup with your catalog.
func Translate(ctx context.Context, m T) string {
	if s, ok := lookup(ctx, string(m)); ok {
		return s
	}
	return string(m)
}

How it works

  • i18n.T is a string type in ui/i18n/i18n.go, its own package, vendored with any component that uses it.
  • The English text is the message id: i18n.T("Close"), i18n.T("Next slide"), i18n.T("Toggle Sidebar").
  • Without a renderer, a message renders as its English text.
  • With one, gsx calls your function everywhere a message renders, in text and in attributes.
  • Renderers bind when gsx generate runs in your module, so only vendored components translate. Importing github.com/gsxhq/gsxui/ui directly renders English.
  • Children, props and attrs never pass through T.
  • gsxui add --overwrite rewrites only the files it vendored, so translate.go survives.

Calendar

Calendar composes month and weekday names, the caption and each day's label. Pass alocale; the zero value is English.

  • Caption and DayLabel substitute {month}, {year}, {weekday} and {day}. Other text is literal.
  • Digits is ten runes replacing 0-9 in visible text. Form values and data attributes stay ASCII.
  • Fill every field. An empty field renders empty; only the all-zero value means English.
  • The client reads the same values from the root, so navigation writes the same text the server did.
يناير ٢٠٢٦
package calendar

import (
	"time"

	"github.com/gsxhq/gsxui/ui"
)

// LocalizedDefaultMonth mirrors Basic's own DefaultMonth (2026-01).
var LocalizedDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

// Arabic is CLDR's ar gregorian data: wide month names (the abbreviated set
// is identical), wide and narrow weekday names, and Arabic-Indic digits.
// The day label puts the day before the month, and the week starts on
// Saturday.
var Arabic = ui.CalendarLocale{
	Months: [12]string{
		"يناير",
		"فبراير",
		"مارس",
		"أبريل",
		"مايو",
		"يونيو",
		"يوليو",
		"أغسطس",
		"سبتمبر",
		"أكتوبر",
		"نوفمبر",
		"ديسمبر",
	},
	MonthsShort: [12]string{
		"يناير",
		"فبراير",
		"مارس",
		"أبريل",
		"مايو",
		"يونيو",
		"يوليو",
		"أغسطس",
		"سبتمبر",
		"أكتوبر",
		"نوفمبر",
		"ديسمبر",
	},
	Weekdays:      [7]string{"الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"},
	WeekdaysShort: [7]string{"ح", "ن", "ث", "ر", "خ", "ج", "س"},
	Caption:       "{month} {year}",
	DayLabel:      "{weekday}، {day} {month} {year}",
	Digits:        "٠١٢٣٤٥٦٧٨٩",
}

// Localized renders the dropdown-caption grid in Arabic under dir="rtl".
// The month is a parameter so the harness's ?month= override can render any
// month for the Go/JS agreement diff.
component Localized(month time.Time) {
	<div dir="rtl" lang="ar">
		<ui.Calendar
			mode="single"
			month={month}
			weekStartsOn={time.Saturday}
			showOutsideDays={true}
			captionLayout="dropdown"
			locale={Arabic}
		/>
	</div>
}

Finding leftover English

Register a translator that wraps every message in markers, render each page, and search the output for English outside the markers. Anything found is text a caller passed in, one of the overridable defaults below, or a gsxui bug worth an issue.

Not translated

  • Defaults a caller can already override by passing the same attribute stay English: aria-label on Spinner, Breadcrumb, Pagination and its Previous/Next links, SidebarRail's aria-label and title, and Carousel's aria-roledescription. Pass a translated value in your markup.
  • When no ui.Toaster is mounted, toaster.js creates its own region with an English aria-label. Mount ui.Toaster and the landmark is a message like the rest.