mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 12:14:17 +00:00
* feat(packages): add apk update flow + pkg-helper v2 protocol - APK update checker/executor via helper IPC (runtime detection, upgrade scan via apk list --upgradable) - BREAKING: pkg-helper v2 protocol (5 actions: check_apk/check_pip/check_npm/exec_apk/exec_pip, code/data fields, renewable 10min deadline, apkMutex, 1MB scanner) - Edition gating: SupportsApk + IsAlpineRuntime double-gate (Standard/Full only) - Backend 3-branch wiring: alpine/apt/yum routes + update_registry, dep_installer helpers - i18n: 5 apk keys (EN/VI/ZH catalogs) - Frontend: source pill Alpine badge, APK in updates-list/summary-bar/update-all modal - E2E tests: apk_e2e build tag covering checker/executor/helper protocol - Docs: packages-apk.md, security/changelog updates - Plans + reports under plans/260417-1500-packages-update-phase2b-apk-pkghelper/ + plans/reports/ * docs(packages): journal Phase 2b apk + pkg-helper v2
414 lines
10 KiB
Go
414 lines
10 KiB
Go
package i18n
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestT_ValidLocaleAndKey tests basic message retrieval with valid locale and key.
|
|
func TestT_ValidLocaleAndKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
key string
|
|
args []any
|
|
wantMsg string
|
|
wantKey bool // if true, expects key to be returned (not found)
|
|
}{
|
|
{
|
|
name: "English - simple message without args",
|
|
locale: LocaleEN,
|
|
key: MsgRequired,
|
|
args: nil,
|
|
wantMsg: "%s is required", // template string
|
|
wantKey: false,
|
|
},
|
|
{
|
|
name: "English - message with args",
|
|
locale: LocaleEN,
|
|
key: MsgRequired,
|
|
args: []any{"email"},
|
|
wantMsg: "email is required",
|
|
wantKey: false,
|
|
},
|
|
{
|
|
name: "Vietnamese - has translation for required",
|
|
locale: LocaleVI,
|
|
key: MsgRequired,
|
|
args: nil,
|
|
wantMsg: "%s là bắt buộc", // Vietnamese translation exists
|
|
wantKey: false,
|
|
},
|
|
{
|
|
name: "Chinese - has translation with args",
|
|
locale: LocaleZH,
|
|
key: MsgRequired,
|
|
args: []any{"username"},
|
|
wantMsg: "username 是必填项", // Chinese translation exists
|
|
wantKey: false,
|
|
},
|
|
{
|
|
name: "English - invalid key returns key itself",
|
|
locale: LocaleEN,
|
|
key: "nonexistent.key",
|
|
args: nil,
|
|
wantMsg: "nonexistent.key",
|
|
wantKey: true,
|
|
},
|
|
{
|
|
name: "Unsupported locale - fallback to English",
|
|
locale: "fr", // French not supported
|
|
key: MsgRequired,
|
|
args: nil,
|
|
wantMsg: "%s is required",
|
|
wantKey: false,
|
|
},
|
|
{
|
|
name: "Empty locale - falls back to English",
|
|
locale: "",
|
|
key: MsgRequired,
|
|
args: nil,
|
|
wantMsg: "%s is required", // empty locale falls back to English
|
|
wantKey: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := T(tt.locale, tt.key, tt.args...)
|
|
|
|
// For keys that exist in catalogs, verify we get the localized message
|
|
// For missing keys, verify we get the key back
|
|
if tt.wantKey {
|
|
if result != tt.key {
|
|
t.Errorf("T(%q, %q) = %q; want key %q", tt.locale, tt.key, result, tt.key)
|
|
}
|
|
} else {
|
|
if result != tt.wantMsg {
|
|
t.Errorf("T(%q, %q, %v) = %q; want %q", tt.locale, tt.key, tt.args, result, tt.wantMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestT_MultipleArgs tests message formatting with multiple arguments.
|
|
func TestT_MultipleArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
key string
|
|
args []any
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "Message with 2 args",
|
|
locale: LocaleEN,
|
|
key: MsgNotFound,
|
|
args: []any{"agent", "abc123"},
|
|
wantMsg: "agent not found: abc123",
|
|
},
|
|
{
|
|
name: "Message with correct number of args",
|
|
locale: LocaleEN,
|
|
key: MsgRequired,
|
|
args: []any{"email"},
|
|
wantMsg: "email is required", // only uses first arg
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := T(tt.locale, tt.key, tt.args...)
|
|
if result != tt.wantMsg {
|
|
t.Errorf("T(%q, %q, %v) = %q; want %q", tt.locale, tt.key, tt.args, result, tt.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestT_EmptyArgsOnTemplateString returns raw template if no args provided.
|
|
func TestT_EmptyArgsOnTemplateString(t *testing.T) {
|
|
result := T(LocaleEN, MsgRequired) // no args
|
|
wantTemplate := "%s is required"
|
|
|
|
if result != wantTemplate {
|
|
t.Errorf("T(LocaleEN, MsgRequired) without args = %q; want template %q", result, wantTemplate)
|
|
}
|
|
}
|
|
|
|
// TestIsSupportedLocale tests locale support checking.
|
|
func TestIsSupportedLocale(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
wantBool bool
|
|
}{
|
|
{
|
|
name: "English is supported",
|
|
locale: LocaleEN,
|
|
wantBool: true,
|
|
},
|
|
{
|
|
name: "Vietnamese is supported",
|
|
locale: LocaleVI,
|
|
wantBool: true,
|
|
},
|
|
{
|
|
name: "Chinese is supported",
|
|
locale: LocaleZH,
|
|
wantBool: true,
|
|
},
|
|
{
|
|
name: "French is not supported",
|
|
locale: "fr",
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "Spanish is not supported",
|
|
locale: "es",
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "German is not supported",
|
|
locale: "de",
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "Empty string is not supported",
|
|
locale: "",
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "Case matters - EN uppercase not supported",
|
|
locale: "EN",
|
|
wantBool: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := IsSupported(tt.locale)
|
|
if result != tt.wantBool {
|
|
t.Errorf("IsSupported(%q) = %v; want %v", tt.locale, result, tt.wantBool)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNormalizeLocale tests locale normalization with fallback logic.
|
|
func TestNormalizeLocale(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
wantNormal string
|
|
}{
|
|
{
|
|
name: "Already normalized - en",
|
|
locale: LocaleEN,
|
|
wantNormal: LocaleEN,
|
|
},
|
|
{
|
|
name: "Already normalized - vi",
|
|
locale: LocaleVI,
|
|
wantNormal: LocaleVI,
|
|
},
|
|
{
|
|
name: "Already normalized - zh",
|
|
locale: LocaleZH,
|
|
wantNormal: LocaleZH,
|
|
},
|
|
{
|
|
name: "en-US prefix stripped to en",
|
|
locale: "en-US",
|
|
wantNormal: LocaleEN,
|
|
},
|
|
{
|
|
name: "en-GB prefix stripped to en",
|
|
locale: "en-GB",
|
|
wantNormal: LocaleEN,
|
|
},
|
|
{
|
|
name: "vi-VN prefix stripped to vi",
|
|
locale: "vi-VN",
|
|
wantNormal: LocaleVI,
|
|
},
|
|
{
|
|
name: "zh-CN prefix stripped to zh",
|
|
locale: "zh-CN",
|
|
wantNormal: LocaleZH,
|
|
},
|
|
{
|
|
name: "zh-TW prefix stripped to zh",
|
|
locale: "zh-TW",
|
|
wantNormal: LocaleZH,
|
|
},
|
|
{
|
|
name: "Unsupported locale defaults to en",
|
|
locale: "fr",
|
|
wantNormal: DefaultLocale,
|
|
},
|
|
{
|
|
name: "Unsupported with region defaults to en",
|
|
locale: "fr-FR",
|
|
wantNormal: DefaultLocale,
|
|
},
|
|
{
|
|
name: "Empty string defaults to en",
|
|
locale: "",
|
|
wantNormal: DefaultLocale,
|
|
},
|
|
{
|
|
name: "Single char defaults to en",
|
|
locale: "x",
|
|
wantNormal: DefaultLocale,
|
|
},
|
|
{
|
|
name: "Unknown prefix defaults to en",
|
|
locale: "de-DE",
|
|
wantNormal: DefaultLocale,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := Normalize(tt.locale)
|
|
if result != tt.wantNormal {
|
|
t.Errorf("Normalize(%q) = %q; want %q", tt.locale, result, tt.wantNormal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFallbackToEnglish tests fallback behavior when key is missing in requested locale.
|
|
func TestFallbackToEnglish(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
key string
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "Vietnamese has translation for required",
|
|
locale: LocaleVI,
|
|
key: MsgRequired,
|
|
wantMsg: "%s là bắt buộc", // Vietnamese has translation
|
|
},
|
|
{
|
|
name: "Chinese has translation for required",
|
|
locale: LocaleZH,
|
|
key: MsgRequired,
|
|
wantMsg: "%s 是必填项", // Chinese has translation
|
|
},
|
|
{
|
|
name: "Key not in any catalog returns key itself",
|
|
locale: LocaleVI,
|
|
key: "totally.fake.key.that.does.not.exist",
|
|
wantMsg: "totally.fake.key.that.does.not.exist",
|
|
},
|
|
{
|
|
name: "Missing key in English returns key",
|
|
locale: LocaleEN,
|
|
key: "totally.fake.key.that.does.not.exist",
|
|
wantMsg: "totally.fake.key.that.does.not.exist",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := T(tt.locale, tt.key)
|
|
if result != tt.wantMsg {
|
|
t.Errorf("T(%q, %q) = %q; want %q", tt.locale, tt.key, result, tt.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLookupFunction tests the internal lookup helper directly.
|
|
func TestLookup_DirectAccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locale string
|
|
key string
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "Direct English lookup",
|
|
locale: LocaleEN,
|
|
key: MsgRequired,
|
|
wantMsg: "%s is required",
|
|
},
|
|
{
|
|
name: "Missing key returns key",
|
|
locale: LocaleEN,
|
|
key: "missing.key",
|
|
wantMsg: "missing.key",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := lookup(tt.locale, tt.key)
|
|
if result != tt.wantMsg {
|
|
t.Errorf("lookup(%q, %q) = %q; want %q", tt.locale, tt.key, result, tt.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMultipleLocalesIndependent ensures catalogs are properly isolated.
|
|
func TestMultipleLocalesIndependent(t *testing.T) {
|
|
// Verify that changing one locale doesn't affect others
|
|
msg_en := T(LocaleEN, MsgRequired)
|
|
msg_vi := T(LocaleVI, MsgRequired)
|
|
msg_zh := T(LocaleZH, MsgRequired)
|
|
|
|
// All should have resolved to something (not the key itself)
|
|
if msg_en == "" {
|
|
t.Error("English message should not be empty")
|
|
}
|
|
if msg_vi == "" {
|
|
t.Error("Vietnamese message should not be empty")
|
|
}
|
|
if msg_zh == "" {
|
|
t.Error("Chinese message should not be empty")
|
|
}
|
|
|
|
// English should be a template
|
|
if msg_en != "%s is required" {
|
|
t.Errorf("English message unexpected: %q", msg_en)
|
|
}
|
|
}
|
|
|
|
// TestI18n_Apk verifies the 5 new apk i18n keys in all 3 locales (Phase 2b).
|
|
func TestI18n_Apk(t *testing.T) {
|
|
cases := []struct {
|
|
locale string
|
|
key string
|
|
want string
|
|
}{
|
|
{LocaleEN, MsgPackagesUpdatesSourceApk, "apk"},
|
|
{LocaleVI, MsgPackagesUpdatesSourceApk, "apk"},
|
|
{LocaleZH, MsgPackagesUpdatesSourceApk, "apk"},
|
|
{LocaleEN, MsgPackagesUpdatesUnavailableApk, "apk not available on this system"},
|
|
{LocaleVI, MsgPackagesUpdatesUnavailableApk, "apk không khả dụng trên hệ thống này"},
|
|
{LocaleZH, MsgPackagesUpdatesUnavailableApk, "此系统不可用 apk"},
|
|
{LocaleEN, MsgPackagesUpdatesReasonLocked, "Package database is locked"},
|
|
{LocaleVI, MsgPackagesUpdatesReasonLocked, "Cơ sở dữ liệu gói đang bị khóa"},
|
|
{LocaleZH, MsgPackagesUpdatesReasonLocked, "软件包数据库已锁定"},
|
|
{LocaleEN, MsgPackagesUpdatesReasonDiskFull, "Disk full"},
|
|
{LocaleVI, MsgPackagesUpdatesReasonDiskFull, "Đĩa đã đầy"},
|
|
{LocaleZH, MsgPackagesUpdatesReasonDiskFull, "磁盘已满"},
|
|
{LocaleEN, MsgPackagesUpdatesReasonHelperUnavailable, "Privileged helper unavailable"},
|
|
{LocaleVI, MsgPackagesUpdatesReasonHelperUnavailable, "Dịch vụ đặc quyền không khả dụng"},
|
|
{LocaleZH, MsgPackagesUpdatesReasonHelperUnavailable, "特权助手不可用"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.locale+"/"+tc.key, func(t *testing.T) {
|
|
got := T(tc.locale, tc.key)
|
|
if got != tc.want {
|
|
t.Errorf("T(%q, %q) = %q, want %q", tc.locale, tc.key, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|