# i18n (zh-CN / en / zh-TW) User-visible **error `msg`** and **notification copy** are never hardcoded in handlers. They come from `internal/pkg/i18n` using the request language. ## Language on every request Canonical tags: `zh-CN`, `en`, `zh-TW`. Parse with `i18n.Parse`; store on `reqctx.RequestContext.Language`. Read with `reqctx.Lang(ctx)` / `reqctx.Language(ctx)`. | Transport | Client sends | Server stores | |-----------|--------------|----------------| | HTTP | `X-Language` if set, else `Accept-Language` | Trace middleware, every request | | TCP | AUTH JSON field `language` | `session.Conn.SetLanguage` for the connection; copied into each command's `RequestContext` | Missing or unknown tags → **`en`** (default). `zh` / `zh-Hans` → `zh-CN`. `zh-TW` / `zh-HK` / `zh-Hant` → `zh-TW`. `en*` → `en`. `POST /v1/sms/codes` requires **`X-Language`** explicitly (`i18n.ParseExplicit`); Accept-Language alone is not enough. Do not put language in JWT. Rate limit still uses `reqctx.RealIP`, not language. ## Errors Sentinel `AppError` keeps an English `Msg` for logs. Envelope `msg` is localized at the transport boundary: - HTTP: `reply.FromContext(ctx, err)` - TCP: `protocol.EncodeErrLang(i18n.Parse(conn.Language()), err)` `WithMsg` is only for **already-localized** text (validator). Do not `WithMsg("unauthorized")` in a handler. Add a new business code: register `err.{code}` in `internal/pkg/i18n/catalog_err.go` for all three languages. ## Validation `label` and `errmsg` are **catalog keys**, not Chinese sentences: ```go Email string `json:"email" validate:"required,email" label:"field.email" errmsg:"required:validate.login.email_required"` ``` `validate.StructLang(reqctx.Lang(ctx), in)`. Default templates are `validate.required` etc. with `{label}` / `{param}`. ## Notification types Push and in-app system notices use type keys, then `i18n.Notify(lang, typ, args)` for title/body. Recipient language is the **target device** session language (TCP) or the user's stored lang — never the sender's. | Type | Key | |------|-----| | Same-device kick | `notify.kick.replaced` | | Friend request | `notify.friend.request` | | Group invite | `notify.group.invite` | | Removed from group | `notify.group.removed` | | Message recall | `notify.message.recall` | `push.Notification.Type` holds the key. Fill `Title`/`Body` with `i18n.Notify` before `Notifier.Notify`. Add new types in `catalog_notify.go` (`.title` / `.body`, `{name}` placeholders). ## Do not - Hardcode `msg` or kick/push strings in `handler/*` or EventLoop. - Localize with the sender language when notifying another user. - Introduce a fourth locale without adding it to `i18n.Lang` and every catalog entry.