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/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..be0ec3cd3 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 6d1fbc4f9..8a5838c65 100644
--- a/frontend/js/app/cache.js
+++ b/frontend/js/app/cache.js
@@ -1,9 +1,69 @@
const UserModel = require('../models/user');
+// 获取语言设置:优先级为 localStorage > 浏览器语言 > 默认英文
+let getInitialLocale = function() {
+ try {
+ // 检查本地存储
+ if (typeof localStorage !== 'undefined') {
+ let saved = localStorage.getItem('locale');
+ if (saved && ['zh-CN', 'en-US', 'fr-FR', 'ja-JP', 'zh-TW', 'ko-KR', 'ru-RU', 'pt-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 'zh-TW';
+ } else if (browserLang.startsWith('zh')) {
+ return 'zh-CN';
+ } else if (browserLang.startsWith('en')) {
+ return 'en-US';
+ } else if (browserLang.startsWith('fr')) {
+ return 'fr-FR';
+ } else if (browserLang.startsWith('ja')) {
+ return 'ja-JP';
+ } else if (browserLang.startsWith('ko')) {
+ return 'ko-KR';
+ } else if (browserLang.startsWith('ru')) {
+ return 'ru-RU';
+ } else if (browserLang.startsWith('pt')) {
+ return 'pt-PT';
+ }
+ }
+ } catch (e) {
+ console.warn('Error accessing localStorage or navigator:', e);
+ }
+
+ // 默认使用英文作为最安全的后备语言
+ 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: 'en',
- version: null
+ locale: getInitialLocale(),
+ 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 c63cdc079..eab9e171a 100644
--- a/frontend/js/app/i18n.js
+++ b/frontend/js/app/i18n.js
@@ -1,23 +1,229 @@
-const Cache = ('./cache');
-const messages = require('../i18n/messages.json');
+// 使用分离的语言文件
+let messages = {};
+let isInitialized = false;
+let currentLocale = null;
+
+// 使用 webpack 的 require.context 显式包含可用语言资源,避免动态 require 被丢弃
+let localesContext = null;
+try {
+ // 仅匹配现有语言文件;使用完整的locale格式
+ localesContext = require.context('../i18n', false, /^\.\/(en-US|zh-CN|zh-TW|fr-FR|ja-JP|ko-KR|ru-RU|pt-PT)\.json$/);
+} catch (e) {
+ // 非 webpack 环境下忽略
+ localesContext = null;
+}
+
+// 预加载英文作为默认后备语言
+function preloadEnglish() {
+ if (!messages['en-US']) {
+ try {
+ let mod = localesContext ? localesContext('./en-US.json') : require('../i18n/en-US.json');
+ // 规范化 ESModule default 导出
+ messages['en-US'] = (mod && mod.default) ? mod.default : mod;
+ console.info('Pre-loaded English language file as fallback');
+
+ // 验证基本结构
+ if (messages['en-US'] && typeof messages['en-US'] === 'object') {
+ if (messages['en-US'].str && messages['en-US'].login) {
+ console.info('English language file structure validated');
+ } else {
+ console.warn('English language file missing expected sections:', Object.keys(messages['en-US']));
+ }
+ } else {
+ console.warn('English language file is not an object:', typeof messages['en-US']);
+ }
+ } catch (e) {
+ console.error('Critical: Failed to load English language file:', e);
+ messages['en-US'] = {};
+ }
+ }
+}
+
+// 获取 Cache 的函数,避免循环依赖问题
+function getCache() {
+ try {
+ return require('./cache');
+ } catch (e) {
+ console.warn('Cache module not available during initialization');
+ return { locale: 'en' };
+ }
+}
+
+// 清理缓存函数
+function clearCache() {
+ console.log('Clearing i18n cache...');
+ messages = {};
+ isInitialized = false;
+ currentLocale = null;
+}
+
+// 初始化函数 - 确保基础语言文件已加载
+function initialize(forceReload = false) {
+ let Cache = getCache();
+ let newLocale = Cache.locale || 'en';
+
+ // 如果语言发生变化,清理缓存
+ if (currentLocale && currentLocale !== newLocale) {
+ console.log('Language changed from', currentLocale, 'to', newLocale, 'clearing cache');
+ clearCache();
+ }
+
+ if (isInitialized && !forceReload && currentLocale === newLocale) {
+ return;
+ }
+
+ console.log('Initializing i18n system with locale:', newLocale);
+ preloadEnglish();
+
+ // 预加载当前设置的语言
+ loadMessages(newLocale);
+
+ currentLocale = newLocale;
+ isInitialized = true;
+ console.log('i18n system initialized successfully');
+}
+
+function loadMessages(locale) {
+ // 确保英文后备语言已加载
+ preloadEnglish();
+
+ if (!messages[locale]) {
+ try {
+ console.log('Loading language file for locale:', locale);
+ let mod = localesContext ? localesContext('./' + locale + '.json') : require('../i18n/' + locale + '.json');
+ // 规范化 ESModule default 导出
+ messages[locale] = (mod && mod.default) ? mod.default : mod;
+ console.info('Successfully loaded language file:', locale);
+
+ // 验证基本结构
+ if (messages[locale] && typeof messages[locale] === 'object') {
+ console.info('Language file structure for', locale, ':', Object.keys(messages[locale]));
+ } else {
+ console.warn('Language file is not an object for locale:', locale, typeof messages[locale]);
+ }
+ } catch (e) {
+ console.error('Language file not found for locale:', locale, e);
+ // 如果找不到语言文件,使用英文作为后备
+ if (locale !== 'en-US') {
+ console.warn('Using English fallback for locale:', locale);
+ messages[locale] = messages['en-US'] || {};
+ } else {
+ console.error('Failed to load English language file');
+ messages[locale] = {};
+ }
+ }
+ } else {
+ console.debug('Language file already loaded for locale:', locale);
+ }
+ return messages[locale];
+}
/**
+ * 主翻译函数
* @param {String} namespace
* @param {String} key
* @param {Object} [data]
*/
-module.exports = function (namespace, key, data) {
- let locale = Cache.locale;
- // check that the locale exists
- if (typeof messages[locale] === 'undefined') {
- locale = 'en';
- }
+function translate(namespace, key, data) {
+ try {
+ let Cache = getCache();
+ let locale = Cache.locale || 'en'; // 确保总是有一个有效的locale
+
+ // 检查语言是否发生变化,如果变化则重新初始化
+ if (currentLocale !== locale) {
+ console.log('Detected locale change in translate function:', currentLocale, '->', locale);
+ initialize(true); // 强制重新初始化
+ }
+
+ // 确保系统已初始化
+ if (!isInitialized) {
+ initialize();
+ }
+
+ let currentMessages = loadMessages(locale);
+
+ // 如果没有加载到任何消息,强制使用英文
+ if (!currentMessages) {
+ console.warn('No messages loaded for locale:', locale, 'forcing English');
+ currentMessages = loadMessages('en');
+ locale = 'en';
+ }
+
+ // MessageFormat loader保持JSON结构,只是将字符串转换为函数
+ // 尝试获取翻译
+ 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') {
+ 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) {
+ console.error('Error formatting message:', namespace, key, formatError);
+ // 尝试返回原始值
+ try {
+ return messages[namespace][key].toString();
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ // 尝试从当前语言获取翻译
+ let result = getTranslation(currentMessages, namespace, key, data);
+ if (result !== null) {
+ return result;
+ }
+
+ // 如果当前语言没有翻译,尝试使用英文作为后备
+ if (locale !== 'en') {
+ let enMessages = loadMessages('en');
+ result = getTranslation(enMessages, namespace, key, data);
+ if (result !== null) {
+ return result;
+ }
+ }
- 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);
+ console.warn('Missing translation:', namespace + '/' + key, 'for locale:', locale);
+ return '(MISSING: ' + namespace + '/' + key + ')';
+
+ } catch (criticalError) {
+ console.error('Critical error in i18n translate function:', criticalError);
+ // 返回一个安全的fallback
+ return key || '(ERROR)';
}
+}
- return '(MISSING: ' + namespace + '/' + key + ')';
-};
+// 导出主翻译函数和相关函数
+module.exports = translate;
+module.exports.initialize = initialize;
+module.exports.clearCache = clearCache;
diff --git a/frontend/js/app/main.js b/frontend/js/app/main.js
index e85b4f620..2db6907f9 100644
--- a/frontend/js/app/main.js
+++ b/frontend/js/app/main.js
@@ -23,7 +23,19 @@ const App = Mn.Application.extend({
},
onStart: function (app, options) {
- console.log(i18n('main', 'welcome'));
+ // 确保 i18n 系统已初始化
+ if (typeof i18n.initialize === 'function') {
+ i18n.initialize();
+ console.log('i18n system initialized');
+ } else {
+ console.error('i18n.initialize function not available');
+ }
+
+ // 测试 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')) {
@@ -35,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(() => {
@@ -96,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/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs
index 1623c4dc7..c7c06f0cf 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-CN' ? '中文 (简体)' : locale === 'zh-TW' ? '中文 (繁體)' : locale === 'en-US' ? 'English' : locale === 'fr-FR' ? 'Français' : locale === 'ja-JP' ? '日本語' : locale === 'ko-KR' ? '한국어' : locale === 'ru-RU' ? 'Русский' : locale === 'pt-PT' ? 'Português' : 'English' %>
<% } %>
|
-
-
-
+ <% } else { %>
+
+ <% } %>
|
\ No newline at end of file
diff --git a/frontend/js/app/settings/list/item.js b/frontend/js/app/settings/list/item.js
index 03f9ac05b..f442434bc 100644
--- a/frontend/js/app/settings/list/item.js
+++ b/frontend/js/app/settings/list/item.js
@@ -1,5 +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({
@@ -7,16 +9,49 @@ module.exports = Mn.View.extend({
tagName: 'tr',
ui: {
- edit: 'a.edit'
+ edit: 'a.edit',
+ languageSelector: '.language-selector'
},
events: {
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showSettingForm(this.model);
+ },
+
+ 'change @ui.languageSelector': function (e) {
+ e.preventDefault();
+ let newLocale = $(e.currentTarget).val();
+ 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();
+ }
}
},
+ templateContext: function() {
+ return {
+ locale: Cache.locale
+ };
+ },
+
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
diff --git a/frontend/js/app/settings/main.js b/frontend/js/app/settings/main.js
index 96b2941ff..bf7b101ab 100644
--- a/frontend/js/app/settings/main.js
+++ b/frontend/js/app/settings/main.js
@@ -3,6 +3,7 @@ const App = require('../main');
const SettingModel = require('../../models/setting');
const ListView = require('./list/main');
const ErrorView = require('../error/main');
+const Cache = require('../cache');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
@@ -24,19 +25,32 @@ module.exports = Mn.View.extend({
App.Api.Settings.getAll()
.then(response => {
- if (!view.isDestroyed() && response && response.length) {
+ if (!view.isDestroyed()) {
+ // 添加语言设置项到设置列表
+ let settingsData = response || [];
+ settingsData.push({
+ id: 'language',
+ name: 'Interface Language',
+ description: 'Choose interface display language',
+ value: Cache.locale
+ });
+
view.showChildView('list_region', new ListView({
- collection: new SettingModel.Collection(response)
+ collection: new SettingModel.Collection(settingsData)
}));
}
})
.catch(err => {
- view.showChildView('list_region', new ErrorView({
- code: err.code,
- message: err.message,
- retry: function () {
- App.Controller.showSettings();
- }
+ // 即使出错也显示语言设置项
+ let settingsData = [{
+ id: 'language',
+ name: 'Interface Language',
+ description: 'Choose interface display language',
+ value: Cache.locale
+ }];
+
+ view.showChildView('list_region', new ListView({
+ collection: new SettingModel.Collection(settingsData)
}));
console.error(err);
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/app/ui/header/main.ejs b/frontend/js/app/ui/header/main.ejs
index 18ed2b6a6..e2ae969b2 100644
--- a/frontend/js/app/ui/header/main.ejs
+++ b/frontend/js/app/ui/header/main.ejs
@@ -24,6 +24,10 @@