spark-server/.cursor/skills/spark-im-v3/i18n.md
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

58 lines
2.5 KiB
Markdown

# 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` (default), `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 → `zh-CN`. `zh` / `zh-Hans` → `zh-CN`. `zh-TW` / `zh-HK` / `zh-Hant` → `zh-TW`. `en*` → `en`.
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.