From ef6d8bb17ba871c8d17dcd4c1a209068112761e7 Mon Sep 17 00:00:00 2001 From: KOMATA <20227709+HynoR@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:37:01 +0800 Subject: [PATCH] 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 --- core/app/service/setting.go | 2 + core/i18n/i18n.go | 95 ++++++++++++++++++++++-------- core/i18n/lang/en.yaml | 10 ++-- core/i18n/lang/es-ES.yaml | 10 ++-- core/i18n/lang/ja.yaml | 10 ++-- core/i18n/lang/ko.yaml | 10 ++-- core/i18n/lang/{ms.yml => ms.yaml} | 0 core/i18n/lang/pt-BR.yaml | 10 ++-- core/i18n/lang/ru.yaml | 10 ++-- core/i18n/lang/tr.yaml | 10 ++-- core/i18n/lang/zh-Hant.yaml | 10 ++-- 11 files changed, 112 insertions(+), 65 deletions(-) rename core/i18n/lang/{ms.yml => ms.yaml} (100%) diff --git a/core/app/service/setting.go b/core/app/service/setting.go index 0741bd82d..b1e4c819f 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -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) } diff --git a/core/i18n/i18n.go b/core/i18n/i18n.go index 802a60850..c8a84ed1b 100644 --- a/core/i18n/i18n.go +++ b/core/i18n/i18n.go @@ -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) +} diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index ebf786366..6579df3fa 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -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" diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index efce69f02..f4dff0f45 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -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" diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index a120817ea..fe2d19913 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -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: "このバックアップアカウントはスケジュールタスクで使用されており、削除できません" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 22149304a..2508f1a47 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -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: "이 백업 계정은 예약된 작업에 사용 중이며 삭제할 수 없습니다" diff --git a/core/i18n/lang/ms.yml b/core/i18n/lang/ms.yaml similarity index 100% rename from core/i18n/lang/ms.yml rename to core/i18n/lang/ms.yaml diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index c40f3daee..2b30d717f 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -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" diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index 084aec387..d2e80b8ed 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -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: "Эта учетная запись резервного копирования используется в запланированных задачах и не может быть удалена" diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index 7b220cc7a..0390bb2e5 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -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" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 18834f545..818b6e4ed 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -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: "該備份帳號已在排程任務中使用,無法刪除"