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' %> <% } %>
-
- <%- 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 @@ <%- i18n('users', 'change-password') %> + + <%- i18n('menu', locale === 'zh' ? 'switch-to-english' : 'switch-to-chinese') %> + + <%- getLogoutText() %> diff --git a/frontend/js/app/ui/header/main.js b/frontend/js/app/ui/header/main.js index 9779b45c3..2b685dc60 100644 --- a/frontend/js/app/ui/header/main.js +++ b/frontend/js/app/ui/header/main.js @@ -14,7 +14,8 @@ module.exports = Mn.View.extend({ ui: { link: 'a', details: 'a.edit-details', - password: 'a.change-password' + password: 'a.change-password', + language: 'a.switch-language' }, events: { @@ -28,6 +29,17 @@ module.exports = Mn.View.extend({ Controller.showUserPasswordForm(Cache.User); }, + 'click @ui.language': function (e) { + e.preventDefault(); + let newLocale = $(e.currentTarget).data('toggle-to'); + if (newLocale && (newLocale === 'zh' || newLocale === 'en')) { + localStorage.setItem('locale', newLocale); + Cache.locale = newLocale; + // 重新加载页面以应用新语言 + window.location.reload(); + } + }, + 'click @ui.link': function (e) { e.preventDefault(); let href = $(e.currentTarget).attr('href'); @@ -58,7 +70,9 @@ module.exports = Mn.View.extend({ } return i18n('str', 'sign-out'); - } + }, + + locale: Cache.locale }, initialize: function () { diff --git a/frontend/js/i18n/README.md b/frontend/js/i18n/README.md new file mode 100644 index 000000000..7f4aebaf0 --- /dev/null +++ b/frontend/js/i18n/README.md @@ -0,0 +1,222 @@ +# Internationalization (i18n) Language Files + +This directory contains multi-language support files for the Nginx Proxy Manager frontend interface. + +## File Structure + +``` +frontend/js/i18n/ +├── 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-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 (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 + +Add new keys to the appropriate language files following this format: + +```json +{ + "group-name": { + "key-name": "translation text" + } +} +``` + +Example: +```json +{ + "settings": { + "new-feature": "New Feature" + } +} +``` + +### 3. Using Translations in Code + +```javascript +i18n('group-name', 'key-name', {parameter-object}) +``` + +Examples: +```javascript +i18n('settings', 'new-feature') +i18n('main', 'version', {version: '2.0.0'}) +``` + +## Adding a New Language + +To add support for a new language (e.g., German `de`): + +### 1. Create New Language File + +```bash +cp en-US.json de-DE.json +``` + +### 2. Translate File Content + +Translate all English text in `de.json` to German + +### 3. Update Webpack Configuration + +Edit `frontend/webpack.config.js`, change: +```javascript +test: /\/(en|zh)\.json$/, +locale: ['en', 'zh'], +``` +to: +```javascript +test: /\/(en|zh|tw|fr|jp|kr|ru|pt|de)\.json$/, +locale: ['en', 'zh', 'tw', 'fr', 'jp', 'kr', 'ru', 'pt', 'de'], +``` + +### 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`: + +```javascript +// Add browser language detection for the new language +if (browserLang.startsWith('de')) { + return 'de'; +} + +// Add to the validation array +if (saved && ['zh', 'en', 'fr', 'jp', 'tw', 'kr', 'ru', 'pt', 'de'].includes(saved)) { + return saved; +} +``` + +### 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. + +## Important Notes + +### 1. Placeholder Syntax + +Use `{variable-name}` format for placeholders: + +```json +"welcome": "Welcome {name}!" +``` + +### 2. Plural Forms + +Use MessageFormat syntax for plural forms: + +```json +"item-count": "{count} {count, select, 1{item} other{items}}" +``` + +### 3. HTML Content + +You can include HTML tags in translations: + +```json +"link": "Visit our website" +``` + +### 4. Consistent Key Names + +All language files should have the same key structure to ensure consistency. + +## Language Switching + +Users can switch languages through: +- Language switching option in the user menu +- Language settings in the Settings page +- Automatic browser language detection + +## Building + +After modifying translation files, you need to rebuild the frontend: + +```bash +cd frontend +yarn build +``` + +## Troubleshooting + +### Common Issues + +- **Translations not showing**: Check if JSON syntax is correct +- **Showing "(MISSING: ...)"**: Check if translation key exists in the language file +- **Build fails**: Check webpack configuration and file paths +- **New language not detected**: Verify language code is added to all necessary configuration files + +### Validation + +Before submitting changes: + +1. **Validate JSON syntax**: Ensure all language files have valid JSON syntax +2. **Check key consistency**: Verify all language files have the same key structure +3. **Test in browser**: Test language switching functionality +4. **Build successfully**: Ensure the frontend builds without errors + +### Development Tips + +- Use a JSON validator to check syntax +- Keep translation keys descriptive and organized +- Test with different browser language settings +- Consider cultural context when translating, not just literal translation +- Maintain consistent terminology across the interface + +## Contributing + +When contributing translations: + +1. **Fork the repository** +2. **Create a feature branch** for your translation work +3. **Follow the existing key structure** in translation files +4. **Test your changes** thoroughly +5. **Submit a pull request** with clear description of changes + +### Translation Guidelines + +- Keep translations concise and clear +- Maintain the same tone as the English version +- Consider UI space constraints +- Use appropriate terminology for technical concepts +- Be consistent with existing translations in the same language + +--- + +For technical implementation details, see the main project documentation and the `frontend/js/app/i18n.js` file. \ No newline at end of file diff --git a/frontend/js/i18n/en-US.json b/frontend/js/i18n/en-US.json new file mode 100644 index 000000000..622a4381b --- /dev/null +++ b/frontend/js/i18n/en-US.json @@ -0,0 +1,305 @@ +{ + "str": { + "email-address": "Email address", + "username": "Username", + "password": "Password", + "sign-in": "Sign in", + "sign-out": "Sign out", + "try-again": "Try again", + "name": "Name", + "email": "Email", + "roles": "Roles", + "created-on": "Created: {date}", + "save": "Save", + "cancel": "Cancel", + "close": "Close", + "enable": "Enable", + "disable": "Disable", + "sure": "Yes I'm Sure", + "disabled": "Disabled", + "choose-file": "Choose file", + "source": "Source", + "destination": "Destination", + "ssl": "SSL", + "access": "Access", + "public": "Public", + "edit": "Edit", + "delete": "Delete", + "logs": "Logs", + "status": "Status", + "online": "Online", + "offline": "Offline", + "unknown": "Unknown", + "expires": "Expires", + "value": "Value", + "please-wait": "Please wait...", + "all": "All", + "any": "Any" + }, + "login": { + "title": "Login to your account" + }, + "main": { + "app": "Nginx Proxy Manager", + "version": "v{version}", + "welcome": "Welcome to Nginx Proxy Manager", + "logged-in": "You are logged in as {name}", + "unknown-error": "Error loading stuff. Please reload the app.", + "unknown-user": "Unknown User", + "sign-in-as": "Sign back in as {name}" + }, + "roles": { + "title": "Roles", + "admin": "Administrator", + "user": "Apache Helicopter" + }, + "menu": { + "dashboard": "Dashboard", + "hosts": "Hosts", + "language": "Language", + "switch-to-english": "Switch to English", + "switch-to-chinese": "切换到中文" + }, + "footer": { + "fork-me": "Fork me on Github", + "copy": "© 2025 jc21.com.", + "theme": "Theme by Tabler" + }, + "dashboard": { + "title": "Hi {name}" + }, + "all-hosts": { + "empty-subtitle": "{manage, select, true{Why don't you create one?} other{And you don't have permission to create one.}}", + "details": "Details", + "enable-ssl": "Enable SSL", + "force-ssl": "Force SSL", + "http2-support": "HTTP/2 Support", + "domain-names": "Domain Names", + "cert-provider": "Certificate Provider", + "block-exploits": "Block Common Exploits", + "caching-enabled": "Cache Assets", + "ssl-certificate": "SSL Certificate", + "none": "None", + "new-cert": "Request a new SSL Certificate", + "with-le": "with Let's Encrypt", + "no-ssl": "This host will not use HTTPS", + "advanced": "Advanced", + "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", + "advanced-config": "Custom Nginx Configuration", + "advanced-config-var-headline": "These proxy details are available as nginx variables:", + "advanced-config-header-info": "Please note, that any add_header or set_header directives added here will not be used by nginx. You will have to add a custom location '/' and add the header in the custom config there.", + "hsts-enabled": "HSTS Enabled", + "hsts-subdomains": "HSTS Subdomains", + "locations": "Custom locations" + }, + "locations": { + "new_location": "Add location", + "path": "/path", + "location_label": "Define location", + "delete": "Delete" + }, + "ssl": { + "letsencrypt": "Let's Encrypt", + "other": "Custom", + "none": "HTTP only", + "letsencrypt-email": "Email Address for Let's Encrypt", + "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", + "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", + "hosts-warning": "These domains must be already configured to point to this installation", + "no-wildcard-without-dns": "Cannot request Let's Encrypt Certificate for wildcard domains when not using DNS challenge", + "dns-challenge": "Use a DNS Challenge", + "certbot-warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.", + "dns-provider": "DNS Provider", + "please-choose": "Please Choose...", + "credentials-file-content": "Credentials File Content", + "credentials-file-content-info": "This plugin requires a configuration file containing an API token or other credentials to your provider", + "stored-as-plaintext-info": "This data will be stored as plaintext in the database and in a file!", + "propagation-seconds": "Propagation Seconds", + "propagation-seconds-info": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.", + "processing-info": "Processing... This might take a few minutes.", + "passphrase-protection-support-info": "Key files protected with a passphrase are not supported." + }, + "proxy-hosts": { + "title": "Proxy Hosts", + "empty": "There are no Proxy Hosts", + "add": "Add Proxy Host", + "form-title": "{id, select, undefined{New} other{Edit}} Proxy Host", + "forward-scheme": "Scheme", + "forward-host": "Forward Hostname / IP", + "forward-port": "Forward Port", + "delete": "Delete Proxy Host", + "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", + "help-title": "What is a Proxy Host?", + "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", + "access-list": "Access List", + "allow-websocket-upgrade": "Websockets Support", + "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", + "custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/", + "search": "Search Host…" + }, + "redirection-hosts": { + "title": "Redirection Hosts", + "empty": "There are no Redirection Hosts", + "add": "Add Redirection Host", + "form-title": "{id, select, undefined{New} other{Edit}} Redirection Host", + "forward-scheme": "Scheme", + "forward-http-status-code": "HTTP Code", + "forward-domain": "Forward Domain", + "preserve-path": "Preserve Path", + "delete": "Delete Redirection Host", + "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?", + "help-title": "What is a Redirection Host?", + "help-content": "A Redirection Host will redirect requests from the incoming domain and push the viewer to another domain.\nThe most common reason to use this type of host is when your website changes domains but you still have search engine or referrer links pointing to the old domain.", + "search": "Search Host…" + }, + "dead-hosts": { + "title": "404 Hosts", + "empty": "There are no 404 Hosts", + "add": "Add 404 Host", + "form-title": "{id, select, undefined{New} other{Edit}} 404 Host", + "delete": "Delete 404 Host", + "delete-confirm": "Are you sure you want to delete this 404 Host?", + "help-title": "What is a 404 Host?", + "help-content": "A 404 Host is simply a host setup that shows a 404 page.\nThis can be useful when your domain is listed in search engines and you want to provide a nicer error page or specifically to tell the search indexers that the domain pages no longer exist.\nAnother benefit of having this host is to track the logs for hits to it and view the referrers.", + "search": "Search Host…" + }, + "streams": { + "title": "Streams", + "empty": "There are no Streams", + "add": "Add Stream", + "form-title": "{id, select, undefined{New} other{Edit}} Stream", + "incoming-port": "Incoming Port", + "forwarding-host": "Forward Host", + "forwarding-port": "Forward Port", + "tcp-forwarding": "TCP Forwarding", + "udp-forwarding": "UDP Forwarding", + "forward-type-error": "At least one type of protocol must be enabled", + "protocol": "Protocol", + "tcp": "TCP", + "udp": "UDP", + "delete": "Delete Stream", + "delete-confirm": "Are you sure you want to delete this Stream?", + "help-title": "What is a Stream?", + "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", + "search": "Search Incoming Port…", + "ssl-certificate": "SSL Certificate for TCP Forwarding", + "tcp+ssl": "TCP+SSL" + }, + "certificates": { + "title": "SSL Certificates", + "empty": "There are no SSL Certificates", + "add": "Add SSL Certificate", + "form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate", + "delete": "Delete SSL Certificate", + "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.", + "help-title": "SSL Certificates", + "help-content": "SSL certificates (correctly known as TLS Certificates) are a form of encryption key which allows your site to be encrypted for the end user.\nNPM uses a service called Let's Encrypt to issue SSL certificates for free.\nIf you have any sort of personal information, passwords, or sensitive data behind NPM, it's probably a good idea to use a certificate.\nNPM also supports DNS authentication for if you're not running your site facing the internet, or if you just want a wildcard certificate.", + "other-certificate": "Certificate", + "other-certificate-key": "Certificate Key", + "other-intermediate-certificate": "Intermediate Certificate", + "force-renew": "Renew Now", + "test-reachability": "Test Server Reachability", + "reachability-title": "Test Server Reachability", + "reachability-info": "Test whether the domains are reachable from the public internet using Site24x7. This is not necessary when using the DNS Challenge.", + "reachability-failed-to-reach-api": "Communication with the API failed, is NPM running correctly?", + "reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.", + "reachability-ok": "Your server is reachable and creating certificates should be possible.", + "reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.", + "reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.", + "reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", + "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", + "download": "Download", + "renew-title": "Renew Let's Encrypt Certificate", + "search": "Search Certificate…", + "in-use": "In use", + "inactive": "Inactive", + "active-domain_names": "Active domain names" + }, + "access-lists": { + "title": "Access Lists", + "empty": "There are no Access Lists", + "add": "Add Access List", + "form-title": "{id, select, undefined{New} other{Edit}} Access List", + "delete": "Delete Access List", + "delete-confirm": "Are you sure you want to delete this access list?", + "public": "Publicly Accessible", + "public-sub": "No Access Restrictions", + "help-title": "What is an Access List?", + "help-content": "Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or that you want to protect from access by unknown clients.", + "item-count": "{count} {count, select, 1{User} other{Users}}", + "client-count": "{count} {count, select, 1{Rule} other{Rules}}", + "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}", + "delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion.", + "details": "Details", + "authorization": "Authorization", + "access": "Access", + "satisfy": "Satisfy", + "satisfy-any": "Satisfy Any", + "pass-auth": "Pass Auth to Host", + "access-add": "Add", + "auth-add": "Add", + "search": "Search Access…" + }, + "users": { + "title": "Users", + "default_error": "Default email address must be changed", + "add": "Add User", + "nickname": "Nickname", + "full-name": "Full Name", + "edit-details": "Edit Details", + "change-password": "Change Password", + "edit-permissions": "Edit Permissions", + "sign-in-as": "Sign in as User", + "form-title": "{id, select, undefined{New} other{Edit}} User", + "delete": "Delete {name, select, undefined{User} other{{name}}}", + "delete-confirm": "Are you sure you want to delete {name}?", + "password-title": "Change Password{self, select, false{ for {name}} other{}}", + "current-password": "Current Password", + "new-password": "New Password", + "confirm-password": "Confirm Password", + "permissions-title": "Permissions for {name}", + "admin-perms": "This user is an Administrator and some items cannot be altered", + "perms-visibility": "Item Visibility", + "perms-visibility-user": "Created Items Only", + "perms-visibility-all": "All Items", + "perm-manage": "Manage", + "perm-view": "View Only", + "perm-hidden": "Hidden", + "search": "Search User…" + }, + "audit-log": { + "title": "Audit Log", + "empty": "There are no logs.", + "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here.", + "proxy-host": "Proxy Host", + "redirection-host": "Redirection Host", + "dead-host": "404 Host", + "stream": "Stream", + "user": "User", + "certificate": "Certificate", + "access-list": "Access List", + "created": "Created {name}", + "updated": "Updated {name}", + "deleted": "Deleted {name}", + "enabled": "Enabled {name}", + "disabled": "Disabled {name}", + "renewed": "Renewed {name}", + "meta-title": "Details for Event", + "view-meta": "View Details", + "date": "Date", + "search": "Search Log…" + }, + "settings": { + "title": "Settings", + "default-site": "Default Site", + "default-site-description": "What to show when Nginx is hit with an unknown Host", + "default-site-congratulations": "Congratulations Page", + "default-site-404": "404 Page", + "default-site-444": "No Response (444)", + "default-site-html": "Custom Page", + "default-site-redirect": "Redirect", + "language": "Interface Language", + "language-description": "Choose interface display language", + "current-language": "Current Language" + } +} diff --git a/frontend/js/i18n/fr-FR.json b/frontend/js/i18n/fr-FR.json new file mode 100644 index 000000000..2de5c735a --- /dev/null +++ b/frontend/js/i18n/fr-FR.json @@ -0,0 +1,306 @@ +{ + "str": { + "email-address": "Adresse e-mail", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "sign-in": "Se connecter", + "sign-out": "Se déconnecter", + "try-again": "Réessayer", + "name": "Nom", + "email": "E-mail", + "roles": "Rôles", + "created-on": "Créé le : {date}", + "save": "Enregistrer", + "cancel": "Annuler", + "close": "Fermer", + "enable": "Activer", + "disable": "Désactiver", + "sure": "Oui, j'en suis sûr", + "disabled": "Désactivé", + "choose-file": "Choisir un fichier", + "source": "Source", + "destination": "Destination", + "ssl": "SSL", + "access": "Accès", + "public": "Public", + "edit": "Modifier", + "delete": "Supprimer", + "logs": "Journaux", + "status": "Statut", + "online": "En ligne", + "offline": "Hors ligne", + "unknown": "Inconnu", + "expires": "Expire", + "value": "Valeur", + "please-wait": "Veuillez patienter...", + "all": "Tout", + "any": "N'importe lequel" + }, + "login": { + "title": "Connectez-vous à votre compte" + }, + "main": { + "app": "Nginx Proxy Manager", + "version": "v{version}", + "welcome": "Bienvenue dans Nginx Proxy Manager", + "logged-in": "Vous êtes connecté en tant que {name}", + "unknown-error": "Erreur lors du chargement. Veuillez recharger l'application.", + "unknown-user": "Utilisateur inconnu", + "sign-in-as": "Se reconnecter en tant que {name}" + }, + "roles": { + "title": "Rôles", + "admin": "Administrateur", + "user": "Utilisateur" + }, + "menu": { + "dashboard": "Tableau de bord", + "hosts": "Hôtes", + "language": "Langue", + "switch-to-english": "Passer à l'anglais", + "switch-to-chinese": "切换到中文", + "switch-to-french": "Passer au français" + }, + "footer": { + "fork-me": "Forkez-moi sur Github", + "copy": "© 2025 jc21.com.", + "theme": "Thème par Tabler" + }, + "dashboard": { + "title": "Bonjour {name}" + }, + "all-hosts": { + "empty-subtitle": "{manage, select, true{Pourquoi ne pas en créer un ?} other{Et vous n'avez pas la permission d'en créer un.}}", + "details": "Détails", + "enable-ssl": "Activer SSL", + "force-ssl": "Forcer SSL", + "http2-support": "Support HTTP/2", + "domain-names": "Noms de domaine", + "cert-provider": "Fournisseur de certificat", + "block-exploits": "Bloquer les exploits communs", + "caching-enabled": "Mettre en cache les ressources", + "ssl-certificate": "Certificat SSL", + "none": "Aucun", + "new-cert": "Demander un nouveau certificat SSL", + "with-le": "avec Let's Encrypt", + "no-ssl": "Cet hôte n'utilisera pas HTTPS", + "advanced": "Avancé", + "advanced-warning": "Entrez votre configuration Nginx personnalisée ici à vos propres risques !", + "advanced-config": "Configuration Nginx personnalisée", + "advanced-config-var-headline": "Ces détails de proxy sont disponibles comme variables nginx :", + "advanced-config-header-info": "Veuillez noter que toute directive add_header ou set_header ajoutée ici ne sera pas utilisée par nginx. Vous devrez ajouter un emplacement personnalisé '/' et ajouter l'en-tête dans la configuration personnalisée.", + "hsts-enabled": "HSTS activé", + "hsts-subdomains": "HSTS sous-domaines", + "locations": "Emplacements personnalisés" + }, + "locations": { + "new_location": "Ajouter un emplacement", + "path": "/chemin", + "location_label": "Définir l'emplacement", + "delete": "Supprimer" + }, + "ssl": { + "letsencrypt": "Let's Encrypt", + "other": "Personnalisé", + "none": "HTTP uniquement", + "letsencrypt-email": "Adresse e-mail pour Let's Encrypt", + "letsencrypt-agree": "J'accepte les Conditions d'utilisation de Let's Encrypt", + "delete-ssl": "Les certificats SSL attachés ne seront PAS supprimés, ils devront être supprimés manuellement.", + "hosts-warning": "Ces domaines doivent déjà être configurés pour pointer vers cette installation", + "no-wildcard-without-dns": "Impossible de demander un certificat Let's Encrypt pour les domaines wildcard sans utiliser le défi DNS", + "dns-challenge": "Utiliser un défi DNS", + "certbot-warning": "Cette section nécessite quelques connaissances sur Certbot et ses plugins DNS. Veuillez consulter la documentation des plugins respectifs.", + "dns-provider": "Fournisseur DNS", + "please-choose": "Veuillez choisir...", + "credentials-file-content": "Contenu du fichier d'identifiants", + "credentials-file-content-info": "Ce plugin nécessite un fichier de configuration contenant un token API ou d'autres identifiants pour votre fournisseur", + "stored-as-plaintext-info": "Ces données seront stockées en texte brut dans la base de données et dans un fichier !", + "propagation-seconds": "Secondes de propagation", + "propagation-seconds-info": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS.", + "processing-info": "Traitement en cours... Cela peut prendre quelques minutes.", + "passphrase-protection-support-info": "Les fichiers de clés protégés par une phrase de passe ne sont pas pris en charge." + }, + "proxy-hosts": { + "title": "Hôtes proxy", + "empty": "Il n'y a pas d'hôtes proxy", + "add": "Ajouter un hôte proxy", + "form-title": "{id, select, undefined{Nouvel} other{Modifier}} hôte proxy", + "forward-scheme": "Schéma", + "forward-host": "Nom d'hôte de transfert / IP", + "forward-port": "Port de transfert", + "delete": "Supprimer l'hôte proxy", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer l'hôte proxy pour : {domains} ?", + "help-title": "Qu'est-ce qu'un hôte proxy ?", + "help-content": "Un hôte proxy est le point d'entrée pour un service web que vous voulez transférer.\nIl fournit une terminaison SSL optionnelle pour votre service qui pourrait ne pas avoir de support SSL intégré.\nLes hôtes proxy sont l'utilisation la plus courante pour le Nginx Proxy Manager.", + "access-list": "Liste d'accès", + "allow-websocket-upgrade": "Support des WebSockets", + "ignore-invalid-upstream-ssl": "Ignorer le SSL amont invalide", + "custom-forward-host-help": "Ajouter un chemin pour le transfert de sous-dossier.\nExemple : 203.0.113.25/chemin/", + "search": "Rechercher un hôte…" + }, + "redirection-hosts": { + "title": "Hôtes de redirection", + "empty": "Il n'y a pas d'hôtes de redirection", + "add": "Ajouter un hôte de redirection", + "form-title": "{id, select, undefined{Nouvel} other{Modifier}} hôte de redirection", + "forward-scheme": "Schéma", + "forward-http-status-code": "Code HTTP", + "forward-domain": "Domaine de transfert", + "preserve-path": "Préserver le chemin", + "delete": "Supprimer l'hôte de redirection", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer l'hôte de redirection pour : {domains} ?", + "help-title": "Qu'est-ce qu'un hôte de redirection ?", + "help-content": "Un hôte de redirection redirigera les demandes du domaine entrant et poussera le visiteur vers un autre domaine.\nLa raison la plus courante d'utiliser ce type d'hôte est lorsque votre site web change de domaine mais que vous avez encore des liens de moteurs de recherche ou de référents pointant vers l'ancien domaine.", + "search": "Rechercher un hôte…" + }, + "dead-hosts": { + "title": "Hôtes 404", + "empty": "Il n'y a pas d'hôtes 404", + "add": "Ajouter un hôte 404", + "form-title": "{id, select, undefined{Nouvel} other{Modifier}} hôte 404", + "delete": "Supprimer l'hôte 404", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer cet hôte 404 ?", + "help-title": "Qu'est-ce qu'un hôte 404 ?", + "help-content": "Un hôte 404 est simplement une configuration d'hôte qui affiche une page 404.\nCela peut être utile lorsque votre domaine est listé dans les moteurs de recherche et que vous voulez fournir une page d'erreur plus agréable ou spécifiquement dire aux indexeurs de recherche que les pages du domaine n'existent plus.\nUn autre avantage d'avoir cet hôte est de suivre les journaux des accès et de voir les référents.", + "search": "Rechercher un hôte…" + }, + "streams": { + "title": "Flux", + "empty": "Il n'y a pas de flux", + "add": "Ajouter un flux", + "form-title": "{id, select, undefined{Nouveau} other{Modifier}} flux", + "incoming-port": "Port entrant", + "forwarding-host": "Hôte de transfert", + "forwarding-port": "Port de transfert", + "tcp-forwarding": "Transfert TCP", + "udp-forwarding": "Transfert UDP", + "forward-type-error": "Au moins un type de protocole doit être activé", + "protocol": "Protocole", + "tcp": "TCP", + "udp": "UDP", + "delete": "Supprimer le flux", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer ce flux ?", + "help-title": "Qu'est-ce qu'un flux ?", + "help-content": "Une fonctionnalité relativement nouvelle pour Nginx, un flux servira à transférer le trafic TCP/UDP directement vers un autre ordinateur sur le réseau.\nSi vous exécutez des serveurs de jeux, FTP ou SSH, cela peut être pratique.", + "search": "Rechercher un port entrant…", + "ssl-certificate": "Certificat SSL pour le transfert TCP", + "tcp+ssl": "TCP+SSL" + }, + "certificates": { + "title": "Certificats SSL", + "empty": "Il n'y a pas de certificats SSL", + "add": "Ajouter un certificat SSL", + "form-title": "Ajouter un certificat {provider, select, letsencrypt{Let's Encrypt} other{personnalisé}}", + "delete": "Supprimer le certificat SSL", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer ce certificat SSL ? Tous les hôtes l'utilisant devront être mis à jour ultérieurement.", + "help-title": "Certificats SSL", + "help-content": "Les certificats SSL (correctement appelés certificats TLS) sont une forme de clé de chiffrement qui permet à votre site d'être chiffré pour l'utilisateur final.\nNPM utilise un service appelé Let's Encrypt pour émettre des certificats SSL gratuitement.\nSi vous avez des informations personnelles, mots de passe ou données sensibles derrière NPM, c'est probablement une bonne idée d'utiliser un certificat.\nNPM prend également en charge l'authentification DNS pour si vous n'exécutez pas votre site face à Internet, ou si vous voulez juste un certificat wildcard.", + "other-certificate": "Certificat", + "other-certificate-key": "Clé de certificat", + "other-intermediate-certificate": "Certificat intermédiaire", + "force-renew": "Renouveler maintenant", + "test-reachability": "Tester l'accessibilité du serveur", + "reachability-title": "Tester l'accessibilité du serveur", + "reachability-info": "Tester si les domaines sont accessibles depuis Internet public en utilisant Site24x7. Ce n'est pas nécessaire lors de l'utilisation du défi DNS.", + "reachability-failed-to-reach-api": "La communication avec l'API a échoué, NPM fonctionne-t-il correctement ?", + "reachability-failed-to-check": "Échec de la vérification de l'accessibilité en raison d'une erreur de communication avec site24x7.com.", + "reachability-ok": "Votre serveur est accessible et la création de certificats devrait être possible.", + "reachability-404": "Il y a un serveur trouvé à ce domaine mais il ne semble pas être Nginx Proxy Manager. Assurez-vous que votre domaine pointe vers l'IP où votre instance NPM fonctionne.", + "reachability-not-resolved": "Il n'y a pas de serveur disponible à ce domaine. Assurez-vous que votre domaine existe et pointe vers l'IP où votre instance NPM fonctionne et si nécessaire le port 80 est transféré dans votre routeur.", + "reachability-wrong-data": "Il y a un serveur trouvé à ce domaine mais il a renvoyé des données inattendues. Est-ce le serveur NPM ? Assurez-vous que votre domaine pointe vers l'IP où votre instance NPM fonctionne.", + "reachability-other": "Il y a un serveur trouvé à ce domaine mais il a renvoyé un code de statut inattendu {code}. Est-ce le serveur NPM ? Assurez-vous que votre domaine pointe vers l'IP où votre instance NPM fonctionne.", + "download": "Télécharger", + "renew-title": "Renouveler le certificat Let's Encrypt", + "search": "Rechercher un certificat…", + "in-use": "En cours d'utilisation", + "inactive": "Inactif", + "active-domain_names": "Noms de domaine actifs" + }, + "access-lists": { + "title": "Listes d'accès", + "empty": "Il n'y a pas de listes d'accès", + "add": "Ajouter une liste d'accès", + "form-title": "{id, select, undefined{Nouvelle} other{Modifier}} liste d'accès", + "delete": "Supprimer la liste d'accès", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer cette liste d'accès ?", + "public": "Accessible publiquement", + "public-sub": "Aucune restriction d'accès", + "help-title": "Qu'est-ce qu'une liste d'accès ?", + "help-content": "Les listes d'accès fournissent une liste noire ou blanche d'adresses IP client spécifiques ainsi qu'une authentification pour les hôtes proxy via l'authentification HTTP de base.\nVous pouvez configurer plusieurs règles client, noms d'utilisateur et mots de passe pour une seule liste d'accès puis l'appliquer à un hôte proxy.\nC'est très utile pour les services web transférés qui n'ont pas de mécanismes d'authentification intégrés ou que vous voulez protéger de l'accès par des clients inconnus.", + "item-count": "{count} {count, select, 1{utilisateur} other{utilisateurs}}", + "client-count": "{count} {count, select, 1{règle} other{règles}}", + "proxy-host-count": "{count} {count, select, 1{hôte proxy} other{hôtes proxy}}", + "delete-has-hosts": "Cette liste d'accès est associée à {count} hôtes proxy. Ils deviendront publiquement disponibles lors de la suppression.", + "details": "Détails", + "authorization": "Autorisation", + "access": "Accès", + "satisfy": "Satisfaire", + "satisfy-any": "Satisfaire n'importe lequel", + "pass-auth": "Passer l'authentification à l'hôte", + "access-add": "Ajouter", + "auth-add": "Ajouter", + "search": "Rechercher un accès…" + }, + "users": { + "title": "Utilisateurs", + "default_error": "L'adresse e-mail par défaut doit être changée", + "add": "Ajouter un utilisateur", + "nickname": "Surnom", + "full-name": "Nom complet", + "edit-details": "Modifier les détails", + "change-password": "Changer le mot de passe", + "edit-permissions": "Modifier les permissions", + "sign-in-as": "Se connecter en tant qu'utilisateur", + "form-title": "{id, select, undefined{Nouvel} other{Modifier}} utilisateur", + "delete": "Supprimer {name, select, undefined{l'utilisateur} other{{name}}}", + "delete-confirm": "Êtes-vous sûr de vouloir supprimer {name} ?", + "password-title": "Changer le mot de passe{self, select, false{ pour {name}} other{}}", + "current-password": "Mot de passe actuel", + "new-password": "Nouveau mot de passe", + "confirm-password": "Confirmer le mot de passe", + "permissions-title": "Permissions pour {name}", + "admin-perms": "Cet utilisateur est un administrateur et certains éléments ne peuvent pas être modifiés", + "perms-visibility": "Visibilité des éléments", + "perms-visibility-user": "Éléments créés uniquement", + "perms-visibility-all": "Tous les éléments", + "perm-manage": "Gérer", + "perm-view": "Lecture seule", + "perm-hidden": "Masqué", + "search": "Rechercher un utilisateur…" + }, + "audit-log": { + "title": "Journal d'audit", + "empty": "Il n'y a pas de journaux.", + "empty-subtitle": "Dès que vous ou un autre utilisateur changez quelque chose, l'historique de ces événements apparaîtra ici.", + "proxy-host": "Hôte proxy", + "redirection-host": "Hôte de redirection", + "dead-host": "Hôte 404", + "stream": "Flux", + "user": "Utilisateur", + "certificate": "Certificat", + "access-list": "Liste d'accès", + "created": "Créé {name}", + "updated": "Mis à jour {name}", + "deleted": "Supprimé {name}", + "enabled": "Activé {name}", + "disabled": "Désactivé {name}", + "renewed": "Renouvelé {name}", + "meta-title": "Détails de l'événement", + "view-meta": "Voir les détails", + "date": "Date", + "search": "Rechercher dans le journal…" + }, + "settings": { + "title": "Paramètres", + "default-site": "Site par défaut", + "default-site-description": "Que montrer quand Nginx reçoit un hôte inconnu", + "default-site-congratulations": "Page de félicitations", + "default-site-404": "Page 404", + "default-site-444": "Aucune réponse (444)", + "default-site-html": "Page personnalisée", + "default-site-redirect": "Redirection", + "language": "Langue de l'interface", + "language-description": "Choisir la langue d'affichage de l'interface", + "current-language": "Langue actuelle" + } +} 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/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/messages.json b/frontend/js/i18n/messages.json deleted file mode 100644 index d409942ff..000000000 --- a/frontend/js/i18n/messages.json +++ /dev/null @@ -1,301 +0,0 @@ -{ - "en": { - "str": { - "email-address": "Email address", - "username": "Username", - "password": "Password", - "sign-in": "Sign in", - "sign-out": "Sign out", - "try-again": "Try again", - "name": "Name", - "email": "Email", - "roles": "Roles", - "created-on": "Created: {date}", - "save": "Save", - "cancel": "Cancel", - "close": "Close", - "enable": "Enable", - "disable": "Disable", - "sure": "Yes I'm Sure", - "disabled": "Disabled", - "choose-file": "Choose file", - "source": "Source", - "destination": "Destination", - "ssl": "SSL", - "access": "Access", - "public": "Public", - "edit": "Edit", - "delete": "Delete", - "logs": "Logs", - "status": "Status", - "online": "Online", - "offline": "Offline", - "unknown": "Unknown", - "expires": "Expires", - "value": "Value", - "please-wait": "Please wait...", - "all": "All", - "any": "Any" - }, - "login": { - "title": "Login to your account" - }, - "main": { - "app": "Nginx Proxy Manager", - "version": "v{version}", - "welcome": "Welcome to Nginx Proxy Manager", - "logged-in": "You are logged in as {name}", - "unknown-error": "Error loading stuff. Please reload the app.", - "unknown-user": "Unknown User", - "sign-in-as": "Sign back in as {name}" - }, - "roles": { - "title": "Roles", - "admin": "Administrator", - "user": "Apache Helicopter" - }, - "menu": { - "dashboard": "Dashboard", - "hosts": "Hosts" - }, - "footer": { - "fork-me": "Fork me on Github", - "copy": "© 2025 jc21.com.", - "theme": "Theme by Tabler" - }, - "dashboard": { - "title": "Hi {name}" - }, - "all-hosts": { - "empty-subtitle": "{manage, select, true{Why don't you create one?} other{And you don't have permission to create one.}}", - "details": "Details", - "enable-ssl": "Enable SSL", - "force-ssl": "Force SSL", - "http2-support": "HTTP/2 Support", - "domain-names": "Domain Names", - "cert-provider": "Certificate Provider", - "block-exploits": "Block Common Exploits", - "caching-enabled": "Cache Assets", - "ssl-certificate": "SSL Certificate", - "none": "None", - "new-cert": "Request a new SSL Certificate", - "with-le": "with Let's Encrypt", - "no-ssl": "This host will not use HTTPS", - "advanced": "Advanced", - "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", - "advanced-config": "Custom Nginx Configuration", - "advanced-config-var-headline": "These proxy details are available as nginx variables:", - "advanced-config-header-info": "Please note, that any add_header or set_header directives added here will not be used by nginx. You will have to add a custom location '/' and add the header in the custom config there.", - "hsts-enabled": "HSTS Enabled", - "hsts-subdomains": "HSTS Subdomains", - "locations": "Custom locations" - }, - "locations": { - "new_location": "Add location", - "path": "/path", - "location_label": "Define location", - "delete": "Delete" - }, - "ssl": { - "letsencrypt": "Let's Encrypt", - "other": "Custom", - "none": "HTTP only", - "letsencrypt-email": "Email Address for Let's Encrypt", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", - "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", - "hosts-warning": "These domains must be already configured to point to this installation", - "no-wildcard-without-dns": "Cannot request Let's Encrypt Certificate for wildcard domains when not using DNS challenge", - "dns-challenge": "Use a DNS Challenge", - "certbot-warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.", - "dns-provider": "DNS Provider", - "please-choose": "Please Choose...", - "credentials-file-content": "Credentials File Content", - "credentials-file-content-info": "This plugin requires a configuration file containing an API token or other credentials to your provider", - "stored-as-plaintext-info": "This data will be stored as plaintext in the database and in a file!", - "propagation-seconds": "Propagation Seconds", - "propagation-seconds-info": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.", - "processing-info": "Processing... This might take a few minutes.", - "passphrase-protection-support-info": "Key files protected with a passphrase are not supported." - }, - "proxy-hosts": { - "title": "Proxy Hosts", - "empty": "There are no Proxy Hosts", - "add": "Add Proxy Host", - "form-title": "{id, select, undefined{New} other{Edit}} Proxy Host", - "forward-scheme": "Scheme", - "forward-host": "Forward Hostname / IP", - "forward-port": "Forward Port", - "delete": "Delete Proxy Host", - "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", - "help-title": "What is a Proxy Host?", - "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", - "access-list": "Access List", - "allow-websocket-upgrade": "Websockets Support", - "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", - "custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/", - "search": "Search Host…" - }, - "redirection-hosts": { - "title": "Redirection Hosts", - "empty": "There are no Redirection Hosts", - "add": "Add Redirection Host", - "form-title": "{id, select, undefined{New} other{Edit}} Redirection Host", - "forward-scheme": "Scheme", - "forward-http-status-code": "HTTP Code", - "forward-domain": "Forward Domain", - "preserve-path": "Preserve Path", - "delete": "Delete Redirection Host", - "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?", - "help-title": "What is a Redirection Host?", - "help-content": "A Redirection Host will redirect requests from the incoming domain and push the viewer to another domain.\nThe most common reason to use this type of host is when your website changes domains but you still have search engine or referrer links pointing to the old domain.", - "search": "Search Host…" - }, - "dead-hosts": { - "title": "404 Hosts", - "empty": "There are no 404 Hosts", - "add": "Add 404 Host", - "form-title": "{id, select, undefined{New} other{Edit}} 404 Host", - "delete": "Delete 404 Host", - "delete-confirm": "Are you sure you want to delete this 404 Host?", - "help-title": "What is a 404 Host?", - "help-content": "A 404 Host is simply a host setup that shows a 404 page.\nThis can be useful when your domain is listed in search engines and you want to provide a nicer error page or specifically to tell the search indexers that the domain pages no longer exist.\nAnother benefit of having this host is to track the logs for hits to it and view the referrers.", - "search": "Search Host…" - }, - "streams": { - "title": "Streams", - "empty": "There are no Streams", - "add": "Add Stream", - "form-title": "{id, select, undefined{New} other{Edit}} Stream", - "incoming-port": "Incoming Port", - "forwarding-host": "Forward Host", - "forwarding-port": "Forward Port", - "tcp-forwarding": "TCP Forwarding", - "udp-forwarding": "UDP Forwarding", - "forward-type-error": "At least one type of protocol must be enabled", - "protocol": "Protocol", - "tcp": "TCP", - "udp": "UDP", - "delete": "Delete Stream", - "delete-confirm": "Are you sure you want to delete this Stream?", - "help-title": "What is a Stream?", - "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", - "search": "Search Incoming Port…", - "ssl-certificate": "SSL Certificate for TCP Forwarding", - "tcp+ssl": "TCP+SSL" - }, - "certificates": { - "title": "SSL Certificates", - "empty": "There are no SSL Certificates", - "add": "Add SSL Certificate", - "form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate", - "delete": "Delete SSL Certificate", - "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.", - "help-title": "SSL Certificates", - "help-content": "SSL certificates (correctly known as TLS Certificates) are a form of encryption key which allows your site to be encrypted for the end user.\nNPM uses a service called Let's Encrypt to issue SSL certificates for free.\nIf you have any sort of personal information, passwords, or sensitive data behind NPM, it's probably a good idea to use a certificate.\nNPM also supports DNS authentication for if you're not running your site facing the internet, or if you just want a wildcard certificate.", - "other-certificate": "Certificate", - "other-certificate-key": "Certificate Key", - "other-intermediate-certificate": "Intermediate Certificate", - "force-renew": "Renew Now", - "test-reachability": "Test Server Reachability", - "reachability-title": "Test Server Reachability", - "reachability-info": "Test whether the domains are reachable from the public internet using Site24x7. This is not necessary when using the DNS Challenge.", - "reachability-failed-to-reach-api": "Communication with the API failed, is NPM running correctly?", - "reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.", - "reachability-ok": "Your server is reachable and creating certificates should be possible.", - "reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.", - "reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "download": "Download", - "renew-title": "Renew Let's Encrypt Certificate", - "search": "Search Certificate…", - "in-use" : "In use", - "inactive": "Inactive", - "active-domain_names": "Active domain names" - }, - "access-lists": { - "title": "Access Lists", - "empty": "There are no Access Lists", - "add": "Add Access List", - "form-title": "{id, select, undefined{New} other{Edit}} Access List", - "delete": "Delete Access List", - "delete-confirm": "Are you sure you want to delete this access list?", - "public": "Publicly Accessible", - "public-sub": "No Access Restrictions", - "help-title": "What is an Access List?", - "help-content": "Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or that you want to protect from access by unknown clients.", - "item-count": "{count} {count, select, 1{User} other{Users}}", - "client-count": "{count} {count, select, 1{Rule} other{Rules}}", - "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}", - "delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion.", - "details": "Details", - "authorization": "Authorization", - "access": "Access", - "satisfy": "Satisfy", - "satisfy-any": "Satisfy Any", - "pass-auth": "Pass Auth to Host", - "access-add": "Add", - "auth-add": "Add", - "search": "Search Access…" - }, - "users": { - "title": "Users", - "default_error": "Default email address must be changed", - "add": "Add User", - "nickname": "Nickname", - "full-name": "Full Name", - "edit-details": "Edit Details", - "change-password": "Change Password", - "edit-permissions": "Edit Permissions", - "sign-in-as": "Sign in as User", - "form-title": "{id, select, undefined{New} other{Edit}} User", - "delete": "Delete {name, select, undefined{User} other{{name}}}", - "delete-confirm": "Are you sure you want to delete {name}?", - "password-title": "Change Password{self, select, false{ for {name}} other{}}", - "current-password": "Current Password", - "new-password": "New Password", - "confirm-password": "Confirm Password", - "permissions-title": "Permissions for {name}", - "admin-perms": "This user is an Administrator and some items cannot be altered", - "perms-visibility": "Item Visibility", - "perms-visibility-user": "Created Items Only", - "perms-visibility-all": "All Items", - "perm-manage": "Manage", - "perm-view": "View Only", - "perm-hidden": "Hidden", - "search": "Search User…" - }, - "audit-log": { - "title": "Audit Log", - "empty": "There are no logs.", - "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here.", - "proxy-host": "Proxy Host", - "redirection-host": "Redirection Host", - "dead-host": "404 Host", - "stream": "Stream", - "user": "User", - "certificate": "Certificate", - "access-list": "Access List", - "created": "Created {name}", - "updated": "Updated {name}", - "deleted": "Deleted {name}", - "enabled": "Enabled {name}", - "disabled": "Disabled {name}", - "renewed": "Renewed {name}", - "meta-title": "Details for Event", - "view-meta": "View Details", - "date": "Date", - "search": "Search Log…" - }, - "settings": { - "title": "Settings", - "default-site": "Default Site", - "default-site-description": "What to show when Nginx is hit with an unknown Host", - "default-site-congratulations": "Congratulations Page", - "default-site-404": "404 Page", - "default-site-444": "No Response (444)", - "default-site-html": "Custom Page", - "default-site-redirect": "Redirect" - } - } -} diff --git a/frontend/js/i18n/pt-PT.json b/frontend/js/i18n/pt-PT.json new file mode 100644 index 000000000..1988212e4 --- /dev/null +++ b/frontend/js/i18n/pt-PT.json @@ -0,0 +1,306 @@ +{ + "str": { + "email-address": "Endereço de e-mail", + "username": "Nome de usuário", + "password": "Senha", + "sign-in": "Entrar", + "sign-out": "Sair", + "try-again": "Tentar novamente", + "name": "Nome", + "email": "E-mail", + "roles": "Funções", + "created-on": "Criado em: {date}", + "save": "Salvar", + "cancel": "Cancelar", + "close": "Fechar", + "enable": "Ativar", + "disable": "Desativar", + "sure": "Sim, tenho certeza", + "disabled": "Desativado", + "choose-file": "Escolher arquivo", + "source": "Origem", + "destination": "Destino", + "ssl": "SSL", + "access": "Acesso", + "public": "Público", + "edit": "Editar", + "delete": "Excluir", + "logs": "Logs", + "status": "Status", + "online": "Online", + "offline": "Offline", + "unknown": "Desconhecido", + "expires": "Expira", + "value": "Valor", + "please-wait": "Por favor, aguarde...", + "all": "Todos", + "any": "Qualquer" + }, + "login": { + "title": "Entre na sua conta" + }, + "main": { + "app": "Nginx Proxy Manager", + "version": "v{version}", + "welcome": "Bem-vindo ao Nginx Proxy Manager", + "logged-in": "Você está logado como {name}", + "unknown-error": "Erro ao carregar. Por favor, recarregue o aplicativo.", + "unknown-user": "Usuário desconhecido", + "sign-in-as": "Entrar novamente como {name}" + }, + "roles": { + "title": "Funções", + "admin": "Administrador", + "user": "Usuário" + }, + "menu": { + "dashboard": "Painel", + "hosts": "Hosts", + "language": "Idioma", + "switch-to-english": "Mudar para inglês", + "switch-to-chinese": "Mudar para chinês", + "switch-to-portuguese": "Mudar para português" + }, + "footer": { + "fork-me": "Fork no Github", + "copy": "© 2025 jc21.com.", + "theme": "Tema por Tabler" + }, + "dashboard": { + "title": "Olá {name}" + }, + "all-hosts": { + "empty-subtitle": "{manage, select, true{Por que não criar um?} other{E você não tem permissão para criar um.}}", + "details": "Detalhes", + "enable-ssl": "Ativar SSL", + "force-ssl": "Forçar SSL", + "http2-support": "Suporte HTTP/2", + "domain-names": "Nomes de domínio", + "cert-provider": "Provedor de certificado", + "block-exploits": "Bloquear exploits comuns", + "caching-enabled": "Cache de recursos", + "ssl-certificate": "Certificado SSL", + "none": "Nenhum", + "new-cert": "Solicitar novo certificado SSL", + "with-le": "com Let's Encrypt", + "no-ssl": "Este host não usará HTTPS", + "advanced": "Avançado", + "advanced-warning": "Digite sua configuração personalizada do Nginx aqui por sua conta e risco!", + "advanced-config": "Configuração personalizada do Nginx", + "advanced-config-var-headline": "Estes detalhes de proxy estão disponíveis como variáveis nginx:", + "advanced-config-header-info": "Note que qualquer diretiva add_header ou set_header adicionada aqui não será usada pelo nginx. Você precisará adicionar um local personalizado '/' e adicionar o cabeçalho na configuração personalizada.", + "hsts-enabled": "HSTS habilitado", + "hsts-subdomains": "HSTS subdomínios", + "locations": "Locais personalizados" + }, + "locations": { + "new_location": "Adicionar local", + "path": "/caminho", + "location_label": "Definir local", + "delete": "Excluir" + }, + "ssl": { + "letsencrypt": "Let's Encrypt", + "other": "Personalizado", + "none": "Apenas HTTP", + "letsencrypt-email": "Endereço de e-mail para Let's Encrypt", + "letsencrypt-agree": "Eu concordo com os Termos de Serviço do Let's Encrypt", + "delete-ssl": "Os certificados SSL anexados NÃO serão removidos, precisarão ser removidos manualmente.", + "hosts-warning": "Estes domínios já devem estar configurados para apontar para esta instalação", + "no-wildcard-without-dns": "Não é possível solicitar certificado Let's Encrypt para domínios curinga sem usar desafio DNS", + "dns-challenge": "Usar desafio DNS", + "certbot-warning": "Esta seção requer algum conhecimento sobre Certbot e seus plugins DNS. Consulte a documentação dos respectivos plugins.", + "dns-provider": "Provedor DNS", + "please-choose": "Por favor, escolha...", + "credentials-file-content": "Conteúdo do arquivo de credenciais", + "credentials-file-content-info": "Este plugin requer um arquivo de configuração contendo um token de API ou outras credenciais para seu provedor", + "stored-as-plaintext-info": "Estes dados serão armazenados como texto simples no banco de dados e em um arquivo!", + "propagation-seconds": "Segundos de propagação", + "propagation-seconds-info": "Deixe vazio para usar o valor padrão do plugin. Número de segundos para aguardar a propagação DNS.", + "processing-info": "Processando... Isso pode levar alguns minutos.", + "passphrase-protection-support-info": "Arquivos de chave protegidos com senha não são suportados." + }, + "proxy-hosts": { + "title": "Hosts proxy", + "empty": "Não há hosts proxy", + "add": "Adicionar host proxy", + "form-title": "{id, select, undefined{Novo} other{Editar}} host proxy", + "forward-scheme": "Esquema", + "forward-host": "Nome do host de encaminhamento / IP", + "forward-port": "Porta de encaminhamento", + "delete": "Excluir host proxy", + "delete-confirm": "Tem certeza de que deseja excluir o host proxy para: {domains}?", + "help-title": "O que é um host proxy?", + "help-content": "Um host proxy é o ponto final de entrada para um serviço web que você deseja encaminhar.\nEle fornece terminação SSL opcional para seu serviço que pode não ter suporte SSL integrado.\nHosts proxy são o uso mais comum para o Nginx Proxy Manager.", + "access-list": "Lista de acesso", + "allow-websocket-upgrade": "Suporte a WebSockets", + "ignore-invalid-upstream-ssl": "Ignorar SSL upstream inválido", + "custom-forward-host-help": "Adicione um caminho para encaminhamento de subpasta.\nExemplo: 203.0.113.25/caminho/", + "search": "Pesquisar host..." + }, + "redirection-hosts": { + "title": "Hosts de redirecionamento", + "empty": "Não há hosts de redirecionamento", + "add": "Adicionar host de redirecionamento", + "form-title": "{id, select, undefined{Novo} other{Editar}} host de redirecionamento", + "forward-scheme": "Esquema", + "forward-http-status-code": "Código HTTP", + "forward-domain": "Domínio de encaminhamento", + "preserve-path": "Preservar caminho", + "delete": "Excluir host de redirecionamento", + "delete-confirm": "Tem certeza de que deseja excluir o host de redirecionamento para: {domains}?", + "help-title": "O que é um host de redirecionamento?", + "help-content": "Um host de redirecionamento redirecionará solicitações do domínio de entrada e empurrará o visualizador para outro domínio.\nA razão mais comum para usar este tipo de host é quando seu site muda de domínio, mas você ainda tem links de mecanismos de busca ou referências apontando para o domínio antigo.", + "search": "Pesquisar host..." + }, + "dead-hosts": { + "title": "Hosts 404", + "empty": "Não há hosts 404", + "add": "Adicionar host 404", + "form-title": "{id, select, undefined{Novo} other{Editar}} host 404", + "delete": "Excluir host 404", + "delete-confirm": "Tem certeza de que deseja excluir este host 404?", + "help-title": "O que é um host 404?", + "help-content": "Um host 404 é simplesmente uma configuração de host que mostra uma página 404.\nIsso pode ser útil quando seu domínio está listado em mecanismos de busca e você quer fornecer uma página de erro mais agradável ou especificamente dizer aos indexadores de busca que as páginas do domínio não existem mais.\nOutro benefício de ter este host é rastrear os logs de acessos a ele e visualizar os referenciadores.", + "search": "Pesquisar host..." + }, + "streams": { + "title": "Streams", + "empty": "Não há streams", + "add": "Adicionar stream", + "form-title": "{id, select, undefined{Novo} other{Editar}} stream", + "incoming-port": "Porta de entrada", + "forwarding-host": "Host de encaminhamento", + "forwarding-port": "Porta de encaminhamento", + "tcp-forwarding": "Encaminhamento TCP", + "udp-forwarding": "Encaminhamento UDP", + "forward-type-error": "Pelo menos um tipo de protocolo deve ser habilitado", + "protocol": "Protocolo", + "tcp": "TCP", + "udp": "UDP", + "delete": "Excluir stream", + "delete-confirm": "Tem certeza de que deseja excluir este stream?", + "help-title": "O que é um stream?", + "help-content": "Um recurso relativamente novo para o Nginx, um stream servirá para encaminhar tráfego TCP/UDP diretamente para outro computador na rede.\nSe você estiver executando servidores de jogos, FTP ou SSH, isso pode ser útil.", + "search": "Pesquisar porta de entrada...", + "ssl-certificate": "Certificado SSL para encaminhamento TCP", + "tcp+ssl": "TCP+SSL" + }, + "certificates": { + "title": "Certificados SSL", + "empty": "Não há certificados SSL", + "add": "Adicionar certificado SSL", + "form-title": "Adicionar certificado {provider, select, letsencrypt{Let's Encrypt} other{personalizado}}", + "delete": "Excluir certificado SSL", + "delete-confirm": "Tem certeza de que deseja excluir este certificado SSL? Qualquer host que o esteja usando precisará ser atualizado posteriormente.", + "help-title": "Certificados SSL", + "help-content": "Certificados SSL (corretamente conhecidos como certificados TLS) são uma forma de chave de criptografia que permite que seu site seja criptografado para o usuário final.\nO NPM usa um serviço chamado Let's Encrypt para emitir certificados SSL gratuitamente.\nSe você tiver qualquer tipo de informação pessoal, senhas ou dados sensíveis atrás do NPM, é provavelmente uma boa ideia usar um certificado.\nO NPM também suporta autenticação DNS para quando você não está executando seu site voltado para a internet, ou se você apenas quer um certificado curinga.", + "other-certificate": "Certificado", + "other-certificate-key": "Chave do certificado", + "other-intermediate-certificate": "Certificado intermediário", + "force-renew": "Renovar agora", + "test-reachability": "Testar acessibilidade do servidor", + "reachability-title": "Testar acessibilidade do servidor", + "reachability-info": "Teste se os domínios são acessíveis da internet pública usando Site24x7. Isso não é necessário ao usar desafio DNS.", + "reachability-failed-to-reach-api": "Falha na comunicação com a API, o NPM está funcionando corretamente?", + "reachability-failed-to-check": "Falha ao verificar a acessibilidade devido a um erro de comunicação com site24x7.com.", + "reachability-ok": "Seu servidor é acessível e criar certificados deve ser possível.", + "reachability-404": "Há um servidor encontrado neste domínio, mas não parece ser o Nginx Proxy Manager. Certifique-se de que seu domínio aponta para o IP onde sua instância NPM está rodando.", + "reachability-not-resolved": "Não há servidor disponível neste domínio. Certifique-se de que seu domínio existe e aponta para o IP onde sua instância NPM está rodando e, se necessário, a porta 80 está encaminhada no seu roteador.", + "reachability-wrong-data": "Há um servidor encontrado neste domínio, mas retornou dados inesperados. É o servidor NPM? Certifique-se de que seu domínio aponta para o IP onde sua instância NPM está rodando.", + "reachability-other": "Há um servidor encontrado neste domínio, mas retornou um código de status inesperado {code}. É o servidor NPM? Certifique-se de que seu domínio aponta para o IP onde sua instância NPM está rodando.", + "download": "Baixar", + "renew-title": "Renovar certificado Let's Encrypt", + "search": "Pesquisar certificado...", + "in-use": "Em uso", + "inactive": "Inativo", + "active-domain_names": "Nomes de domínio ativos" + }, + "access-lists": { + "title": "Listas de acesso", + "empty": "Não há listas de acesso", + "add": "Adicionar lista de acesso", + "form-title": "{id, select, undefined{Nova} other{Editar}} lista de acesso", + "delete": "Excluir lista de acesso", + "delete-confirm": "Tem certeza de que deseja excluir esta lista de acesso?", + "public": "Publicamente acessível", + "public-sub": "Sem restrições de acesso", + "help-title": "O que é uma lista de acesso?", + "help-content": "Listas de acesso fornecem uma lista negra ou branca de endereços IP de clientes específicos junto com autenticação para hosts proxy via autenticação HTTP básica.\nVocê pode configurar múltiplas regras de cliente, nomes de usuário e senhas para uma única lista de acesso e depois aplicá-la a um host proxy.\nIsso é mais útil para serviços web encaminhados que não têm mecanismos de autenticação integrados ou que você quer proteger do acesso por clientes desconhecidos.", + "item-count": "{count} {count, select, 1{usuário} other{usuários}}", + "client-count": "{count} {count, select, 1{regra} other{regras}}", + "proxy-host-count": "{count} {count, select, 1{host proxy} other{hosts proxy}}", + "delete-has-hosts": "Esta lista de acesso está associada a {count} hosts proxy. Eles se tornarão publicamente disponíveis após a exclusão.", + "details": "Detalhes", + "authorization": "Autorização", + "access": "Acesso", + "satisfy": "Satisfazer", + "satisfy-any": "Satisfazer qualquer", + "pass-auth": "Passar autenticação para o host", + "access-add": "Adicionar", + "auth-add": "Adicionar", + "search": "Pesquisar acesso..." + }, + "users": { + "title": "Usuários", + "default_error": "O endereço de e-mail padrão deve ser alterado", + "add": "Adicionar usuário", + "nickname": "Apelido", + "full-name": "Nome completo", + "edit-details": "Editar detalhes", + "change-password": "Alterar senha", + "edit-permissions": "Editar permissões", + "sign-in-as": "Entrar como usuário", + "form-title": "{id, select, undefined{Novo} other{Editar}} usuário", + "delete": "Excluir {name, select, undefined{usuário} other{{name}}}", + "delete-confirm": "Tem certeza de que deseja excluir {name}?", + "password-title": "Alterar senha{self, select, false{ para {name}} other{}}", + "current-password": "Senha atual", + "new-password": "Nova senha", + "confirm-password": "Confirmar senha", + "permissions-title": "Permissões para {name}", + "admin-perms": "Este usuário é um administrador e alguns itens não podem ser alterados", + "perms-visibility": "Visibilidade de itens", + "perms-visibility-user": "Apenas itens criados", + "perms-visibility-all": "Todos os itens", + "perm-manage": "Gerenciar", + "perm-view": "Apenas visualizar", + "perm-hidden": "Oculto", + "search": "Pesquisar usuário..." + }, + "audit-log": { + "title": "Log de auditoria", + "empty": "Não há logs.", + "empty-subtitle": "Assim que você ou outro usuário alterar algo, o histórico desses eventos aparecerá aqui.", + "proxy-host": "Host proxy", + "redirection-host": "Host de redirecionamento", + "dead-host": "Host 404", + "stream": "Stream", + "user": "Usuário", + "certificate": "Certificado", + "access-list": "Lista de acesso", + "created": "Criado {name}", + "updated": "Atualizado {name}", + "deleted": "Excluído {name}", + "enabled": "Habilitado {name}", + "disabled": "Desabilitado {name}", + "renewed": "Renovado {name}", + "meta-title": "Detalhes do evento", + "view-meta": "Ver detalhes", + "date": "Data", + "search": "Pesquisar log..." + }, + "settings": { + "title": "Configurações", + "default-site": "Site padrão", + "default-site-description": "O que mostrar quando o Nginx recebe um host desconhecido", + "default-site-congratulations": "Página de parabéns", + "default-site-404": "Página 404", + "default-site-444": "Sem resposta (444)", + "default-site-html": "Página personalizada", + "default-site-redirect": "Redirecionamento", + "language": "Idioma da interface", + "language-description": "Escolha o idioma de exibição da interface", + "current-language": "Idioma atual" + } +} diff --git a/frontend/js/i18n/ru-RU.json b/frontend/js/i18n/ru-RU.json new file mode 100644 index 000000000..e73c003ca --- /dev/null +++ b/frontend/js/i18n/ru-RU.json @@ -0,0 +1,306 @@ +{ + "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 Proxy Manager", + "version": "v{version}", + "welcome": "Добро пожаловать в Nginx Proxy Manager", + "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-russian": "Переключиться на русский" + }, + "footer": { + "fork-me": "Форкнуть на Github", + "copy": "© 2025 jc21.com.", + "theme": "Тема от 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": "Невозможно запросить сертификат Let's Encrypt для доменов с подстановочными знаками без использования DNS-вызова", + "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": "Прокси-хост — это входящая конечная точка для веб-сервиса, который вы хотите перенаправить.\nОн обеспечивает дополнительное SSL-завершение для вашего сервиса, у которого может не быть встроенной поддержки SSL.\nПрокси-хосты — самое распространённое использование Nginx Proxy Manager.", + "access-list": "Список доступа", + "allow-websocket-upgrade": "Поддержка WebSocket", + "ignore-invalid-upstream-ssl": "Игнорировать недействительный восходящий SSL", + "custom-forward-host-help": "Добавьте путь для пересылки подпапки.\nПример: 203.0.113.25/путь/", + "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": "SSL-сертификат для TCP-пересылки", + "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-сертификатов бесплатно.\nЕсли у вас есть какая-либо личная информация, пароли или конфиденциальные данные за NPM, вероятно, хорошая идея использовать сертификат.\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 Proxy Manager. Убедитесь, что ваш домен указывает на IP, где работает ваш экземпляр NPM.", + "reachability-not-resolved": "На этом домене нет доступного сервера. Убедитесь, что ваш домен существует и указывает на IP, где работает ваш экземпляр NPM, и при необходимости порт 80 перенаправлен в вашем маршрутизаторе.", + "reachability-wrong-data": "На этом домене найден сервер, но он вернул неожиданные данные. Это сервер NPM? Убедитесь, что ваш домен указывает на IP, где работает ваш экземпляр NPM.", + "reachability-other": "На этом домене найден сервер, но он вернул неожиданный код состояния {code}. Это сервер NPM? Убедитесь, что ваш домен указывает на IP, где работает ваш экземпляр NPM.", + "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{ для {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/zh-CN.json b/frontend/js/i18n/zh-CN.json new file mode 100644 index 000000000..5643eb3e1 --- /dev/null +++ b/frontend/js/i18n/zh-CN.json @@ -0,0 +1,305 @@ +{ + "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": "切换到中文" + }, + "footer": { + "fork-me": "在 Github 上 Fork 我", + "copy": "© 2025 jc21.com.", + "theme": "主题来自 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": "代理主机是您要转发的 Web 服务的传入端点。\n它为可能没有内置 SSL 支持的服务提供可选的 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 证书。\n如果您在 NPM 后面有任何个人信息、密码或敏感数据,使用证书可能是个好主意。\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这对于没有内置身份验证机制的转发 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} 个代理主机关联。删除后它们将变为公开可访问。", + "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": "当前语言" + } +} \ No newline at end of file diff --git a/frontend/js/i18n/zh-TW.json b/frontend/js/i18n/zh-TW.json new file mode 100644 index 000000000..42a5853e0 --- /dev/null +++ b/frontend/js/i18n/zh-TW.json @@ -0,0 +1,305 @@ +{ + "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": "切換到中文" + }, + "footer": { + "fork-me": "在 Github 上 Fork 我", + "copy": "© 2025 jc21.com.", + "theme": "主題來自 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": "代理主機是您要轉發的 Web 服務的傳入端點。\n它為可能沒有內建 SSL 支援的服務提供可選的 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 憑證。\n如果您在 NPM 後面有任何個人資訊、密碼或敏感資料,使用憑證可能是個好主意。\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這對於沒有內建身份驗證機制的轉發 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} 個代理主機相關聯。刪除後它們將變為公開可存取。", + "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": "目前語言" + } +} \ No newline at end of file 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()); } }); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 05350a475..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: /\bmessages\.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'], + // Use CLDR/BCP 47 locale codes supported by messageformat + locale: ['en', 'zh', 'fr', 'ja', 'zh-TW', 'ko', 'ru', 'pt'], strictNumberSign: false } },