Files
61993b4387 feat(i18n): add Russian (ru) language — full parity across backend, web & desktop (#1391)
* docs(i18n): design spec for Russian (ru) language support

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): register ru locale constant and IsSupported

* feat(i18n): scaffold ru catalog (en placeholders)

* feat(i18n): translate ru backend catalog to Russian

* fix(i18n): polish ru catalog grammar per review

* feat(i18n): wire ru into system messages, config enum, parity test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): add ru to web language constants and system-message locales

* feat(i18n): scaffold ru web locale dir (en placeholders)

* feat(i18n): wire ru resources into web i18next config

* feat(i18n): translate ru web locale to Russian (41 namespaces)

* fix(i18n): normalize ru web terminology (арендатор, привязка)

* fix(i18n): restore dropped 'named' in ru setup OAuth hint

* feat(i18n): wire ru into desktop i18next and language pickers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): add ru desktop locale (Russian, 16 namespaces)

* test(i18n): extend traces/hooks locale guards to cover ru

* chore(deploy): add repeatable fork-dev local build+deploy tooling

Adds a one-command, idempotent way to run goclaw from a locally built
image that merges fresh upstream dev with un-merged fork features
(Telegram trigger-words PR #1383 + Russian locale), independent of the
published :latest image.

- scripts/deploy-forkdev.sh: fetch origin/dev -> merge feature branches
  into local/fork-dev worktree -> push fork/dev -> build -> up -d.
- docker-compose.forkdev.yml: overlay pinning image to goclaw:forkdev
  with pull_policy:never so :latest never silently replaces the build.
- .gitignore: track the deploy helper (past deploy-*.sh), ignore the
  ../.goclaw-forkdev integration worktree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telegram): agent-callable message reaction (message action=react)

Adds a "react" action to the message tool so an agent can set an emoji
reaction on an existing message (e.g. mark its own status post 👍 once
fully paid). Mirrors the ChannelEditor wiring: ReactionSetter capability
-> Manager.ReactToMessage -> telegram Channel.ReactToMessage (validates
against Telegram's supported reaction set; ✅/❌ are rejected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(config): remove ignored session scoping controls (#1405)

Co-authored-by: Collective Developer <man@collective.dev>

* feat(telegram): agent-callable message reaction (message action=react) (#1407)

Adds a "react" action to the message tool so an agent can set an emoji
reaction on an existing message (e.g. mark its own status post 👍 once
fully paid). Mirrors the ChannelEditor wiring: ReactionSetter capability
-> Manager.ReactToMessage -> telegram Channel.ReactToMessage (validates
against Telegram's supported reaction set; ✅/❌ are rejected).

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: build & publish fork image to GHCR (temporary, pre-upstream)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(memory): use 'russian' FTS config for memory_chunks recall

Memory keyword search (memory_search.go FTS + the generated tsv column)
used the 'simple' text-search config, which does no stemming. Russian
grammatical cases therefore never matched: a genitive query token
("Дианы") could not hit the stored nominative token ("Диана"), so
memory_search returned nothing for natural queries even though the fact
was saved. This is the sole recall path on deployments without an
embedding provider (ChatGPT-OAuth/codex cannot embed), so recall was
effectively dead there.

Switch both sides to the Snowball 'russian' config so index and query
lexemes stem to the same root ("диан"), restoring case-insensitive
recall. Migration 000094 recreates the generated tsv column (a GENERATED
expression cannot be ALTERed in place; ADD COLUMN backfills all rows).

Surface parity: desktop/SQLite N/A — SQLite memory search uses LIKE, a
separate mechanism unaffected by the PG regconfig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: ナム <namnhfreelancer@gmail.com>
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-11 12:25:38 +07:00

83 lines
1.9 KiB
Go

// Package i18n provides a lightweight message catalog for localizing
// user-facing error messages. Messages use fmt.Sprintf-style templates.
package i18n
import "maps"
import "fmt"
// Supported locales.
const (
LocaleEN = "en"
LocaleVI = "vi"
LocaleZH = "zh"
LocaleKO = "ko"
LocaleRU = "ru"
DefaultLocale = LocaleEN
)
// catalogs maps locale → key → message template.
var catalogs = map[string]map[string]string{}
// register adds a set of messages for a locale. Called from catalog_*.go init().
func register(locale string, msgs map[string]string) {
if catalogs[locale] == nil {
catalogs[locale] = make(map[string]string, len(msgs))
}
maps.Copy(catalogs[locale], msgs)
}
// T returns a localized message for the given key.
// If the key is not found in the requested locale, it falls back to English.
// If the key is not found in English either, the key itself is returned.
// Optional args are applied via fmt.Sprintf if the template contains verbs.
func T(locale, key string, args ...any) string {
msg := lookup(locale, key)
if len(args) > 0 {
return fmt.Sprintf(msg, args...)
}
return msg
}
func lookup(locale, key string) string {
if cat, ok := catalogs[locale]; ok {
if msg, ok := cat[key]; ok {
return msg
}
}
// Fallback to English
if locale != LocaleEN {
if cat, ok := catalogs[LocaleEN]; ok {
if msg, ok := cat[key]; ok {
return msg
}
}
}
return key
}
// IsSupported returns true if the locale is a known language.
func IsSupported(locale string) bool {
switch locale {
case LocaleEN, LocaleVI, LocaleZH, LocaleKO, LocaleRU:
return true
}
return false
}
// Normalize returns a supported locale or the default.
func Normalize(locale string) string {
if IsSupported(locale) {
return locale
}
// Handle common prefixes: "en-US" → "en", "vi-VN" → "vi", "zh-CN" → "zh"
if len(locale) >= 2 {
prefix := locale[:2]
if IsSupported(prefix) {
return prefix
}
}
return DefaultLocale
}