fix: improve logic and fix bug in i18n module (#11019)

* feat: Implement language caching and improve language handling in i18n module

* refactor: Optimize i18n initialization with sync.Once for thread safety

* fix: Replace logging with fmt.Println for language file loading errors in i18n module

* fix: Correct format string in error logging for language file loading in i18n module

* fix: Update language files and improve error handling in i18n module

* fix: Update Malay language file extension from .yml to .yaml and add new translations in i18n module

* fix: Improve error messages for language file loading in i18n module

* fix: Ensure cached database language is set correctly during i18n initialization

* fix: Enhance language detection in i18n module by using Accept-Language header
This commit is contained in:
KOMATA 2025-11-21 15:37:01 +08:00 committed by GitHub
parent 99c2eb04c9
commit ef6d8bb17b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 112 additions and 65 deletions

View file

@ -26,6 +26,7 @@ import (
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/i18n"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/1Panel-dev/1Panel/core/utils/controller"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
@ -148,6 +149,7 @@ func (u *SettingService) Update(key, value string) error {
case "UserName", "Password":
_ = global.SESSION.Clean()
case "Language":
i18n.SetCachedDBLanguage(value)
if err := xpack.Sync(constant.SyncLanguage); err != nil {
global.LOG.Errorf("sync language to node failed, err: %v", err)
}

View file

@ -2,8 +2,12 @@ package i18n
import (
"embed"
"github.com/1Panel-dev/1Panel/core/app/repo"
"fmt"
"strings"
"sync"
"sync/atomic"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/global"
@ -13,6 +17,21 @@ import (
"gopkg.in/yaml.v3"
)
const defaultLang = "en"
var langFiles = map[string]string{
"zh": "lang/zh.yaml",
"en": "lang/en.yaml",
"zh-Hant": "lang/zh-Hant.yaml",
"pt-BR": "lang/pt-BR.yaml",
"ja": "lang/ja.yaml",
"ru": "lang/ru.yaml",
"ms": "lang/ms.yaml",
"ko": "lang/ko.yaml",
"tr": "lang/tr.yaml",
"es-ES": "lang/es-ES.yaml",
}
func GetMsgWithMap(key string, maps map[string]interface{}) string {
var content string
if maps == nil {
@ -114,41 +133,49 @@ func UseI18n() gin.HandlerFunc {
return func(context *gin.Context) {
lang := context.GetHeader("Accept-Language")
if lang == "" {
lang = GetLanguageFromDB()
lang = GetLanguage()
}
global.I18n = i18n.NewLocalizer(bundle, lang)
}
}
func Init() {
bundle = i18n.NewBundle(language.Chinese)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
_, _ = bundle.LoadMessageFileFS(fs, "lang/zh.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/en.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/zh-Hant.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/fa.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/pt.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/pt-BR.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/ja.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/ru.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/ms.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/ko.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/tr.yaml")
_, _ = bundle.LoadMessageFileFS(fs, "lang/es-ES.yaml")
lang := GetLanguageFromDB()
global.I18n = i18n.NewLocalizer(bundle, lang)
initOnce.Do(func() {
bundle = i18n.NewBundle(language.Chinese)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
isSuccess := true
for _, file := range langFiles {
if _, err := bundle.LoadMessageFileFS(fs, file); err != nil {
fmt.Printf("[i18n] load language file %s failed: %v\n", file, err)
isSuccess = false
}
}
if !isSuccess {
panic("[i18n] failed to init language files, See log above for details")
}
dbLang := getLanguageFromDBInternal()
if dbLang == "" {
dbLang = defaultLang
}
SetCachedDBLanguage(dbLang)
global.I18n = i18n.NewLocalizer(bundle, dbLang)
})
}
func UseI18nForCmd(lang string) {
if lang == "" {
lang = "en"
}
if bundle == nil {
Init()
}
if lang == "" {
lang = defaultLang
}
global.I18nForCmd = i18n.NewLocalizer(bundle, lang)
}
func GetMsgByKeyForCmd(key string) string {
if global.I18nForCmd == nil {
UseI18nForCmd("")
@ -158,6 +185,7 @@ func GetMsgByKeyForCmd(key string) string {
})
return content
}
func GetMsgWithMapForCmd(key string, maps map[string]interface{}) string {
if global.I18nForCmd == nil {
UseI18nForCmd("")
@ -181,13 +209,30 @@ func GetMsgWithMapForCmd(key string, maps map[string]interface{}) string {
}
}
func GetLanguageFromDB() string {
func getLanguageFromDBInternal() string {
if global.DB == nil {
return "en"
return defaultLang
}
lang, _ := repo.NewISettingRepo().GetValueByKey("Language")
if lang == "" {
return "en"
return defaultLang
}
return lang
}
var cachedDBLang atomic.Value
var initOnce sync.Once
func GetLanguage() string {
if v := cachedDBLang.Load(); v != nil {
return v.(string)
}
return defaultLang
}
func SetCachedDBLanguage(lang string) {
if lang == "" {
lang = defaultLang
}
cachedDBLang.Store(lang)
}

View file

@ -37,11 +37,11 @@ MasterNode: "Master Node"
# app
CustomAppStoreFileValid: "Application store package requires .tar.gz format"
ErrFileNotFound: "{{ .name }} file does not exist"
AppBackup: 'Application backup',
AppBackupPush: 'Transfer application backup file {{.file}} to node {{ .name }}',
ErrSourceTargetSame: 'Source node and target node cannot be the same!',
AppInstall: 'Install application {{ .name }} on node {{ .targetNode }}',
AppInstallCheck: 'Check application installation environment',
AppBackup: 'Application backup'
AppBackupPush: 'Transfer application backup file {{.file}} to node {{ .name }}'
ErrSourceTargetSame: 'Source node and target node cannot be the same!'
AppInstall: 'Install application {{ .name }} on node {{ .targetNode }}'
AppInstallCheck: 'Check application installation environment'
# backup
ErrBackupInUsed: "This backup account is used in scheduled tasks and cannot be deleted"

View file

@ -37,11 +37,11 @@ MasterNode: "Nodo Maestro"
# app
CustomAppStoreFileValid: "El paquete de la tienda de aplicaciones debe tener formato .tar.gz"
ErrFileNotFound: "El archivo {{ .name }} no existe"
AppBackup: 'Copia de seguridad de aplicación',
AppBackupPush: 'Transferir archivo de copia de seguridad de aplicación {{.file}} al nodo {{ .name }}',
ErrSourceTargetSame: '¡El nodo de origen y el nodo de destino no pueden ser el mismo!',
AppInstall: 'Instalar aplicación {{ .name }} en nodo {{ .targetNode }}',
AppInstallCheck: 'Verificar entorno de instalación de aplicación',
AppBackup: 'Copia de seguridad de aplicación'
AppBackupPush: 'Transferir archivo de copia de seguridad de aplicación {{.file}} al nodo {{ .name }}'
ErrSourceTargetSame: '¡El nodo de origen y el nodo de destino no pueden ser el mismo!'
AppInstall: 'Instalar aplicación {{ .name }} en nodo {{ .targetNode }}'
AppInstallCheck: 'Verificar entorno de instalación de aplicación'
# backup
ErrBackupInUsed: "Esta cuenta de respaldo se utiliza en tareas programadas y no se puede eliminar"

View file

@ -37,11 +37,11 @@ MasterNode: "マスターノード"
# app
CustomAppStoreFileValid: "アプリストアパッケージは .tar.gz 形式である必要があります"
ErrFileNotFound: "{{ .name }} ファイルが存在しません"
AppBackup: 'アプリケーションバックアップ',
AppBackupPush: 'アプリケーションバックアップファイル {{.file}} をノード {{ .name }} に転送',
ErrSourceTargetSame: 'ソースノードとターゲットノードは同じにできません!',
AppInstall: 'ノード {{ .targetNode }} にアプリケーション {{ .name }} をインストール',
AppInstallCheck: 'アプリケーションインストール環境を確認',
AppBackup: 'アプリケーションバックアップ'
AppBackupPush: 'アプリケーションバックアップファイル {{.file}} をノード {{ .name }} に転送'
ErrSourceTargetSame: 'ソースノードとターゲットノードは同じにできません!'
AppInstall: 'ノード {{ .targetNode }} にアプリケーション {{ .name }} をインストール'
AppInstallCheck: 'アプリケーションインストール環境を確認'
# backup
ErrBackupInUsed: "このバックアップアカウントはスケジュールタスクで使用されており、削除できません"

View file

@ -37,11 +37,11 @@ MasterNode: "마스터 노드"
# app
CustomAppStoreFileValid: "앱 스토어 패키지는 .tar.gz 형식이어야 합니다"
ErrFileNotFound: "{{ .name }} 파일이 존재하지 않습니다"
AppBackup: '애플리케이션 백업',
AppBackupPush: '애플리케이션 백업 파일 {{.file}}을(를) 노드 {{ .name }}(으)로 전송',
ErrSourceTargetSame: '소스 노드와 대상 노드는 동일할 수 없습니다!',
AppInstall: '노드 {{ .targetNode }}에 애플리케이션 {{ .name }} 설치',
AppInstallCheck: '애플리케이션 설치 환경 확인',
AppBackup: '애플리케이션 백업'
AppBackupPush: '애플리케이션 백업 파일 {{.file}}을(를) 노드 {{ .name }}(으)로 전송'
ErrSourceTargetSame: '소스 노드와 대상 노드는 동일할 수 없습니다!'
AppInstall: '노드 {{ .targetNode }}에 애플리케이션 {{ .name }} 설치'
AppInstallCheck: '애플리케이션 설치 환경 확인'
# backup
ErrBackupInUsed: "이 백업 계정은 예약된 작업에 사용 중이며 삭제할 수 없습니다"

View file

@ -37,11 +37,11 @@ MasterNode: "Nó Mestre"
# app
CustomAppStoreFileValid: "O pacote da loja de aplicativos deve estar no formato .tar.gz"
ErrFileNotFound: "Arquivo {{ .name }} não encontrado"
AppBackup: 'Backup de aplicação',
AppBackupPush: 'Transferir arquivo de backup de aplicação {{.file}} para o nó {{ .name }}',
ErrSourceTargetSame: 'O nó de origem e o nó de destino não podem ser os mesmos!',
AppInstall: 'Instalar aplicação {{ .name }} no nó {{ .targetNode }}',
AppInstallCheck: 'Verificar ambiente de instalação da aplicação',
AppBackup: 'Backup de aplicação'
AppBackupPush: 'Transferir arquivo de backup de aplicação {{.file}} para o nó {{ .name }}'
ErrSourceTargetSame: 'O nó de origem e o nó de destino não podem ser os mesmos!'
AppInstall: 'Instalar aplicação {{ .name }} no nó {{ .targetNode }}'
AppInstallCheck: 'Verificar ambiente de instalação da aplicação'
# backup
ErrBackupInUsed: "Esta conta de backup está em uso em tarefas agendadas e não pode ser excluída"

View file

@ -37,11 +37,11 @@ MasterNode: "Главный узел"
# app
CustomAppStoreFileValid: "Пакет магазина приложений должен быть в формате .tar.gz"
ErrFileNotFound: "Файл {{ .name }} не найден"
AppBackup: 'Резервная копия приложения',
AppBackupPush: 'Передать файл резервной копии приложения {{.file}} на узел {{ .name }}',
ErrSourceTargetSame: 'Исходный узел и целевой узел не могут быть одинаковыми!',
AppInstall: 'Установить приложение {{ .name }} на узел {{ .targetNode }}',
AppInstallCheck: 'Проверить среду установки приложения',
AppBackup: 'Резервная копия приложения'
AppBackupPush: 'Передать файл резервной копии приложения {{.file}} на узел {{ .name }}'
ErrSourceTargetSame: 'Исходный узел и целевой узел не могут быть одинаковыми!'
AppInstall: 'Установить приложение {{ .name }} на узел {{ .targetNode }}'
AppInstallCheck: 'Проверить среду установки приложения'
# backup
ErrBackupInUsed: "Эта учетная запись резервного копирования используется в запланированных задачах и не может быть удалена"

View file

@ -37,11 +37,11 @@ MasterNode: "Ana Düğüm"
# app
CustomAppStoreFileValid: "Uygulama mağazası paketi .tar.gz formatında olmalıdır"
ErrFileNotFound: "{{ .name }} dosyası mevcut değil"
AppBackup: 'Uygulama yedekleme',
AppBackupPush: 'Uygulama yedek dosyası {{.file}} düğüm {{ .name }} a aktar',
ErrSourceTargetSame: 'Kaynak düğüm ve hedef düğüm aynı olamaz!',
AppInstall: 'Düğüm {{ .targetNode }} üzerine {{ .name }} uygulamasını yükle',
AppInstallCheck: 'Uygulama kurulum ortamını kontrol et',
AppBackup: 'Uygulama yedekleme'
AppBackupPush: 'Uygulama yedek dosyası {{.file}} düğüm {{ .name }} a aktar'
ErrSourceTargetSame: 'Kaynak düğüm ve hedef düğüm aynı olamaz!'
AppInstall: 'Düğüm {{ .targetNode }} üzerine {{ .name }} uygulamasını yükle'
AppInstallCheck: 'Uygulama kurulum ortamını kontrol et'
# backup
ErrBackupInUsed: "Bu yedekleme hesabı zamanlanmış görevlerde kullanılıyor ve silinemez"

View file

@ -37,11 +37,11 @@ MasterNode: "主節點"
#app
CustomAppStoreFileValid: "應用商店包需要 .tar.gz 格式"
ErrFileNotFound: "{{ .name }} 檔案不存在"
AppBackup: '應用備份',
AppBackupPush: '傳輸應用備份文件 {{.file}} 到節點 {{ .name }}',
ErrSourceTargetSame: '源節點和目標節點不能相同!',
AppInstall: '在 {{ .targetNode }} 節點安裝應用 {{ .name }}',
AppInstallCheck: '檢查應用安裝環境',
AppBackup: '應用備份'
AppBackupPush: '傳輸應用備份文件 {{.file}} 到節點 {{ .name }}'
ErrSourceTargetSame: '源節點和目標節點不能相同!'
AppInstall: '在 {{ .targetNode }} 節點安裝應用 {{ .name }}'
AppInstallCheck: '檢查應用安裝環境'
#backup
ErrBackupInUsed: "該備份帳號已在排程任務中使用,無法刪除"