mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 18:15:47 +00:00
* 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>
83 lines
1.9 KiB
Go
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
|
|
}
|