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:
sakuradairong
2026-06-23 23:12:27 +08:00
parent f9326f65c5
commit fdc7af186e
11 changed files with 1045 additions and 656 deletions

View File

@@ -1,73 +1,103 @@
import re """Batch i18n localization for remaining simple files"""
import os import re, os
os.chdir('/root/github_projects/dashboard') os.chdir('/root/github_projects/dashboard')
# List of files and their replacements # Helper: replace first occurrence after a marker
tasks = [ def replace_in_file(filepath, replacements):
{
'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']
with open(filepath) as f: with open(filepath) as f:
content = f.read() content = f.read()
changed = False changed = False
for old, new in replacements:
# 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']:
if old in content: if old in content:
content = content.replace(old, new) content = content.replace(old, new, 1)
changed = True changed = True
print(f"{filepath}: '{old}' -> '{new}'") print(f" {os.path.basename(filepath)}: replaced '{old[:40]}'")
else: else:
print(f"{filepath}: WARNING - '{old}' not found!") print(f" WARN: '{old[:40]}' not in {os.path.basename(filepath)}")
if changed: if changed:
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
f.write(content) 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

View File

@@ -2,7 +2,8 @@ export default {
common: { common: {
loading: "加载中...", loading: "加载中...",
restrictedAccessHeading: "您无权访问", restrictedAccessHeading: "您无权访问",
restrictedAccessDescription: "您似乎无权访问此页面。只有具有相应权限的用户才能访问此页面。请联系您的网络管理员获取更多信息。", restrictedAccessDescription:
"您似乎无权访问此页面。只有具有相应权限的用户才能访问此页面。请联系您的网络管理员获取更多信息。",
error: "错误", error: "错误",
success: "成功", success: "成功",
cancel: "取消", cancel: "取消",
@@ -79,6 +80,10 @@ export default {
blockUser: "阻止用户", blockUser: "阻止用户",
removePeersFromGroup: "从组中移除节点", removePeersFromGroup: "从组中移除节点",
routingPeer: "路由节点", routingPeer: "路由节点",
newNetworkRoute: "新网络路由",
existingNetwork: "现有网络",
debugBundle: "调试包",
metricPriority: "较低的度量值具有更高的优先级。",
peerGroup: "节点组", peerGroup: "节点组",
expires: "过期", expires: "过期",
distributionGroups: "分发组", distributionGroups: "分发组",
@@ -90,7 +95,25 @@ export default {
routingPeerHelp: "指定单个节点作为路由节点", routingPeerHelp: "指定单个节点作为路由节点",
exitNode: "出口节点", exitNode: "出口节点",
networkRoute: "网络路由", 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: { navigation: {
controlCenter: "控制中心", controlCenter: "控制中心",
@@ -120,7 +143,7 @@ export default {
auditEvents: "审计事件", auditEvents: "审计事件",
settings: "设置", settings: "设置",
documentation: "文档", documentation: "文档",
helpAndSupport: "帮助和支持" helpAndSupport: "帮助和支持",
}, },
table: { table: {
search: "搜索...", search: "搜索...",
@@ -141,7 +164,7 @@ export default {
cancel: "取消", cancel: "取消",
rows: "行", rows: "行",
selectAll: "全选", selectAll: "全选",
selectRow: "选择行" selectRow: "选择行",
}, },
auth: { auth: {
login: "登录", login: "登录",
@@ -159,14 +182,15 @@ export default {
accountBlocked: "账户已被阻止", accountBlocked: "账户已被阻止",
accountPending: "账户待审批", accountPending: "账户待审批",
sessionExpired: "会话已过期", sessionExpired: "会话已过期",
sessionExpiredDescription: "您的登录会话似乎不再活跃或已过期。请重新登录以继续使用应用。", sessionExpiredDescription:
"您的登录会话似乎不再活跃或已过期。请重新登录以继续使用应用。",
loginRequired: "需要登录", loginRequired: "需要登录",
unauthorized: "未授权", unauthorized: "未授权",
forbidden: "禁止访问", forbidden: "禁止访问",
accessDenied: "访问被拒绝", accessDenied: "访问被拒绝",
tooManyRequests: "请求过多", tooManyRequests: "请求过多",
tryAgainLater: "请稍后再试", tryAgainLater: "请稍后再试",
contactAdmin: "请联系管理员" contactAdmin: "请联系管理员",
}, },
errors: { errors: {
generic: "发生错误", generic: "发生错误",
@@ -177,7 +201,7 @@ export default {
validationError: "验证错误", validationError: "验证错误",
permissionDenied: "权限被拒绝", permissionDenied: "权限被拒绝",
resourceNotFound: "资源未找到", resourceNotFound: "资源未找到",
serviceUnavailable: "服务不可用" serviceUnavailable: "服务不可用",
}, },
peers: { peers: {
title: "节点", title: "节点",
@@ -219,12 +243,14 @@ export default {
pendingApprovals: "待审批", pendingApprovals: "待审批",
noPeers: "暂无节点", noPeers: "暂无节点",
noAccessiblePeersTitle: "此节点没有可访问的节点", noAccessiblePeersTitle: "此节点没有可访问的节点",
noAccessiblePeersDescription: "向您的网络添加更多节点,或检查访问控制策略。", noAccessiblePeersDescription:
"向您的网络添加更多节点,或检查访问控制策略。",
searchPlaceholder: "按名称或 IP 搜索节点...", searchPlaceholder: "按名称或 IP 搜索节点...",
selectPeer: "选择一个节点...", selectPeer: "选择一个节点...",
noPeersAvailable: "没有可选的节点。", noPeersAvailable: "没有可选的节点。",
noPeersMatching: "没有匹配的节点。", noPeersMatching: "没有匹配的节点。",
updateRequired: "请将 NetBird 更新到 v0.36.6 或更高版本,才能将此节点用作路由节点。", updateRequired:
"请将 NetBird 更新到 v0.36.6 或更高版本,才能将此节点用作路由节点。",
serialNumber: "序列号", serialNumber: "序列号",
loginExpiration: "会话过期", loginExpiration: "会话过期",
enableLoginExpiration: "启用人会话过期", enableLoginExpiration: "启用人会话过期",
@@ -235,9 +261,25 @@ export default {
enableSSH: "启用 SSH 访问", enableSSH: "启用 SSH 访问",
disableSSH: "禁用 SSH 访问", disableSSH: "禁用 SSH 访问",
disableSSHConfirmation: "禁用 SSH 访问?", disableSSHConfirmation: "禁用 SSH 访问?",
disableSSHDescription: "从 NetBird v0.61.0 开始,一旦 SSH 访问被禁用,将无法再从控制台重新启用。您需要创建一个明确的访问控制策略并更新 NetBird 客户端以恢复 SSH 功能。", disableSSHDescription:
"从 NetBird v0.61.0 开始,一旦 SSH 访问被禁用,将无法再从控制台重新启用。您需要创建一个明确的访问控制策略并更新 NetBird 客户端以恢复 SSH 功能。",
sshLearnMore: "了解更多", 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 连接。", connectTooltipOffline: "只有节点在线时才能通过 SSH 或 RDP 连接。",
expirationDisabledTooltip: "通过 setup-key 添加的所有节点都会禁用过期。", expirationDisabledTooltip: "通过 setup-key 添加的所有节点都会禁用过期。",
justNow: "刚刚", justNow: "刚刚",
@@ -255,12 +297,14 @@ export default {
groupsAssigning: "正在更新所选节点的组...", groupsAssigning: "正在更新所选节点的组...",
assigningGroups: "正在分配组...", assigningGroups: "正在分配组...",
groupsAssignedSuccess: "组已成功分配", groupsAssignedSuccess: "组已成功分配",
assignGroupsDescription: "将以下组分配给所选节点。除非选择覆盖,否则将保留先前分配的组。", assignGroupsDescription:
"将以下组分配给所选节点。除非选择覆盖,否则将保留先前分配的组。",
overwriteGroups: "覆盖现有组", overwriteGroups: "覆盖现有组",
overwriteGroupsHelp: "使用所选组覆盖节点的现有组。先前分配的组将被移除。", overwriteGroupsHelp: "使用所选组覆盖节点的现有组。先前分配的组将被移除。",
overwrite: "覆盖", overwrite: "覆盖",
overwriteGroupsConfirm: "覆盖现有组?", overwriteGroupsConfirm: "覆盖现有组?",
overwriteGroupsConfirmDescription: "确定要覆盖所选 {count} 个节点的现有组吗?此操作无法撤销。", overwriteGroupsConfirmDescription:
"确定要覆盖所选 {count} 个节点的现有组吗?此操作无法撤销。",
addGroups: "添加组", addGroups: "添加组",
assignedGroups: "已分配的组", assignedGroups: "已分配的组",
groupsSaved: "节点的组已成功保存", groupsSaved: "节点的组已成功保存",
@@ -279,10 +323,11 @@ export default {
windows: "Windows", windows: "Windows",
macos: "macOS", macos: "macOS",
android: "Android", android: "Android",
ios: "iOS" ios: "iOS",
}, },
updateAvailable: "有新版本可用", updateAvailable: "有新版本可用",
updateDescription: "NetBird 有新版本可用。请更新客户端以获取最新功能和错误修复。", updateDescription:
"NetBird 有新版本可用。请更新客户端以获取最新功能和错误修复。",
downloadChangelog: "下载与更新日志", downloadChangelog: "下载与更新日志",
dnsLabelCopied: "DNS 标签已复制到剪贴板", dnsLabelCopied: "DNS 标签已复制到剪贴板",
ipCopied: "IP 地址已复制到剪贴板", ipCopied: "IP 地址已复制到剪贴板",
@@ -297,7 +342,8 @@ export default {
domain: "域名", domain: "域名",
region: "地区", region: "地区",
regionCopied: "地区已复制到剪贴板", regionCopied: "地区已复制到剪贴板",
peerNotFoundDescription: "您尝试访问的节点不存在。可能已被删除,或您没有查看权限。请验证 URL 或返回控制台。", peerNotFoundDescription:
"您尝试访问的节点不存在。可能已被删除,或您没有查看权限。请验证 URL 或返回控制台。",
tabOverview: "概览", tabOverview: "概览",
tabNetworkRoutes: "网络路由", tabNetworkRoutes: "网络路由",
tabAccessiblePeers: "可访问节点", tabAccessiblePeers: "可访问节点",
@@ -307,6 +353,14 @@ export default {
assignGroups: "分配组", assignGroups: "分配组",
peerSaved: "节点已成功保存", peerSaved: "节点已成功保存",
peerSaving: "正在保存节点...", 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: "远程访问", remoteAccess: "远程访问",
remoteAccessDescription: "通过 SSH 或 RDP 直接连接到此节点。", remoteAccessDescription: "通过 SSH 或 RDP 直接连接到此节点。",
domainName: "域名", domainName: "域名",
@@ -320,40 +374,50 @@ export default {
peerIpv6Updated: "NetBird 节点 IPv6 已成功更新", peerIpv6Updated: "NetBird 节点 IPv6 已成功更新",
peerIpv6Updating: "正在更新节点 IPv6...", peerIpv6Updating: "正在更新节点 IPv6...",
noServicesForPeer: "此节点未配置服务", noServicesForPeer: "此节点未配置服务",
addServicesDescription: "将您的服务添加到此节点,并通过 NetBird 反向代理安全地暴露它们", addServicesDescription:
"将您的服务添加到此节点,并通过 NetBird 反向代理安全地暴露它们",
editPeerName: "编辑节点名称", editPeerName: "编辑节点名称",
editPeerNameDescription: "为您的节点设置一个易于识别的名称。", editPeerNameDescription: "为您的节点设置一个易于识别的名称。",
peerNamePlaceholder: "例如AWS 服务器", peerNamePlaceholder: "例如AWS 服务器",
domainNamePreview: "域名预览", domainNamePreview: "域名预览",
domainNamePreviewHelp: "如果域名已存在,我们会添加一个递增数字后缀。", domainNamePreviewHelp: "如果域名已存在,我们会添加一个递增数字后缀。",
userDevicesDescription: "笔记本电脑、手机和其他由用户操作的私人设备,通常在用户使用 SSO 登录时添加。", userDevicesDescription:
"笔记本电脑、手机和其他由用户操作的私人设备,通常在用户使用 SSO 登录时添加。",
learnMore: "了解更多", learnMore: "了解更多",
addNewDeviceTitle: "添加新设备到您的网络", addNewDeviceTitle: "添加新设备到您的网络",
addNewDeviceDescription: "首先,安装 NetBird 并使用您的电子邮件账户登录。之后您应该已连接。如有其他问题,请查看我们的", addNewDeviceDescription:
"首先,安装 NetBird 并使用您的电子邮件账户登录。之后您应该已连接。如有其他问题,请查看我们的",
installationGuide: "安装指南", installationGuide: "安装指南",
serversDescription: "服务器、虚拟机、自治代理和其他无用户的无人值守机器,通常使用安装密钥注册。", serversDescription:
addNewServerTitle: "添加新服务器到您的网络", "服务器、虚拟机、自治代理和其他无用户的无人值守机器,通常使用安装密钥注册。",
addNewServerDescription: "首先,在服务器上安装 NetBird 并使用安装密钥注册。如有其他问题,请查看我们的", addNewServerTitle: "添加新服务器到您的网络",
saveGroups: "保存组", addNewServerDescription:
"首先,在服务器上安装 NetBird 并使用安装密钥注册。如有其他问题,请查看我们的",
saveGroups: "保存组",
sessionExpiration: "会话过期", sessionExpiration: "会话过期",
sessionExpirationDescription: "启用后,要求 SSO 登录的节点在会话过期后必须重新身份验证。", sessionExpirationDescription:
inactivityExpirationDescription: "启用后,要求用户在与管理界面断开连接 10 分钟后重新身份验证。", "启用后,要求 SSO 登录的节点在会话过期后必须重新身份验证。",
inactivityExpirationDescription:
"启用后,要求用户在与管理界面断开连接 10 分钟后重新身份验证。",
setupKeyPeerExpirationDisabled: "此设置对使用安装密钥添加的所有节点禁用。", setupKeyPeerExpirationDisabled: "此设置对使用安装密钥添加的所有节点禁用。",
noPermissionToUpdateSetting: "您没有更新此设置所需的权限。", noPermissionToUpdateSetting: "您没有更新此设置所需的权限。",
globalSettingDisabled: "全局设置 {setting} 当前已禁用。请启用全局设置以便能够按节点单独切换。", globalSettingDisabled:
"全局设置 {setting} 当前已禁用。请启用全局设置以便能够按节点单独切换。",
goToSettings: "前往设置", goToSettings: "前往设置",
expirationUpdateSuccess: "过期时间已成功更新", expirationUpdateSuccess: "过期时间已成功更新",
expirationUpdating: "正在更新设置...", expirationUpdating: "正在更新设置...",
peerSessionExpiration: "节点会话过期", peerSessionExpiration: "节点会话过期",
requireLoginAfterDisconnect: "断开连接后要求重新登录", requireLoginAfterDisconnect: "断开连接后要求重新登录",
getStarted: "开始使用 NetBird", getStarted: "开始使用 NetBird",
getStartedDescription: "看起来您还没有任何连接的设备。\n开始使用向您的网络中添加一台设备。", getStartedDescription:
"看起来您还没有任何连接的设备。\n开始使用向您的网络中添加一台设备。",
learnMoreInOur: "在我们的", learnMoreInOur: "在我们的",
gettingStartedGuide: "入门指南", gettingStartedGuide: "入门指南",
userPeersDescription: "查看此用户注册的所有节点。", userPeersDescription: "查看此用户注册的所有节点。",
accessiblePeersDesc: "此节点可以连接到 NetBird 网络中的以下节点。", accessiblePeersDesc: "此节点可以连接到 NetBird 网络中的以下节点。",
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。", networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
remoteJobsDesc: "远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。" remoteJobsDesc:
"远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。",
}, },
policies: { policies: {
title: "策略", title: "策略",
@@ -403,50 +467,63 @@ saveGroups: "保存组",
portsPlaceholder: "例如443", portsPlaceholder: "例如443",
addPolicy: "添加策略", addPolicy: "添加策略",
createNewPolicy: "创建新策略", createNewPolicy: "创建新策略",
createNewPolicyDescription: "看起来您还没有任何策略。策略可以按特定协议和端口允许连接。", createNewPolicyDescription:
"看起来您还没有任何策略。策略可以按特定协议和端口允许连接。",
noPoliciesForGroup: "此组尚未在任何策略中使用", noPoliciesForGroup: "此组尚未在任何策略中使用",
noPoliciesForGroupDescription: "将组作为策略中的源或目标分配,以在此处查看其列表。", noPoliciesForGroupDescription:
temporaryPoliciesTooltip: "显示由 NetBird 浏览器客户端创建的临时策略。这些策略是临时的,将在一段时间后自动删除。", "将组作为策略中的源或目标分配,以在此处查看其列表。",
temporaryPoliciesTooltip:
"显示由 NetBird 浏览器客户端创建的临时策略。这些策略是临时的,将在一段时间后自动删除。",
learnMoreAbout: "了解更多关于", learnMoreAbout: "了解更多关于",
accessControls: "访问控制", accessControls: "访问控制",
policyActions: "策略操作", policyActions: "策略操作",
policyEnabledSuccess: "策略已成功启用", policyEnabledSuccess: "策略已成功启用",
policyDisabledSuccess: "策略已成功禁用", policyDisabledSuccess: "策略已成功禁用",
confirmDeleteTitle: "删除 '{name}'", confirmDeleteTitle: "删除 '{name}'",
confirmDeleteDescription: "确定要删除此访问控制策略吗?此操作无法撤销。", confirmDeleteDescription: "确定要删除此访问控制策略吗?此操作无法撤销。",
updatePolicy: "更新访问控制策略", updatePolicy: "更新访问控制策略",
modalDescription: "使用此策略限制对资源组的访问。", modalDescription: "使用此策略限制对资源组的访问。",
tabPolicy: "策略", tabPolicy: "策略",
tabNameDescription: "名称与描述", tabNameDescription: "名称与描述",
protocolHelp: "仅允许指定的网络协议。要更改流量方向和端口,请选择 TCP 或 UDP 协议。", protocolHelp:
selectProtocol: "选择协议...", "仅允许指定的网络协议。要更改流量方向和端口,请选择 TCP 或 UDP 协议。",
netbirdSshHelp: "为 SSH 专用策略选择 NetBird SSH 以进行细粒度访问控制,或使用端口 22 的 TCP 进行基本网络级 SSH 访问", selectProtocol: "选择协议...",
sourceHelp: "通常是一组用户设备(例如开发人员、市场人员)或点对点连接中将访问目标的单个设备。", netbirdSshHelp:
selectSource: "选择源...", "为 SSH 专用策略选择 NetBird SSH 以进行细粒度访问控制,或使用端口 22 的 TCP 进行基本网络级 SSH 访问",
destinationHelp: "通常是一组节点或资源(例如服务器、数据库、内部服务),将由源访问。也可以是单个节点或资源。", sourceHelp:
selectDestination: "选择目标...", "通常是一组用户设备(例如开发人员、市场人员)或点对点连接中将访问目标的单个设备。",
resourcesBidirectionalWarning: "某些目标组包含资源。资源仅支持入站流量,无法发起连接。", selectSource: "选择源...",
sshResourceWarning: "SSH 访问仅适用于节点,不适用于路由资源。请确保您的目标组包含用于 SSH 连接的节点。", destinationHelp:
sshAccess: "SSH 访问", "通常是一组节点或资源(例如服务器、数据库、内部服务),将由源访问。也可以是单个节点或资源。",
sshAccessHelp: "选择'完全访问'以允许任何本地用户进行 SSH或选择'受限访问'以指定每个组允许使用的本地用户。", selectDestination: "选择目标...",
ports: "端口", resourcesBidirectionalWarning:
portsHelp: "仅允许对指定端口的网络流量和访问。选择 1 到 65535 之间的端口或端口范围。", "某些目标组包含资源。资源仅支持入站流量,无法发起连接。",
enablePolicy: "启用策略", sshResourceWarning:
enablePolicyHelp: "使用此开关启用或禁用策略。", "SSH 访问仅适用于节点,不适用于路由资源。请确保您的目标组包含用于 SSH 连接的节点。",
ruleName: "规则名称", sshAccess: "SSH 访问",
ruleNameHelp: "为策略设置一个易于识别的名称。", sshAccessHelp:
ruleNamePlaceholder: "例如:开发人员到服务器", "选择'完全访问'以允许任何本地用户进行 SSH或选择'受限访问'以指定每个组允许使用的本地用户。",
policyDescriptionLabel: "描述(可选)", ports: "端口",
policyDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。", portsHelp:
policyDescriptionPlaceholder: "例如:允许开发人员访问服务器,并允许服务器访问开发人员。", "仅允许对指定端口的网络流量和访问。选择 1 到 65535 之间的端口或端口范围。",
accessControlDescription: "创建规则以管理网络中的访问,并定义节点可以连接的内容。", enablePolicy: "启用策略",
enablePolicyHelp: "使用此开关启用或禁用策略。",
ruleName: "规则名称",
ruleNameHelp: "为策略设置一个易于识别的名称。",
ruleNamePlaceholder: "例如:开发人员到服务器",
policyDescriptionLabel: "描述(可选)",
policyDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
policyDescriptionPlaceholder:
"例如:允许开发人员访问服务器,并允许服务器访问开发人员。",
accessControlDescription:
"创建规则以管理网络中的访问,并定义节点可以连接的内容。",
policyCreated: "策略 '{name}' 创建成功", policyCreated: "策略 '{name}' 创建成功",
policyUpdated: "策略 '{name}' 更新成功", policyUpdated: "策略 '{name}' 更新成功",
policyDeleted: "策略 '{name}' 已删除", policyDeleted: "策略 '{name}' 已删除",
policyEnableLoading: "正在启用策略...", policyEnableLoading: "正在启用策略...",
policyDisableLoading: "正在禁用策略...", policyDisableLoading: "正在禁用策略...",
policySaveLoading: "正在保存策略...", policySaveLoading: "正在保存策略...",
policyDeleteLoading: "正在删除策略..." policyDeleteLoading: "正在删除策略...",
}, },
groups: { groups: {
title: "组", title: "组",
@@ -490,11 +567,11 @@ accessControlDescription: "创建规则以管理网络中的访问,并定义
unused: "未使用", unused: "未使用",
usage: "使用情况", usage: "使用情况",
inUse: "正在使用", inUse: "正在使用",
nameservers: "名称服务器", nameservers: "名称服务器",
zones: "区域", zones: "区域",
routes: "路由", routes: "路由",
setupKeys: "安装密钥", setupKeys: "安装密钥",
viewDetails: "查看详情", viewDetails: "查看详情",
rename: "重命名", rename: "重命名",
groupsDescription: "将节点、用户和资源组织到组中以管理访问。", groupsDescription: "将节点、用户和资源组织到组中以管理访问。",
allGroups: "所有组", allGroups: "所有组",
@@ -507,7 +584,7 @@ viewDetails: "查看详情",
renameDisabledIdP: "此组由 IdP 颁发,无法重命名。", renameDisabledIdP: "此组由 IdP 颁发,无法重命名。",
deleteDisabledIdP: "此组由 IdP 颁发,无法删除。", deleteDisabledIdP: "此组由 IdP 颁发,无法删除。",
deleteDisabledInUse: "请先移除此组的依赖关系后再删除。", deleteDisabledInUse: "请先移除此组的依赖关系后再删除。",
assignedGroups: "已分配的组" assignedGroups: "已分配的组",
}, },
users: { users: {
title: "用户", title: "用户",
@@ -564,7 +641,7 @@ viewDetails: "查看详情",
approving: "正在批准用户...", approving: "正在批准用户...",
userRejected: "用户 {name} 已拒绝", userRejected: "用户 {name} 已拒绝",
rejecting: "正在拒绝用户...", rejecting: "正在拒绝用户...",
copyUserId: "复制用户 ID", copyUserId: "复制用户 ID",
copyUserIdSuccess: "用户 ID 已复制到剪贴板", copyUserIdSuccess: "用户 ID 已复制到剪贴板",
networkAdmin: "网络管理员", networkAdmin: "网络管理员",
billingAdmin: "账单管理员", billingAdmin: "账单管理员",
@@ -573,13 +650,15 @@ copyUserId: "复制用户 ID",
lastLoginOn: "最后登录于", lastLoginOn: "最后登录于",
showInvites: "显示邀请", showInvites: "显示邀请",
addNewUsers: "添加新用户", addNewUsers: "添加新用户",
addNewUsersDescription: "看起来您还没有任何用户。开始使用,邀请用户加入您的账户。", addNewUsersDescription:
"看起来您还没有任何用户。开始使用,邀请用户加入您的账户。",
addUser: "添加用户", addUser: "添加用户",
localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身份验证。", localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身份验证。",
team: "团队", team: "团队",
usersPageDescription: "管理用户及其权限。同域名电子邮件用户在首次登录时会自动添加。", usersPageDescription:
"管理用户及其权限。同域名电子邮件用户在首次登录时会自动添加。",
expiresIn: "过期时间", expiresIn: "过期时间",
expiresInHelp: "邀请过期前的天数。" expiresInHelp: "邀请过期前的天数。",
}, },
serviceUsers: { serviceUsers: {
title: "服务用户", title: "服务用户",
@@ -608,15 +687,16 @@ localAuthDisabled: "本地身份验证已禁用。请使用您的 IdP 进行身
userDeleted: "服务用户 '{name}' 已删除", userDeleted: "服务用户 '{name}' 已删除",
createLoading: "正在创建服务用户...", createLoading: "正在创建服务用户...",
updateLoading: "正在更新服务用户...", updateLoading: "正在更新服务用户...",
deleteLoading: "正在删除服务用户...", deleteLoading: "正在删除服务用户...",
serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自动化访问。", serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自动化访问。",
serviceUsersEmptyDescription: "看起来您还没有任何服务用户。开始使用,创建一个服务用户。", serviceUsersEmptyDescription:
"看起来您还没有任何服务用户。开始使用,创建一个服务用户。",
blocked: "已阻止", blocked: "已阻止",
accessTokens: "访问令牌", accessTokens: "访问令牌",
accessTokensDescription: "访问令牌提供对 NetBird API 的访问权限。", accessTokensDescription: "访问令牌提供对 NetBird API 的访问权限。",
tokenName: "名称", tokenName: "名称",
tokenNameHelp: "为令牌设置一个易于识别的名称", tokenNameHelp: "为令牌设置一个易于识别的名称",
tokenExpiresIn: "过期时间" tokenExpiresIn: "过期时间",
}, },
settings: { settings: {
title: "设置", title: "设置",
@@ -642,7 +722,7 @@ serviceUsersDescription: "使用服务用户创建 API 令牌,避免丢失自
confirmPassword: "确认密码", confirmPassword: "确认密码",
twoFactor: "两步验证", twoFactor: "两步验证",
enable2FA: "启用两步验证", enable2FA: "启用两步验证",
disable2FA: "禁用两步验证", disable2FA: "禁用两步验证",
authentication: "身份验证", authentication: "身份验证",
setupKeys: "安装密钥", setupKeys: "安装密钥",
identityProviders: "身份提供者", identityProviders: "身份提供者",
@@ -659,7 +739,8 @@ disable2FA: "禁用两步验证",
deleteAccountSuccess: "NetBird 账户已成功删除。", deleteAccountSuccess: "NetBird 账户已成功删除。",
deleteAccountLoading: "正在删除账户...", deleteAccountLoading: "正在删除账户...",
deleteAccountCardTitle: "删除 NetBird 账户", deleteAccountCardTitle: "删除 NetBird 账户",
deleteAccountWarning: "在继续删除 NetBird 账户之前,请注意此操作不可撤销。一旦您的账户被删除,您将永久失去对所有关联数据的访问权限,包括您的节点、用户、组、策略和路由。", deleteAccountWarning:
"在继续删除 NetBird 账户之前,请注意此操作不可撤销。一旦您的账户被删除,您将永久失去对所有关联数据的访问权限,包括您的节点、用户、组、策略和路由。",
deleteAccountButton: "删除账户", deleteAccountButton: "删除账户",
endpointUrls: "端点 URL", endpointUrls: "端点 URL",
endpointUrlsHelp: "将这些添加到您的身份提供者配置中", endpointUrlsHelp: "将这些添加到您的身份提供者配置中",
@@ -674,65 +755,83 @@ disable2FA: "禁用两步验证",
beta: "Beta", beta: "Beta",
selectInterval: "选择间隔...", selectInterval: "选择间隔...",
userApprovalRequired: "需要用户审批", userApprovalRequired: "需要用户审批",
userApprovalHelp: "对通过域名匹配加入的新用户要求手动审批。用户在被审批前将被阻止。", userApprovalHelp:
"对通过域名匹配加入的新用户要求手动审批。用户在被审批前将被阻止。",
enableLocalMFA: "启用本地多因素认证", enableLocalMFA: "启用本地多因素认证",
localMfaHelp: "对使用本地凭据认证的用户要求多因素认证。", localMfaHelp: "对使用本地凭据认证的用户要求多因素认证。",
peerSessionExpiration: "节点会话过期", peerSessionExpiration: "节点会话过期",
peerSessionExpirationHelp: "对通过 SSO 注册的节点请求定期重新认证。", peerSessionExpirationHelp: "对通过 SSO 注册的节点请求定期重新认证。",
sessionExpiration: "会话过期", sessionExpiration: "会话过期",
sessionExpirationHelp: "使用 SSO 登录添加的每个节点在此时间后需要重新认证。", sessionExpirationHelp:
"使用 SSO 登录添加的每个节点在此时间后需要重新认证。",
requireLoginAfterDisconnect: "断开连接后要求重新登录", requireLoginAfterDisconnect: "断开连接后要求重新登录",
requireLoginHelp: "启用后,用户从管理界面断开连接 10 分钟后将需要重新认证。", requireLoginHelp:
"启用后,用户从管理界面断开连接 10 分钟后将需要重新认证。",
automaticUpdates: "自动更新", automaticUpdates: "自动更新",
automaticUpdatesHelp: "配置 NetBird 客户端如何接收更新通知。启用后,用户将被提示安装所选版本。这至少需要 NetBird v0.61.0。", automaticUpdatesHelp:
"配置 NetBird 客户端如何接收更新通知。启用后,用户将被提示安装所选版本。这至少需要 NetBird v0.61.0。",
versionCustomPrefix: "版本", versionCustomPrefix: "版本",
versionCustomPlaceholder: "例如0.52.2", versionCustomPlaceholder: "例如0.52.2",
forceAutomaticUpdates: "强制自动更新", forceAutomaticUpdates: "强制自动更新",
forceAutomaticUpdatesHelp: "启用后,更新将在后台自动安装,无需用户交互。", forceAutomaticUpdatesHelp: "启用后,更新将在后台自动安装,无需用户交互。",
automaticUpdatesWarning: "启用自动更新将在更新过程中重启 NetBird 客户端,可能暂时中断活动连接。在生产环境中请谨慎使用。", automaticUpdatesWarning:
"启用自动更新将在更新过程中重启 NetBird 客户端,可能暂时中断活动连接。在生产环境中请谨慎使用。",
exposeServicesFromCli: "通过 CLI 暴露服务", exposeServicesFromCli: "通过 CLI 暴露服务",
exposeServicesFromCliHelp: "允许节点通过 NetBird 反向代理使用 CLI 暴露本地服务。这至少需要 NetBird v0.66.0。", exposeServicesFromCliHelp:
"允许节点通过 NetBird 反向代理使用 CLI 暴露本地服务。这至少需要 NetBird v0.66.0。",
enablePeerExpose: "启用节点暴露", enablePeerExpose: "启用节点暴露",
enablePeerExposeHelp: "启用后,节点可以通过公共 URL 暴露本地 HTTP 服务。", enablePeerExposeHelp: "启用后,节点可以通过公共 URL 暴露本地 HTTP 服务。",
allowedPeerGroups: "允许的节点组", allowedPeerGroups: "允许的节点组",
allowedPeerGroupsHelp: "选择允许暴露服务的节点组。至少需要一个组。", allowedPeerGroupsHelp: "选择允许暴露服务的节点组。至少需要一个组。",
selectPeerGroups: "选择节点组...", selectPeerGroups: "选择节点组...",
experimental: "实验性", experimental: "实验性",
experimentalHelp: "惰性连接是一个实验性功能。功能和行为可能会发展。NetBird 不再维护始终在线的连接,而是根据活动或信令按需激活它们。", experimentalHelp:
"惰性连接是一个实验性功能。功能和行为可能会发展。NetBird 不再维护始终在线的连接,而是根据活动或信令按需激活它们。",
enableLazyConnections: "启用惰性连接", enableLazyConnections: "启用惰性连接",
enableLazyConnectionsHelp: "惰性连接根据活动或信令按需激活,而不是维护始终在线的连接。", enableLazyConnectionsHelp:
"惰性连接根据活动或信令按需激活,而不是维护始终在线的连接。",
enableGroupPropagation: "启用用户组传播", enableGroupPropagation: "启用用户组传播",
groupPropagationHelp: "允许从用户的自动组向节点传播组,共享成员资格信息。", groupPropagationHelp: "允许从用户的自动组向节点传播组,共享成员资格信息。",
enableJwtGroupSync: "启用 JWT 组同步", enableJwtGroupSync: "启用 JWT 组同步",
jwtGroupSyncHelp: "从 JWT 声明中提取并同步组与用户的自动组,根据令牌自动创建组。", jwtGroupSyncHelp:
"从 JWT 声明中提取并同步组与用户的自动组,根据令牌自动创建组。",
jwtClaim: "JWT 声明", jwtClaim: "JWT 声明",
jwtClaimHelp: "指定用于提取组名称的 JWT 声明,例如 roles 或 groups以添加到账户组此声明应包含组名称列表。", jwtClaimHelp:
"指定用于提取组名称的 JWT 声明,例如 roles 或 groups以添加到账户组此声明应包含组名称列表。",
jwtClaimPlaceholder: "例如roles", jwtClaimPlaceholder: "例如roles",
jwtAllowGroups: "JWT 允许的组", jwtAllowGroups: "JWT 允许的组",
jwtAllowGroupsHelp: "限制指定组名称对 NetBird 的访问,例如 NetBird 用户。要使用这些组,您需要先在您的 IdP 中配置它们。", jwtAllowGroupsHelp:
"限制指定组名称对 NetBird 的访问,例如 NetBird 用户。要使用这些组,您需要先在您的 IdP 中配置它们。",
addGroupPlaceholder: "添加组并按 Enter", addGroupPlaceholder: "添加组并按 Enter",
jwtGroupAccessWarning: "为防止失去访问权限,请确保您是该组的成员。", jwtGroupAccessWarning: "为防止失去访问权限,请确保您是该组的成员。",
dnsDomain: "DNS 域名", dnsDomain: "DNS 域名",
dnsDomainHelp: "为您的网络指定自定义节点 DNS 域名。此域名不应指向已在其他地方使用的域名,以避免覆盖 DNS 结果。", dnsDomainHelp:
"为您的网络指定自定义节点 DNS 域名。此域名不应指向已在其他地方使用的域名,以避免覆盖 DNS 结果。",
dnsDomainHostedPlaceholder: "netbird.cloud", dnsDomainHostedPlaceholder: "netbird.cloud",
dnsDomainSelfhostedPlaceholder: "netbird.selfhosted", dnsDomainSelfhostedPlaceholder: "netbird.selfhosted",
networkRange: "网络范围", networkRange: "网络范围",
networkRangeHelp: "以 CIDR 格式为您的网络指定自定义 IPv4 范围。更改后将重新分配所有节点 IP。", networkRangeHelp:
"以 CIDR 格式为您的网络指定自定义 IPv4 范围。更改后将重新分配所有节点 IP。",
networkRangePlaceholder: "例如 100.64.0.0/16", networkRangePlaceholder: "例如 100.64.0.0/16",
ipv6NetworkRange: "IPv6 网络范围", ipv6NetworkRange: "IPv6 网络范围",
ipv6NetworkRangeHelp: "以 CIDR 格式为您的网络指定自定义 IPv6 范围。更改后将重新分配所有节点 IPv6 地址。", ipv6NetworkRangeHelp:
"以 CIDR 格式为您的网络指定自定义 IPv6 范围。更改后将重新分配所有节点 IPv6 地址。",
ipv6NetworkRangePlaceholder: "例如 fd00:1234:5678::/64", ipv6NetworkRangePlaceholder: "例如 fd00:1234:5678::/64",
ipv6EnabledGroups: "IPv6 启用组", ipv6EnabledGroups: "IPv6 启用组",
ipv6EnabledGroupsHelp: "所选组中的节点将接收 IPv6 覆盖地址(双栈)。删除所有组以禁用 IPv6。更改将在保存时应用并重启受影响的客户端。", ipv6EnabledGroupsHelp:
"所选组中的节点将接收 IPv6 覆盖地址(双栈)。删除所有组以禁用 IPv6。更改将在保存时应用并重启受影响的客户端。",
selectIpv6Groups: "选择启用 IPv6 的组...", selectIpv6Groups: "选择启用 IPv6 的组...",
ipv6PrefixLengthError: "前缀长度必须介于 /48 和 /112 之间", ipv6PrefixLengthError: "前缀长度必须介于 /48 和 /112 之间",
ipv6FormatError: "请输入有效的 IPv6 CIDR 范围,例如 fd00:1234::/64", ipv6FormatError: "请输入有效的 IPv6 CIDR 范围,例如 fd00:1234::/64",
enableDnsWildcardRouting: "启用 DNS 通配符路由", enableDnsWildcardRouting: "启用 DNS 通配符路由",
dnsWildcardRoutingHelp: "允许使用 DNS 通配符进行路由。这需要 NetBird 客户端 v0.35 或更高版本。更改仅在重启客户端后生效。", dnsWildcardRoutingHelp:
"允许使用 DNS 通配符进行路由。这需要 NetBird 客户端 v0.35 或更高版本。更改仅在重启客户端后生效。",
configureIdpDescription: "为您的网络配置用于用户身份验证的身份提供者。", configureIdpDescription: "为您的网络配置用于用户身份验证的身份提供者。",
setupKeysDescription: "安装密钥是预身份验证密钥,允许在您的网络中注册新机器。", setupKeysDescription:
"安装密钥是预身份验证密钥,允许在您的网络中注册新机器。",
restrictDashboard: "限制普通用户访问仪表板", restrictDashboard: "限制普通用户访问仪表板",
restrictDashboardHelp: "对仪表板的访问将受到限制,普通用户将无法查看任何节点。", restrictDashboardHelp:
"对仪表板的访问将受到限制,普通用户将无法查看任何节点。",
name: "名称", name: "名称",
type: "类型", type: "类型",
idpProviderType: "提供商类型", idpProviderType: "提供商类型",
@@ -742,7 +841,7 @@ disable2FA: "禁用两步验证",
idpClientId: "客户端 ID", idpClientId: "客户端 ID",
idpClientSecret: "客户端密钥", idpClientSecret: "客户端密钥",
idpIssuerUrl: "Issuer URL", idpIssuerUrl: "Issuer URL",
idpIssuerUrlHelp: "此提供商的 OIDC issuer URL" idpIssuerUrlHelp: "此提供商的 OIDC issuer URL",
}, },
reverseProxy: { reverseProxy: {
title: "反向代理", title: "反向代理",
@@ -786,8 +885,10 @@ disable2FA: "禁用两步验证",
accessLogsDescription: "查看反向代理服务的访问日志", accessLogsDescription: "查看反向代理服务的访问日志",
noAccessLogs: "暂无访问日志", noAccessLogs: "暂无访问日志",
servicesDescription: "通过 NetBird 的反向代理安全地暴露服务。", servicesDescription: "通过 NetBird 的反向代理安全地暴露服务。",
betaNoticeCloud: "NetBird 的反向代理目前处于测试阶段,在此期间免费使用。功能、特性和定价可能在上线时发生变化。", betaNoticeCloud:
betaNoticeSelfHosted: "NetBird 的反向代理目前处于测试阶段。功能、特性和定价可能在上线时发生变化。", "NetBird 的反向代理目前处于测试阶段,在此期间免费使用。功能、特性和定价可能在上线时发生变化。",
betaNoticeSelfHosted:
"NetBird 的反向代理目前处于测试阶段。功能、特性和定价可能在上线时发生变化。",
saveChanges: "保存更改", saveChanges: "保存更改",
addServiceBtn: "添加服务", addServiceBtn: "添加服务",
editServiceBtn: "编辑服务", editServiceBtn: "编辑服务",
@@ -800,7 +901,8 @@ disable2FA: "禁用两步验证",
advancedSettings: "高级设置", advancedSettings: "高级设置",
netBirdOnlyAccess: "仅 NetBird 访问", netBirdOnlyAccess: "仅 NetBird 访问",
netBirdOnlyAccessDescription: "仅所选 NetBird 组内的已连接节点可访问。", netBirdOnlyAccessDescription: "仅所选 NetBird 组内的已连接节点可访问。",
netBirdOnlyAccessTooltip: "仅 NetBird 访问需要至少一个已连接嵌入式代理netbird proxy的代理集群。所选集群没有嵌入式代理。请连接一个嵌入式代理以启用此选项。", netBirdOnlyAccessTooltip:
"仅 NetBird 访问需要至少一个已连接嵌入式代理netbird proxy的代理集群。所选集群没有嵌入式代理。请连接一个嵌入式代理以启用此选项。",
sso: "SSO单点登录", sso: "SSO单点登录",
ssoDescription: "要求用户通过 SSO 身份验证才能访问此服务。", ssoDescription: "要求用户通过 SSO 身份验证才能访问此服务。",
password: "密码", password: "密码",
@@ -809,9 +911,11 @@ disable2FA: "禁用两步验证",
pinCodeDescription: "要求输入数字 PIN 码才能访问此服务。", pinCodeDescription: "要求输入数字 PIN 码才能访问此服务。",
httpHeaders: "HTTP 请求头", httpHeaders: "HTTP 请求头",
httpHeadersDescription: "要求特定 HTTP 请求头才能访问此服务。", httpHeadersDescription: "要求特定 HTTP 请求头才能访问此服务。",
netBirdOnlyServiceNotice: "此服务仅可通过 NetBird 访问。默认会应用允许 NetBird 网络范围的规则。您在此处添加的规则会叠加在默认规则之上。", netBirdOnlyServiceNotice:
"此服务仅可通过 NetBird 访问。默认会应用允许 NetBird 网络范围的规则。您在此处添加的规则会叠加在默认规则之上。",
preserveClientSourceIp: "保留客户端源 IP", preserveClientSourceIp: "保留客户端源 IP",
preserveClientSourceIpHelp: "使用 PROXY Protocol v2 将流量转发到后端时,保留客户端源 IP 地址。", preserveClientSourceIpHelp:
"使用 PROXY Protocol v2 将流量转发到后端时,保留客户端源 IP 地址。",
sessionIdleTimeout: "会话空闲超时", sessionIdleTimeout: "会话空闲超时",
sessionIdleTimeoutHelp: "空闲超过此时间后关闭 UDP 会话。留空表示无超时。", sessionIdleTimeoutHelp: "空闲超过此时间后关闭 UDP 会话。留空表示无超时。",
connectionTimeout: "连接超时", connectionTimeout: "连接超时",
@@ -820,23 +924,28 @@ disable2FA: "禁用两步验证",
passHostHeader: "传递 Host 请求头", passHostHeader: "传递 Host 请求头",
passHostHeaderHelp: "将原始 Host 请求头转发到后端,而不是改写为目标地址。", passHostHeaderHelp: "将原始 Host 请求头转发到后端,而不是改写为目标地址。",
rewriteRedirects: "重写重定向", rewriteRedirects: "重写重定向",
rewriteRedirectsHelp: "重写后端响应中的 Location 请求头,使用公共域名而不是内部后端地址。", rewriteRedirectsHelp:
"重写后端响应中的 Location 请求头,使用公共域名而不是内部后端地址。",
directUpstream: "直连上游", directUpstream: "直连上游",
directUpstreamHelp: "从代理主机直接拨号上游目标,而不是通过 WireGuard 隧道。当上游在无 WireGuard 连接的情况下可达时打开。", directUpstreamHelp:
directUpstreamHelpCluster: "代理集群目标必需且锁定开启:集群没有可回退的 WireGuard 端点。", "代理主机直接拨号上游目标,而不是通过 WireGuard 隧道。当上游在无 WireGuard 连接的情况下可达时打开。",
directUpstreamTooltip: "直连上游仅在至少一个已连接嵌入式代理netbird proxy的集群上可配置。所选集群没有嵌入式代理。", directUpstreamHelpCluster:
"对代理集群目标必需且锁定开启:集群没有可回退的 WireGuard 端点。",
directUpstreamTooltip:
"直连上游仅在至少一个已连接嵌入式代理netbird proxy的集群上可配置。所选集群没有嵌入式代理。",
learnMoreServices: "服务", learnMoreServices: "服务",
learnMoreAuthentication: "身份验证", learnMoreAuthentication: "身份验证",
learnMoreAccessControl: "访问控制", learnMoreAccessControl: "访问控制",
learnMoreSettings: "设置", learnMoreSettings: "设置",
noProtectionTitle: "未配置保护", noProtectionTitle: "未配置保护",
noProtectionDescription: "此服务未配置身份验证或访问控制规则。它将对互联网上的所有人公开可访问。确定要继续吗?", noProtectionDescription:
"此服务未配置身份验证或访问控制规则。它将对互联网上的所有人公开可访问。确定要继续吗?",
httpsService: "HTTPS 服务", httpsService: "HTTPS 服务",
tlsPassthrough: "TLS 直通", tlsPassthrough: "TLS 直通",
tcpService: "TCP 服务", tcpService: "TCP 服务",
udpService: "UDP 服务", udpService: "UDP 服务",
forwardTrafficDesc: "将流量直接转发到您的后端服务。", forwardTrafficDesc: "将流量直接转发到您的后端服务。",
exposeServicesDesc: "通过 NetBird 的反向代理安全地暴露服务。" exposeServicesDesc: "通过 NetBird 的反向代理安全地暴露服务。",
}, },
dns: { dns: {
title: "DNS", title: "DNS",
@@ -898,7 +1007,8 @@ disable2FA: "禁用两步验证",
distributionGroupsLabel: "分配组", distributionGroupsLabel: "分配组",
zoneGroupsHelp: "将此区域及其记录广播给属于以下组的节点。", zoneGroupsHelp: "将此区域及其记录广播给属于以下组的节点。",
enableSearchDomains: "启用搜索域名", enableSearchDomains: "启用搜索域名",
searchDomainHelpZone: "例如,'server.company.internal' 将可通过'server'访问", searchDomainHelpZone:
"例如,'server.company.internal' 将可通过'server'访问",
enableDNSZone: "启用 DNS 区域", enableDNSZone: "启用 DNS 区域",
enableDisableDNSZone: "使用此开关启用或禁用 DNS 区域。", enableDisableDNSZone: "使用此开关启用或禁用 DNS 区域。",
addDNSZone: "添加 DNS 区域", addDNSZone: "添加 DNS 区域",
@@ -925,12 +1035,13 @@ disable2FA: "禁用两步验证",
dnsZones: "DNS 区域", dnsZones: "DNS 区域",
dns: "DNS", dns: "DNS",
recordTypeAAAA: "AAAA", recordTypeAAAA: "AAAA",
recordTypeCNAME: "CNAME" recordTypeCNAME: "CNAME",
}, },
networks: { networks: {
title: "网络", title: "网络",
description: "管理组织的网络和路由", description: "管理组织的网络和路由",
pageDescription: "无需在每台机器上安装 NetBird即可访问 LAN 和 VPC 中的内部资源。", pageDescription:
"无需在每台机器上安装 NetBird即可访问 LAN 和 VPC 中的内部资源。",
networkName: "网络名称", networkName: "网络名称",
networkNamePlaceholder: "例如:工程网络", networkNamePlaceholder: "例如:工程网络",
createNetwork: "创建网络", createNetwork: "创建网络",
@@ -957,9 +1068,11 @@ disable2FA: "禁用两步验证",
addRoutingPeer: "添加路由节点", addRoutingPeer: "添加路由节点",
removeRoutingPeer: "移除路由节点", removeRoutingPeer: "移除路由节点",
networkRoutes: "网络路由", networkRoutes: "网络路由",
routesDescription: "访问其他网络,如局域网和 VPC无需在每个资源上安装 NetBird。", routesDescription:
"访问其他网络,如局域网和 VPC无需在每个资源上安装 NetBird。",
learnMoreAbout: "了解更多关于", learnMoreAbout: "了解更多关于",
newNetworksRecommendation: "我们建议使用新的网络概念来更轻松地可视化和管理对资源的访问。", newNetworksRecommendation:
"我们建议使用新的网络概念来更轻松地可视化和管理对资源的访问。",
goToNetworks: "前往网络", goToNetworks: "前往网络",
createRoute: "创建路由", createRoute: "创建路由",
editRoute: "编辑路由", editRoute: "编辑路由",
@@ -987,20 +1100,23 @@ disable2FA: "禁用两步验证",
resourceDescriptionHelp: "写一个简短的描述为此资源添加更多上下文。", resourceDescriptionHelp: "写一个简短的描述为此资源添加更多上下文。",
resourceDescriptionPlaceholder: "例如:生产环境、开发环境", resourceDescriptionPlaceholder: "例如:生产环境、开发环境",
resourceGroupsLabel: "资源组", resourceGroupsLabel: "资源组",
resourceGroupsHelp: "将此资源添加到组例如数据库、Web 服务器)中,并在访问策略中引用该组以简化管理。", resourceGroupsHelp:
"将此资源添加到组例如数据库、Web 服务器)中,并在访问策略中引用该组以简化管理。",
resourceGroupsPlaceholder: "添加或选择资源组...", resourceGroupsPlaceholder: "添加或选择资源组...",
accessControl: "访问控制", accessControl: "访问控制",
resourceTab: "资源", resourceTab: "资源",
optionalSettings: "可选设置", optionalSettings: "可选设置",
accessControlPolicies: "访问控制策略", accessControlPolicies: "访问控制策略",
accessControlPoliciesHelp: "定义允许访问此资源的源组。您还可以限制对特定协议和端口的访问。如果没有策略,将无法访问此资源。", accessControlPoliciesHelp:
"定义允许访问此资源的源组。您还可以限制对特定协议和端口的访问。如果没有策略,将无法访问此资源。",
routeType: "路由类型", routeType: "路由类型",
routeTypeHelp: "选择路由类型以添加网络范围或域名列表。", routeTypeHelp: "选择路由类型以添加网络范围或域名列表。",
routeTypeNetworkRange: "网络范围", routeTypeNetworkRange: "网络范围",
routeTypeDomains: "域名", routeTypeDomains: "域名",
networkRange: "网络范围", networkRange: "网络范围",
networkRangeHelp: "添加私有 IPv4 或 IPv6 地址或范围", 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: "域名", domains: "域名",
distributionGroups: "分发组", distributionGroups: "分发组",
networkIdentifier: "网络标识符", networkIdentifier: "网络标识符",
@@ -1009,7 +1125,7 @@ disable2FA: "禁用两步验证",
metricPlaceholder: "输入度量值 (1-9999)", metricPlaceholder: "输入度量值 (1-9999)",
additionalSettings: "其他设置", additionalSettings: "其他设置",
accessControlGroups: "访问控制组", accessControlGroups: "访问控制组",
autoApply: "自动应用" autoApply: "自动应用",
}, },
postureChecks: { postureChecks: {
title: "姿态检查", title: "姿态检查",
@@ -1044,14 +1160,17 @@ disable2FA: "禁用两步验证",
postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。", postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。",
postureCheckNamePlaceholder: "例如NetBird 版本 > 0.25.0", postureCheckNamePlaceholder: "例如NetBird 版本 > 0.25.0",
postureCheckDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。", postureCheckDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
postureCheckDescriptionPlaceholder: "例如:检查 NetBird 版本是否大于 0.25.0", postureCheckDescriptionPlaceholder:
"例如:检查 NetBird 版本是否大于 0.25.0",
netBirdClientVersion: "NetBird 客户端版本", netBirdClientVersion: "NetBird 客户端版本",
netBirdClientVersionHelp: "根据特定的 NetBird 客户端版本限制对节点的访问。", netBirdClientVersionHelp: "根据特定的 NetBird 客户端版本限制对节点的访问。",
netBirdClientVersionCheck: "客户端版本检查", netBirdClientVersionCheck: "客户端版本检查",
minimumRequiredVersion: "最低所需版本", minimumRequiredVersion: "最低所需版本",
minimumRequiredVersionHelp: "仅具有指定最低 NetBird 客户端版本的对等节点才能访问网络。", minimumRequiredVersionHelp:
"仅具有指定最低 NetBird 客户端版本的对等节点才能访问网络。",
minimumRequiredVersionPlaceholder: "例如0.25.0", 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: "国家与地区", countryAndRegion: "国家与地区",
countryAndRegionHelp: "根据国家或地区限制网络中的访问。", countryAndRegionHelp: "根据国家或地区限制网络中的访问。",
countryAndRegionCheck: "国家与地区检查", countryAndRegionCheck: "国家与地区检查",
@@ -1075,7 +1194,8 @@ disable2FA: "禁用两步验证",
processHelp: "根据对等节点上正在运行的进程限制网络中的访问。", processHelp: "根据对等节点上正在运行的进程限制网络中的访问。",
processCheck: "进程检查", processCheck: "进程检查",
processes: "进程", processes: "进程",
processesHelp: "添加进程的可执行文件路径。您可以为 Linux、macOS 和 Windows 定义路径。仅当进程在系统上运行时,对等节点才允许连接。", processesHelp:
"添加进程的可执行文件路径。您可以为 Linux、macOS 和 Windows 定义路径。仅当进程在系统上运行时,对等节点才允许连接。",
addProcess: "添加进程", addProcess: "添加进程",
linuxPathPlaceholder: "/usr/local/bin/netbird", linuxPathPlaceholder: "/usr/local/bin/netbird",
macPathPlaceholder: "/Applications/NetBird.app/Contents/MacOS/netbird", macPathPlaceholder: "/Applications/NetBird.app/Contents/MacOS/netbird",
@@ -1091,9 +1211,10 @@ disable2FA: "禁用两步验证",
validCidr: "请输入有效的 CIDR例如192.168.1.0/24", validCidr: "请输入有效的 CIDR例如192.168.1.0/24",
cidrPlaceholder: "例如172.16.0.0/16", cidrPlaceholder: "例如172.16.0.0/16",
noChecks: "您还没有添加任何态势检查", noChecks: "您还没有添加任何态势检查",
noChecksDescription: "添加各种态势检查以进一步限制网络中的访问。例如,仅允许具有特定 NetBird 客户端版本、操作系统或位置的客户端连接。", noChecksDescription:
"添加各种态势检查以进一步限制网络中的访问。例如,仅允许具有特定 NetBird 客户端版本、操作系统或位置的客户端连接。",
browseChecks: "浏览检查", browseChecks: "浏览检查",
newPostureCheck: "新建态势检查" newPostureCheck: "新建态势检查",
}, },
setupKeys: { setupKeys: {
title: "安装密钥", title: "安装密钥",
@@ -1134,13 +1255,27 @@ disable2FA: "禁用两步验证",
expiresIn: "过期时间", expiresIn: "过期时间",
expiresInHelp: "密钥过期前的天数。", expiresInHelp: "密钥过期前的天数。",
expiresInHelpEmpty: "留空表示永不过期。", expiresInHelpEmpty: "留空表示永不过期。",
expiresInSuffix: "天" expiresInSuffix: "天",
all: "全部",
oneOff: "一次性",
reusable: "可重复使用",
status: "状态",
groupNotUsedTitle: "此组尚未在任何安装密钥中使用",
groupNotUsedDescription:
"在创建新的安装密钥时分配此组,以便在此处查看它们。",
getStartedDescription:
"添加安装密钥以在您的网络中注册新设备。该密钥在初始设置期间将设备链接到您的账户。",
learnMore: "了解更多关于",
groups: "组",
usage: "使用情况",
lastUsedOn: "上次使用于",
}, },
activity: { activity: {
title: "活动", title: "活动",
description: "查看审计事件和活动日志", description: "查看审计事件和活动日志",
auditEvents: "审计事件", auditEvents: "审计事件",
auditEventsDescription: "审计整个网络中的配置变更、访问策略更新以及节点注册和登录事件。", auditEventsDescription:
"审计整个网络中的配置变更、访问策略更新以及节点注册和登录事件。",
noEvents: "暂无事件", noEvents: "暂无事件",
searchPlaceholder: "搜索事件...", searchPlaceholder: "搜索事件...",
searchByAuditNameUserPeerMeta: "按审计名称、用户、节点、元数据搜索...", searchByAuditNameUserPeerMeta: "按审计名称、用户、节点、元数据搜索...",
@@ -1150,7 +1285,7 @@ disable2FA: "禁用两步验证",
timestamp: "时间戳", timestamp: "时间戳",
ipAddress: "IP 地址", ipAddress: "IP 地址",
details: "详情", details: "详情",
code: "代码" code: "代码",
}, },
controlCenter: { controlCenter: {
title: "控制中心", title: "控制中心",
@@ -1161,10 +1296,10 @@ disable2FA: "禁用两步验证",
totalGroups: "组总数", totalGroups: "组总数",
totalUsers: "用户总数", totalUsers: "用户总数",
totalNetworks: "网络总数", totalNetworks: "网络总数",
networkOverview: "网络概览" networkOverview: "网络概览",
}, },
onboarding: { onboarding: {
title: "开始使用 NetBird", title: "开始使用 NetBird",
addResource: "添加您的第一个资源" addResource: "添加您的第一个资源",
} },
}; };

View File

@@ -23,7 +23,9 @@ type Props = {
user: User; user: User;
}; };
export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>): ColumnDef<AccessToken>[] { export function AccessTokensTableColumns(
t: ReturnType<typeof useTranslations>,
): ColumnDef<AccessToken>[] {
return [ return [
{ {
accessorKey: "name", accessorKey: "name",
@@ -39,7 +41,9 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
{ {
accessorKey: "expiration_date", accessorKey: "expiration_date",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>{t("expires")}</DataTableHeader>; return (
<DataTableHeader column={column}>{t("expires")}</DataTableHeader>
);
}, },
cell: ({ row }) => ( cell: ({ row }) => (
<ExpirationDateRow date={row.original.expiration_date} /> <ExpirationDateRow date={row.original.expiration_date} />
@@ -48,7 +52,9 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
{ {
accessorKey: "last_used", accessorKey: "last_used",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>; return (
<DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>
);
}, },
sortingFn: "datetime", sortingFn: "datetime",
cell: ({ row }) => { cell: ({ row }) => {
@@ -64,7 +70,7 @@ export function AccessTokensTableColumns(t: ReturnType<typeof useTranslations>):
header: "", header: "",
cell: ({ row }) => <AccessTokenActionCell access_token={row.original} />, cell: ({ row }) => <AccessTokenActionCell access_token={row.original} />,
}, },
]; ];
} }
export default function AccessTokensTable({ user }: Readonly<Props>) { export default function AccessTokensTable({ user }: Readonly<Props>) {
@@ -92,7 +98,7 @@ export default function AccessTokensTable({ user }: Readonly<Props>) {
<Card className={"mt-5 w-full"}> <Card className={"mt-5 w-full"}>
{tokens && tokens.length > 0 ? ( {tokens && tokens.length > 0 ? (
<DataTable <DataTable
text={"Access Tokens"} text={t("accessTokens")}
tableClassName={"mt-0"} tableClassName={"mt-0"}
minimal={true} minimal={true}
showSearchAndFilters={false} showSearchAndFilters={false}
@@ -106,10 +112,8 @@ export default function AccessTokensTable({ user }: Readonly<Props>) {
<div className={"bg-nb-gray-950 overflow-hidden"}> <div className={"bg-nb-gray-950 overflow-hidden"}>
<NoResults <NoResults
className={"py-3"} className={"py-3"}
title={"No access tokens"} title={t("noAccessTokens")}
description={ description={t("noAccessTokensDesc")}
"You don't have any access tokens yet. You can add a token to access the NetBird API."
}
icon={<IconApi size={20} className={"fill-nb-gray-300"} />} icon={<IconApi size={20} className={"fill-nb-gray-300"} />}
/> />
</div> </div>

View File

@@ -1,3 +1,4 @@
import { useTranslations } from "next-intl";
import Button from "@components/Button"; import Button from "@components/Button";
import { import {
DropdownMenu, DropdownMenu,
@@ -18,6 +19,7 @@ import { RouteModalContent } from "@/modules/routes/RouteModal";
export default function AddRouteDropdownButton() { export default function AddRouteDropdownButton() {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const [existingNetworkModal, setExistingNetworkModal] = useState(false); const [existingNetworkModal, setExistingNetworkModal] = useState(false);
const t = useTranslations("common");
const { peer } = usePeer(); const { peer } = usePeer();
const { permission } = usePermissions(); const { permission } = usePermissions();
@@ -44,7 +46,7 @@ export default function AddRouteDropdownButton() {
}} }}
> >
<Button variant={"primary"}> <Button variant={"primary"}>
Add Route {t("addRoute")}
<ChevronDown size={16} /> <ChevronDown size={16} />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -61,10 +63,10 @@ export default function AddRouteDropdownButton() {
size={"small"} size={"small"}
/> />
<div className={"flex flex-col text-left"}> <div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>New Network Route</div> <div className={"text-left text-white"}>
<div className={"text-xs"}> {t("newNetworkRoute")}
Create a new network route with this peer
</div> </div>
<div className={"text-xs"}>{t("newNetworkRouteDesc")}</div>
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
@@ -83,10 +85,10 @@ export default function AddRouteDropdownButton() {
size={"small"} size={"small"}
/> />
<div className={"flex flex-col text-left"}> <div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>Existing Network</div> <div className={"text-left text-white"}>
<div className={"text-xs"}> {t("existingNetwork")}
Add this peer to an existing network
</div> </div>
<div className={"text-xs"}>{t("existingNetworkDesc")}</div>
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -23,35 +23,6 @@ interface PeerEditIPModalProps {
version: IPVersion; 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({ export function PeerEditIPModal({
open, open,
onOpenChange, onOpenChange,
@@ -60,6 +31,42 @@ export function PeerEditIPModal({
version, version,
}: Readonly<PeerEditIPModalProps>) { }: Readonly<PeerEditIPModalProps>) {
const t = useTranslations("peers"); 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 } = const { title, description, placeholder, errorMessage, validate } =
config[version]; config[version];
const [ip, setIP] = useState(currentIP); const [ip, setIP] = useState(currentIP);
@@ -99,7 +106,7 @@ export function PeerEditIPModal({
<div className={"flex gap-3 w-full justify-end"}> <div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}> <ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}> <Button variant={"secondary"} className={"w-full"}>
{t("cancel")} {tc("cancel")}
</Button> </Button>
</ModalClose> </ModalClose>
@@ -109,7 +116,7 @@ export function PeerEditIPModal({
onClick={() => onSave(trim(ip))} onClick={() => onSave(trim(ip))}
disabled={isDisabled} disabled={isDisabled}
> >
Save {tc("save")}
</Button> </Button>
</div> </div>
</ModalFooter> </ModalFooter>

View File

@@ -1,3 +1,4 @@
import { useTranslations } from "next-intl";
import Card from "@components/Card"; import Card from "@components/Card";
import { DataTable } from "@components/table/DataTable"; import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader"; import DataTableHeader from "@components/table/DataTableHeader";
@@ -20,11 +21,14 @@ type Props = {
peer: Peer; peer: Peer;
}; };
export const RouteTableColumns: ColumnDef<Route>[] = [ function RouteTableColumns(
t: ReturnType<typeof useTranslations>,
): ColumnDef<Route>[] {
return [
{ {
accessorKey: "network_id", accessorKey: "network_id",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>Name</DataTableHeader>; return <DataTableHeader column={column}>{t("name")}</DataTableHeader>;
}, },
sortingFn: "text", sortingFn: "text",
cell: ({ row }) => <PeerRouteNameCell route={row.original} />, cell: ({ row }) => <PeerRouteNameCell route={row.original} />,
@@ -32,7 +36,9 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
{ {
accessorKey: "network", accessorKey: "network",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>Network</DataTableHeader>; return (
<DataTableHeader column={column}>{t("network")}</DataTableHeader>
);
}, },
cell: ({ row }) => ( cell: ({ row }) => (
<GroupedRouteNetworkRangeCell <GroupedRouteNetworkRangeCell
@@ -46,7 +52,9 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
accessorFn: (r) => r.groups?.length, accessorFn: (r) => r.groups?.length,
header: ({ column }) => { header: ({ column }) => {
return ( return (
<DataTableHeader column={column}>Distribution Groups</DataTableHeader> <DataTableHeader column={column}>
{t("distributionGroups")}
</DataTableHeader>
); );
}, },
cell: ({ row }) => <RouteDistributionGroupsCell route={row.original} />, cell: ({ row }) => <RouteDistributionGroupsCell route={row.original} />,
@@ -56,7 +64,7 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
accessorKey: "enabled", accessorKey: "enabled",
sortingFn: "basic", sortingFn: "basic",
header: ({ column }) => ( header: ({ column }) => (
<DataTableHeader column={column}>Active</DataTableHeader> <DataTableHeader column={column}>{t("active")}</DataTableHeader>
), ),
cell: ({ row }) => <PeerRouteActiveCell route={row.original} />, cell: ({ row }) => <PeerRouteActiveCell route={row.original} />,
}, },
@@ -65,13 +73,15 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
header: "", header: "",
cell: ({ row }) => <PeerRouteActionCell route={row.original} />, cell: ({ row }) => <PeerRouteActionCell route={row.original} />,
}, },
]; ];
}
export default function PeerRoutesTable({ export default function PeerRoutesTable({
peerRoutes, peerRoutes,
isLoading, isLoading,
peer, peer,
}: Props) { }: Props) {
const t = useTranslations("common");
// Default sorting state of the table // Default sorting state of the table
const [sorting, setSorting] = useState<SortingState>([ const [sorting, setSorting] = useState<SortingState>([
{ {
@@ -87,15 +97,13 @@ export default function PeerRoutesTable({
wrapperProps={{ wrapperProps={{
className: cn("w-full"), className: cn("w-full"),
}} }}
text={"Network Routes"} text={t("networkRoutes")}
tableClassName={"mt-0"} tableClassName={"mt-0"}
getStartedCard={ getStartedCard={
<NoResults <NoResults
className={"py-4"} className={"py-4"}
title={"This peer has no network routes"} title={t("noNetworkRoutes")}
description={ description={t("noNetworkRoutesDesc")}
"You don't have any assigned network routes yet. You can add this peer to an existing network or create a new network route."
}
icon={ icon={
<NetworkRoutesIcon size={20} className={"fill-nb-gray-300"} /> <NetworkRoutesIcon size={20} className={"fill-nb-gray-300"} />
} }
@@ -107,7 +115,7 @@ export default function PeerRoutesTable({
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
columns={RouteTableColumns} columns={RouteTableColumns(t)}
data={peerRoutes} data={peerRoutes}
paginationPaddingClassName={"px-0 pt-8"} paginationPaddingClassName={"px-0 pt-8"}
/> />

View File

@@ -25,7 +25,7 @@ import { Group } from "@/interfaces/Group";
import { orderBy } from "lodash"; import { orderBy } from "lodash";
import CircleIcon from "@/assets/icons/CircleIcon"; import CircleIcon from "@/assets/icons/CircleIcon";
import Badge from "@components/Badge"; import Badge from "@components/Badge";
import { cn, singularize } from "@utils/helpers"; import { cn } from "@utils/helpers";
import { Modal } from "@components/modal/Modal"; import { Modal } from "@components/modal/Modal";
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal"; import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
import PoliciesProvider from "@/contexts/PoliciesProvider"; import PoliciesProvider from "@/contexts/PoliciesProvider";
@@ -37,6 +37,7 @@ import { isNetbirdSSHProtocolSupported } from "@utils/version";
export const PeerSSHToggle = () => { export const PeerSSHToggle = () => {
const t = useTranslations("peers"); const t = useTranslations("peers");
const tc = useTranslations("common");
const { permission } = usePermissions(); const { permission } = usePermissions();
const { peer, toggleSSH, setSSHInstructionsModal } = usePeer(); const { peer, toggleSSH, setSSHInstructionsModal } = usePeer();
const { data: policies } = useFetchApi<Policy[]>( const { data: policies } = useFetchApi<Policy[]>(
@@ -82,25 +83,22 @@ export const PeerSSHToggle = () => {
const disableDashboardSSH = async () => { const disableDashboardSSH = async () => {
const choice = await confirm({ const choice = await confirm({
title: `Disable SSH Access?`, title: t("disableSSHConfirmation"),
description: ( description: (
<div> <div>
Starting from NetBird v0.61.0, once SSH access is disabled, you cannot {t("disableSSHDescription")}{" "}
re-enable it again from the dashboard. You&apos;ll need to create an
explicit access control policy and update your NetBird client to
restore SSH functionality.{" "}
<InlineLink <InlineLink
href={"https://docs.netbird.io/manage/peers/ssh"} href={"https://docs.netbird.io/manage/peers/ssh"}
target={"_blank"} target={"_blank"}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
Learn more {t("sshLearnMore")}
<ExternalLinkIcon size={12} /> <ExternalLinkIcon size={12} />
</InlineLink> </InlineLink>
</div> </div>
), ),
confirmText: "Disable", confirmText: tc("disable"),
cancelText: "Cancel", cancelText: tc("cancel"),
type: "warning", type: "warning",
maxWidthClass: "max-w-xl", maxWidthClass: "max-w-xl",
}); });
@@ -114,9 +112,7 @@ export const PeerSSHToggle = () => {
content={ content={
<div className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}> <div className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}>
<LockIcon size={14} /> <LockIcon size={14} />
<span> <span>{t("noPermissionToUpdateSetting")}</span>
{`You don't have the required permissions to update this setting.`}
</span>
</div> </div>
} }
interactive={false} interactive={false}
@@ -135,9 +131,7 @@ export const PeerSSHToggle = () => {
{t("sshAccess")} {t("sshAccess")}
</> </>
} }
helpText={ helpText={t("sshAccessHelp")}
"Enable the SSH server on this peer to access the machine via an secure shell."
}
/> />
</FullTooltip> </FullTooltip>
<PeerSSHPolicyInfo peer={peer} /> <PeerSSHPolicyInfo peer={peer} />
@@ -148,10 +142,7 @@ export const PeerSSHToggle = () => {
<Label>{t("sshAccess")}</Label> <Label>{t("sshAccess")}</Label>
</div> </div>
<HelpText> <HelpText>{t("sshSetupHelp")}</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>
{!isNetbirdSSHProtocolSupported(peer.version) && {!isNetbirdSSHProtocolSupported(peer.version) &&
enabledPolicies?.length > 0 && enabledPolicies?.length > 0 &&
@@ -166,9 +157,7 @@ export const PeerSSHToggle = () => {
} }
className="my-3" className="my-3"
> >
You have SSH access configured but your client runs on an older {t("sshOldVersionWarning")}
NetBird version. Please update your NetBird client to v.0.61.0+ in
order to allow SSH connections.
</Callout> </Callout>
)} )}
@@ -183,9 +172,7 @@ export const PeerSSHToggle = () => {
} }
className="my-3" className="my-3"
> >
You have an SSH access policy configured, but the SSH server {t("sshServerNotEnabled")}
isn&apos;t enabled on this client. Enable the SSH server to allow SSH
connections.
</Callout> </Callout>
)} )}
@@ -200,9 +187,7 @@ export const PeerSSHToggle = () => {
} }
className="my-3" className="my-3"
> >
Your SSH server is enabled, but starting from NetBird v0.61.0, SSH {t("sshNeedsPolicy")}
requires an explicit access control policy. Please create an SSH
access control policy in order to allow SSH connections.
</Callout> </Callout>
)} )}
@@ -214,7 +199,7 @@ export const PeerSSHToggle = () => {
disabled={!permission?.policies.create} disabled={!permission?.policies.create}
> >
<CirclePlusIcon size={14} /> <CirclePlusIcon size={14} />
Create SSH Policy {t("createSSHPolicy")}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -300,11 +285,9 @@ export const PeerSSHToggle = () => {
/> />
<div> <div>
<span className={"font-medium text-xs"}> <span className={"font-medium text-xs"}>
{singularize( {t("activePoliciesCount", {
"Active Policies", count: enabledPolicies?.length,
enabledPolicies?.length, })}
true,
)}
</span> </span>
</div> </div>
</Badge> </Badge>

View File

@@ -1,3 +1,4 @@
import { useTranslations } from "next-intl";
import Button from "@components/Button"; import Button from "@components/Button";
import { import {
DropdownMenu, DropdownMenu,
@@ -16,6 +17,7 @@ import { CreateDebugJobModalContent } from "../jobs/CreateDebugJobModal";
export const RemoteJobDropdownButton = () => { export const RemoteJobDropdownButton = () => {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const t = useTranslations("common");
const { peer } = usePeer(); const { peer } = usePeer();
const { permission } = usePermissions(); const { permission } = usePermissions();
const isConnected = peer?.connected; const isConnected = peer?.connected;
@@ -39,7 +41,7 @@ export const RemoteJobDropdownButton = () => {
}} }}
> >
<Button variant={"primary"} disabled={disabled}> <Button variant={"primary"} disabled={disabled}>
Run Remote Job {t("runRemoteJob")}
<ChevronDown size={16} /> <ChevronDown size={16} />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -52,10 +54,12 @@ export const RemoteJobDropdownButton = () => {
} }
> >
<div> <div>
Peer{" "} {t.rich("peerOfflineRemoteJob", {
<span className={"text-white font-medium"}>{peer.name}</span>{" "} name: peer.name,
is currently offline. Please connect the peer to run remote bold: (chunks) => (
jobs. <span className={"text-white font-medium"}>{chunks}</span>
),
})}
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -73,10 +77,8 @@ export const RemoteJobDropdownButton = () => {
size={"small"} size={"small"}
/> />
<div className={"flex flex-col text-left"}> <div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>Debug Bundle</div> <div className={"text-left text-white"}>{t("debugBundle")}</div>
<div className={"text-xs"}> <div className={"text-xs"}>{t("debugBundleDesc")}</div>
Collect debug information for troubleshooting
</div>
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -1,3 +1,4 @@
import { useTranslations } from "next-intl";
import FullTooltip from "@components/FullTooltip"; import FullTooltip from "@components/FullTooltip";
import { ArrowUpDown, InfoIcon } from "lucide-react"; import { ArrowUpDown, InfoIcon } from "lucide-react";
@@ -9,13 +10,14 @@ export default function RouteMetricCell({
metric, metric,
useHoverStyle = true, useHoverStyle = true,
}: Readonly<Props>) { }: Readonly<Props>) {
const t = useTranslations("common");
return ( return (
<FullTooltip <FullTooltip
hoverButton={useHoverStyle} hoverButton={useHoverStyle}
isAction={true} isAction={true}
content={ content={
<div className={"text-xs max-w-xs flex gap-2 items-center"}> <div className={"text-xs max-w-xs flex gap-2 items-center"}>
<div>Lower metrics have higher priority.</div> <div>{t("metricPriority")}</div>
</div> </div>
} }
> >

View File

@@ -44,12 +44,16 @@ import SetupKeyModal from "@/modules/setup-keys/SetupKeyModal";
import SetupKeyNameCell from "@/modules/setup-keys/SetupKeyNameCell"; import SetupKeyNameCell from "@/modules/setup-keys/SetupKeyNameCell";
import SetupKeyUsageCell from "@/modules/setup-keys/SetupKeyUsageCell"; 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 [ return [
{ {
accessorKey: "name", accessorKey: "name",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>{t("nameAndKey")}</DataTableHeader>; return (
<DataTableHeader column={column}>{t("nameAndKey")}</DataTableHeader>
);
}, },
sortingFn: "text", sortingFn: "text",
cell: ({ row }) => ( cell: ({ row }) => (
@@ -88,11 +92,13 @@ export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): Co
{ {
accessorKey: "last_used", accessorKey: "last_used",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>; return (
<DataTableHeader column={column}>{t("lastUsed")}</DataTableHeader>
);
}, },
sortingFn: "datetime", sortingFn: "datetime",
cell: ({ row }) => ( 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", accessorKey: "expires",
header: ({ column }) => { header: ({ column }) => {
return <DataTableHeader column={column}>{t("expires")}</DataTableHeader>; return (
<DataTableHeader column={column}>{t("expires")}</DataTableHeader>
);
}, },
cell: ({ row }) => { cell: ({ row }) => {
let expires = dayjs(row.original.expires); let expires = dayjs(row.original.expires);
@@ -136,7 +144,7 @@ export function SetupKeysTableColumns(t: ReturnType<typeof useTranslations>): Co
return <SetupKeyActionCell setupKey={row.original} />; return <SetupKeyActionCell setupKey={row.original} />;
}, },
}, },
]; ];
} }
type Props = { type Props = {
@@ -185,10 +193,8 @@ export default function SetupKeysTable({
// only offers groups that actually appear in the table. // only offers groups that actually appear in the table.
const tableGroups = useMemo<Group[]>( const tableGroups = useMemo<Group[]>(
() => () =>
(uniqBy( (uniqBy(setupKeys?.flatMap((k) => k.groups || []), "name") as Group[]) ||
setupKeys?.flatMap((k) => k.groups || []), [],
"name",
) as Group[]) || [],
[setupKeys], [setupKeys],
); );
@@ -197,27 +203,27 @@ export default function SetupKeysTable({
// re-route it through the consolidated filter UI. // re-route it through the consolidated filter UI.
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>( const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
() => [ () => [
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" }, { value: undefined, label: t("all"), dotClass: "bg-nb-gray-500" },
{ value: true, label: "Valid", dotClass: "bg-green-500" }, { value: true, label: t("active"), dotClass: "bg-green-500" },
{ value: false, label: "Expired", dotClass: "bg-nb-gray-700" }, { value: false, label: t("expired"), dotClass: "bg-nb-gray-700" },
], ],
[], [t],
); );
const usageOptions = useMemo<RadioOption<string | undefined>[]>( const usageOptions = useMemo<RadioOption<string | undefined>[]>(
() => [ () => [
{ value: undefined, label: "All" }, { value: undefined, label: t("all") },
{ value: "one-off", label: "One-off" }, { value: "one-off", label: t("oneOff") },
{ value: "reusable", label: "Reusable" }, { value: "reusable", label: t("reusable") },
], ],
[], [t],
); );
const filterDefs = useMemo<TableFilterDef[]>( const filterDefs = useMemo<TableFilterDef[]>(
() => [ () => [
{ {
id: "valid", id: "valid",
label: "Status", label: t("status"),
renderPicker: (p) => ( renderPicker: (p) => (
<RadioPicker <RadioPicker
value={p.value as boolean | undefined} value={p.value as boolean | undefined}
@@ -231,7 +237,7 @@ export default function SetupKeysTable({
}, },
{ {
id: "type", id: "type",
label: "Usage", label: t("usage"),
renderPicker: (p) => ( renderPicker: (p) => (
<RadioPicker <RadioPicker
value={p.value as string | undefined} value={p.value as string | undefined}
@@ -245,7 +251,7 @@ export default function SetupKeysTable({
}, },
{ {
id: "group_names", id: "group_names",
label: "Groups", label: t("groups"),
renderPicker: (p) => ( renderPicker: (p) => (
<GroupsPicker <GroupsPicker
value={p.value as string[] | undefined} value={p.value as string[] | undefined}
@@ -257,7 +263,7 @@ export default function SetupKeysTable({
formatChip: (v) => formatGroupsChip(v as string[] | undefined), formatChip: (v) => formatGroupsChip(v as string[] | undefined),
}, },
], ],
[statusOptions, usageOptions, tableGroups], [statusOptions, usageOptions, tableGroups, t],
); );
return ( return (
@@ -273,14 +279,14 @@ export default function SetupKeysTable({
inset={false} inset={false}
minimal={isGroupPage} minimal={isGroupPage}
keepStateInLocalStorage={!isGroupPage} keepStateInLocalStorage={!isGroupPage}
text={"Setup Keys"} text={t("title")}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
initialPageSize={25} initialPageSize={25}
showResetFilterButton={false} showResetFilterButton={false}
columns={SetupKeysTableColumns(t)} columns={SetupKeysTableColumns(t)}
data={setupKeys} data={setupKeys}
searchPlaceholder={"Search by name, type or group..."} searchPlaceholder={t("searchPlaceholder")}
columnVisibility={{ columnVisibility={{
valid: false, valid: false,
group_strings: false, group_strings: false,
@@ -295,10 +301,8 @@ export default function SetupKeysTable({
<NoResults <NoResults
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />} icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
className={"py-4"} className={"py-4"}
title={"This group is not used within any setup keys yet"} title={t("groupNotUsedTitle")}
description={ description={t("groupNotUsedDescription")}
"Assign this group when creating a new setup key to see them listed here."
}
> >
<Button <Button
variant={"primary"} variant={"primary"}
@@ -307,7 +311,7 @@ export default function SetupKeysTable({
disabled={!permission.setup_keys.create} disabled={!permission.setup_keys.create}
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
Create Key {t("createSetupKey")}
</Button> </Button>
</NoResults> </NoResults>
) : ( ) : (
@@ -321,10 +325,8 @@ export default function SetupKeysTable({
size={"large"} size={"large"}
/> />
} }
title={"Create Setup Key"} title={t("createSetupKey")}
description={ description={t("getStartedDescription")}
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
}
button={ button={
<Button <Button
variant={"primary"} variant={"primary"}
@@ -334,19 +336,19 @@ export default function SetupKeysTable({
data-testid="open-create-setup-key" data-testid="open-create-setup-key"
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
Create Key {t("createSetupKey")}
</Button> </Button>
} }
learnMore={ learnMore={
<> <>
Learn more about {t("learnMore")}
<InlineLink <InlineLink
href={ href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys" "https://docs.netbird.io/how-to/register-machines-using-setup-keys"
} }
target={"_blank"} target={"_blank"}
> >
Setup Keys {t("title")}
<ExternalLinkIcon size={12} /> <ExternalLinkIcon size={12} />
</InlineLink> </InlineLink>
</> </>
@@ -365,7 +367,7 @@ export default function SetupKeysTable({
data-testid="open-create-setup-key" data-testid="open-create-setup-key"
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
Create Key {t("createSetupKey")}
</Button> </Button>
)} )}
</> </>