Skip to content

Supports multiple languages in the web interface #4712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions backend/models/access_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 12 additions & 0 deletions backend/models/audit-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
10 changes: 10 additions & 0 deletions backend/models/certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions backend/models/dead_host.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 9 additions & 3 deletions backend/models/now_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()');
};
7 changes: 7 additions & 0 deletions backend/models/proxy_host.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions backend/models/redirection_host.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions backend/models/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions backend/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/html/index.ejs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% var title = 'Nginx Proxy Manager' %>
<%- include partials/header.ejs %>

<div id="app" class="page">
<div id="app" class="page" data-version="<%= version %>">
<span class="loader"></span>
</div>

Expand Down
64 changes: 62 additions & 2 deletions frontend/js/app/cache.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/app/dashboard/main.ejs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="page-header">
<h1 class="page-title"><%- i18n('dashboard', 'title', {name: getUserName()}) %></h1>
<h1 class="page-title"><%- i18n('dashboard', 'title', {name: getUserName() || 'Unknown User'}) %></h1>
</div>

<% if (columns) { %>
Expand Down
22 changes: 21 additions & 1 deletion frontend/js/app/dashboard/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading