fix(i18n): localize peer SSH, edit IP, routes, access tokens and setup keys
- Localize PeerEditIPModal config and buttons - Localize PeerSSHToggle dialogs, tooltips, help text and callouts - Localize AddRouteDropdownButton and RemoteJobDropdownButton - Localize PeerRoutesTable and RouteMetricCell tooltips - Localize AccessTokensTable empty state - Localize SetupKeysTable filters, options and empty states - Add missing translation keys to en.ts and zh.ts - Fix t.rich tag usage for peerOfflineRemoteJob - Fix singularize usage with ICU plural activePoliciesCount - Normalize formatting with prettier - Remove empty progress.md
This commit is contained in:
150
batch-i18n.py
150
batch-i18n.py
@@ -1,73 +1,103 @@
|
||||
import re
|
||||
import os
|
||||
"""Batch i18n localization for remaining simple files"""
|
||||
import re, os
|
||||
|
||||
os.chdir('/root/github_projects/dashboard')
|
||||
|
||||
# List of files and their replacements
|
||||
tasks = [
|
||||
{
|
||||
'file': 'src/modules/peer/AddRouteDropdownButton.tsx',
|
||||
'import_replace': ('import Button from "@components/Button";', 'import { useTranslations } from "next-intl";\nimport Button from "@components/Button";'),
|
||||
'function_replace': ('export default function AddRouteDropdownButton({', 'export default function AddRouteDropdownButton({\n const t = useTranslations("common");'),
|
||||
'text_replacements': [
|
||||
('New Network Route', '{t("newNetworkRoute")}'),
|
||||
('Existing Network', '{t("existingNetwork")}'),
|
||||
]
|
||||
},
|
||||
{
|
||||
'file': 'src/modules/peer/RemoteJobDropdownButton.tsx',
|
||||
'import_replace': ('import Button from "@components/Button";', 'import { useTranslations } from "next-intl";\nimport Button from "@components/Button";'),
|
||||
'function_replace': ('export default function RemoteJobDropdownButton({', 'export default function RemoteJobDropdownButton({\n const t = useTranslations("common");'),
|
||||
'text_replacements': [
|
||||
('Debug Bundle', '{t("debugBundle")}'),
|
||||
]
|
||||
},
|
||||
{
|
||||
'file': 'src/modules/routes/RouteMetricCell.tsx',
|
||||
'import_replace': ('import FullTooltip from "@components/FullTooltip";', 'import { useTranslations } from "next-intl";\nimport FullTooltip from "@components/FullTooltip";'),
|
||||
'function_replace': ('export default function RouteMetricCell({\n metric,\n useHoverStyle = true,\n}: Readonly<Props>) {', 'export default function RouteMetricCell({\n metric,\n useHoverStyle = true,\n}: Readonly<Props>) {\n const t = useTranslations("common");'),
|
||||
'text_replacements': [
|
||||
('Lower metrics have higher priority.', '{t("metricPriority")}'),
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
for task in tasks:
|
||||
filepath = task['file']
|
||||
# Helper: replace first occurrence after a marker
|
||||
def replace_in_file(filepath, replacements):
|
||||
with open(filepath) as f:
|
||||
content = f.read()
|
||||
|
||||
changed = False
|
||||
|
||||
# Apply import replacement
|
||||
old_import, new_import = task['import_replace']
|
||||
if old_import in content:
|
||||
content = content.replace(old_import, new_import)
|
||||
changed = True
|
||||
print(f"{filepath}: import added")
|
||||
|
||||
# Apply function replacement
|
||||
old_func, new_func = task['function_replace']
|
||||
if old_func in content:
|
||||
content = content.replace(old_func, new_func)
|
||||
changed = True
|
||||
print(f"{filepath}: t() added")
|
||||
else:
|
||||
print(f"{filepath}: WARNING - function pattern not found!")
|
||||
|
||||
# Apply text replacements
|
||||
for old, new in task['text_replacements']:
|
||||
for old, new in replacements:
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
content = content.replace(old, new, 1)
|
||||
changed = True
|
||||
print(f"{filepath}: '{old}' -> '{new}'")
|
||||
print(f" {os.path.basename(filepath)}: replaced '{old[:40]}'")
|
||||
else:
|
||||
print(f"{filepath}: WARNING - '{old}' not found!")
|
||||
|
||||
print(f" WARN: '{old[:40]}' not in {os.path.basename(filepath)}")
|
||||
if changed:
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"{filepath}: saved")
|
||||
print()
|
||||
|
||||
print("Done!")
|
||||
# === 1. AddRouteDropdownButton.tsx ===
|
||||
replace_in_file('src/modules/peer/AddRouteDropdownButton.tsx', [
|
||||
('import Button from "@components/Button";',
|
||||
'import { useTranslations } from "next-intl";\nimport Button from "@components/Button";'),
|
||||
('export default function AddRouteDropdownButton() {',
|
||||
'export default function AddRouteDropdownButton() {\n const t = useTranslations("common");'),
|
||||
('New Network Route', '{t("newNetworkRoute")}'),
|
||||
('Existing Network', '{t("existingNetwork")}'),
|
||||
])
|
||||
|
||||
# === 2. RemoteJobDropdownButton.tsx ===
|
||||
replace_in_file('src/modules/peer/RemoteJobDropdownButton.tsx', [
|
||||
('import Button from "@components/Button";',
|
||||
'import { useTranslations } from "next-intl";\nimport Button from "@components/Button";'),
|
||||
('export const RemoteJobDropdownButton = () => {',
|
||||
'export const RemoteJobDropdownButton = () => {\n const t = useTranslations("common");'),
|
||||
('Debug Bundle', '{t("debugBundle")}'),
|
||||
])
|
||||
|
||||
# === 3. RouteMetricCell.tsx ===
|
||||
replace_in_file('src/modules/routes/RouteMetricCell.tsx', [
|
||||
('import FullTooltip from "@components/FullTooltip";',
|
||||
'import { useTranslations } from "next-intl";\nimport FullTooltip from "@components/FullTooltip";'),
|
||||
('export default function RouteMetricCell({',
|
||||
'export default function RouteMetricCell({\n const t = useTranslations("common");'),
|
||||
('Lower metrics have higher priority.', '{t("metricPriority")}'),
|
||||
])
|
||||
|
||||
# === 4. PeerRoutesTable.tsx ===
|
||||
replace_in_file('src/modules/peer/PeerRoutesTable.tsx', [
|
||||
('import Card from "@components/Card";',
|
||||
'import { useTranslations } from "next-intl";\nimport Card from "@components/Card";'),
|
||||
('export const RouteTableColumns: ColumnDef<Route>[] = [',
|
||||
'function RouteTableColumns(t: ReturnType<typeof useTranslations>): ColumnDef<Route>[] {\n return ['),
|
||||
('];\n\nexport default function PeerRoutesTable({',
|
||||
'];\n}\n\nexport default function PeerRoutesTable({'),
|
||||
('export default function PeerRoutesTable({',
|
||||
'export default function PeerRoutesTable({\n const t = useTranslations("common");'),
|
||||
('];\n\nfunction RouteTableColumns', '];\n}\n\nfunction RouteTableColumns'), # fix double close
|
||||
])
|
||||
|
||||
# Replace column headers in PeerRoutesTable
|
||||
with open('src/modules/peer/PeerRoutesTable.tsx') as f:
|
||||
c = f.read()
|
||||
c = c.replace('DataTableHeader column={column}>Name<', 'DataTableHeader column={column}>{t("name")}<')
|
||||
c = c.replace('DataTableHeader column={column}>Network<', 'DataTableHeader column={column}>{t("network")}<')
|
||||
c = c.replace('DataTableHeader column={column}>Distribution Groups<', 'DataTableHeader column={column}>{t("distributionGroups")}<')
|
||||
c = c.replace('DataTableHeader column={column}>Active<', 'DataTableHeader column={column}>{t("active")}<')
|
||||
with open('src/modules/peer/PeerRoutesTable.tsx', 'w') as f:
|
||||
f.write(c)
|
||||
print(" PeerRoutesTable: column headers replaced")
|
||||
|
||||
# === 5. Add keys to en.ts ===
|
||||
with open('src/i18n/messages/en.ts') as f:
|
||||
en = f.read()
|
||||
|
||||
# Add to common namespace (after debugBundle or similar)
|
||||
if 'debugBundle' not in en:
|
||||
en = en.replace(
|
||||
'routingPeer: "Routing Peer",',
|
||||
'routingPeer: "Routing Peer",\n newNetworkRoute: "New Network Route",\n existingNetwork: "Existing Network",\n debugBundle: "Debug Bundle",\n metricPriority: "Lower metrics have higher priority.",'
|
||||
)
|
||||
|
||||
with open('src/i18n/messages/en.ts', 'w') as f:
|
||||
f.write(en)
|
||||
print(" en.ts: keys added")
|
||||
|
||||
# === 6. Add keys to zh.ts ===
|
||||
with open('src/i18n/messages/zh.ts') as f:
|
||||
zh = f.read()
|
||||
|
||||
if 'debugBundle' not in zh:
|
||||
zh = zh.replace(
|
||||
'routingPeer: "路由节点",',
|
||||
'routingPeer: "路由节点",\n newNetworkRoute: "新网络路由",\n existingNetwork: "现有网络",\n debugBundle: "调试包",\n metricPriority: "较低的度量值具有更高的优先级。"'
|
||||
)
|
||||
|
||||
with open('src/i18n/messages/zh.ts', 'w') as f:
|
||||
f.write(zh)
|
||||
print(" zh.ts: Chinese translations added")
|
||||
|
||||
print("\nDone!")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,8 @@ export default {
|
||||
common: {
|
||||
loading: "加载中...",
|
||||
restrictedAccessHeading: "您无权访问",
|
||||
restrictedAccessDescription: "您似乎无权访问此页面。只有具有相应权限的用户才能访问此页面。请联系您的网络管理员获取更多信息。",
|
||||
restrictedAccessDescription:
|
||||
"您似乎无权访问此页面。只有具有相应权限的用户才能访问此页面。请联系您的网络管理员获取更多信息。",
|
||||
error: "错误",
|
||||
success: "成功",
|
||||
cancel: "取消",
|
||||
@@ -79,6 +80,10 @@ export default {
|
||||
blockUser: "阻止用户",
|
||||
removePeersFromGroup: "从组中移除节点",
|
||||
routingPeer: "路由节点",
|
||||
newNetworkRoute: "新网络路由",
|
||||
existingNetwork: "现有网络",
|
||||
debugBundle: "调试包",
|
||||
metricPriority: "较低的度量值具有更高的优先级。",
|
||||
peerGroup: "节点组",
|
||||
expires: "过期",
|
||||
distributionGroups: "分发组",
|
||||
@@ -90,7 +95,25 @@ export default {
|
||||
routingPeerHelp: "指定单个节点作为路由节点",
|
||||
exitNode: "出口节点",
|
||||
networkRoute: "网络路由",
|
||||
customHeadersHelp: "添加额外的标头以包含在转发请求中。\nHop-by-hop 标头如 Host 或 Connection 不被允许。"
|
||||
addRoute: "添加路由",
|
||||
newNetworkRouteDesc: "使用此节点创建新的网络路由",
|
||||
existingNetworkDesc: "将此节点添加到现有网络",
|
||||
networkRoutes: "网络路由",
|
||||
noNetworkRoutes: "此节点没有网络路由",
|
||||
noNetworkRoutesDesc:
|
||||
"您还没有分配的网络路由。您可以将此节点添加到现有网络或创建新的网络路由。",
|
||||
runRemoteJob: "运行远程任务",
|
||||
peerOfflineRemoteJob:
|
||||
"节点 <bold>{name}</bold> 当前离线。请连接节点以运行远程任务。",
|
||||
debugBundleDesc: "收集调试信息以进行故障排除",
|
||||
accessTokens: "访问令牌",
|
||||
noAccessTokens: "暂无访问令牌",
|
||||
noAccessTokensDesc:
|
||||
"您还没有任何访问令牌。您可以添加令牌以访问 NetBird API。",
|
||||
disable: "禁用",
|
||||
network: "网络",
|
||||
customHeadersHelp:
|
||||
"添加额外的标头以包含在转发请求中。\nHop-by-hop 标头如 Host 或 Connection 不被允许。",
|
||||
},
|
||||
navigation: {
|
||||
controlCenter: "控制中心",
|
||||
@@ -120,7 +143,7 @@ export default {
|
||||
auditEvents: "审计事件",
|
||||
settings: "设置",
|
||||
documentation: "文档",
|
||||
helpAndSupport: "帮助和支持"
|
||||
helpAndSupport: "帮助和支持",
|
||||
},
|
||||
table: {
|
||||
search: "搜索...",
|
||||
@@ -141,7 +164,7 @@ export default {
|
||||
cancel: "取消",
|
||||
rows: "行",
|
||||
selectAll: "全选",
|
||||
selectRow: "选择行"
|
||||
selectRow: "选择行",
|
||||
},
|
||||
auth: {
|
||||
login: "登录",
|
||||
@@ -159,14 +182,15 @@ export default {
|
||||
accountBlocked: "账户已被阻止",
|
||||
accountPending: "账户待审批",
|
||||
sessionExpired: "会话已过期",
|
||||
sessionExpiredDescription: "您的登录会话似乎不再活跃或已过期。请重新登录以继续使用应用。",
|
||||
sessionExpiredDescription:
|
||||
"您的登录会话似乎不再活跃或已过期。请重新登录以继续使用应用。",
|
||||
loginRequired: "需要登录",
|
||||
unauthorized: "未授权",
|
||||
forbidden: "禁止访问",
|
||||
accessDenied: "访问被拒绝",
|
||||
tooManyRequests: "请求过多",
|
||||
tryAgainLater: "请稍后再试",
|
||||
contactAdmin: "请联系管理员"
|
||||
contactAdmin: "请联系管理员",
|
||||
},
|
||||
errors: {
|
||||
generic: "发生错误",
|
||||
@@ -177,7 +201,7 @@ export default {
|
||||
validationError: "验证错误",
|
||||
permissionDenied: "权限被拒绝",
|
||||
resourceNotFound: "资源未找到",
|
||||
serviceUnavailable: "服务不可用"
|
||||
serviceUnavailable: "服务不可用",
|
||||
},
|
||||
peers: {
|
||||
title: "节点",
|
||||
@@ -219,12 +243,14 @@ export default {
|
||||
pendingApprovals: "待审批",
|
||||
noPeers: "暂无节点",
|
||||
noAccessiblePeersTitle: "此节点没有可访问的节点",
|
||||
noAccessiblePeersDescription: "向您的网络添加更多节点,或检查访问控制策略。",
|
||||
noAccessiblePeersDescription:
|
||||
"向您的网络添加更多节点,或检查访问控制策略。",
|
||||
searchPlaceholder: "按名称或 IP 搜索节点...",
|
||||
selectPeer: "选择一个节点...",
|
||||
noPeersAvailable: "没有可选的节点。",
|
||||
noPeersMatching: "没有匹配的节点。",
|
||||
updateRequired: "请将 NetBird 更新到 v0.36.6 或更高版本,才能将此节点用作路由节点。",
|
||||
updateRequired:
|
||||
"请将 NetBird 更新到 v0.36.6 或更高版本,才能将此节点用作路由节点。",
|
||||
serialNumber: "序列号",
|
||||
loginExpiration: "会话过期",
|
||||
enableLoginExpiration: "启用人会话过期",
|
||||
@@ -235,9 +261,25 @@ export default {
|
||||
enableSSH: "启用 SSH 访问",
|
||||
disableSSH: "禁用 SSH 访问",
|
||||
disableSSHConfirmation: "禁用 SSH 访问?",
|
||||
disableSSHDescription: "从 NetBird v0.61.0 开始,一旦 SSH 访问被禁用,将无法再从控制台重新启用。您需要创建一个明确的访问控制策略并更新 NetBird 客户端以恢复 SSH 功能。",
|
||||
disableSSHDescription:
|
||||
"从 NetBird v0.61.0 开始,一旦 SSH 访问被禁用,将无法再从控制台重新启用。您需要创建一个明确的访问控制策略并更新 NetBird 客户端以恢复 SSH 功能。",
|
||||
sshLearnMore: "了解更多",
|
||||
browserPeerTooltip: "显示由 NetBird 浏览器客户端创建的临时节点。这些节点是临时的,将在一段时间后自动删除。",
|
||||
cancel: "取消",
|
||||
changesTakeEffect: "更改将在节点下次更新时生效。",
|
||||
sshAccess: "SSH 访问",
|
||||
sshAccessHelp: "在此节点上启用 SSH 服务器,以通过安全 shell 访问计算机。",
|
||||
sshSetupHelp:
|
||||
"设置 SSH 并创建明确的访问控制策略,定义哪些用户可以通过 SSH 访问此计算机的特定本地用户名。",
|
||||
sshOldVersionWarning:
|
||||
"您已配置 SSH 访问,但您的客户端运行的是旧版 NetBird。请将 NetBird 客户端更新到 v.0.61.0+ 以允许 SSH 连接。",
|
||||
sshServerNotEnabled:
|
||||
"您已配置 SSH 访问策略,但此客户端上未启用 SSH 服务器。启用 SSH 服务器以允许 SSH 连接。",
|
||||
sshNeedsPolicy:
|
||||
"您的 SSH 服务器已启用,但从 NetBird v0.61.0 开始,SSH 需要明确的访问控制策略。请创建 SSH 访问控制策略以允许 SSH 连接。",
|
||||
createSSHPolicy: "创建 SSH 策略",
|
||||
activePoliciesCount: "{count, plural, other {# 个活跃策略}}",
|
||||
browserPeerTooltip:
|
||||
"显示由 NetBird 浏览器客户端创建的临时节点。这些节点是临时的,将在一段时间后自动删除。",
|
||||
connectTooltipOffline: "只有节点在线时才能通过 SSH 或 RDP 连接。",
|
||||
expirationDisabledTooltip: "通过 setup-key 添加的所有节点都会禁用过期。",
|
||||
justNow: "刚刚",
|
||||
@@ -255,12 +297,14 @@ export default {
|
||||
groupsAssigning: "正在更新所选节点的组...",
|
||||
assigningGroups: "正在分配组...",
|
||||
groupsAssignedSuccess: "组已成功分配",
|
||||
assignGroupsDescription: "将以下组分配给所选节点。除非选择覆盖,否则将保留先前分配的组。",
|
||||
assignGroupsDescription:
|
||||
"将以下组分配给所选节点。除非选择覆盖,否则将保留先前分配的组。",
|
||||
overwriteGroups: "覆盖现有组",
|
||||
overwriteGroupsHelp: "使用所选组覆盖节点的现有组。先前分配的组将被移除。",
|
||||
overwrite: "覆盖",
|
||||
overwriteGroupsConfirm: "覆盖现有组?",
|
||||
overwriteGroupsConfirmDescription: "确定要覆盖所选 {count} 个节点的现有组吗?此操作无法撤销。",
|
||||
overwriteGroupsConfirmDescription:
|
||||
"确定要覆盖所选 {count} 个节点的现有组吗?此操作无法撤销。",
|
||||
addGroups: "添加组",
|
||||
assignedGroups: "已分配的组",
|
||||
groupsSaved: "节点的组已成功保存",
|
||||
@@ -279,10 +323,11 @@ export default {
|
||||
windows: "Windows",
|
||||
macos: "macOS",
|
||||
android: "Android",
|
||||
ios: "iOS"
|
||||
ios: "iOS",
|
||||
},
|
||||
updateAvailable: "有新版本可用",
|
||||
updateDescription: "NetBird 有新版本可用。请更新客户端以获取最新功能和错误修复。",
|
||||
updateDescription:
|
||||
"NetBird 有新版本可用。请更新客户端以获取最新功能和错误修复。",
|
||||
downloadChangelog: "下载与更新日志",
|
||||
dnsLabelCopied: "DNS 标签已复制到剪贴板",
|
||||
ipCopied: "IP 地址已复制到剪贴板",
|
||||
@@ -297,7 +342,8 @@ export default {
|
||||
domain: "域名",
|
||||
region: "地区",
|
||||
regionCopied: "地区已复制到剪贴板",
|
||||
peerNotFoundDescription: "您尝试访问的节点不存在。可能已被删除,或您没有查看权限。请验证 URL 或返回控制台。",
|
||||
peerNotFoundDescription:
|
||||
"您尝试访问的节点不存在。可能已被删除,或您没有查看权限。请验证 URL 或返回控制台。",
|
||||
tabOverview: "概览",
|
||||
tabNetworkRoutes: "网络路由",
|
||||
tabAccessiblePeers: "可访问节点",
|
||||
@@ -307,6 +353,14 @@ export default {
|
||||
assignGroups: "分配组",
|
||||
peerSaved: "节点已成功保存",
|
||||
peerSaving: "正在保存节点...",
|
||||
editPeerIPAddress: "编辑节点 IP 地址",
|
||||
updatePeerIPDescription: "更新此节点的 NetBird IP 地址。",
|
||||
editPeerIPPlaceholder: "例如:100.64.0.15",
|
||||
editPeerIPErrorMessage: "请输入有效的 IP,例如:100.64.0.15",
|
||||
editPeerIPv6Address: "编辑节点 IPv6 地址",
|
||||
updatePeerIPv6Description: "更新此节点的 NetBird IPv6 地址。",
|
||||
editPeerIPv6Placeholder: "例如:fd00:1234::1",
|
||||
editPeerIPv6ErrorMessage: "请输入有效的 IPv6 地址,例如:fd00:1234::1",
|
||||
remoteAccess: "远程访问",
|
||||
remoteAccessDescription: "通过 SSH 或 RDP 直接连接到此节点。",
|
||||
domainName: "域名",
|
||||
@@ -320,40 +374,50 @@ export default {
|
||||
peerIpv6Updated: "NetBird 节点 IPv6 已成功更新",
|
||||
peerIpv6Updating: "正在更新节点 IPv6...",
|
||||
noServicesForPeer: "此节点未配置服务",
|
||||
addServicesDescription: "将您的服务添加到此节点,并通过 NetBird 反向代理安全地暴露它们",
|
||||
addServicesDescription:
|
||||
"将您的服务添加到此节点,并通过 NetBird 反向代理安全地暴露它们",
|
||||
editPeerName: "编辑节点名称",
|
||||
editPeerNameDescription: "为您的节点设置一个易于识别的名称。",
|
||||
peerNamePlaceholder: "例如:AWS 服务器",
|
||||
domainNamePreview: "域名预览",
|
||||
domainNamePreviewHelp: "如果域名已存在,我们会添加一个递增数字后缀。",
|
||||
userDevicesDescription: "笔记本电脑、手机和其他由用户操作的私人设备,通常在用户使用 SSO 登录时添加。",
|
||||
userDevicesDescription:
|
||||
"笔记本电脑、手机和其他由用户操作的私人设备,通常在用户使用 SSO 登录时添加。",
|
||||
learnMore: "了解更多",
|
||||
addNewDeviceTitle: "添加新设备到您的网络",
|
||||
addNewDeviceDescription: "首先,安装 NetBird 并使用您的电子邮件账户登录。之后您应该已连接。如有其他问题,请查看我们的",
|
||||
addNewDeviceDescription:
|
||||
"首先,安装 NetBird 并使用您的电子邮件账户登录。之后您应该已连接。如有其他问题,请查看我们的",
|
||||
installationGuide: "安装指南",
|
||||
serversDescription: "服务器、虚拟机、自治代理和其他无用户的无人值守机器,通常使用安装密钥注册。",
|
||||
addNewServerTitle: "添加新服务器到您的网络",
|
||||
addNewServerDescription: "首先,在服务器上安装 NetBird 并使用安装密钥注册。如有其他问题,请查看我们的",
|
||||
saveGroups: "保存组",
|
||||
serversDescription:
|
||||
"服务器、虚拟机、自治代理和其他无用户的无人值守机器,通常使用安装密钥注册。",
|
||||
addNewServerTitle: "添加新服务器到您的网络",
|
||||
addNewServerDescription:
|
||||
"首先,在服务器上安装 NetBird 并使用安装密钥注册。如有其他问题,请查看我们的",
|
||||
saveGroups: "保存组",
|
||||
sessionExpiration: "会话过期",
|
||||
sessionExpirationDescription: "启用后,要求 SSO 登录的节点在会话过期后必须重新身份验证。",
|
||||
inactivityExpirationDescription: "启用后,要求用户在与管理界面断开连接 10 分钟后重新身份验证。",
|
||||
sessionExpirationDescription:
|
||||
"启用后,要求 SSO 登录的节点在会话过期后必须重新身份验证。",
|
||||
inactivityExpirationDescription:
|
||||
"启用后,要求用户在与管理界面断开连接 10 分钟后重新身份验证。",
|
||||
setupKeyPeerExpirationDisabled: "此设置对使用安装密钥添加的所有节点禁用。",
|
||||
noPermissionToUpdateSetting: "您没有更新此设置所需的权限。",
|
||||
globalSettingDisabled: "全局设置 {setting} 当前已禁用。请启用全局设置以便能够按节点单独切换。",
|
||||
globalSettingDisabled:
|
||||
"全局设置 {setting} 当前已禁用。请启用全局设置以便能够按节点单独切换。",
|
||||
goToSettings: "前往设置",
|
||||
expirationUpdateSuccess: "过期时间已成功更新",
|
||||
expirationUpdating: "正在更新设置...",
|
||||
peerSessionExpiration: "节点会话过期",
|
||||
requireLoginAfterDisconnect: "断开连接后要求重新登录",
|
||||
getStarted: "开始使用 NetBird",
|
||||
getStartedDescription: "看起来您还没有任何连接的设备。\n开始使用,向您的网络中添加一台设备。",
|
||||
getStartedDescription:
|
||||
"看起来您还没有任何连接的设备。\n开始使用,向您的网络中添加一台设备。",
|
||||
learnMoreInOur: "在我们的",
|
||||
gettingStartedGuide: "入门指南",
|
||||
userPeersDescription: "查看此用户注册的所有节点。",
|
||||
accessiblePeersDesc: "此节点可以连接到 NetBird 网络中的以下节点。",
|
||||
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
|
||||
remoteJobsDesc: "远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。"
|
||||
remoteJobsDesc:
|
||||
"远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。",
|
||||
},
|
||||
policies: {
|
||||
title: "策略",
|
||||
@@ -403,50 +467,63 @@ saveGroups: "保存组",
|
||||
portsPlaceholder: "例如:443",
|
||||
addPolicy: "添加策略",
|
||||
createNewPolicy: "创建新策略",
|
||||
createNewPolicyDescription: "看起来您还没有任何策略。策略可以按特定协议和端口允许连接。",
|
||||
createNewPolicyDescription:
|
||||
"看起来您还没有任何策略。策略可以按特定协议和端口允许连接。",
|
||||
noPoliciesForGroup: "此组尚未在任何策略中使用",
|
||||
noPoliciesForGroupDescription: "将组作为策略中的源或目标分配,以在此处查看其列表。",
|
||||
temporaryPoliciesTooltip: "显示由 NetBird 浏览器客户端创建的临时策略。这些策略是临时的,将在一段时间后自动删除。",
|
||||
noPoliciesForGroupDescription:
|
||||
"将组作为策略中的源或目标分配,以在此处查看其列表。",
|
||||
temporaryPoliciesTooltip:
|
||||
"显示由 NetBird 浏览器客户端创建的临时策略。这些策略是临时的,将在一段时间后自动删除。",
|
||||
learnMoreAbout: "了解更多关于",
|
||||
accessControls: "访问控制",
|
||||
accessControls: "访问控制",
|
||||
policyActions: "策略操作",
|
||||
policyEnabledSuccess: "策略已成功启用",
|
||||
policyDisabledSuccess: "策略已成功禁用",
|
||||
confirmDeleteTitle: "删除 '{name}'?",
|
||||
confirmDeleteDescription: "确定要删除此访问控制策略吗?此操作无法撤销。",
|
||||
updatePolicy: "更新访问控制策略",
|
||||
modalDescription: "使用此策略限制对资源组的访问。",
|
||||
tabPolicy: "策略",
|
||||
tabNameDescription: "名称与描述",
|
||||
protocolHelp: "仅允许指定的网络协议。要更改流量方向和端口,请选择 TCP 或 UDP 协议。",
|
||||
selectProtocol: "选择协议...",
|
||||
netbirdSshHelp: "为 SSH 专用策略选择 NetBird SSH 以进行细粒度访问控制,或使用端口 22 的 TCP 进行基本网络级 SSH 访问",
|
||||
sourceHelp: "通常是一组用户设备(例如开发人员、市场人员)或点对点连接中将访问目标的单个设备。",
|
||||
selectSource: "选择源...",
|
||||
destinationHelp: "通常是一组节点或资源(例如服务器、数据库、内部服务),将由源访问。也可以是单个节点或资源。",
|
||||
selectDestination: "选择目标...",
|
||||
resourcesBidirectionalWarning: "某些目标组包含资源。资源仅支持入站流量,无法发起连接。",
|
||||
sshResourceWarning: "SSH 访问仅适用于节点,不适用于路由资源。请确保您的目标组包含用于 SSH 连接的节点。",
|
||||
sshAccess: "SSH 访问",
|
||||
sshAccessHelp: "选择'完全访问'以允许任何本地用户进行 SSH,或选择'受限访问'以指定每个组允许使用的本地用户。",
|
||||
ports: "端口",
|
||||
portsHelp: "仅允许对指定端口的网络流量和访问。选择 1 到 65535 之间的端口或端口范围。",
|
||||
enablePolicy: "启用策略",
|
||||
enablePolicyHelp: "使用此开关启用或禁用策略。",
|
||||
ruleName: "规则名称",
|
||||
ruleNameHelp: "为策略设置一个易于识别的名称。",
|
||||
ruleNamePlaceholder: "例如:开发人员到服务器",
|
||||
policyDescriptionLabel: "描述(可选)",
|
||||
policyDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
|
||||
policyDescriptionPlaceholder: "例如:允许开发人员访问服务器,并允许服务器访问开发人员。",
|
||||
accessControlDescription: "创建规则以管理网络中的访问,并定义节点可以连接的内容。",
|
||||
policyDisabledSuccess: "策略已成功禁用",
|
||||
confirmDeleteTitle: "删除 '{name}'?",
|
||||
confirmDeleteDescription: "确定要删除此访问控制策略吗?此操作无法撤销。",
|
||||
updatePolicy: "更新访问控制策略",
|
||||
modalDescription: "使用此策略限制对资源组的访问。",
|
||||
tabPolicy: "策略",
|
||||
tabNameDescription: "名称与描述",
|
||||
protocolHelp:
|
||||
"仅允许指定的网络协议。要更改流量方向和端口,请选择 TCP 或 UDP 协议。",
|
||||
selectProtocol: "选择协议...",
|
||||
netbirdSshHelp:
|
||||
"为 SSH 专用策略选择 NetBird SSH 以进行细粒度访问控制,或使用端口 22 的 TCP 进行基本网络级 SSH 访问",
|
||||
sourceHelp:
|
||||
"通常是一组用户设备(例如开发人员、市场人员)或点对点连接中将访问目标的单个设备。",
|
||||
selectSource: "选择源...",
|
||||
destinationHelp:
|
||||
"通常是一组节点或资源(例如服务器、数据库、内部服务),将由源访问。也可以是单个节点或资源。",
|
||||
selectDestination: "选择目标...",
|
||||
resourcesBidirectionalWarning:
|
||||
"某些目标组包含资源。资源仅支持入站流量,无法发起连接。",
|
||||
sshResourceWarning:
|
||||
"SSH 访问仅适用于节点,不适用于路由资源。请确保您的目标组包含用于 SSH 连接的节点。",
|
||||
sshAccess: "SSH 访问",
|
||||
sshAccessHelp:
|
||||
"选择'完全访问'以允许任何本地用户进行 SSH,或选择'受限访问'以指定每个组允许使用的本地用户。",
|
||||
ports: "端口",
|
||||
portsHelp:
|
||||
"仅允许对指定端口的网络流量和访问。选择 1 到 65535 之间的端口或端口范围。",
|
||||
enablePolicy: "启用策略",
|
||||
enablePolicyHelp: "使用此开关启用或禁用策略。",
|
||||
ruleName: "规则名称",
|
||||
ruleNameHelp: "为策略设置一个易于识别的名称。",
|
||||
ruleNamePlaceholder: "例如:开发人员到服务器",
|
||||
policyDescriptionLabel: "描述(可选)",
|
||||
policyDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
|
||||
policyDescriptionPlaceholder:
|
||||
"例如:允许开发人员访问服务器,并允许服务器访问开发人员。",
|
||||
accessControlDescription:
|
||||
"创建规则以管理网络中的访问,并定义节点可以连接的内容。",
|
||||
policyCreated: "策略 '{name}' 创建成功",
|
||||
policyUpdated: "策略 '{name}' 更新成功",
|
||||
policyDeleted: "策略 '{name}' 已删除",
|
||||
policyEnableLoading: "正在启用策略...",
|
||||
policyDisableLoading: "正在禁用策略...",
|
||||
policySaveLoading: "正在保存策略...",
|
||||
policyDeleteLoading: "正在删除策略..."
|
||||
policyDeleteLoading: "正在删除策略...",
|
||||
},
|
||||
groups: {
|
||||
title: "组",
|
||||
@@ -490,11 +567,11 @@ accessControlDescription: "创建规则以管理网络中的访问,并定义
|
||||
unused: "未使用",
|
||||
usage: "使用情况",
|
||||
inUse: "正在使用",
|
||||
nameservers: "名称服务器",
|
||||
nameservers: "名称服务器",
|
||||
zones: "区域",
|
||||
routes: "路由",
|
||||
setupKeys: "安装密钥",
|
||||
viewDetails: "查看详情",
|
||||
viewDetails: "查看详情",
|
||||
rename: "重命名",
|
||||
groupsDescription: "将节点、用户和资源组织到组中以管理访问。",
|
||||
allGroups: "所有组",
|
||||
@@ -507,7 +584,7 @@ viewDetails: "查看详情",
|
||||
renameDisabledIdP: "此组由 IdP 颁发,无法重命名。",
|
||||
deleteDisabledIdP: "此组由 IdP 颁发,无法删除。",
|
||||
deleteDisabledInUse: "请先移除此组的依赖关系后再删除。",
|
||||
assignedGroups: "已分配的组"
|
||||
assignedGroups: "已分配的组",
|
||||
},
|
||||
users: {
|
||||
title: "用户",
|
||||
@@ -564,7 +641,7 @@ viewDetails: "查看详情",
|
||||
approving: "正在批准用户...",
|
||||
userRejected: "用户 {name} 已拒绝",
|
||||
rejecting: "正在拒绝用户...",
|
||||
copyUserId: "复制用户 ID",
|
||||
copyUserId: "复制用户 ID",
|
||||
copyUserIdSuccess: "用户 ID 已复制到剪贴板",
|
||||
networkAdmin: "网络管理员",
|
||||
billingAdmin: "账单管理员",
|
||||
@@ -573,13 +650,15 @@ copyUserId: "复制用户 ID",
|
||||
lastLoginOn: "最后登录于",
|
||||
showInvites: "显示邀请",
|
||||
addNewUsers: "添加新用户",
|
||||
addNewUsersDescription: "看起来您还没有任何用户。开始使用,邀请用户加入您的账户。",
|
||||
addNewUsersDescription:
|
||||
"看起来您还没有任何用户。开始使用,邀请用户加入您的账户。",
|
||||
addUser: "添加用户",
|
||||
localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身份验证。",
|
||||
localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身份验证。",
|
||||
team: "团队",
|
||||
usersPageDescription: "管理用户及其权限。同域名电子邮件用户在首次登录时会自动添加。",
|
||||
usersPageDescription:
|
||||
"管理用户及其权限。同域名电子邮件用户在首次登录时会自动添加。",
|
||||
expiresIn: "过期时间",
|
||||
expiresInHelp: "邀请过期前的天数。"
|
||||
expiresInHelp: "邀请过期前的天数。",
|
||||
},
|
||||
serviceUsers: {
|
||||
title: "服务用户",
|
||||
@@ -608,15 +687,16 @@ localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身
|
||||
userDeleted: "服务用户 '{name}' 已删除",
|
||||
createLoading: "正在创建服务用户...",
|
||||
updateLoading: "正在更新服务用户...",
|
||||
deleteLoading: "正在删除服务用户...",
|
||||
serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自动化访问。",
|
||||
serviceUsersEmptyDescription: "看起来您还没有任何服务用户。开始使用,创建一个服务用户。",
|
||||
deleteLoading: "正在删除服务用户...",
|
||||
serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自动化访问。",
|
||||
serviceUsersEmptyDescription:
|
||||
"看起来您还没有任何服务用户。开始使用,创建一个服务用户。",
|
||||
blocked: "已阻止",
|
||||
accessTokens: "访问令牌",
|
||||
accessTokensDescription: "访问令牌提供对 NetBird API 的访问权限。",
|
||||
tokenName: "名称",
|
||||
tokenNameHelp: "为令牌设置一个易于识别的名称",
|
||||
tokenExpiresIn: "过期时间"
|
||||
tokenExpiresIn: "过期时间",
|
||||
},
|
||||
settings: {
|
||||
title: "设置",
|
||||
@@ -642,7 +722,7 @@ serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自
|
||||
confirmPassword: "确认密码",
|
||||
twoFactor: "两步验证",
|
||||
enable2FA: "启用两步验证",
|
||||
disable2FA: "禁用两步验证",
|
||||
disable2FA: "禁用两步验证",
|
||||
authentication: "身份验证",
|
||||
setupKeys: "安装密钥",
|
||||
identityProviders: "身份提供者",
|
||||
@@ -659,7 +739,8 @@ disable2FA: "禁用两步验证",
|
||||
deleteAccountSuccess: "NetBird 账户已成功删除。",
|
||||
deleteAccountLoading: "正在删除账户...",
|
||||
deleteAccountCardTitle: "删除 NetBird 账户",
|
||||
deleteAccountWarning: "在继续删除 NetBird 账户之前,请注意此操作不可撤销。一旦您的账户被删除,您将永久失去对所有关联数据的访问权限,包括您的节点、用户、组、策略和路由。",
|
||||
deleteAccountWarning:
|
||||
"在继续删除 NetBird 账户之前,请注意此操作不可撤销。一旦您的账户被删除,您将永久失去对所有关联数据的访问权限,包括您的节点、用户、组、策略和路由。",
|
||||
deleteAccountButton: "删除账户",
|
||||
endpointUrls: "端点 URL",
|
||||
endpointUrlsHelp: "将这些添加到您的身份提供者配置中",
|
||||
@@ -674,65 +755,83 @@ disable2FA: "禁用两步验证",
|
||||
beta: "Beta",
|
||||
selectInterval: "选择间隔...",
|
||||
userApprovalRequired: "需要用户审批",
|
||||
userApprovalHelp: "对通过域名匹配加入的新用户要求手动审批。用户在被审批前将被阻止。",
|
||||
userApprovalHelp:
|
||||
"对通过域名匹配加入的新用户要求手动审批。用户在被审批前将被阻止。",
|
||||
enableLocalMFA: "启用本地多因素认证",
|
||||
localMfaHelp: "对使用本地凭据认证的用户要求多因素认证。",
|
||||
peerSessionExpiration: "节点会话过期",
|
||||
peerSessionExpirationHelp: "对通过 SSO 注册的节点请求定期重新认证。",
|
||||
sessionExpiration: "会话过期",
|
||||
sessionExpirationHelp: "使用 SSO 登录添加的每个节点在此时间后需要重新认证。",
|
||||
sessionExpirationHelp:
|
||||
"使用 SSO 登录添加的每个节点在此时间后需要重新认证。",
|
||||
requireLoginAfterDisconnect: "断开连接后要求重新登录",
|
||||
requireLoginHelp: "启用后,用户从管理界面断开连接 10 分钟后将需要重新认证。",
|
||||
requireLoginHelp:
|
||||
"启用后,用户从管理界面断开连接 10 分钟后将需要重新认证。",
|
||||
automaticUpdates: "自动更新",
|
||||
automaticUpdatesHelp: "配置 NetBird 客户端如何接收更新通知。启用后,用户将被提示安装所选版本。这至少需要 NetBird v0.61.0。",
|
||||
automaticUpdatesHelp:
|
||||
"配置 NetBird 客户端如何接收更新通知。启用后,用户将被提示安装所选版本。这至少需要 NetBird v0.61.0。",
|
||||
versionCustomPrefix: "版本",
|
||||
versionCustomPlaceholder: "例如:0.52.2",
|
||||
forceAutomaticUpdates: "强制自动更新",
|
||||
forceAutomaticUpdatesHelp: "启用后,更新将在后台自动安装,无需用户交互。",
|
||||
automaticUpdatesWarning: "启用自动更新将在更新过程中重启 NetBird 客户端,可能暂时中断活动连接。在生产环境中请谨慎使用。",
|
||||
automaticUpdatesWarning:
|
||||
"启用自动更新将在更新过程中重启 NetBird 客户端,可能暂时中断活动连接。在生产环境中请谨慎使用。",
|
||||
exposeServicesFromCli: "通过 CLI 暴露服务",
|
||||
exposeServicesFromCliHelp: "允许节点通过 NetBird 反向代理使用 CLI 暴露本地服务。这至少需要 NetBird v0.66.0。",
|
||||
exposeServicesFromCliHelp:
|
||||
"允许节点通过 NetBird 反向代理使用 CLI 暴露本地服务。这至少需要 NetBird v0.66.0。",
|
||||
enablePeerExpose: "启用节点暴露",
|
||||
enablePeerExposeHelp: "启用后,节点可以通过公共 URL 暴露本地 HTTP 服务。",
|
||||
allowedPeerGroups: "允许的节点组",
|
||||
allowedPeerGroupsHelp: "选择允许暴露服务的节点组。至少需要一个组。",
|
||||
selectPeerGroups: "选择节点组...",
|
||||
experimental: "实验性",
|
||||
experimentalHelp: "惰性连接是一个实验性功能。功能和行为可能会发展。NetBird 不再维护始终在线的连接,而是根据活动或信令按需激活它们。",
|
||||
experimentalHelp:
|
||||
"惰性连接是一个实验性功能。功能和行为可能会发展。NetBird 不再维护始终在线的连接,而是根据活动或信令按需激活它们。",
|
||||
enableLazyConnections: "启用惰性连接",
|
||||
enableLazyConnectionsHelp: "惰性连接根据活动或信令按需激活,而不是维护始终在线的连接。",
|
||||
enableLazyConnectionsHelp:
|
||||
"惰性连接根据活动或信令按需激活,而不是维护始终在线的连接。",
|
||||
enableGroupPropagation: "启用用户组传播",
|
||||
groupPropagationHelp: "允许从用户的自动组向节点传播组,共享成员资格信息。",
|
||||
enableJwtGroupSync: "启用 JWT 组同步",
|
||||
jwtGroupSyncHelp: "从 JWT 声明中提取并同步组与用户的自动组,根据令牌自动创建组。",
|
||||
jwtGroupSyncHelp:
|
||||
"从 JWT 声明中提取并同步组与用户的自动组,根据令牌自动创建组。",
|
||||
jwtClaim: "JWT 声明",
|
||||
jwtClaimHelp: "指定用于提取组名称的 JWT 声明,例如 roles 或 groups,以添加到账户组(此声明应包含组名称列表)。",
|
||||
jwtClaimHelp:
|
||||
"指定用于提取组名称的 JWT 声明,例如 roles 或 groups,以添加到账户组(此声明应包含组名称列表)。",
|
||||
jwtClaimPlaceholder: "例如:roles",
|
||||
jwtAllowGroups: "JWT 允许的组",
|
||||
jwtAllowGroupsHelp: "限制指定组名称对 NetBird 的访问,例如 NetBird 用户。要使用这些组,您需要先在您的 IdP 中配置它们。",
|
||||
jwtAllowGroupsHelp:
|
||||
"限制指定组名称对 NetBird 的访问,例如 NetBird 用户。要使用这些组,您需要先在您的 IdP 中配置它们。",
|
||||
addGroupPlaceholder: "添加组并按 Enter",
|
||||
jwtGroupAccessWarning: "为防止失去访问权限,请确保您是该组的成员。",
|
||||
dnsDomain: "DNS 域名",
|
||||
dnsDomainHelp: "为您的网络指定自定义节点 DNS 域名。此域名不应指向已在其他地方使用的域名,以避免覆盖 DNS 结果。",
|
||||
dnsDomainHelp:
|
||||
"为您的网络指定自定义节点 DNS 域名。此域名不应指向已在其他地方使用的域名,以避免覆盖 DNS 结果。",
|
||||
dnsDomainHostedPlaceholder: "netbird.cloud",
|
||||
dnsDomainSelfhostedPlaceholder: "netbird.selfhosted",
|
||||
networkRange: "网络范围",
|
||||
networkRangeHelp: "以 CIDR 格式为您的网络指定自定义 IPv4 范围。更改后将重新分配所有节点 IP。",
|
||||
networkRangeHelp:
|
||||
"以 CIDR 格式为您的网络指定自定义 IPv4 范围。更改后将重新分配所有节点 IP。",
|
||||
networkRangePlaceholder: "例如 100.64.0.0/16",
|
||||
ipv6NetworkRange: "IPv6 网络范围",
|
||||
ipv6NetworkRangeHelp: "以 CIDR 格式为您的网络指定自定义 IPv6 范围。更改后将重新分配所有节点 IPv6 地址。",
|
||||
ipv6NetworkRangeHelp:
|
||||
"以 CIDR 格式为您的网络指定自定义 IPv6 范围。更改后将重新分配所有节点 IPv6 地址。",
|
||||
ipv6NetworkRangePlaceholder: "例如 fd00:1234:5678::/64",
|
||||
ipv6EnabledGroups: "IPv6 启用组",
|
||||
ipv6EnabledGroupsHelp: "所选组中的节点将接收 IPv6 覆盖地址(双栈)。删除所有组以禁用 IPv6。更改将在保存时应用并重启受影响的客户端。",
|
||||
ipv6EnabledGroupsHelp:
|
||||
"所选组中的节点将接收 IPv6 覆盖地址(双栈)。删除所有组以禁用 IPv6。更改将在保存时应用并重启受影响的客户端。",
|
||||
selectIpv6Groups: "选择启用 IPv6 的组...",
|
||||
ipv6PrefixLengthError: "前缀长度必须介于 /48 和 /112 之间",
|
||||
ipv6FormatError: "请输入有效的 IPv6 CIDR 范围,例如 fd00:1234::/64",
|
||||
enableDnsWildcardRouting: "启用 DNS 通配符路由",
|
||||
dnsWildcardRoutingHelp: "允许使用 DNS 通配符进行路由。这需要 NetBird 客户端 v0.35 或更高版本。更改仅在重启客户端后生效。",
|
||||
dnsWildcardRoutingHelp:
|
||||
"允许使用 DNS 通配符进行路由。这需要 NetBird 客户端 v0.35 或更高版本。更改仅在重启客户端后生效。",
|
||||
configureIdpDescription: "为您的网络配置用于用户身份验证的身份提供者。",
|
||||
setupKeysDescription: "安装密钥是预身份验证密钥,允许在您的网络中注册新机器。",
|
||||
setupKeysDescription:
|
||||
"安装密钥是预身份验证密钥,允许在您的网络中注册新机器。",
|
||||
restrictDashboard: "限制普通用户访问仪表板",
|
||||
restrictDashboardHelp: "对仪表板的访问将受到限制,普通用户将无法查看任何节点。",
|
||||
restrictDashboardHelp:
|
||||
"对仪表板的访问将受到限制,普通用户将无法查看任何节点。",
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
idpProviderType: "提供商类型",
|
||||
@@ -742,7 +841,7 @@ disable2FA: "禁用两步验证",
|
||||
idpClientId: "客户端 ID",
|
||||
idpClientSecret: "客户端密钥",
|
||||
idpIssuerUrl: "Issuer URL",
|
||||
idpIssuerUrlHelp: "此提供商的 OIDC issuer URL"
|
||||
idpIssuerUrlHelp: "此提供商的 OIDC issuer URL",
|
||||
},
|
||||
reverseProxy: {
|
||||
title: "反向代理",
|
||||
@@ -786,8 +885,10 @@ disable2FA: "禁用两步验证",
|
||||
accessLogsDescription: "查看反向代理服务的访问日志",
|
||||
noAccessLogs: "暂无访问日志",
|
||||
servicesDescription: "通过 NetBird 的反向代理安全地暴露服务。",
|
||||
betaNoticeCloud: "NetBird 的反向代理目前处于测试阶段,在此期间免费使用。功能、特性和定价可能在上线时发生变化。",
|
||||
betaNoticeSelfHosted: "NetBird 的反向代理目前处于测试阶段。功能、特性和定价可能在上线时发生变化。",
|
||||
betaNoticeCloud:
|
||||
"NetBird 的反向代理目前处于测试阶段,在此期间免费使用。功能、特性和定价可能在上线时发生变化。",
|
||||
betaNoticeSelfHosted:
|
||||
"NetBird 的反向代理目前处于测试阶段。功能、特性和定价可能在上线时发生变化。",
|
||||
saveChanges: "保存更改",
|
||||
addServiceBtn: "添加服务",
|
||||
editServiceBtn: "编辑服务",
|
||||
@@ -800,7 +901,8 @@ disable2FA: "禁用两步验证",
|
||||
advancedSettings: "高级设置",
|
||||
netBirdOnlyAccess: "仅 NetBird 访问",
|
||||
netBirdOnlyAccessDescription: "仅所选 NetBird 组内的已连接节点可访问。",
|
||||
netBirdOnlyAccessTooltip: "仅 NetBird 访问需要至少一个已连接嵌入式代理(netbird proxy)的代理集群。所选集群没有嵌入式代理。请连接一个嵌入式代理以启用此选项。",
|
||||
netBirdOnlyAccessTooltip:
|
||||
"仅 NetBird 访问需要至少一个已连接嵌入式代理(netbird proxy)的代理集群。所选集群没有嵌入式代理。请连接一个嵌入式代理以启用此选项。",
|
||||
sso: "SSO(单点登录)",
|
||||
ssoDescription: "要求用户通过 SSO 身份验证才能访问此服务。",
|
||||
password: "密码",
|
||||
@@ -809,9 +911,11 @@ disable2FA: "禁用两步验证",
|
||||
pinCodeDescription: "要求输入数字 PIN 码才能访问此服务。",
|
||||
httpHeaders: "HTTP 请求头",
|
||||
httpHeadersDescription: "要求特定 HTTP 请求头才能访问此服务。",
|
||||
netBirdOnlyServiceNotice: "此服务仅可通过 NetBird 访问。默认会应用允许 NetBird 网络范围的规则。您在此处添加的规则会叠加在默认规则之上。",
|
||||
netBirdOnlyServiceNotice:
|
||||
"此服务仅可通过 NetBird 访问。默认会应用允许 NetBird 网络范围的规则。您在此处添加的规则会叠加在默认规则之上。",
|
||||
preserveClientSourceIp: "保留客户端源 IP",
|
||||
preserveClientSourceIpHelp: "使用 PROXY Protocol v2 将流量转发到后端时,保留客户端源 IP 地址。",
|
||||
preserveClientSourceIpHelp:
|
||||
"使用 PROXY Protocol v2 将流量转发到后端时,保留客户端源 IP 地址。",
|
||||
sessionIdleTimeout: "会话空闲超时",
|
||||
sessionIdleTimeoutHelp: "空闲超过此时间后关闭 UDP 会话。留空表示无超时。",
|
||||
connectionTimeout: "连接超时",
|
||||
@@ -820,23 +924,28 @@ disable2FA: "禁用两步验证",
|
||||
passHostHeader: "传递 Host 请求头",
|
||||
passHostHeaderHelp: "将原始 Host 请求头转发到后端,而不是改写为目标地址。",
|
||||
rewriteRedirects: "重写重定向",
|
||||
rewriteRedirectsHelp: "重写后端响应中的 Location 请求头,使用公共域名而不是内部后端地址。",
|
||||
rewriteRedirectsHelp:
|
||||
"重写后端响应中的 Location 请求头,使用公共域名而不是内部后端地址。",
|
||||
directUpstream: "直连上游",
|
||||
directUpstreamHelp: "从代理主机直接拨号上游目标,而不是通过 WireGuard 隧道。当上游在无 WireGuard 连接的情况下可达时打开。",
|
||||
directUpstreamHelpCluster: "对代理集群目标必需且锁定开启:集群没有可回退的 WireGuard 端点。",
|
||||
directUpstreamTooltip: "直连上游仅在至少一个已连接嵌入式代理(netbird proxy)的集群上可配置。所选集群没有嵌入式代理。",
|
||||
directUpstreamHelp:
|
||||
"从代理主机直接拨号上游目标,而不是通过 WireGuard 隧道。当上游在无 WireGuard 连接的情况下可达时打开。",
|
||||
directUpstreamHelpCluster:
|
||||
"对代理集群目标必需且锁定开启:集群没有可回退的 WireGuard 端点。",
|
||||
directUpstreamTooltip:
|
||||
"直连上游仅在至少一个已连接嵌入式代理(netbird proxy)的集群上可配置。所选集群没有嵌入式代理。",
|
||||
learnMoreServices: "服务",
|
||||
learnMoreAuthentication: "身份验证",
|
||||
learnMoreAccessControl: "访问控制",
|
||||
learnMoreSettings: "设置",
|
||||
noProtectionTitle: "未配置保护",
|
||||
noProtectionDescription: "此服务未配置身份验证或访问控制规则。它将对互联网上的所有人公开可访问。确定要继续吗?",
|
||||
noProtectionDescription:
|
||||
"此服务未配置身份验证或访问控制规则。它将对互联网上的所有人公开可访问。确定要继续吗?",
|
||||
httpsService: "HTTPS 服务",
|
||||
tlsPassthrough: "TLS 直通",
|
||||
tcpService: "TCP 服务",
|
||||
udpService: "UDP 服务",
|
||||
forwardTrafficDesc: "将流量直接转发到您的后端服务。",
|
||||
exposeServicesDesc: "通过 NetBird 的反向代理安全地暴露服务。"
|
||||
exposeServicesDesc: "通过 NetBird 的反向代理安全地暴露服务。",
|
||||
},
|
||||
dns: {
|
||||
title: "DNS",
|
||||
@@ -898,7 +1007,8 @@ disable2FA: "禁用两步验证",
|
||||
distributionGroupsLabel: "分配组",
|
||||
zoneGroupsHelp: "将此区域及其记录广播给属于以下组的节点。",
|
||||
enableSearchDomains: "启用搜索域名",
|
||||
searchDomainHelpZone: "例如,'server.company.internal' 将可通过'server'访问",
|
||||
searchDomainHelpZone:
|
||||
"例如,'server.company.internal' 将可通过'server'访问",
|
||||
enableDNSZone: "启用 DNS 区域",
|
||||
enableDisableDNSZone: "使用此开关启用或禁用 DNS 区域。",
|
||||
addDNSZone: "添加 DNS 区域",
|
||||
@@ -925,12 +1035,13 @@ disable2FA: "禁用两步验证",
|
||||
dnsZones: "DNS 区域",
|
||||
dns: "DNS",
|
||||
recordTypeAAAA: "AAAA",
|
||||
recordTypeCNAME: "CNAME"
|
||||
recordTypeCNAME: "CNAME",
|
||||
},
|
||||
networks: {
|
||||
title: "网络",
|
||||
description: "管理组织的网络和路由",
|
||||
pageDescription: "无需在每台机器上安装 NetBird,即可访问 LAN 和 VPC 中的内部资源。",
|
||||
pageDescription:
|
||||
"无需在每台机器上安装 NetBird,即可访问 LAN 和 VPC 中的内部资源。",
|
||||
networkName: "网络名称",
|
||||
networkNamePlaceholder: "例如:工程网络",
|
||||
createNetwork: "创建网络",
|
||||
@@ -957,9 +1068,11 @@ disable2FA: "禁用两步验证",
|
||||
addRoutingPeer: "添加路由节点",
|
||||
removeRoutingPeer: "移除路由节点",
|
||||
networkRoutes: "网络路由",
|
||||
routesDescription: "访问其他网络,如局域网和 VPC,无需在每个资源上安装 NetBird。",
|
||||
routesDescription:
|
||||
"访问其他网络,如局域网和 VPC,无需在每个资源上安装 NetBird。",
|
||||
learnMoreAbout: "了解更多关于",
|
||||
newNetworksRecommendation: "我们建议使用新的网络概念来更轻松地可视化和管理对资源的访问。",
|
||||
newNetworksRecommendation:
|
||||
"我们建议使用新的网络概念来更轻松地可视化和管理对资源的访问。",
|
||||
goToNetworks: "前往网络",
|
||||
createRoute: "创建路由",
|
||||
editRoute: "编辑路由",
|
||||
@@ -987,20 +1100,23 @@ disable2FA: "禁用两步验证",
|
||||
resourceDescriptionHelp: "写一个简短的描述为此资源添加更多上下文。",
|
||||
resourceDescriptionPlaceholder: "例如:生产环境、开发环境",
|
||||
resourceGroupsLabel: "资源组",
|
||||
resourceGroupsHelp: "将此资源添加到组(例如数据库、Web 服务器)中,并在访问策略中引用该组以简化管理。",
|
||||
resourceGroupsHelp:
|
||||
"将此资源添加到组(例如数据库、Web 服务器)中,并在访问策略中引用该组以简化管理。",
|
||||
resourceGroupsPlaceholder: "添加或选择资源组...",
|
||||
accessControl: "访问控制",
|
||||
resourceTab: "资源",
|
||||
optionalSettings: "可选设置",
|
||||
accessControlPolicies: "访问控制策略",
|
||||
accessControlPoliciesHelp: "定义允许访问此资源的源组。您还可以限制对特定协议和端口的访问。如果没有策略,将无法访问此资源。",
|
||||
accessControlPoliciesHelp:
|
||||
"定义允许访问此资源的源组。您还可以限制对特定协议和端口的访问。如果没有策略,将无法访问此资源。",
|
||||
routeType: "路由类型",
|
||||
routeTypeHelp: "选择路由类型以添加网络范围或域名列表。",
|
||||
routeTypeNetworkRange: "网络范围",
|
||||
routeTypeDomains: "域名",
|
||||
networkRange: "网络范围",
|
||||
networkRangeHelp: "添加私有 IPv4 或 IPv6 地址或范围",
|
||||
networkRangePlaceholder: "例如:172.16.0.1, 172.16.0.0/16, 2001:db8::1 或 2001:db8::/64",
|
||||
networkRangePlaceholder:
|
||||
"例如:172.16.0.1, 172.16.0.0/16, 2001:db8::1 或 2001:db8::/64",
|
||||
domains: "域名",
|
||||
distributionGroups: "分发组",
|
||||
networkIdentifier: "网络标识符",
|
||||
@@ -1009,7 +1125,7 @@ disable2FA: "禁用两步验证",
|
||||
metricPlaceholder: "输入度量值 (1-9999)",
|
||||
additionalSettings: "其他设置",
|
||||
accessControlGroups: "访问控制组",
|
||||
autoApply: "自动应用"
|
||||
autoApply: "自动应用",
|
||||
},
|
||||
postureChecks: {
|
||||
title: "姿态检查",
|
||||
@@ -1044,14 +1160,17 @@ disable2FA: "禁用两步验证",
|
||||
postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。",
|
||||
postureCheckNamePlaceholder: "例如:NetBird 版本 > 0.25.0",
|
||||
postureCheckDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
|
||||
postureCheckDescriptionPlaceholder: "例如:检查 NetBird 版本是否大于 0.25.0",
|
||||
postureCheckDescriptionPlaceholder:
|
||||
"例如:检查 NetBird 版本是否大于 0.25.0",
|
||||
netBirdClientVersion: "NetBird 客户端版本",
|
||||
netBirdClientVersionHelp: "根据特定的 NetBird 客户端版本限制对节点的访问。",
|
||||
netBirdClientVersionCheck: "客户端版本检查",
|
||||
minimumRequiredVersion: "最低所需版本",
|
||||
minimumRequiredVersionHelp: "仅具有指定最低 NetBird 客户端版本的对等节点才能访问网络。",
|
||||
minimumRequiredVersionHelp:
|
||||
"仅具有指定最低 NetBird 客户端版本的对等节点才能访问网络。",
|
||||
minimumRequiredVersionPlaceholder: "例如:0.25.0",
|
||||
minimumRequiredVersionError: "请输入有效版本,例如:0.2, 0.2.0, 0.2.0-alpha.1",
|
||||
minimumRequiredVersionError:
|
||||
"请输入有效版本,例如:0.2, 0.2.0, 0.2.0-alpha.1",
|
||||
countryAndRegion: "国家与地区",
|
||||
countryAndRegionHelp: "根据国家或地区限制网络中的访问。",
|
||||
countryAndRegionCheck: "国家与地区检查",
|
||||
@@ -1075,7 +1194,8 @@ disable2FA: "禁用两步验证",
|
||||
processHelp: "根据对等节点上正在运行的进程限制网络中的访问。",
|
||||
processCheck: "进程检查",
|
||||
processes: "进程",
|
||||
processesHelp: "添加进程的可执行文件路径。您可以为 Linux、macOS 和 Windows 定义路径。仅当进程在系统上运行时,对等节点才允许连接。",
|
||||
processesHelp:
|
||||
"添加进程的可执行文件路径。您可以为 Linux、macOS 和 Windows 定义路径。仅当进程在系统上运行时,对等节点才允许连接。",
|
||||
addProcess: "添加进程",
|
||||
linuxPathPlaceholder: "/usr/local/bin/netbird",
|
||||
macPathPlaceholder: "/Applications/NetBird.app/Contents/MacOS/netbird",
|
||||
@@ -1091,9 +1211,10 @@ disable2FA: "禁用两步验证",
|
||||
validCidr: "请输入有效的 CIDR,例如:192.168.1.0/24",
|
||||
cidrPlaceholder: "例如:172.16.0.0/16",
|
||||
noChecks: "您还没有添加任何态势检查",
|
||||
noChecksDescription: "添加各种态势检查以进一步限制网络中的访问。例如,仅允许具有特定 NetBird 客户端版本、操作系统或位置的客户端连接。",
|
||||
noChecksDescription:
|
||||
"添加各种态势检查以进一步限制网络中的访问。例如,仅允许具有特定 NetBird 客户端版本、操作系统或位置的客户端连接。",
|
||||
browseChecks: "浏览检查",
|
||||
newPostureCheck: "新建态势检查"
|
||||
newPostureCheck: "新建态势检查",
|
||||
},
|
||||
setupKeys: {
|
||||
title: "安装密钥",
|
||||
@@ -1134,13 +1255,27 @@ disable2FA: "禁用两步验证",
|
||||
expiresIn: "过期时间",
|
||||
expiresInHelp: "密钥过期前的天数。",
|
||||
expiresInHelpEmpty: "留空表示永不过期。",
|
||||
expiresInSuffix: "天"
|
||||
expiresInSuffix: "天",
|
||||
all: "全部",
|
||||
oneOff: "一次性",
|
||||
reusable: "可重复使用",
|
||||
status: "状态",
|
||||
groupNotUsedTitle: "此组尚未在任何安装密钥中使用",
|
||||
groupNotUsedDescription:
|
||||
"在创建新的安装密钥时分配此组,以便在此处查看它们。",
|
||||
getStartedDescription:
|
||||
"添加安装密钥以在您的网络中注册新设备。该密钥在初始设置期间将设备链接到您的账户。",
|
||||
learnMore: "了解更多关于",
|
||||
groups: "组",
|
||||
usage: "使用情况",
|
||||
lastUsedOn: "上次使用于",
|
||||
},
|
||||
activity: {
|
||||
title: "活动",
|
||||
description: "查看审计事件和活动日志",
|
||||
auditEvents: "审计事件",
|
||||
auditEventsDescription: "审计整个网络中的配置变更、访问策略更新以及节点注册和登录事件。",
|
||||
auditEventsDescription:
|
||||
"审计整个网络中的配置变更、访问策略更新以及节点注册和登录事件。",
|
||||
noEvents: "暂无事件",
|
||||
searchPlaceholder: "搜索事件...",
|
||||
searchByAuditNameUserPeerMeta: "按审计名称、用户、节点、元数据搜索...",
|
||||
@@ -1150,7 +1285,7 @@ disable2FA: "禁用两步验证",
|
||||
timestamp: "时间戳",
|
||||
ipAddress: "IP 地址",
|
||||
details: "详情",
|
||||
code: "代码"
|
||||
code: "代码",
|
||||
},
|
||||
controlCenter: {
|
||||
title: "控制中心",
|
||||
@@ -1161,10 +1296,10 @@ disable2FA: "禁用两步验证",
|
||||
totalGroups: "组总数",
|
||||
totalUsers: "用户总数",
|
||||
totalNetworks: "网络总数",
|
||||
networkOverview: "网络概览"
|
||||
networkOverview: "网络概览",
|
||||
},
|
||||
onboarding: {
|
||||
title: "开始使用 NetBird",
|
||||
addResource: "添加您的第一个资源"
|
||||
}
|
||||
addResource: "添加您的第一个资源",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,9 @@ type Props = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>): ColumnDef<AccessToken>[] {
|
||||
export function AccessTokensTableColumns(
|
||||
t: ReturnType<typeof useTranslations>,
|
||||
): ColumnDef<AccessToken>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -39,7 +41,9 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
|
||||
{
|
||||
accessorKey: "expiration_date",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("expires")}</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("expires")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpirationDateRow date={row.original.expiration_date} />
|
||||
@@ -48,7 +52,9 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
|
||||
{
|
||||
accessorKey: "last_used",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => {
|
||||
@@ -64,7 +70,7 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
|
||||
header: "",
|
||||
cell: ({ row }) => <AccessTokenActionCell access_token={row.original} />,
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
export default function AccessTokensTable({ user }: Readonly<Props>) {
|
||||
@@ -92,7 +98,7 @@ export default function AccessTokensTable({ user }: Readonly<Props>) {
|
||||
<Card className={"mt-5 w-full"}>
|
||||
{tokens && tokens.length > 0 ? (
|
||||
<DataTable
|
||||
text={"Access Tokens"}
|
||||
text={t("accessTokens")}
|
||||
tableClassName={"mt-0"}
|
||||
minimal={true}
|
||||
showSearchAndFilters={false}
|
||||
@@ -106,10 +112,8 @@ export default function AccessTokensTable({ user }: Readonly<Props>) {
|
||||
<div className={"bg-nb-gray-950 overflow-hidden"}>
|
||||
<NoResults
|
||||
className={"py-3"}
|
||||
title={"No access tokens"}
|
||||
description={
|
||||
"You don't have any access tokens yet. You can add a token to access the NetBird API."
|
||||
}
|
||||
title={t("noAccessTokens")}
|
||||
description={t("noAccessTokensDesc")}
|
||||
icon={<IconApi size={20} className={"fill-nb-gray-300"} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,6 +19,7 @@ import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||
export default function AddRouteDropdownButton() {
|
||||
const [modal, setModal] = useState(false);
|
||||
const [existingNetworkModal, setExistingNetworkModal] = useState(false);
|
||||
const t = useTranslations("common");
|
||||
const { peer } = usePeer();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
@@ -44,7 +46,7 @@ export default function AddRouteDropdownButton() {
|
||||
}}
|
||||
>
|
||||
<Button variant={"primary"}>
|
||||
Add Route
|
||||
{t("addRoute")}
|
||||
<ChevronDown size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -61,10 +63,10 @@ export default function AddRouteDropdownButton() {
|
||||
size={"small"}
|
||||
/>
|
||||
<div className={"flex flex-col text-left"}>
|
||||
<div className={"text-left text-white"}>New Network Route</div>
|
||||
<div className={"text-xs"}>
|
||||
Create a new network route with this peer
|
||||
<div className={"text-left text-white"}>
|
||||
{t("newNetworkRoute")}
|
||||
</div>
|
||||
<div className={"text-xs"}>{t("newNetworkRouteDesc")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -83,10 +85,10 @@ export default function AddRouteDropdownButton() {
|
||||
size={"small"}
|
||||
/>
|
||||
<div className={"flex flex-col text-left"}>
|
||||
<div className={"text-left text-white"}>Existing Network</div>
|
||||
<div className={"text-xs"}>
|
||||
Add this peer to an existing network
|
||||
<div className={"text-left text-white"}>
|
||||
{t("existingNetwork")}
|
||||
</div>
|
||||
<div className={"text-xs"}>{t("existingNetworkDesc")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -23,35 +23,6 @@ interface PeerEditIPModalProps {
|
||||
version: IPVersion;
|
||||
}
|
||||
|
||||
const config: Record<
|
||||
IPVersion,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
errorMessage: string;
|
||||
validate: (ip: string) => boolean;
|
||||
}
|
||||
> = {
|
||||
v4: {
|
||||
title: "Edit Peer IP Address",
|
||||
description: "Update the NetBird IP address for this peer.",
|
||||
placeholder: "e.g., 100.64.0.15",
|
||||
errorMessage: "Please enter a valid IP, e.g., 100.64.0.15",
|
||||
validate: (ip: string) =>
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
|
||||
ip,
|
||||
),
|
||||
},
|
||||
v6: {
|
||||
title: "Edit Peer IPv6 Address",
|
||||
description: "Update the NetBird IPv6 address for this peer.",
|
||||
placeholder: "e.g., fd00:1234::1",
|
||||
errorMessage: "Please enter a valid IPv6 address, e.g., fd00:1234::1",
|
||||
validate: (ip: string) => cidr.isValidAddress(ip) && ip.includes(":"),
|
||||
},
|
||||
};
|
||||
|
||||
export function PeerEditIPModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -60,6 +31,42 @@ export function PeerEditIPModal({
|
||||
version,
|
||||
}: Readonly<PeerEditIPModalProps>) {
|
||||
const t = useTranslations("peers");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
const config = useMemo<
|
||||
Record<
|
||||
IPVersion,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
errorMessage: string;
|
||||
validate: (ip: string) => boolean;
|
||||
}
|
||||
>
|
||||
>(
|
||||
() => ({
|
||||
v4: {
|
||||
title: t("editPeerIPAddress"),
|
||||
description: t("updatePeerIPDescription"),
|
||||
placeholder: t("editPeerIPPlaceholder"),
|
||||
errorMessage: t("editPeerIPErrorMessage"),
|
||||
validate: (ip: string) =>
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
|
||||
ip,
|
||||
),
|
||||
},
|
||||
v6: {
|
||||
title: t("editPeerIPv6Address"),
|
||||
description: t("updatePeerIPv6Description"),
|
||||
placeholder: t("editPeerIPv6Placeholder"),
|
||||
errorMessage: t("editPeerIPv6ErrorMessage"),
|
||||
validate: (ip: string) => cidr.isValidAddress(ip) && ip.includes(":"),
|
||||
},
|
||||
}),
|
||||
[t, version],
|
||||
);
|
||||
|
||||
const { title, description, placeholder, errorMessage, validate } =
|
||||
config[version];
|
||||
const [ip, setIP] = useState(currentIP);
|
||||
@@ -99,7 +106,7 @@ export function PeerEditIPModal({
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
{t("cancel")}
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
@@ -109,7 +116,7 @@ export function PeerEditIPModal({
|
||||
onClick={() => onSave(trim(ip))}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Save
|
||||
{tc("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import Card from "@components/Card";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
@@ -20,11 +21,14 @@ type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
function RouteTableColumns(
|
||||
t: ReturnType<typeof useTranslations>,
|
||||
): ColumnDef<Route>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "network_id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>{t("name")}</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <PeerRouteNameCell route={row.original} />,
|
||||
@@ -32,7 +36,9 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
{
|
||||
accessorKey: "network",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Network</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("network")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<GroupedRouteNetworkRangeCell
|
||||
@@ -46,7 +52,9 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
accessorFn: (r) => r.groups?.length,
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<DataTableHeader column={column}>Distribution Groups</DataTableHeader>
|
||||
<DataTableHeader column={column}>
|
||||
{t("distributionGroups")}
|
||||
</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <RouteDistributionGroupsCell route={row.original} />,
|
||||
@@ -56,7 +64,7 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
accessorKey: "enabled",
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Active</DataTableHeader>
|
||||
<DataTableHeader column={column}>{t("active")}</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => <PeerRouteActiveCell route={row.original} />,
|
||||
},
|
||||
@@ -65,13 +73,15 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
header: "",
|
||||
cell: ({ row }) => <PeerRouteActionCell route={row.original} />,
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
export default function PeerRoutesTable({
|
||||
peerRoutes,
|
||||
isLoading,
|
||||
peer,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
// Default sorting state of the table
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
@@ -87,15 +97,13 @@ export default function PeerRoutesTable({
|
||||
wrapperProps={{
|
||||
className: cn("w-full"),
|
||||
}}
|
||||
text={"Network Routes"}
|
||||
text={t("networkRoutes")}
|
||||
tableClassName={"mt-0"}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={"This peer has no network routes"}
|
||||
description={
|
||||
"You don't have any assigned network routes yet. You can add this peer to an existing network or create a new network route."
|
||||
}
|
||||
title={t("noNetworkRoutes")}
|
||||
description={t("noNetworkRoutesDesc")}
|
||||
icon={
|
||||
<NetworkRoutesIcon size={20} className={"fill-nb-gray-300"} />
|
||||
}
|
||||
@@ -107,7 +115,7 @@ export default function PeerRoutesTable({
|
||||
isLoading={isLoading}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={RouteTableColumns}
|
||||
columns={RouteTableColumns(t)}
|
||||
data={peerRoutes}
|
||||
paginationPaddingClassName={"px-0 pt-8"}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Group } from "@/interfaces/Group";
|
||||
import { orderBy } from "lodash";
|
||||
import CircleIcon from "@/assets/icons/CircleIcon";
|
||||
import Badge from "@components/Badge";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
|
||||
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||
@@ -37,6 +37,7 @@ import { isNetbirdSSHProtocolSupported } from "@utils/version";
|
||||
|
||||
export const PeerSSHToggle = () => {
|
||||
const t = useTranslations("peers");
|
||||
const tc = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { peer, toggleSSH, setSSHInstructionsModal } = usePeer();
|
||||
const { data: policies } = useFetchApi<Policy[]>(
|
||||
@@ -82,25 +83,22 @@ export const PeerSSHToggle = () => {
|
||||
|
||||
const disableDashboardSSH = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Disable SSH Access?`,
|
||||
title: t("disableSSHConfirmation"),
|
||||
description: (
|
||||
<div>
|
||||
Starting from NetBird v0.61.0, once SSH access is disabled, you cannot
|
||||
re-enable it again from the dashboard. You'll need to create an
|
||||
explicit access control policy and update your NetBird client to
|
||||
restore SSH functionality.{" "}
|
||||
{t("disableSSHDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/peers/ssh"}
|
||||
target={"_blank"}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Learn more
|
||||
{t("sshLearnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
),
|
||||
confirmText: "Disable",
|
||||
cancelText: "Cancel",
|
||||
confirmText: tc("disable"),
|
||||
cancelText: tc("cancel"),
|
||||
type: "warning",
|
||||
maxWidthClass: "max-w-xl",
|
||||
});
|
||||
@@ -114,9 +112,7 @@ export const PeerSSHToggle = () => {
|
||||
content={
|
||||
<div className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this setting.`}
|
||||
</span>
|
||||
<span>{t("noPermissionToUpdateSetting")}</span>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
@@ -135,9 +131,7 @@ export const PeerSSHToggle = () => {
|
||||
{t("sshAccess")}
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
helpText={t("sshAccessHelp")}
|
||||
/>
|
||||
</FullTooltip>
|
||||
<PeerSSHPolicyInfo peer={peer} />
|
||||
@@ -148,10 +142,7 @@ export const PeerSSHToggle = () => {
|
||||
<Label>{t("sshAccess")}</Label>
|
||||
</div>
|
||||
|
||||
<HelpText>
|
||||
Set up SSH and create an explicit access control policy defining which
|
||||
users can access specific local usernames of this machine via SSH.
|
||||
</HelpText>
|
||||
<HelpText>{t("sshSetupHelp")}</HelpText>
|
||||
|
||||
{!isNetbirdSSHProtocolSupported(peer.version) &&
|
||||
enabledPolicies?.length > 0 &&
|
||||
@@ -166,9 +157,7 @@ export const PeerSSHToggle = () => {
|
||||
}
|
||||
className="my-3"
|
||||
>
|
||||
You have SSH access configured but your client runs on an older
|
||||
NetBird version. Please update your NetBird client to v.0.61.0+ in
|
||||
order to allow SSH connections.
|
||||
{t("sshOldVersionWarning")}
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
@@ -183,9 +172,7 @@ export const PeerSSHToggle = () => {
|
||||
}
|
||||
className="my-3"
|
||||
>
|
||||
You have an SSH access policy configured, but the SSH server
|
||||
isn't enabled on this client. Enable the SSH server to allow SSH
|
||||
connections.
|
||||
{t("sshServerNotEnabled")}
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
@@ -200,9 +187,7 @@ export const PeerSSHToggle = () => {
|
||||
}
|
||||
className="my-3"
|
||||
>
|
||||
Your SSH server is enabled, but starting from NetBird v0.61.0, SSH
|
||||
requires an explicit access control policy. Please create an SSH
|
||||
access control policy in order to allow SSH connections.
|
||||
{t("sshNeedsPolicy")}
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
@@ -214,7 +199,7 @@ export const PeerSSHToggle = () => {
|
||||
disabled={!permission?.policies.create}
|
||||
>
|
||||
<CirclePlusIcon size={14} />
|
||||
Create SSH Policy
|
||||
{t("createSSHPolicy")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -300,11 +285,9 @@ export const PeerSSHToggle = () => {
|
||||
/>
|
||||
<div>
|
||||
<span className={"font-medium text-xs"}>
|
||||
{singularize(
|
||||
"Active Policies",
|
||||
enabledPolicies?.length,
|
||||
true,
|
||||
)}
|
||||
{t("activePoliciesCount", {
|
||||
count: enabledPolicies?.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Badge>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -16,6 +17,7 @@ import { CreateDebugJobModalContent } from "../jobs/CreateDebugJobModal";
|
||||
|
||||
export const RemoteJobDropdownButton = () => {
|
||||
const [modal, setModal] = useState(false);
|
||||
const t = useTranslations("common");
|
||||
const { peer } = usePeer();
|
||||
const { permission } = usePermissions();
|
||||
const isConnected = peer?.connected;
|
||||
@@ -39,7 +41,7 @@ export const RemoteJobDropdownButton = () => {
|
||||
}}
|
||||
>
|
||||
<Button variant={"primary"} disabled={disabled}>
|
||||
Run Remote Job
|
||||
{t("runRemoteJob")}
|
||||
<ChevronDown size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -52,10 +54,12 @@ export const RemoteJobDropdownButton = () => {
|
||||
}
|
||||
>
|
||||
<div>
|
||||
Peer{" "}
|
||||
<span className={"text-white font-medium"}>{peer.name}</span>{" "}
|
||||
is currently offline. Please connect the peer to run remote
|
||||
jobs.
|
||||
{t.rich("peerOfflineRemoteJob", {
|
||||
name: peer.name,
|
||||
bold: (chunks) => (
|
||||
<span className={"text-white font-medium"}>{chunks}</span>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -73,10 +77,8 @@ export const RemoteJobDropdownButton = () => {
|
||||
size={"small"}
|
||||
/>
|
||||
<div className={"flex flex-col text-left"}>
|
||||
<div className={"text-left text-white"}>Debug Bundle</div>
|
||||
<div className={"text-xs"}>
|
||||
Collect debug information for troubleshooting
|
||||
</div>
|
||||
<div className={"text-left text-white"}>{t("debugBundle")}</div>
|
||||
<div className={"text-xs"}>{t("debugBundleDesc")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ArrowUpDown, InfoIcon } from "lucide-react";
|
||||
|
||||
@@ -9,13 +10,14 @@ export default function RouteMetricCell({
|
||||
metric,
|
||||
useHoverStyle = true,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
return (
|
||||
<FullTooltip
|
||||
hoverButton={useHoverStyle}
|
||||
isAction={true}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs flex gap-2 items-center"}>
|
||||
<div>Lower metrics have higher priority.</div>
|
||||
<div>{t("metricPriority")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -44,12 +44,16 @@ import SetupKeyModal from "@/modules/setup-keys/SetupKeyModal";
|
||||
import SetupKeyNameCell from "@/modules/setup-keys/SetupKeyNameCell";
|
||||
import SetupKeyUsageCell from "@/modules/setup-keys/SetupKeyUsageCell";
|
||||
|
||||
export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): ColumnDef<SetupKey>[] {
|
||||
export function SetupKeysTableColumns(
|
||||
t: ReturnType<typeof useTranslations>,
|
||||
): ColumnDef<SetupKey>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("nameAndKey")}</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("nameAndKey")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
@@ -88,11 +92,13 @@ export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): Co
|
||||
{
|
||||
accessorKey: "last_used",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => (
|
||||
<LastTimeRow date={row.original.last_used} text={"Last used on"} />
|
||||
<LastTimeRow date={row.original.last_used} text={t("lastUsedOn")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -117,7 +123,9 @@ export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): Co
|
||||
{
|
||||
accessorKey: "expires",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("expires")}</DataTableHeader>;
|
||||
return (
|
||||
<DataTableHeader column={column}>{t("expires")}</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
let expires = dayjs(row.original.expires);
|
||||
@@ -136,7 +144,7 @@ export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): Co
|
||||
return <SetupKeyActionCell setupKey={row.original} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@@ -185,10 +193,8 @@ export default function SetupKeysTable({
|
||||
// only offers groups that actually appear in the table.
|
||||
const tableGroups = useMemo<Group[]>(
|
||||
() =>
|
||||
(uniqBy(
|
||||
setupKeys?.flatMap((k) => k.groups || []),
|
||||
"name",
|
||||
) as Group[]) || [],
|
||||
(uniqBy(setupKeys?.flatMap((k) => k.groups || []), "name") as Group[]) ||
|
||||
[],
|
||||
[setupKeys],
|
||||
);
|
||||
|
||||
@@ -197,27 +203,27 @@ export default function SetupKeysTable({
|
||||
// re-route it through the consolidated filter UI.
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Valid", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Expired", dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: t("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: t("active"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: t("expired"), dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const usageOptions = useMemo<RadioOption<string | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: "All" },
|
||||
{ value: "one-off", label: "One-off" },
|
||||
{ value: "reusable", label: "Reusable" },
|
||||
{ value: undefined, label: t("all") },
|
||||
{ value: "one-off", label: t("oneOff") },
|
||||
{ value: "reusable", label: t("reusable") },
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const filterDefs = useMemo<TableFilterDef[]>(
|
||||
() => [
|
||||
{
|
||||
id: "valid",
|
||||
label: "Status",
|
||||
label: t("status"),
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -231,7 +237,7 @@ export default function SetupKeysTable({
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
label: "Usage",
|
||||
label: t("usage"),
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as string | undefined}
|
||||
@@ -245,7 +251,7 @@ export default function SetupKeysTable({
|
||||
},
|
||||
{
|
||||
id: "group_names",
|
||||
label: "Groups",
|
||||
label: t("groups"),
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -257,7 +263,7 @@ export default function SetupKeysTable({
|
||||
formatChip: (v) => formatGroupsChip(v as string[] | undefined),
|
||||
},
|
||||
],
|
||||
[statusOptions, usageOptions, tableGroups],
|
||||
[statusOptions, usageOptions, tableGroups, t],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -273,14 +279,14 @@ export default function SetupKeysTable({
|
||||
inset={false}
|
||||
minimal={isGroupPage}
|
||||
keepStateInLocalStorage={!isGroupPage}
|
||||
text={"Setup Keys"}
|
||||
text={t("title")}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
initialPageSize={25}
|
||||
showResetFilterButton={false}
|
||||
columns={SetupKeysTableColumns(t)}
|
||||
data={setupKeys}
|
||||
searchPlaceholder={"Search by name, type or group..."}
|
||||
searchPlaceholder={t("searchPlaceholder")}
|
||||
columnVisibility={{
|
||||
valid: false,
|
||||
group_strings: false,
|
||||
@@ -295,10 +301,8 @@ export default function SetupKeysTable({
|
||||
<NoResults
|
||||
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
|
||||
className={"py-4"}
|
||||
title={"This group is not used within any setup keys yet"}
|
||||
description={
|
||||
"Assign this group when creating a new setup key to see them listed here."
|
||||
}
|
||||
title={t("groupNotUsedTitle")}
|
||||
description={t("groupNotUsedDescription")}
|
||||
>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
@@ -307,7 +311,7 @@ export default function SetupKeysTable({
|
||||
disabled={!permission.setup_keys.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Key
|
||||
{t("createSetupKey")}
|
||||
</Button>
|
||||
</NoResults>
|
||||
) : (
|
||||
@@ -321,10 +325,8 @@ export default function SetupKeysTable({
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Create Setup Key"}
|
||||
description={
|
||||
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
|
||||
}
|
||||
title={t("createSetupKey")}
|
||||
description={t("getStartedDescription")}
|
||||
button={
|
||||
<Button
|
||||
variant={"primary"}
|
||||
@@ -334,19 +336,19 @@ export default function SetupKeysTable({
|
||||
data-testid="open-create-setup-key"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Key
|
||||
{t("createSetupKey")}
|
||||
</Button>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more about
|
||||
{t("learnMore")}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Setup Keys
|
||||
{t("title")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
@@ -365,7 +367,7 @@ export default function SetupKeysTable({
|
||||
data-testid="open-create-setup-key"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Key
|
||||
{t("createSetupKey")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user