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.Tis a string type inui/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 generateruns in your module, so only vendored components translate. Importinggithub.com/gsxhq/gsxui/uidirectly renders English. - Children, props and
attrsnever pass throughT. gsxui add --overwriterewrites only the files it vendored, sotranslate.gosurvives.
Calendar
Calendar composes month and weekday names, the caption and each day's label. Pass alocale; the zero value is English.
CaptionandDayLabelsubstitute{month},{year},{weekday}and{day}. Other text is literal.Digitsis 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-labelon Spinner, Breadcrumb, Pagination and its Previous/Next links, SidebarRail'saria-labelandtitle, and Carousel'saria-roledescription. Pass a translated value in your markup. - When no
ui.Toasteris mounted,toaster.jscreates its own region with an Englisharia-label. Mountui.Toasterand the landmark is a message like the rest.