From 0e039e74aa8fbdd0d4f27b79ec87ba9394d530e9 Mon Sep 17 00:00:00 2001 From: Qi Ma Date: Wed, 13 Aug 2025 05:14:22 +0000 Subject: [PATCH 1/4] Supports multiple languages in the web interface. The interface will automatically detect your browser's language preference, or you can manually select your preferred language in the Settings page --- README.md | 29 +++ frontend/js/app/cache.js | 42 +++- frontend/js/app/i18n.js | 64 +++++- frontend/js/app/settings/list/item.ejs | 41 +++- frontend/js/app/settings/list/item.js | 21 +- frontend/js/app/settings/main.js | 30 ++- frontend/js/app/ui/header/main.ejs | 4 + frontend/js/app/ui/header/main.js | 18 +- frontend/js/i18n/README.md | 212 +++++++++++++++++ frontend/js/i18n/en.json | 305 ++++++++++++++++++++++++ frontend/js/i18n/fr.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/jp.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/kr.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/messages.json | 301 ------------------------ frontend/js/i18n/pt.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/ru.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/tw.json | 306 +++++++++++++++++++++++++ frontend/js/i18n/zh.json | 305 ++++++++++++++++++++++++ frontend/webpack.config.js | 4 +- 19 files changed, 2879 insertions(+), 333 deletions(-) create mode 100644 frontend/js/i18n/README.md create mode 100644 frontend/js/i18n/en.json create mode 100644 frontend/js/i18n/fr.json create mode 100644 frontend/js/i18n/jp.json create mode 100644 frontend/js/i18n/kr.json delete mode 100644 frontend/js/i18n/messages.json create mode 100644 frontend/js/i18n/pt.json create mode 100644 frontend/js/i18n/ru.json create mode 100644 frontend/js/i18n/tw.json create mode 100644 frontend/js/i18n/zh.json diff --git a/README.md b/README.md index 2116a55ae..6df14ea01 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ This project comes as a pre-built docker image that enables you to easily forward to your websites running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. +--- + - [Quick Setup](#quick-setup) - [Full Setup](https://nginxproxymanager.com/setup/) - [Screenshots](https://nginxproxymanager.com/screenshots/) @@ -35,6 +37,7 @@ so that the barrier for entry here is low. - Access Lists and basic HTTP Authentication for your hosts - Advanced Nginx configuration available for super users - User management, permissions and audit log +- **Multi-language Support**: Interface available in 8 languages (English, 简体中文, 繁體中文, Français, 日本語, 한국어, Русский, Português) ## Hosting your home network @@ -97,6 +100,32 @@ Password: changeme Immediately after logging in with this default user you will be asked to modify your details and change your password. +## Language Support + +Nginx Proxy Manager supports multiple languages in the web interface. The interface will automatically detect your browser's language preference, or you can manually select your preferred language in the Settings page. + +### Available Languages + +- **English** (en) - Default language +- **简体中文** (zh) - Simplified Chinese +- **繁體中文** (tw) - Traditional Chinese +- **Français** (fr) - French +- **日本語** (jp) - Japanese +- **한국어** (kr) - Korean +- **Русский** (ru) - Russian +- **Português** (pt) - Portuguese + +### Changing Language + +1. Log in to the admin interface +2. Go to **Settings** in the main menu +3. Find the **Interface Language** section +4. Select your preferred language from the dropdown +5. The interface will automatically reload with the new language + +For technical details about translations and contributing new languages, see [frontend/js/i18n/README.md](frontend/js/i18n/README.md). + + ## Contributing All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. diff --git a/frontend/js/app/cache.js b/frontend/js/app/cache.js index 6d1fbc4f9..21163939b 100644 --- a/frontend/js/app/cache.js +++ b/frontend/js/app/cache.js @@ -1,8 +1,48 @@ const UserModel = require('../models/user'); +// 获取语言设置:优先级为 localStorage > 浏览器语言 > 默认中文 +let getInitialLocale = function() { + try { + // 检查本地存储 + if (typeof localStorage !== 'undefined') { + let saved = localStorage.getItem('locale'); + if (saved && ['zh', 'en', 'fr', 'jp', 'tw', 'kr', 'ru', 'pt'].includes(saved)) { + return saved; + } + } + + // 检查浏览器语言 + if (typeof navigator !== 'undefined') { + let browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase(); + if (browserLang.startsWith('zh-tw') || browserLang.startsWith('zh-hk')) { + return 'tw'; + } else if (browserLang.startsWith('zh')) { + return 'zh'; + } else if (browserLang.startsWith('en')) { + return 'en'; + } else if (browserLang.startsWith('fr')) { + return 'fr'; + } else if (browserLang.startsWith('ja')) { + return 'jp'; + } else if (browserLang.startsWith('ko')) { + return 'kr'; + } else if (browserLang.startsWith('ru')) { + return 'ru'; + } else if (browserLang.startsWith('pt')) { + return 'pt'; + } + } + } catch (e) { + console.warn('Error accessing localStorage or navigator:', e); + } + + // 默认使用中文 + return 'zh'; +}; + let cache = { User: new UserModel.Model(), - locale: 'en', + locale: getInitialLocale(), version: null }; diff --git a/frontend/js/app/i18n.js b/frontend/js/app/i18n.js index c63cdc079..a6173e6b7 100644 --- a/frontend/js/app/i18n.js +++ b/frontend/js/app/i18n.js @@ -1,5 +1,32 @@ -const Cache = ('./cache'); -const messages = require('../i18n/messages.json'); +const Cache = require('./cache'); + +// 使用分离的语言文件 +let messages = {}; + +function loadMessages(locale) { + if (!messages[locale]) { + try { + messages[locale] = require('../i18n/' + locale + '.json'); + console.info('Successfully loaded language file:', locale); + } catch (e) { + console.error('Language file not found for locale:', locale, e); + // 如果找不到语言文件,尝试加载英文作为后备 + if (locale !== 'en') { + try { + messages[locale] = require('../i18n/en.json'); + console.warn('Using English fallback for locale:', locale); + } catch (fallbackError) { + console.error('Failed to load fallback language file (en.json):', fallbackError); + messages[locale] = {}; + } + } else { + console.error('Failed to load English language file'); + messages[locale] = {}; + } + } + } + return messages[locale]; +} /** * @param {String} namespace @@ -8,16 +35,31 @@ const messages = require('../i18n/messages.json'); */ module.exports = function (namespace, key, data) { let locale = Cache.locale; - // check that the locale exists - if (typeof messages[locale] === 'undefined') { - locale = 'en'; - } - - if (typeof messages[locale][namespace] !== 'undefined' && typeof messages[locale][namespace][key] !== 'undefined') { - return messages[locale][namespace][key](data); - } else if (locale !== 'en' && typeof messages['en'][namespace] !== 'undefined' && typeof messages['en'][namespace][key] !== 'undefined') { - return messages['en'][namespace][key](data); + let currentMessages = loadMessages(locale); + + // 检查当前语言是否有对应的翻译 + if (currentMessages && currentMessages[namespace] && typeof currentMessages[namespace][key] !== 'undefined') { + try { + return currentMessages[namespace][key](data); + } catch (formatError) { + console.error('Error formatting message:', namespace, key, formatError); + return currentMessages[namespace][key].toString(); + } + } + + // 如果当前语言没有翻译,尝试使用英文作为后备 + if (locale !== 'en') { + let enMessages = loadMessages('en'); + if (enMessages && enMessages[namespace] && typeof enMessages[namespace][key] !== 'undefined') { + try { + return enMessages[namespace][key](data); + } catch (formatError) { + console.error('Error formatting English fallback message:', namespace, key, formatError); + return enMessages[namespace][key].toString(); + } + } } + console.warn('Missing translation:', namespace + '/' + key, 'for locale:', locale); return '(MISSING: ' + namespace + '/' + key + ')'; }; diff --git a/frontend/js/app/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs index 1623c4dc7..f09c4da53 100644 --- a/frontend/js/app/settings/list/item.ejs +++ b/frontend/js/app/settings/list/item.ejs @@ -1,21 +1,48 @@ -
<%- i18n('settings', 'default-site') %>
+
+ <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site') %> + <% } else if (id === 'language') { %> + <%- i18n('settings', 'language') %> + <% } %> +
- <%- i18n('settings', 'default-site-description') %> + <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site-description') %> + <% } else if (id === 'language') { %> + <%- i18n('settings', 'language-description') %> + <% } %>
<% if (id === 'default-site') { %> <%- i18n('settings', 'default-site-' + value) %> + <% } else if (id === 'language') { %> + <%- i18n('settings', 'current-language') %>: <%- locale === 'zh' ? '中文 (简体)' : locale === 'tw' ? '中文 (繁體)' : locale === 'en' ? 'English' : locale === 'fr' ? 'Français' : locale === 'jp' ? '日本語' : locale === 'kr' ? '한국어' : locale === 'ru' ? 'Русский' : locale === 'pt' ? 'Português' : 'English' %> <% } %>
- @@ -27,14 +27,14 @@ <% if (id === 'language') { %>
<% } else { %> diff --git a/frontend/js/app/settings/list/item.js b/frontend/js/app/settings/list/item.js index 613a9bbbb..f442434bc 100644 --- a/frontend/js/app/settings/list/item.js +++ b/frontend/js/app/settings/list/item.js @@ -1,6 +1,7 @@ const Mn = require('backbone.marionette'); const App = require('../../main'); const Cache = require('../../cache'); +const i18n = require('../../i18n'); const template = require('./item.ejs'); module.exports = Mn.View.extend({ @@ -21,9 +22,24 @@ module.exports = Mn.View.extend({ 'change @ui.languageSelector': function (e) { e.preventDefault(); let newLocale = $(e.currentTarget).val(); - if (newLocale && ['zh', 'en', 'fr', 'jp', 'tw', 'kr', 'ru', 'pt'].includes(newLocale)) { + if (newLocale && ['zh-CN', 'en-US', 'fr-FR', 'ja-JP', 'zh-TW', 'ko-KR', 'ru-RU', 'pt-PT'].includes(newLocale)) { + console.log('Language selector changed to:', newLocale); + + // 清理i18n缓存 + if (typeof i18n.clearCache === 'function') { + i18n.clearCache(); + } + + // 更新缓存和本地存储 localStorage.setItem('locale', newLocale); Cache.locale = newLocale; + + // 重新初始化i18n系统 + if (typeof i18n.initialize === 'function') { + i18n.initialize(true); + } + + console.log('Reloading page to apply new language:', newLocale); // 重新加载页面以应用新语言 window.location.reload(); } diff --git a/frontend/js/i18n/README.md b/frontend/js/i18n/README.md index e3211f071..7f4aebaf0 100644 --- a/frontend/js/i18n/README.md +++ b/frontend/js/i18n/README.md @@ -6,35 +6,36 @@ This directory contains multi-language support files for the Nginx Proxy Manager ``` frontend/js/i18n/ -├── zh.json # Chinese (Simplified) translation file -├── tw.json # Chinese (Traditional) translation file -├── en.json # English translation file -├── fr.json # French translation file -├── jp.json # Japanese translation file -├── kr.json # Korean translation file -├── ru.json # Russian translation file -├── pt.json # Portuguese translation file +├── zh-CN.json # Chinese (Simplified) translation file +├── zh-TW.json # Chinese (Traditional) translation file +├── en-US.json # English (US) translation file +├── fr-FR.json # French translation file +├── ja-JP.json # Japanese translation file +├── ko-KR.json # Korean translation file +├── ru-RU.json # Russian translation file +├── pt-PT.json # Portuguese translation file └── README.md # Documentation ``` ## Supported Languages -- **zh** - Chinese (Simplified) - 中文 (简体) -- **tw** - Chinese (Traditional) - 中文 (繁體) -- **en** - English -- **fr** - French (Français) -- **jp** - Japanese (日本語) -- **kr** - Korean (한국어) -- **ru** - Russian (Русский) -- **pt** - Portuguese (Português) +- **zh-CN** - Chinese (Simplified) - 中文 (简体) +- **zh-TW** - Chinese (Traditional) - 中文 (繁體) +- **en-US** - English (US) +- **fr-FR** - French (Français) +- **ja-JP** - Japanese (日本語) +- **ko-KR** - Korean (한국어) +- **ru-RU** - Russian (Русский) +- **pt-PT** - Portuguese (Português) ## How to Modify Translations ### 1. Editing Existing Translations Edit the corresponding language file: -- Chinese translation: Edit `zh.json` -- English translation: Edit `en.json` +- Chinese (Simplified) translation: Edit `zh-CN.json` +- Chinese (Traditional) translation: Edit `zh-TW.json` +- English (US) translation: Edit `en-US.json` - And so on for other languages... ### 2. Adding New Translation Keys @@ -77,7 +78,7 @@ To add support for a new language (e.g., German `de`): ### 1. Create New Language File ```bash -cp en.json de.json +cp en-US.json de-DE.json ``` ### 2. Translate File Content @@ -97,7 +98,16 @@ test: /\/(en|zh|tw|fr|jp|kr|ru|pt|de)\.json$/, locale: ['en', 'zh', 'tw', 'fr', 'jp', 'kr', 'ru', 'pt', 'de'], ``` -### 4. Update Language Detection Logic +### 4. Update i18n import context + +Update the bundler include list in `frontend/js/app/i18n.js` to make sure Webpack packs the new language file: + +```javascript +// Add 'de' to the regex list so the file is bundled +localesContext = require.context('../i18n', false, /^\.\/(en|zh|tw|fr|ja|ko|ru|pt|de)\.json$/); +``` + +### 5. Update Language Detection Logic Edit the language detection logic in `frontend/js/app/cache.js`: @@ -113,7 +123,7 @@ if (saved && ['zh', 'en', 'fr', 'jp', 'tw', 'kr', 'ru', 'pt', 'de'].includes(sav } ``` -### 5. Update Language Selector +### 6. Update Language Selector Edit `frontend/js/app/settings/list/item.js` and `frontend/js/app/settings/list/item.ejs` to include the new language option in the dropdown. diff --git a/frontend/js/i18n/en.json b/frontend/js/i18n/en-US.json similarity index 100% rename from frontend/js/i18n/en.json rename to frontend/js/i18n/en-US.json diff --git a/frontend/js/i18n/fr.json b/frontend/js/i18n/fr-FR.json similarity index 100% rename from frontend/js/i18n/fr.json rename to frontend/js/i18n/fr-FR.json diff --git a/frontend/js/i18n/ja-JP.json b/frontend/js/i18n/ja-JP.json new file mode 100644 index 000000000..cb848f936 --- /dev/null +++ b/frontend/js/i18n/ja-JP.json @@ -0,0 +1,13 @@ +{ + "str": { + "email-address": "メールアドレス", + "username": "ユーザー名", + "password": "パスワード", + "sign-in": "サインイン" + }, + "login": { + "title": "アカウントにログイン" + } +} + + diff --git a/frontend/js/i18n/jp.json b/frontend/js/i18n/jp.json deleted file mode 100644 index df154a9a8..000000000 --- a/frontend/js/i18n/jp.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "str": { - "email-address": "メールアドレス", - "username": "ユーザー名", - "password": "パスワード", - "sign-in": "サインイン", - "sign-out": "サインアウト", - "try-again": "再試行", - "name": "名前", - "email": "メール", - "roles": "ロール", - "created-on": "作成日: {date}", - "save": "保存", - "cancel": "キャンセル", - "close": "閉じる", - "enable": "有効", - "disable": "無効", - "sure": "はい、確実です", - "disabled": "無効", - "choose-file": "ファイルを選択", - "source": "ソース", - "destination": "宛先", - "ssl": "SSL", - "access": "アクセス", - "public": "パブリック", - "edit": "編集", - "delete": "削除", - "logs": "ログ", - "status": "ステータス", - "online": "オンライン", - "offline": "オフライン", - "unknown": "不明", - "expires": "期限切れ", - "value": "値", - "please-wait": "お待ちください...", - "all": "すべて", - "any": "任意" - }, - "login": { - "title": "アカウントにログイン" - }, - "main": { - "app": "Nginx プロキシマネージャー", - "version": "v{version}", - "welcome": "Nginx プロキシマネージャーへようこそ", - "logged-in": "{name} としてログインしています", - "unknown-error": "読み込みエラーが発生しました。アプリを再読み込みしてください。", - "unknown-user": "不明なユーザー", - "sign-in-as": "{name} として再ログイン" - }, - "roles": { - "title": "ロール", - "admin": "管理者", - "user": "ユーザー" - }, - "menu": { - "dashboard": "ダッシュボード", - "hosts": "ホスト", - "language": "言語", - "switch-to-english": "英語に切り替える", - "switch-to-chinese": "中文に切り替える", - "switch-to-japanese": "日本語に切り替える" - }, - "footer": { - "fork-me": "Github でフォーク", - "copy": "© 2025 jc21.com.", - "theme": "テーマ by Tabler" - }, - "dashboard": { - "title": "こんにちは {name}" - }, - "all-hosts": { - "empty-subtitle": "{manage, select, true{作成しませんか?} other{作成する権限がありません。}}", - "details": "詳細", - "enable-ssl": "SSL を有効にする", - "force-ssl": "SSL を強制する", - "http2-support": "HTTP/2 サポート", - "domain-names": "ドメイン名", - "cert-provider": "証明書プロバイダー", - "block-exploits": "一般的な脆弱性をブロック", - "caching-enabled": "アセットをキャッシュ", - "ssl-certificate": "SSL 証明書", - "none": "なし", - "new-cert": "新しい SSL 証明書をリクエスト", - "with-le": "Let's Encrypt を使用", - "no-ssl": "このホストは HTTPS を使用しません", - "advanced": "詳細設定", - "advanced-warning": "カスタム Nginx 設定をここに入力してください。自己責任で!", - "advanced-config": "カスタム Nginx 設定", - "advanced-config-var-headline": "これらのプロキシの詳細は nginx 変数として利用できます:", - "advanced-config-header-info": "ここで追加される add_header または set_header ディレクティブは nginx で使用されないことに注意してください。カスタムロケーション '/' を追加し、カスタム設定でヘッダーを追加する必要があります。", - "hsts-enabled": "HSTS 有効", - "hsts-subdomains": "HSTS サブドメイン", - "locations": "カスタムロケーション" - }, - "locations": { - "new_location": "ロケーションを追加", - "path": "/パス", - "location_label": "ロケーションを定義", - "delete": "削除" - }, - "ssl": { - "letsencrypt": "Let's Encrypt", - "other": "カスタム", - "none": "HTTP のみ", - "letsencrypt-email": "Let's Encrypt のメールアドレス", - "letsencrypt-agree": "Let's Encrypt 利用規約に同意します", - "delete-ssl": "添付された SSL 証明書は削除されません。手動で削除する必要があります。", - "hosts-warning": "これらのドメインは、このインストールを指すように既に設定されている必要があります", - "no-wildcard-without-dns": "DNS チャレンジを使用しない場合、ワイルドカードドメインに対して Let's Encrypt 証明書をリクエストできません", - "dns-challenge": "DNS チャレンジを使用", - "certbot-warning": "このセクションでは、Certbot とその DNS プラグインに関する知識が必要です。それぞれのプラグインのドキュメントを参照してください。", - "dns-provider": "DNS プロバイダー", - "please-choose": "選択してください...", - "credentials-file-content": "認証情報ファイルの内容", - "credentials-file-content-info": "このプラグインには、プロバイダーへの API トークンまたはその他の認証情報を含む設定ファイルが必要です", - "stored-as-plaintext-info": "このデータはデータベースとファイルにプレーンテキストとして保存されます!", - "propagation-seconds": "伝播秒数", - "propagation-seconds-info": "プラグインのデフォルト値を使用するには空のままにします。DNS 伝播を待つ秒数。", - "processing-info": "処理中... 数分かかる場合があります。", - "passphrase-protection-support-info": "パスフレーズで保護されたキーファイルはサポートされていません。" - }, - "proxy-hosts": { - "title": "プロキシホスト", - "empty": "プロキシホストがありません", - "add": "プロキシホストを追加", - "form-title": "{id, select, undefined{新しい} other{編集}} プロキシホスト", - "forward-scheme": "スキーム", - "forward-host": "転送ホスト名 / IP", - "forward-port": "転送ポート", - "delete": "プロキシホストを削除", - "delete-confirm": "次のドメインのプロキシホストを削除してもよろしいですか: {domains}?", - "help-title": "プロキシホストとは?", - "help-content": "プロキシホストは、転送したいウェブサービスの着信エンドポイントです。\nSSL サポートが組み込まれていない可能性があるサービスに対して、オプションの SSL 終端を提供します。\nプロキシホストは Nginx プロキシマネージャーの最も一般的な使用法です。", - "access-list": "アクセスリスト", - "allow-websocket-upgrade": "WebSocket サポート", - "ignore-invalid-upstream-ssl": "無効なアップストリーム SSL を無視", - "custom-forward-host-help": "サブフォルダー転送のパスを追加します。\n例: 203.0.113.25/path/", - "search": "ホストを検索..." - }, - "redirection-hosts": { - "title": "リダイレクトホスト", - "empty": "リダイレクトホストがありません", - "add": "リダイレクトホストを追加", - "form-title": "{id, select, undefined{新しい} other{編集}} リダイレクトホスト", - "forward-scheme": "スキーム", - "forward-http-status-code": "HTTP コード", - "forward-domain": "転送ドメイン", - "preserve-path": "パスを保持", - "delete": "リダイレクトホストを削除", - "delete-confirm": "次のドメインのリダイレクトホストを削除してもよろしいですか: {domains}?", - "help-title": "リダイレクトホストとは?", - "help-content": "リダイレクトホストは、着信ドメインからのリクエストをリダイレクトし、閲覧者を別のドメインにプッシュします。\nこのタイプのホストを使用する最も一般的な理由は、ウェブサイトのドメインが変更されたが、まだ古いドメインを指す検索エンジンや参照リンクがある場合です。", - "search": "ホストを検索..." - }, - "dead-hosts": { - "title": "404 ホスト", - "empty": "404 ホストがありません", - "add": "404 ホストを追加", - "form-title": "{id, select, undefined{新しい} other{編集}} 404 ホスト", - "delete": "404 ホストを削除", - "delete-confirm": "この 404 ホストを削除してもよろしいですか?", - "help-title": "404 ホストとは?", - "help-content": "404 ホストは、単に 404 ページを表示するホスト設定です。\nこれは、ドメインが検索エンジンにリストされていて、より良いエラーページを提供したい場合や、ドメインページがもう存在しないことを検索インデクサーに具体的に伝えたい場合に便利です。\nこのホストを持つもう一つの利点は、それへのヒットのログを追跡し、参照元を表示することです。", - "search": "ホストを検索..." - }, - "streams": { - "title": "ストリーム", - "empty": "ストリームがありません", - "add": "ストリームを追加", - "form-title": "{id, select, undefined{新しい} other{編集}} ストリーム", - "incoming-port": "着信ポート", - "forwarding-host": "転送ホスト", - "forwarding-port": "転送ポート", - "tcp-forwarding": "TCP 転送", - "udp-forwarding": "UDP 転送", - "forward-type-error": "少なくとも一つのプロトコルタイプを有効にする必要があります", - "protocol": "プロトコル", - "tcp": "TCP", - "udp": "UDP", - "delete": "ストリームを削除", - "delete-confirm": "このストリームを削除してもよろしいですか?", - "help-title": "ストリームとは?", - "help-content": "Nginx にとって比較的新しい機能であるストリームは、TCP/UDP トラフィックをネットワーク上の別のコンピューターに直接転送するのに役立ちます。\nゲームサーバー、FTP、または SSH サーバーを実行している場合、これは便利です。", - "search": "着信ポートを検索...", - "ssl-certificate": "TCP 転送用の SSL 証明書", - "tcp+ssl": "TCP+SSL" - }, - "certificates": { - "title": "SSL 証明書", - "empty": "SSL 証明書がありません", - "add": "SSL 証明書を追加", - "form-title": "{provider, select, letsencrypt{Let's Encrypt} other{カスタム}} 証明書を追加", - "delete": "SSL 証明書を削除", - "delete-confirm": "この SSL 証明書を削除してもよろしいですか?これを使用しているホストは後で更新する必要があります。", - "help-title": "SSL 証明書", - "help-content": "SSL 証明書(正確には TLS 証明書)は、サイトをエンドユーザーに対して暗号化できる暗号化キーの一種です。\nNPM は Let's Encrypt というサービスを使用して SSL 証明書を無料で発行します。\nNPM の背後に個人情報、パスワード、または機密データがある場合は、証明書を使用することをお勧めします。\nNPM は、インターネットに面してサイトを実行していない場合、またはワイルドカード証明書が必要な場合の DNS 認証もサポートしています。", - "other-certificate": "証明書", - "other-certificate-key": "証明書キー", - "other-intermediate-certificate": "中間証明書", - "force-renew": "今すぐ更新", - "test-reachability": "サーバーの到達可能性をテスト", - "reachability-title": "サーバーの到達可能性をテスト", - "reachability-info": "Site24x7 を使用してドメインがパブリックインターネットから到達可能かテストします。DNS チャレンジを使用する場合、これは必要ありません。", - "reachability-failed-to-reach-api": "API との通信に失敗しました。NPM は正常に動作していますか?", - "reachability-failed-to-check": "site24x7.com との通信エラーにより、到達可能性の確認に失敗しました。", - "reachability-ok": "サーバーは到達可能で、証明書の作成が可能です。", - "reachability-404": "このドメインでサーバーが見つかりましたが、Nginx プロキシマネージャーではないようです。ドメインが NPM インスタンスが実行されている IP を指していることを確認してください。", - "reachability-not-resolved": "このドメインで利用可能なサーバーがありません。ドメインが存在し、NPM インスタンスが実行されている IP を指していることを確認し、必要に応じてルーターでポート 80 を転送してください。", - "reachability-wrong-data": "このドメインでサーバーが見つかりましたが、予期しないデータが返されました。NPM サーバーですか?ドメインが NPM インスタンスが実行されている IP を指していることを確認してください。", - "reachability-other": "このドメインでサーバーが見つかりましたが、予期しないステータスコード {code} が返されました。NPM サーバーですか?ドメインが NPM インスタンスが実行されている IP を指していることを確認してください。", - "download": "ダウンロード", - "renew-title": "Let's Encrypt 証明書を更新", - "search": "証明書を検索...", - "in-use": "使用中", - "inactive": "非アクティブ", - "active-domain_names": "アクティブなドメイン名" - }, - "access-lists": { - "title": "アクセスリスト", - "empty": "アクセスリストがありません", - "add": "アクセスリストを追加", - "form-title": "{id, select, undefined{新しい} other{編集}} アクセスリスト", - "delete": "アクセスリストを削除", - "delete-confirm": "このアクセスリストを削除してもよろしいですか?", - "public": "パブリックアクセス可能", - "public-sub": "アクセス制限なし", - "help-title": "アクセスリストとは?", - "help-content": "アクセスリストは、特定のクライアント IP アドレスのブラックリストまたはホワイトリストと、基本的な HTTP 認証を介したプロキシホストの認証を提供します。\n単一のアクセスリストに対して複数のクライアントルール、ユーザー名、パスワードを設定し、それをプロキシホストに適用できます。\nこれは、認証メカニズムが組み込まれていない転送ウェブサービスや、未知のクライアントからのアクセスから保護したいサービスに最も有用です。", - "item-count": "{count} {count, select, 1{ユーザー} other{ユーザー}}", - "client-count": "{count} {count, select, 1{ルール} other{ルール}}", - "proxy-host-count": "{count} {count, select, 1{プロキシホスト} other{プロキシホスト}}", - "delete-has-hosts": "このアクセスリストは {count} プロキシホストに関連付けられています。削除すると、それらはパブリックアクセス可能になります。", - "details": "詳細", - "authorization": "認証", - "access": "アクセス", - "satisfy": "満たす", - "satisfy-any": "いずれかを満たす", - "pass-auth": "認証をホストに渡す", - "access-add": "追加", - "auth-add": "追加", - "search": "アクセスを検索..." - }, - "users": { - "title": "ユーザー", - "default_error": "デフォルトのメールアドレスを変更する必要があります", - "add": "ユーザーを追加", - "nickname": "ニックネーム", - "full-name": "フルネーム", - "edit-details": "詳細を編集", - "change-password": "パスワードを変更", - "edit-permissions": "権限を編集", - "sign-in-as": "ユーザーとしてサインイン", - "form-title": "{id, select, undefined{新しい} other{編集}} ユーザー", - "delete": "{name, select, undefined{ユーザー} other{{name}}} を削除", - "delete-confirm": "{name} を削除してもよろしいですか?", - "password-title": "パスワードを変更{self, select, false{ for {name}} other{}}", - "current-password": "現在のパスワード", - "new-password": "新しいパスワード", - "confirm-password": "パスワードを確認", - "permissions-title": "{name} の権限", - "admin-perms": "このユーザーは管理者で、一部の項目は変更できません", - "perms-visibility": "項目の可視性", - "perms-visibility-user": "作成した項目のみ", - "perms-visibility-all": "すべての項目", - "perm-manage": "管理", - "perm-view": "表示のみ", - "perm-hidden": "非表示", - "search": "ユーザーを検索..." - }, - "audit-log": { - "title": "監査ログ", - "empty": "ログがありません。", - "empty-subtitle": "あなたまたは他のユーザーが何かを変更すると、そのイベントの履歴がここに表示されます。", - "proxy-host": "プロキシホスト", - "redirection-host": "リダイレクトホスト", - "dead-host": "404 ホスト", - "stream": "ストリーム", - "user": "ユーザー", - "certificate": "証明書", - "access-list": "アクセスリスト", - "created": "{name} を作成", - "updated": "{name} を更新", - "deleted": "{name} を削除", - "enabled": "{name} を有効化", - "disabled": "{name} を無効化", - "renewed": "{name} を更新", - "meta-title": "イベントの詳細", - "view-meta": "詳細を表示", - "date": "日付", - "search": "ログを検索..." - }, - "settings": { - "title": "設定", - "default-site": "デフォルトサイト", - "default-site-description": "Nginx が不明なホストでヒットされたときに表示するもの", - "default-site-congratulations": "おめでとうページ", - "default-site-404": "404 ページ", - "default-site-444": "応答なし (444)", - "default-site-html": "カスタムページ", - "default-site-redirect": "リダイレクト", - "language": "インターフェース言語", - "language-description": "インターフェースの表示言語を選択", - "current-language": "現在の言語" - } -} diff --git a/frontend/js/i18n/ko-KR.json b/frontend/js/i18n/ko-KR.json new file mode 100644 index 000000000..136892485 --- /dev/null +++ b/frontend/js/i18n/ko-KR.json @@ -0,0 +1,13 @@ +{ + "str": { + "email-address": "이메일 주소", + "username": "사용자명", + "password": "비밀번호", + "sign-in": "로그인" + }, + "login": { + "title": "계정에 로그인" + } +} + + diff --git a/frontend/js/i18n/kr.json b/frontend/js/i18n/kr.json deleted file mode 100644 index 38ec2ffa5..000000000 --- a/frontend/js/i18n/kr.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "str": { - "email-address": "이메일 주소", - "username": "사용자명", - "password": "비밀번호", - "sign-in": "로그인", - "sign-out": "로그아웃", - "try-again": "다시 시도", - "name": "이름", - "email": "이메일", - "roles": "역할", - "created-on": "생성일: {date}", - "save": "저장", - "cancel": "취소", - "close": "닫기", - "enable": "활성화", - "disable": "비활성화", - "sure": "네, 확실합니다", - "disabled": "비활성화됨", - "choose-file": "파일 선택", - "source": "소스", - "destination": "대상", - "ssl": "SSL", - "access": "액세스", - "public": "공개", - "edit": "편집", - "delete": "삭제", - "logs": "로그", - "status": "상태", - "online": "온라인", - "offline": "오프라인", - "unknown": "알 수 없음", - "expires": "만료", - "value": "값", - "please-wait": "잠시 기다려 주세요...", - "all": "모두", - "any": "모든" - }, - "login": { - "title": "계정에 로그인" - }, - "main": { - "app": "Nginx 프록시 매니저", - "version": "v{version}", - "welcome": "Nginx 프록시 매니저에 오신 것을 환영합니다", - "logged-in": "{name}로 로그인되었습니다", - "unknown-error": "로딩 오류가 발생했습니다. 앱을 새로고침해 주세요.", - "unknown-user": "알 수 없는 사용자", - "sign-in-as": "{name}로 다시 로그인" - }, - "roles": { - "title": "역할", - "admin": "관리자", - "user": "사용자" - }, - "menu": { - "dashboard": "대시보드", - "hosts": "호스트", - "language": "언어", - "switch-to-english": "영어로 전환", - "switch-to-chinese": "중국어로 전환", - "switch-to-korean": "한국어로 전환" - }, - "footer": { - "fork-me": "Github에서 포크하기", - "copy": "© 2025 jc21.com.", - "theme": "테마 by Tabler" - }, - "dashboard": { - "title": "안녕하세요 {name}" - }, - "all-hosts": { - "empty-subtitle": "{manage, select, true{하나 생성하지 않으시겠어요?} other{생성할 권한이 없습니다.}}", - "details": "세부사항", - "enable-ssl": "SSL 활성화", - "force-ssl": "SSL 강제", - "http2-support": "HTTP/2 지원", - "domain-names": "도메인 이름", - "cert-provider": "인증서 공급자", - "block-exploits": "일반적인 취약점 차단", - "caching-enabled": "자산 캐시", - "ssl-certificate": "SSL 인증서", - "none": "없음", - "new-cert": "새 SSL 인증서 요청", - "with-le": "Let's Encrypt 사용", - "no-ssl": "이 호스트는 HTTPS를 사용하지 않습니다", - "advanced": "고급", - "advanced-warning": "사용자 정의 Nginx 구성을 여기에 입력하세요. 위험은 본인이 감수하세요!", - "advanced-config": "사용자 정의 Nginx 구성", - "advanced-config-var-headline": "이러한 프록시 세부사항은 nginx 변수로 사용할 수 있습니다:", - "advanced-config-header-info": "여기에 추가된 모든 add_header 또는 set_header 지시문은 nginx에서 사용되지 않음을 유의하세요. 사용자 정의 위치 '/'를 추가하고 사용자 정의 구성에 헤더를 추가해야 합니다.", - "hsts-enabled": "HSTS 활성화", - "hsts-subdomains": "HSTS 서브도메인", - "locations": "사용자 정의 위치" - }, - "locations": { - "new_location": "위치 추가", - "path": "/경로", - "location_label": "위치 정의", - "delete": "삭제" - }, - "ssl": { - "letsencrypt": "Let's Encrypt", - "other": "사용자 정의", - "none": "HTTP만", - "letsencrypt-email": "Let's Encrypt용 이메일 주소", - "letsencrypt-agree": "Let's Encrypt 서비스 약관에 동의합니다", - "delete-ssl": "첨부된 SSL 인증서는 제거되지 않으며, 수동으로 제거해야 합니다.", - "hosts-warning": "이러한 도메인은 이미 이 설치를 가리키도록 구성되어야 합니다", - "no-wildcard-without-dns": "DNS 챌린지를 사용하지 않을 때는 와일드카드 도메인에 대해 Let's Encrypt 인증서를 요청할 수 없습니다", - "dns-challenge": "DNS 챌린지 사용", - "certbot-warning": "이 섹션은 Certbot과 그 DNS 플러그인에 대한 지식이 필요합니다. 해당 플러그인 문서를 참조하세요.", - "dns-provider": "DNS 공급자", - "please-choose": "선택해 주세요...", - "credentials-file-content": "자격 증명 파일 내용", - "credentials-file-content-info": "이 플러그인은 공급자에 대한 API 토큰 또는 기타 자격 증명이 포함된 구성 파일이 필요합니다", - "stored-as-plaintext-info": "이 데이터는 데이터베이스와 파일에 평문으로 저장됩니다!", - "propagation-seconds": "전파 초", - "propagation-seconds-info": "플러그인 기본값을 사용하려면 비워두세요. DNS 전파를 기다리는 초 수입니다.", - "processing-info": "처리 중... 몇 분 정도 걸릴 수 있습니다.", - "passphrase-protection-support-info": "암호구로 보호된 키 파일은 지원되지 않습니다." - }, - "proxy-hosts": { - "title": "프록시 호스트", - "empty": "프록시 호스트가 없습니다", - "add": "프록시 호스트 추가", - "form-title": "{id, select, undefined{새} other{편집}} 프록시 호스트", - "forward-scheme": "스킴", - "forward-host": "전달 호스트명 / IP", - "forward-port": "전달 포트", - "delete": "프록시 호스트 삭제", - "delete-confirm": "다음 도메인의 프록시 호스트를 삭제하시겠습니까: {domains}?", - "help-title": "프록시 호스트란 무엇인가요?", - "help-content": "프록시 호스트는 전달하려는 웹 서비스의 들어오는 엔드포인트입니다.\nSSL 지원이 내장되지 않은 서비스에 대해 선택적 SSL 종료를 제공합니다.\n프록시 호스트는 Nginx 프록시 매니저의 가장 일반적인 사용법입니다.", - "access-list": "액세스 목록", - "allow-websocket-upgrade": "WebSocket 지원", - "ignore-invalid-upstream-ssl": "잘못된 업스트림 SSL 무시", - "custom-forward-host-help": "하위 폴더 전달을 위한 경로를 추가합니다.\n예시: 203.0.113.25/path/", - "search": "호스트 검색..." - }, - "redirection-hosts": { - "title": "리디렉션 호스트", - "empty": "리디렉션 호스트가 없습니다", - "add": "리디렉션 호스트 추가", - "form-title": "{id, select, undefined{새} other{편집}} 리디렉션 호스트", - "forward-scheme": "스킴", - "forward-http-status-code": "HTTP 코드", - "forward-domain": "전달 도메인", - "preserve-path": "경로 보존", - "delete": "리디렉션 호스트 삭제", - "delete-confirm": "다음 도메인의 리디렉션 호스트를 삭제하시겠습니까: {domains}?", - "help-title": "리디렉션 호스트란 무엇인가요?", - "help-content": "리디렉션 호스트는 들어오는 도메인의 요청을 리디렉션하고 보는 사람을 다른 도메인으로 보냅니다.\n이 유형의 호스트를 사용하는 가장 일반적인 이유는 웹사이트가 도메인을 변경했지만 여전히 이전 도메인을 가리키는 검색 엔진이나 참조 링크가 있는 경우입니다.", - "search": "호스트 검색..." - }, - "dead-hosts": { - "title": "404 호스트", - "empty": "404 호스트가 없습니다", - "add": "404 호스트 추가", - "form-title": "{id, select, undefined{새} other{편집}} 404 호스트", - "delete": "404 호스트 삭제", - "delete-confirm": "이 404 호스트를 삭제하시겠습니까?", - "help-title": "404 호스트란 무엇인가요?", - "help-content": "404 호스트는 단순히 404 페이지를 보여주는 호스트 설정입니다.\n이는 도메인이 검색 엔진에 나열되어 있고 더 나은 오류 페이지를 제공하거나 도메인 페이지가 더 이상 존재하지 않음을 검색 인덱서에 구체적으로 알리고 싶을 때 유용할 수 있습니다.\n이 호스트를 갖는 또 다른 이점은 그것에 대한 히트 로그를 추적하고 참조자를 볼 수 있다는 것입니다.", - "search": "호스트 검색..." - }, - "streams": { - "title": "스트림", - "empty": "스트림이 없습니다", - "add": "스트림 추가", - "form-title": "{id, select, undefined{새} other{편집}} 스트림", - "incoming-port": "들어오는 포트", - "forwarding-host": "전달 호스트", - "forwarding-port": "전달 포트", - "tcp-forwarding": "TCP 전달", - "udp-forwarding": "UDP 전달", - "forward-type-error": "적어도 하나의 프로토콜 유형이 활성화되어야 합니다", - "protocol": "프로토콜", - "tcp": "TCP", - "udp": "UDP", - "delete": "스트림 삭제", - "delete-confirm": "이 스트림을 삭제하시겠습니까?", - "help-title": "스트림이란 무엇인가요?", - "help-content": "Nginx에게는 상대적으로 새로운 기능인 스트림은 TCP/UDP 트래픽을 네트워크의 다른 컴퓨터로 직접 전달하는 데 사용됩니다.\n게임 서버, FTP 또는 SSH 서버를 실행하고 있다면 이것이 편리할 수 있습니다.", - "search": "들어오는 포트 검색...", - "ssl-certificate": "TCP 전달을 위한 SSL 인증서", - "tcp+ssl": "TCP+SSL" - }, - "certificates": { - "title": "SSL 인증서", - "empty": "SSL 인증서가 없습니다", - "add": "SSL 인증서 추가", - "form-title": "{provider, select, letsencrypt{Let's Encrypt} other{사용자 정의}} 인증서 추가", - "delete": "SSL 인증서 삭제", - "delete-confirm": "이 SSL 인증서를 삭제하시겠습니까? 이를 사용하는 모든 호스트는 나중에 업데이트해야 합니다.", - "help-title": "SSL 인증서", - "help-content": "SSL 인증서(정확히는 TLS 인증서)는 사이트가 최종 사용자에게 암호화될 수 있도록 하는 암호화 키의 한 형태입니다.\nNPM은 Let's Encrypt라는 서비스를 사용하여 SSL 인증서를 무료로 발급합니다.\nNPM 뒤에 개인 정보, 비밀번호 또는 민감한 데이터가 있다면 인증서를 사용하는 것이 좋은 아이디어일 것입니다.\nNPM은 또한 인터넷에 면한 사이트를 실행하지 않거나 와일드카드 인증서를 원하는 경우 DNS 인증을 지원합니다.", - "other-certificate": "인증서", - "other-certificate-key": "인증서 키", - "other-intermediate-certificate": "중간 인증서", - "force-renew": "지금 갱신", - "test-reachability": "서버 연결 가능성 테스트", - "reachability-title": "서버 연결 가능성 테스트", - "reachability-info": "Site24x7을 사용하여 도메인이 공용 인터넷에서 접근 가능한지 테스트합니다. DNS 챌린지를 사용할 때는 필요하지 않습니다.", - "reachability-failed-to-reach-api": "API와의 통신에 실패했습니다. NPM이 올바르게 실행되고 있나요?", - "reachability-failed-to-check": "site24x7.com과의 통신 오류로 인해 연결 가능성 확인에 실패했습니다.", - "reachability-ok": "서버에 접근할 수 있으며 인증서 생성이 가능해야 합니다.", - "reachability-404": "이 도메인에서 서버를 찾았지만 Nginx 프록시 매니저가 아닌 것 같습니다. 도메인이 NPM 인스턴스가 실행되는 IP를 가리키는지 확인하세요.", - "reachability-not-resolved": "이 도메인에서 사용 가능한 서버가 없습니다. 도메인이 존재하고 NPM 인스턴스가 실행되는 IP를 가리키는지 확인하고, 필요한 경우 라우터에서 포트 80을 전달하세요.", - "reachability-wrong-data": "이 도메인에서 서버를 찾았지만 예상하지 못한 데이터를 반환했습니다. NPM 서버인가요? 도메인이 NPM 인스턴스가 실행되는 IP를 가리키는지 확인하세요.", - "reachability-other": "이 도메인에서 서버를 찾았지만 예상하지 못한 상태 코드 {code}를 반환했습니다. NPM 서버인가요? 도메인이 NPM 인스턴스가 실행되는 IP를 가리키는지 확인하세요.", - "download": "다운로드", - "renew-title": "Let's Encrypt 인증서 갱신", - "search": "인증서 검색...", - "in-use": "사용 중", - "inactive": "비활성", - "active-domain_names": "활성 도메인 이름" - }, - "access-lists": { - "title": "액세스 목록", - "empty": "액세스 목록이 없습니다", - "add": "액세스 목록 추가", - "form-title": "{id, select, undefined{새} other{편집}} 액세스 목록", - "delete": "액세스 목록 삭제", - "delete-confirm": "이 액세스 목록을 삭제하시겠습니까?", - "public": "공개적으로 접근 가능", - "public-sub": "액세스 제한 없음", - "help-title": "액세스 목록이란 무엇인가요?", - "help-content": "액세스 목록은 특정 클라이언트 IP 주소의 블랙리스트 또는 화이트리스트와 기본 HTTP 인증을 통한 프록시 호스트 인증을 제공합니다.\n단일 액세스 목록에 대해 여러 클라이언트 규칙, 사용자명 및 비밀번호를 구성한 다음 프록시 호스트에 적용할 수 있습니다.\n이는 인증 메커니즘이 내장되지 않은 전달된 웹 서비스나 알 수 없는 클라이언트의 액세스로부터 보호하려는 서비스에 가장 유용합니다.", - "item-count": "{count}명의 {count, select, 1{사용자} other{사용자}}", - "client-count": "{count}개의 {count, select, 1{규칙} other{규칙}}", - "proxy-host-count": "{count}개의 {count, select, 1{프록시 호스트} other{프록시 호스트}}", - "delete-has-hosts": "이 액세스 목록은 {count}개의 프록시 호스트와 연관되어 있습니다. 삭제 시 공개적으로 접근 가능해집니다.", - "details": "세부사항", - "authorization": "인증", - "access": "액세스", - "satisfy": "만족", - "satisfy-any": "아무거나 만족", - "pass-auth": "호스트에 인증 전달", - "access-add": "추가", - "auth-add": "추가", - "search": "액세스 검색..." - }, - "users": { - "title": "사용자", - "default_error": "기본 이메일 주소를 변경해야 합니다", - "add": "사용자 추가", - "nickname": "닉네임", - "full-name": "전체 이름", - "edit-details": "세부사항 편집", - "change-password": "비밀번호 변경", - "edit-permissions": "권한 편집", - "sign-in-as": "사용자로 로그인", - "form-title": "{id, select, undefined{새} other{편집}} 사용자", - "delete": "{name, select, undefined{사용자} other{{name}}} 삭제", - "delete-confirm": "{name}을 삭제하시겠습니까?", - "password-title": "비밀번호 변경{self, select, false{ for {name}} other{}}", - "current-password": "현재 비밀번호", - "new-password": "새 비밀번호", - "confirm-password": "비밀번호 확인", - "permissions-title": "{name}의 권한", - "admin-perms": "이 사용자는 관리자이며 일부 항목은 변경할 수 없습니다", - "perms-visibility": "항목 가시성", - "perms-visibility-user": "생성된 항목만", - "perms-visibility-all": "모든 항목", - "perm-manage": "관리", - "perm-view": "보기 전용", - "perm-hidden": "숨김", - "search": "사용자 검색..." - }, - "audit-log": { - "title": "감사 로그", - "empty": "로그가 없습니다.", - "empty-subtitle": "귀하나 다른 사용자가 무언가를 변경하는 즉시 해당 이벤트의 기록이 여기에 표시됩니다.", - "proxy-host": "프록시 호스트", - "redirection-host": "리디렉션 호스트", - "dead-host": "404 호스트", - "stream": "스트림", - "user": "사용자", - "certificate": "인증서", - "access-list": "액세스 목록", - "created": "{name} 생성", - "updated": "{name} 업데이트", - "deleted": "{name} 삭제", - "enabled": "{name} 활성화", - "disabled": "{name} 비활성화", - "renewed": "{name} 갱신", - "meta-title": "이벤트 세부사항", - "view-meta": "세부사항 보기", - "date": "날짜", - "search": "로그 검색..." - }, - "settings": { - "title": "설정", - "default-site": "기본 사이트", - "default-site-description": "Nginx가 알 수 없는 호스트로 요청을 받을 때 표시할 내용", - "default-site-congratulations": "축하 페이지", - "default-site-404": "404 페이지", - "default-site-444": "응답 없음 (444)", - "default-site-html": "사용자 정의 페이지", - "default-site-redirect": "리디렉션", - "language": "인터페이스 언어", - "language-description": "인터페이스 표시 언어 선택", - "current-language": "현재 언어" - } -} diff --git a/frontend/js/i18n/pt.json b/frontend/js/i18n/pt-PT.json similarity index 100% rename from frontend/js/i18n/pt.json rename to frontend/js/i18n/pt-PT.json diff --git a/frontend/js/i18n/ru.json b/frontend/js/i18n/ru-RU.json similarity index 100% rename from frontend/js/i18n/ru.json rename to frontend/js/i18n/ru-RU.json diff --git a/frontend/js/i18n/zh.json b/frontend/js/i18n/zh-CN.json similarity index 100% rename from frontend/js/i18n/zh.json rename to frontend/js/i18n/zh-CN.json diff --git a/frontend/js/i18n/tw.json b/frontend/js/i18n/zh-TW.json similarity index 90% rename from frontend/js/i18n/tw.json rename to frontend/js/i18n/zh-TW.json index 8b43f9172..42a5853e0 100644 --- a/frontend/js/i18n/tw.json +++ b/frontend/js/i18n/zh-TW.json @@ -15,7 +15,7 @@ "close": "關閉", "enable": "啟用", "disable": "停用", - "sure": "是的,我確定", + "sure": "確認", "disabled": "已停用", "choose-file": "選擇檔案", "source": "來源", @@ -46,7 +46,7 @@ "logged-in": "您已登入為 {name}", "unknown-error": "載入失敗。請重新載入應用程式。", "unknown-user": "未知使用者", - "sign-in-as": "以 {name} 身份重新登入" + "sign-in-as": "以 {name} 身分重新登入" }, "roles": { "title": "角色", @@ -58,8 +58,7 @@ "hosts": "主機", "language": "語言", "switch-to-english": "切換到英文", - "switch-to-chinese": "切換到簡體中文", - "switch-to-traditional": "切換到繁體中文" + "switch-to-chinese": "切換到中文" }, "footer": { "fork-me": "在 Github 上 Fork 我", @@ -106,10 +105,10 @@ "letsencrypt-email": "Let's Encrypt 電子郵件地址", "letsencrypt-agree": "我同意 Let's Encrypt 服務條款", "delete-ssl": "附加的 SSL 憑證不會被刪除,需要手動刪除。", - "hosts-warning": "這些網域必須已設定為指向此安裝", + "hosts-warning": "這些網域名稱必須已設定為指向此安裝", "no-wildcard-without-dns": "不使用 DNS 挑戰時無法為萬用字元網域申請 Let's Encrypt 憑證", "dns-challenge": "使用 DNS 挑戰", - "certbot-warning": "此部分需要對 Certbot 及其 DNS 外掛有一定瞭解。請查閱相關外掛文件。", + "certbot-warning": "此部分需要對 Certbot 及其 DNS 外掛有一定了解。請查閱相關外掛文件。", "dns-provider": "DNS 提供者", "please-choose": "請選擇...", "credentials-file-content": "憑證檔案內容", @@ -127,14 +126,14 @@ "form-title": "{id, select, undefined{新建} other{編輯}} 代理主機", "forward-scheme": "協定", "forward-host": "轉發主機名稱 / IP", - "forward-port": "轉發連接埠", + "forward-port": "轉發埠", "delete": "刪除代理主機", "delete-confirm": "您確定要刪除以下網域的代理主機嗎:{domains}?", "help-title": "什麼是代理主機?", - "help-content": "代理主機是您要轉發的網路服務的傳入端點。\n它為可能沒有內建 SSL 支援的服務提供可選的 SSL 終止。\n代理主機是 Nginx 代理管理器最常見的用途。", + "help-content": "代理主機是您要轉發的 Web 服務的傳入端點。\n它為可能沒有內建 SSL 支援的服務提供可選的 SSL 終止。\n代理主機是 Nginx 代理管理器最常見的用途。", "access-list": "存取清單", "allow-websocket-upgrade": "WebSocket 支援", - "ignore-invalid-upstream-ssl": "忽略無效的上游 SSL", + "ignore-invalid-upstream-ssl": "忽略無效 SSL", "custom-forward-host-help": "為子資料夾轉發新增路徑。\n範例:203.0.113.25/path/", "search": "搜尋主機…" }, @@ -150,7 +149,7 @@ "delete": "刪除重新導向主機", "delete-confirm": "您確定要刪除以下網域的重新導向主機嗎:{domains}?", "help-title": "什麼是重新導向主機?", - "help-content": "重新導向主機會將來自傳入網域的請求重新導向並將檢視者推送到另一個網域。\n使用此類主機的最常見原因是當您的網站更改網域但您仍有指向舊網域的搜尋引擎或推薦連結時。", + "help-content": "重新導向主機會將來自傳入網域的請求重新導向並將查看者推送到另一個網域。\n使用此類主機的最常見原因是當您的網站更改網域但您仍有指向舊網域的搜尋引擎或推薦連結時。", "search": "搜尋主機…" }, "dead-hosts": { @@ -161,7 +160,7 @@ "delete": "刪除 404 主機", "delete-confirm": "您確定要刪除此 404 主機嗎?", "help-title": "什麼是 404 主機?", - "help-content": "404 主機只是顯示 404 頁面的主機設定。\n當您的網域在搜尋引擎中列出並且您想要提供更好的錯誤頁面或專門告訴搜尋索引器網域頁面不再存在時,這可能很有用。\n擁有此主機的另一個好處是追蹤對它的命中日誌並檢視推薦者。", + "help-content": "404 主機只是顯示 404 頁面的主機設定。\n當您的網域在搜尋引擎中列出並且您想要提供更好的錯誤頁面或專門告訴搜尋索引器網域頁面不再存在時,這可能很有用。\n擁有此主機的另一個好處是追蹤對它的點擊日誌並查看推薦者。", "search": "搜尋主機…" }, "streams": { @@ -169,9 +168,9 @@ "empty": "沒有串流", "add": "新增串流", "form-title": "{id, select, undefined{新建} other{編輯}} 串流", - "incoming-port": "傳入連接埠", + "incoming-port": "傳入埠", "forwarding-host": "轉發主機", - "forwarding-port": "轉發連接埠", + "forwarding-port": "轉發埠", "tcp-forwarding": "TCP 轉發", "udp-forwarding": "UDP 轉發", "forward-type-error": "至少必須啟用一種協定類型", @@ -182,7 +181,7 @@ "delete-confirm": "您確定要刪除此串流嗎?", "help-title": "什麼是串流?", "help-content": "對於 Nginx 來說相對較新的功能,串流將用於將 TCP/UDP 流量直接轉發到網路上的另一台電腦。\n如果您正在執行遊戲伺服器、FTP 或 SSH 伺服器,這會很方便。", - "search": "搜尋傳入連接埠…", + "search": "搜尋傳入埠…", "ssl-certificate": "TCP 轉發的 SSL 憑證", "tcp+ssl": "TCP+SSL" }, @@ -194,7 +193,7 @@ "delete": "刪除 SSL 憑證", "delete-confirm": "您確定要刪除此 SSL 憑證嗎?使用它的任何主機都需要稍後更新。", "help-title": "SSL 憑證", - "help-content": "SSL 憑證(正確稱為 TLS 憑證)是一種加密金鑰形式,允許您的網站為最終使用者加密。\nNPM 使用名為 Let's Encrypt 的服務免費頒發 SSL 憑證。\n如果您在 NPM 後面有任何個人資訊、密碼或敏感資料,使用憑證可能是個好主意。\nNPM 還支援 DNS 身份驗證,適用於您不在網際網路上執行網站的情況,或者如果您只想要萬用字元憑證。", + "help-content": "SSL 憑證(正確稱為 TLS 憑證)是一種加密金鑰形式,允許您的網站為終端使用者加密。\nNPM 使用名為 Let's Encrypt 的服務免費頒發 SSL 憑證。\n如果您在 NPM 後面有任何個人資訊、密碼或敏感資料,使用憑證可能是個好主意。\nNPM 還支援 DNS 身份驗證,適用於您不在網際網路上執行網站的情況,或者如果您只想要萬用字元憑證。", "other-certificate": "憑證", "other-certificate-key": "憑證金鑰", "other-intermediate-certificate": "中間憑證", @@ -206,7 +205,7 @@ "reachability-failed-to-check": "由於與 site24x7.com 的通訊錯誤,無法檢查可達性。", "reachability-ok": "您的伺服器可達,應該可以建立憑證。", "reachability-404": "在此網域找到了伺服器,但它似乎不是 Nginx 代理管理器。請確保您的網域指向執行 NPM 執行個體的 IP。", - "reachability-not-resolved": "此網域沒有可用的伺服器。請確保您的網域存在並指向執行 NPM 執行個體的 IP,如有必要,在路由器中轉發連接埠 80。", + "reachability-not-resolved": "此網域沒有可用的伺服器。請確保您的網域存在並指向執行 NPM 執行個體的 IP,如有必要,在路由器中轉發埠 80。", "reachability-wrong-data": "在此網域找到了伺服器,但它傳回了意外的資料。是 NPM 伺服器嗎?請確保您的網域指向執行 NPM 執行個體的 IP。", "reachability-other": "在此網域找到了伺服器,但它傳回了意外的狀態碼 {code}。是 NPM 伺服器嗎?請確保您的網域指向執行 NPM 執行個體的 IP。", "download": "下載", @@ -214,7 +213,7 @@ "search": "搜尋憑證…", "in-use": "使用中", "inactive": "未啟用", - "active-domain_names": "作用中網域名稱" + "active-domain_names": "活動網域名稱" }, "access-lists": { "title": "存取清單", @@ -226,11 +225,11 @@ "public": "公開存取", "public-sub": "無存取限制", "help-title": "什麼是存取清單?", - "help-content": "存取清單提供特定用戶端 IP 地址的黑名單或白名單,以及透過基本 HTTP 身份驗證對代理主機進行身份驗證。\n您可以為單個存取清單設定多個用戶端規則、使用者名稱和密碼,然後將其套用於代理主機。\n這對於沒有內建身份驗證機制的轉發網路服務或您想要保護免受未知用戶端存取的服務最有用。", + "help-content": "存取清單提供特定用戶端 IP 位址的黑名單或白名單,以及透過基本 HTTP 身份驗證對代理主機進行身份驗證。\n您可以為單個存取清單設定多個用戶端規則、使用者名稱和密碼,然後將其套用於代理主機。\n這對於沒有內建身份驗證機制的轉發 Web 服務或您想要保護免受未知用戶端存取的服務最有用。", "item-count": "{count} 個{count, select, 1{使用者} other{使用者}}", "client-count": "{count} 個{count, select, 1{規則} other{規則}}", "proxy-host-count": "{count} 個{count, select, 1{代理主機} other{代理主機}}", - "delete-has-hosts": "此存取清單與 {count} 個代理主機關聯。刪除後它們將變為公開可存取。", + "delete-has-hosts": "此存取清單與 {count} 個代理主機相關聯。刪除後它們將變為公開可存取。", "details": "詳細資訊", "authorization": "授權", "access": "存取", @@ -248,13 +247,13 @@ "nickname": "暱稱", "full-name": "全名", "edit-details": "編輯詳細資訊", - "change-password": "更改密碼", + "change-password": "變更密碼", "edit-permissions": "編輯權限", - "sign-in-as": "以使用者身份登入", + "sign-in-as": "以使用者身分登入", "form-title": "{id, select, undefined{新建} other{編輯}} 使用者", "delete": "刪除 {name, select, undefined{使用者} other{{name}}}", "delete-confirm": "您確定要刪除 {name} 嗎?", - "password-title": "更改密碼{self, select, false{ for {name}} other{}}", + "password-title": "變更密碼{self, select, false{ for {name}} other{}}", "current-password": "目前密碼", "new-password": "新密碼", "confirm-password": "確認密碼", @@ -303,4 +302,4 @@ "language-description": "選擇介面顯示語言", "current-language": "目前語言" } -} +} \ No newline at end of file diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 0f39f6f40..2d2edba6e 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -50,14 +50,15 @@ module.exports = { // other: { type: 'javascript/auto', // <= Set the module.type explicitly - test: /\/(en|zh|fr|jp|tw|kr|ru|pt)\.json$/, + test: /\/(en|zh|fr|ja|tw|ko|ru|pt)\.json$/, loader: 'messageformat-loader', options: { biDiSupport: false, disablePluralKeyChecks: false, formatters: null, intlSupport: false, - locale: ['en', 'zh', 'fr', 'jp', 'tw', 'kr', 'ru', 'pt'], + // Use CLDR/BCP 47 locale codes supported by messageformat + locale: ['en', 'zh', 'fr', 'ja', 'zh-TW', 'ko', 'ru', 'pt'], strictNumberSign: false } }, From 155c754c6da74a955f74d92265b4ffc0c79ee5a5 Mon Sep 17 00:00:00 2001 From: Qi Ma Date: Thu, 14 Aug 2025 10:32:41 +0000 Subject: [PATCH 3/4] Enhance i18n Translation Function,Fix i18n Initialization in Main Application --- backend/models/access_list.js | 7 +++++ backend/models/audit-log.js | 12 +++++++++ backend/models/certificate.js | 10 ++++++++ backend/models/dead_host.js | 7 +++++ backend/models/now_helper.js | 12 ++++++--- backend/models/proxy_host.js | 7 +++++ backend/models/redirection_host.js | 7 +++++ backend/models/stream.js | 7 +++++ backend/models/user.js | 7 +++++ frontend/html/index.ejs | 2 +- frontend/js/app/cache.js | 22 +++++++++++++++- frontend/js/app/dashboard/main.ejs | 2 +- frontend/js/app/dashboard/main.js | 22 +++++++++++++++- frontend/js/app/i18n.js | 23 +++++++++++++++-- frontend/js/app/main.js | 41 +++++++++++++++++++++++++++--- frontend/js/app/ui/footer/main.ejs | 11 +++++++- frontend/js/app/ui/footer/main.js | 20 ++++++++++++++- frontend/js/lib/helpers.js | 18 ++++++++++++- frontend/js/login/main.js | 12 +++++++++ 19 files changed, 234 insertions(+), 15 deletions(-) diff --git a/backend/models/access_list.js b/backend/models/access_list.js index 959df05f3..d091820fa 100644 --- a/backend/models/access_list.js +++ b/backend/models/access_list.js @@ -34,6 +34,13 @@ class AccessList extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js index 45a4b4602..229a06282 100644 --- a/backend/models/audit-log.js +++ b/backend/models/audit-log.js @@ -23,6 +23,18 @@ class AuditLog extends Model { this.modified_on = now(); } + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } + return json; + } + static get name () { return 'AuditLog'; } diff --git a/backend/models/certificate.js b/backend/models/certificate.js index d4ea21ad5..46171a933 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -46,6 +46,16 @@ class Certificate extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } + if (json.expires_on) { + json.expires_on = new Date(json.expires_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js index 3386caabf..43984a4e4 100644 --- a/backend/models/dead_host.js +++ b/backend/models/dead_host.js @@ -48,6 +48,13 @@ class DeadHost extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js index dec70c3de..58309d434 100644 --- a/backend/models/now_helper.js +++ b/backend/models/now_helper.js @@ -5,9 +5,15 @@ const Model = require('objection').Model; Model.knex(db); module.exports = function () { + // Return consistent datetime format for all database types if (config.isSqlite()) { - // eslint-disable-next-line - return Model.raw("datetime('now','localtime')"); + // SQLite: Return ISO format + return Model.raw("datetime('now')"); + } else if (config.isPostgres()) { + // PostgreSQL: Return ISO format + return Model.raw("NOW()"); + } else { + // MySQL: Return ISO format + return Model.raw("NOW()"); } - return Model.raw('NOW()'); }; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 07aa5dd3c..51da04e13 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -52,6 +52,13 @@ class ProxyHost extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js index 801627916..ffb21866d 100644 --- a/backend/models/redirection_host.js +++ b/backend/models/redirection_host.js @@ -51,6 +51,13 @@ class RedirectionHost extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/stream.js b/backend/models/stream.js index 5d1cb6c1c..3235c1ef5 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -31,6 +31,13 @@ class Stream extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/backend/models/user.js b/backend/models/user.js index 78fd3dd67..15202c2d7 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -31,6 +31,13 @@ class User extends Model { $parseDatabaseJson(json) { json = super.$parseDatabaseJson(json); + // Ensure dates are properly formatted + if (json.created_on) { + json.created_on = new Date(json.created_on).toISOString(); + } + if (json.modified_on) { + json.modified_on = new Date(json.modified_on).toISOString(); + } return helpers.convertIntFieldsToBool(json, boolFields); } diff --git a/frontend/html/index.ejs b/frontend/html/index.ejs index ae08b012e..e4108baab 100644 --- a/frontend/html/index.ejs +++ b/frontend/html/index.ejs @@ -1,7 +1,7 @@ <% var title = 'Nginx Proxy Manager' %> <%- include partials/header.ejs %> -
+
diff --git a/frontend/js/app/cache.js b/frontend/js/app/cache.js index 5c9d6869f..8a5838c65 100644 --- a/frontend/js/app/cache.js +++ b/frontend/js/app/cache.js @@ -40,10 +40,30 @@ let getInitialLocale = function() { return 'en-US'; }; +// 尝试从DOM获取初始版本号 +let getInitialVersion = function() { + try { + if (typeof document !== 'undefined') { + const appElement = document.getElementById('app'); + if (appElement && appElement.dataset.version) { + return appElement.dataset.version; + } + + const loginElement = document.getElementById('login'); + if (loginElement && loginElement.dataset.version) { + return loginElement.dataset.version; + } + } + } catch (e) { + console.warn('Error getting initial version:', e); + } + return null; +}; + let cache = { User: new UserModel.Model(), locale: getInitialLocale(), - version: null + version: getInitialVersion() }; module.exports = cache; diff --git a/frontend/js/app/dashboard/main.ejs b/frontend/js/app/dashboard/main.ejs index c00aa6d0f..4f2a823e9 100644 --- a/frontend/js/app/dashboard/main.ejs +++ b/frontend/js/app/dashboard/main.ejs @@ -1,5 +1,5 @@ <% if (columns) { %> diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js index ba4a99a67..d0b1f74e2 100644 --- a/frontend/js/app/dashboard/main.js +++ b/frontend/js/app/dashboard/main.js @@ -28,7 +28,27 @@ module.exports = Mn.View.extend({ return { getUserName: function () { - return Cache.User.get('nickname') || Cache.User.get('name'); + const nickname = Cache.User.get('nickname'); + const name = Cache.User.get('name'); + const email = Cache.User.get('email'); + + // 调试信息(可以在生产环境中移除) + console.log('Debug getUserName:', { + nickname: nickname, + name: name, + email: email, + userData: Cache.User.toJSON() + }); + + // 优先级:nickname > name > email的用户名部分 > 默认值 + let displayName = nickname || name; + + if (!displayName && email) { + // 如果没有名字但有邮箱,使用邮箱的用户名部分 + displayName = email.split('@')[0]; + } + + return displayName || 'Unknown User'; }, getHostStat: function (type) { diff --git a/frontend/js/app/i18n.js b/frontend/js/app/i18n.js index 111defe76..eab9e171a 100644 --- a/frontend/js/app/i18n.js +++ b/frontend/js/app/i18n.js @@ -153,17 +153,36 @@ function translate(namespace, key, data) { // 尝试获取翻译 function getTranslation(messages, namespace, key, data) { if (!messages || typeof messages !== 'object') { + console.warn('Invalid messages object:', messages); return null; } if (messages[namespace] && typeof messages[namespace][key] !== 'undefined') { try { let value = messages[namespace][key]; + console.log('i18n Debug:', {namespace, key, data, valueType: typeof value, value}); + // MessageFormat loader将字符串转换为函数 if (typeof value === 'function') { - return value(data || {}); + let result = value(data || {}); + console.log('i18n function result:', result); + return result; + } else if (typeof value === 'string') { + // 如果还是字符串,进行简单的模板替换 + let result = value; + if (data && typeof data === 'object') { + Object.keys(data).forEach(placeholder => { + const regex = new RegExp(`\\{${placeholder}\\}`, 'g'); + const replacement = data[placeholder]; + if (replacement !== undefined && replacement !== null) { + result = result.replace(regex, replacement); + } + }); + } + console.log('i18n string result:', result); + return result; } else { - // 如果还是字符串,直接返回 + console.warn('Unexpected value type:', typeof value, value); return value; } } catch (formatError) { diff --git a/frontend/js/app/main.js b/frontend/js/app/main.js index bcf41cf6d..2db6907f9 100644 --- a/frontend/js/app/main.js +++ b/frontend/js/app/main.js @@ -26,9 +26,16 @@ const App = Mn.Application.extend({ // 确保 i18n 系统已初始化 if (typeof i18n.initialize === 'function') { i18n.initialize(); + console.log('i18n system initialized'); + } else { + console.error('i18n.initialize function not available'); } - console.log(i18n('main', 'welcome')); + // 测试 i18n 功能 + console.log('Testing i18n with welcome message:', i18n('main', 'welcome')); + console.log('Testing i18n with version:', i18n('main', 'version', {version: '2.11.3'})); + console.log('Testing i18n with name:', i18n('dashboard', 'title', {name: 'Test User'})); + console.log('Testing i18n with date:', i18n('str', 'created-on', {date: '2024-01-01'})); // Check if token is coming through if (this.getParam('token')) { @@ -40,6 +47,17 @@ const App = Mn.Application.extend({ .then(result => { Cache.version = [result.version.major, result.version.minor, result.version.revision].join('.'); }) + .catch(err => { + console.warn('Failed to get API version:', err.message); + // 如果API调用失败,确保Cache.version有一个回退值 + if (!Cache.version) { + // 尝试从DOM获取编译时版本号 + const appElement = document.getElementById('app'); + if (appElement && appElement.dataset.version) { + Cache.version = appElement.dataset.version; + } + } + }) .then(Api.Tokens.refresh) .then(this.bootstrap) .then(() => { @@ -101,8 +119,25 @@ const App = Mn.Application.extend({ bootstrap: function () { return Api.Users.getById('me', ['permissions']) .then(response => { - Cache.User.set(response); - Tokens.setCurrentName(response.nickname || response.name); + console.log('Bootstrap user response:', response); + if (response && typeof response === 'object') { + Cache.User.set(response); + Tokens.setCurrentName(response.nickname || response.name || response.email || 'Unknown User'); + } else { + console.error('Invalid user response:', response); + } + }) + .catch(error => { + console.error('Bootstrap failed:', error); + // 设置一个默认用户以避免应用崩溃 + Cache.User.set({ + id: 0, + name: 'Unknown User', + nickname: '', + email: '', + roles: [], + permissions: null + }); }); }, diff --git a/frontend/js/app/ui/footer/main.ejs b/frontend/js/app/ui/footer/main.ejs index 99c2630a0..6822347b6 100644 --- a/frontend/js/app/ui/footer/main.ejs +++ b/frontend/js/app/ui/footer/main.ejs @@ -9,7 +9,16 @@
- <%- i18n('main', 'version', {version: getVersion()}) %> + <% + var currentVersion = getVersion(); + var versionText = ''; + try { + versionText = i18n('main', 'version', {version: currentVersion}); + } catch (e) { + console.warn('i18n version failed:', e); + versionText = 'v' + (currentVersion || '0.0.0'); + } + %><%- versionText %> <%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %> <%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %>
diff --git a/frontend/js/app/ui/footer/main.js b/frontend/js/app/ui/footer/main.js index 73f515e68..1f33decc1 100644 --- a/frontend/js/app/ui/footer/main.js +++ b/frontend/js/app/ui/footer/main.js @@ -8,7 +8,25 @@ module.exports = Mn.View.extend({ templateContext: { getVersion: function () { - return Cache.version || '0.0.0'; + // 优先使用API获取的版本号,其次使用编译时版本号,最后使用默认值 + if (Cache.version) { + return Cache.version; + } + + // 尝试从全局变量获取编译时版本号 + if (typeof window !== 'undefined' && window.APP_VERSION) { + return window.APP_VERSION; + } + + // 尝试从body元素的data属性获取版本号 + if (typeof document !== 'undefined') { + const appElement = document.getElementById('app'); + if (appElement && appElement.dataset.version) { + return appElement.dataset.version; + } + } + + return '0.0.0'; } } }); diff --git a/frontend/js/lib/helpers.js b/frontend/js/lib/helpers.js index 21ce74243..47528644b 100644 --- a/frontend/js/lib/helpers.js +++ b/frontend/js/lib/helpers.js @@ -17,10 +17,26 @@ module.exports = { * @returns {String} */ formatDbDate: function (date, format) { + if (!date) { + return ''; + } + if (typeof date === 'number') { return moment.unix(date).format(format); } - return moment(date).format(format); + // Handle various date string formats from database + const parsedDate = moment(date); + if (!parsedDate.isValid()) { + // Try parsing as ISO date + const isoDate = moment(date, moment.ISO_8601); + if (isoDate.isValid()) { + return isoDate.format(format); + } + // If still invalid, return the original string or empty + return date.toString() || ''; + } + + return parsedDate.format(format); } }; diff --git a/frontend/js/login/main.js b/frontend/js/login/main.js index 03fdc7e56..30f465dee 100644 --- a/frontend/js/login/main.js +++ b/frontend/js/login/main.js @@ -1,11 +1,23 @@ const Mn = require('backbone.marionette'); const LoginView = require('./ui/login'); +const i18n = require('../app/i18n'); const App = Mn.Application.extend({ region: '#login', UI: null, onStart: function (/*app, options*/) { + // 确保 i18n 系统已初始化 + if (typeof i18n.initialize === 'function') { + i18n.initialize(); + console.log('i18n system initialized for login page'); + } else { + console.error('i18n.initialize function not available on login page'); + } + + // 测试 i18n 功能 + console.log('Testing login i18n with version:', i18n('main', 'version', {version: '2.11.3'})); + this.getRegion().show(new LoginView()); } }); From 03b05482919dcc717f6334a1de12cf5d74913070 Mon Sep 17 00:00:00 2001 From: Qi Ma Date: Thu, 14 Aug 2025 10:43:54 +0000 Subject: [PATCH 4/4] Fixed ESLint Errors in CI --- backend/models/now_helper.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js index 58309d434..be0ec3cd3 100644 --- a/backend/models/now_helper.js +++ b/backend/models/now_helper.js @@ -8,12 +8,12 @@ module.exports = function () { // Return consistent datetime format for all database types if (config.isSqlite()) { // SQLite: Return ISO format - return Model.raw("datetime('now')"); + return Model.raw('datetime(\'now\')'); } else if (config.isPostgres()) { // PostgreSQL: Return ISO format - return Model.raw("NOW()"); + return Model.raw('NOW()'); } else { // MySQL: Return ISO format - return Model.raw("NOW()"); + return Model.raw('NOW()'); } };