Skip to content

feat(i18n): expand Chinese localization and fix env substitution#671

Open
sakuradairong wants to merge 54 commits into
netbirdio:mainfrom
sakuradairong:localize-zh-v2.39.0
Open

feat(i18n): expand Chinese localization and fix env substitution#671
sakuradairong wants to merge 54 commits into
netbirdio:mainfrom
sakuradairong:localize-zh-v2.39.0

Conversation

@sakuradairong

@sakuradairong sakuradairong commented Jun 18, 2026

Copy link
Copy Markdown

Summary

  • Expand the Chinese i18n bundle (en/zh) to cover Groups, Networks, Network Routes, Posture Checks, DNS, Reverse Proxy, Audit, and Settings pages, plus their modals.
  • Fix docker/server.js to substitute both $VAR and $$VAR placeholders at startup, apply no-store to .js assets, and render OidcTrustedDomains.js from the template.
  • Add a safePath guard in the static server so request paths cannot escape the served web root.
  • Localize remaining reverse proxy modal labels, descriptions, tooltips, and the "Learn more about" doc links; move table column definitions for Networks, MinimalPeers, ServiceUsers, and Activity tables into useMemo hooks so they can consume useTranslations (column headers, code, role, status, address, last seen, etc.).
  • Add missing translation keys: dns.selectType/selectTTL, activity.code, policies.all/protocols, postureChecks.descriptionOptional, serviceUsers.{role,status,learnMoreAbout}, networks.policies, and the full reverseProxy modal set (NetBird-Only Access, SSO, Password, PIN Code, HTTP Headers, Preserve Client Source IP, Session/Connection Timeout, Pass Host Header, Rewrite Redirects, Direct Upstream, and the per-tab "Learn more" links).
  • Address CodeRabbit review: localize confirmation dialogs (noProtectionTitle/Description), service mode labels (httpsService, tlsPassthrough, tcpService, udpService), modal descriptions, tooltip text (GROUP_TOOLTIP_TEXT → translation keys), timestamp [at] token, RestrictedAccess heading/description, MultipleGroups badge, PeerNameCell aria-label interpolation, PeerVersionCell latestUrl null-check, DataTableMultiSelectPopup double-count fix, AccessControlTable bidirectional ||??, and zh.ts ICU plural format for activePoliciesCount.

Verification

  • npx tsc --noEmit
  • npm run build
  • git diff --check
  • Translation key validator (en + zh) for all touched .tsx files
  • Production smoke test on https://ne.rainycy.top/ confirms Chinese rendering and that no unexpanded placeholders leak into the served bundle.

Out of scope

  • Upgrading audited high-severity dependencies (next, js-cookie, etc.) — should be handled in a separate PR.
  • npm run lint — the project still calls the removed next lint script; this is a repo-wide config issue, not a regression introduced here.
  • Translation key gaps in pre-existing dev-authored commits on the same branch (e.g. policies.learnMore, peers.operatingSystem.linux); these are inherited from earlier work and tracked separately.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added multi-language support (English and Chinese) with localized dashboard content, navigation, dialogs, and table controls.
    • Introduced a locale switcher and cookie/browser-based locale detection with safe fallbacks.
  • Refactor

    • Updated dashboard pages and shared components to use translation-driven text, placeholders, tooltips, and status labels across tables and empty states.
  • Deployment

    • Updated the container to serve the built dashboard via a Node-based static server on port 80.

- Expand en.ts/zh.ts with extensive translation keys for all modules
- Localize Peers table, peer detail page, peer action cells
- Localize Access Control table, modal, action cells
- Localize Groups table, action cells, main page
- Add common helpers (GroupsRow, NoPeersGettingStarted) translations

Continuation of the localization effort.
- UsersTable with status, role, group filters localized
- ServiceUsersTable localized
- Team main pages (users, service-users) localized
- Add new i18n keys for additional status, role, and action labels
…twork Routes pages

- Localize DNS pages (nameservers, zones, settings)
- Localize Reverse Proxy pages (services, custom-domains, clusters, logs)
- Localize Settings page with all vertical tab labels
- Localize Posture Checks page
- Localize Network Routes page
- Add comprehensive translation keys for all above modules
- Fix import paths for reverse-proxy and posture-checks tables
- Update Dockerfile to use node:22-alpine with custom static server
- Add server.js for serving Next.js static export with path resolution
- Fix path.join issue with absolute paths in Node.js
- Remove middleware.ts (incompatible with output: 'export')
- Add NextIntlClientProvider to AppLayout for static export compatibility
- Update routing to use zh as default locale
@CLAassistant

CLAassistant commented Jun 18, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds next-intl localization across dashboard pages, shared components, and feature tables/modals, introduces English and Chinese message bundles, and replaces the dashboard container with a Node static server for exported assets.

Changes

Dashboard i18n and runtime migration

Layer / File(s) Summary
Runtime and locale bootstrap
docker/*, next.config.js, package.json, src/i18n/*, src/layouts/AppLayout.tsx, src/layouts/Header.tsx, src/modules/settings/LanguageTab.tsx, src/i18n/messages/*
The container runtime now serves exported assets with docker/server.js, and the app is wired for next-intl routing, locale request/config loading, locale persistence, locale switching, and message catalogs.
Dashboard pages and navigation localization
src/app/(dashboard)/*page.tsx, src/layouts/Navigation.tsx, src/modules/settings/*Tab.tsx
Dashboard route pages, sidebar labels, and settings tabs switch their visible text to translation keys while preserving existing data loading, permission checks, and navigation structure.
Shared UI and table primitives
src/components/table/*, src/components/ui/*, src/components/PeerSelector.tsx, src/components/NoPeersGettingStarted.tsx, src/modules/common-table-rows/GroupsRow.tsx, src/modules/groups/*Selector.tsx, src/modules/control-center/nodes/SelectGroupNode.tsx, src/modules/peer/MinimalPeersTable.tsx, src/components/ui/RestrictedAccess.tsx
Reusable tables, selectors, empty states, group helpers, and restricted-access text now use translated labels, placeholders, modal copy, and fallback text.
Feature tables and modals
src/modules/access-control/*, src/modules/activity/ActivityTable.tsx, src/modules/groups/table/*, src/modules/users/UsersTable.tsx, src/modules/users/ServiceUsersTable.tsx, src/modules/peers/*, src/modules/networks/*, src/modules/reverse-proxy/ReverseProxyModal.tsx, src/modules/dns/*, src/modules/posture-checks/modal/PostureCheckModal.tsx
Access-control, activity, groups, service-users, users, peers, network, reverse-proxy, DNS, and posture-check tables, action cells, and modals are localized, with several table column definitions moved into component-local memoized builders.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • netbirdio/dashboard#485: Both PRs modify src/app/(dashboard)/peer/page.tsx, so they overlap on the peer detail page component.
  • netbirdio/dashboard#528: Both PRs touch src/app/(dashboard)/dns/zones/page.tsx, which is one of the localized DNS route pages here.
  • netbirdio/dashboard#552: Both PRs overlap on reverse-proxy page components that this PR localizes.

Poem

🐇 I hopped through pages, strings, and signs,
and stitched two tongues through rabbit lines.
The static shell now hums at night,
while en and zh both shine bright.
A tidy burrow, softly sprung—
the dashboard sings in every tongue.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides detailed summary, verification steps, and explicitly marks out-of-scope items, but does not follow the required template structure with Issue ticket, Documentation checkboxes, and Docs PR URL. Follow the repository's PR description template: add Issue ticket number/link section, select Documentation checkbox with explanation, and include Docs PR URL if applicable.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: i18n expansion and Docker environment variable substitution fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/modules/reverse-proxy/ReverseProxyModal.tsx (1)

457-463: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the unprotected-service confirmation text.

Lines 457-460 are still hardcoded English, so this critical warning stays untranslated while adjacent buttons are localized.

🌐 Proposed fix
       const confirmed = await confirm({
-        title: "No Protection Configured",
-        description:
-          "This service has no authentication or access control rules configured. It will be publicly accessible to everyone on the internet. Are you sure you want to continue?",
+        title: t("noProtectionConfiguredTitle"),
+        description: t("noProtectionConfiguredDescription"),
         type: "warning",
         confirmText: reverseProxy ? t("saveChanges") : t("addServiceBtn"),
         cancelText: tCommon("cancel"),
         maxWidthClass: "max-w-lg",
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 457 - 463, The
confirmation dialog in ReverseProxyModal.tsx contains hardcoded English strings
for the title and description fields while the confirmText and cancelText use
translation functions (t() and tCommon()). Wrap the title "No Protection
Configured" and the description "This service has no authentication or access
control rules configured..." with the appropriate translation function call
(likely t() based on the existing pattern) instead of keeping them as hardcoded
strings, ensuring consistency with the already-localized button texts.
src/modules/dns/zones/DNSRecordModal.tsx (2)

176-181: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the modal header description too.

The title is translated, but the description is still hardcoded English, so this modal remains partially untranslated.

Suggested direction
description={
-  record
-    ? `Update record of '${zone.domain}' zone`
-    : `Add new record to the '${zone.domain}' zone`
+  record
+    ? t("updateRecordDescription", { zone: zone.domain })
+    : t("addRecordDescription", { zone: zone.domain })
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/dns/zones/DNSRecordModal.tsx` around lines 176 - 181, The modal
description in the DNSRecordModal component is using hardcoded English strings
while the title is properly translated using the t() function. Replace the
hardcoded description strings in the conditional logic (the ternary operator for
both the update and add cases) with translation calls using the t() function,
similar to how the title is handled, while keeping the dynamic zone.domain value
interpolated within the translated strings.

205-206: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate select placeholders to avoid English fallback text.

"Select type..." and "Select TTL..." are still hardcoded and will leak English in non-English locales.

Suggested direction
- <SelectValue placeholder="Select type..." />
+ <SelectValue placeholder={t("selectRecordType")} />

- <SelectValue placeholder="Select TTL..." />
+ <SelectValue placeholder={t("selectTtl")} />

Also applies to: 301-302

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/dns/zones/DNSRecordModal.tsx` around lines 205 - 206, The
SelectValue component placeholders contain hardcoded English text ("Select
type..." and "Select TTL...") that will not be translated for non-English
locales. Replace the hardcoded placeholder strings in both SelectValue
components (at the location with "Select type..." and the similar one with
"Select TTL...") with appropriate translation function calls to use localized
placeholder text instead of the hardcoded English strings.
src/app/(dashboard)/network/page.tsx (1)

261-285: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the full high-availability copy instead of only status tokens.

This section still shows English sentences and labels in a localized page, which results in mixed-language UX for Chinese users.

Suggested direction
- <span className={"text-yellow-400 font-medium"}>{tCommon("inactive")}</span>{" "}
- for this network.
+ {t("highAvailabilityInactiveMessage")}

- <span className={"text-green-500 font-medium"}>{tCommon("active")}</span>{" "}
- for this network.
+ {t("highAvailabilityActiveMessage")}

- High Availability
+ {t("highAvailability")}

- You can add more routing peers to increase the availability of this network.
+ {t("highAvailabilityEnabledHelp")}

- Go ahead and add more routing peers or groups with routing peers to enable high availability for this network.
+ {t("highAvailabilityDisabledHelp")}

Also applies to: 297-315

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/network/page.tsx around lines 261 - 285, The
disabledText and enabledText useMemo hooks are only localizing the status tokens
(inactive/active) while leaving the surrounding English sentences hardcoded,
creating a mixed-language experience for non-English users. Extract the entire
message strings for "High availability is currently {status} for this network"
and "High availability is {status} for this network" into translation keys, then
use tCommon() to retrieve and render the fully localized messages instead of
interpolating English text with localized tokens.
src/modules/control-center/nodes/SelectGroupNode.tsx (1)

45-55: ⚠️ Potential issue | 🟡 Minor

countLabel memo misses locale dependency and returns untranslated strings.

At line 45, useMemo calls t(...) but the dependency list at line 55 omits t, so switching locales won't recompute the memoized value. Additionally, lines 52 and 54 hardcode "Resource(s)" and "Peer(s)" without translation keys, resulting in mixed-language output when both counts are present.

Suggested fix
-  }, [group]);
+  }, [group, t]);

Add translation keys for the hardcoded strings or refactor the branches to use consistent translation calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/control-center/nodes/SelectGroupNode.tsx` around lines 45 - 55,
The countLabel useMemo hook is missing the translation function `t` in its
dependency array, so locale changes won't trigger recomputation. Additionally,
the hardcoded strings "Resource(s)" and "Peer(s)" in lines 52 and 54 are not
being translated, creating mixed-language output. Add `t` to the dependency
array of the useMemo hook at line 55, and replace the hardcoded "Resource(s)"
and "Peer(s)" strings with appropriate translation key calls using the `t()`
function to ensure consistent translation throughout the countLabel computation.
🟡 Minor comments (14)
src/i18n/messages/zh.ts-732-732 (1)

732-732: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent plural format with English translation.

The English version (line 730 in en.ts) uses ICU MessageFormat plural handling:

activePoliciesCount: "{count, plural, one {# Active Policy} other {# Active Policies}}"

While the Chinese version uses simple string interpolation:

activePoliciesCount: "{count} 个活跃策略"

This inconsistency may cause issues if the consuming code expects plural formatting. For consistency and to leverage next-intl's plural handling, consider using a format like:

activePoliciesCount: "{count, plural, other {# 个活跃策略}}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/messages/zh.ts` at line 732, The Chinese translation for
activePoliciesCount in zh.ts uses simple string interpolation instead of ICU
MessageFormat plural syntax. Update the activePoliciesCount value in zh.ts to
match the English plural format pattern by replacing the simple string
interpolation with the ICU MessageFormat plural syntax, using the format
"{count, plural, other {# 个活跃策略}}" to ensure consistency with the English
version and proper plural handling by next-intl.
src/modules/access-control/table/AccessControlTable.tsx-258-263 (1)

258-263: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translation key "of" likely incorrect for the plural parameter.

formatCheckboxChip uses the third parameter to display selected count as "${count} ${plural}". Passing tTable("of") produces text like "2 of" which is grammatically incomplete. This should probably be a plural noun like "protocols".

💡 Suggested fix
 formatChip: (v) =>
   formatCheckboxChip(
     v as string[] | undefined,
     protocolOptions,
-    tTable("of"),
+    t("protocols"),
   ),

Ensure the translation key "protocols" exists in the "policies" namespace, or use an appropriate existing key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/access-control/table/AccessControlTable.tsx` around lines 258 -
263, The third parameter passed to the formatCheckboxChip function call
(currently tTable("of")) is used to display text like "${count} ${plural}", and
passing the translation key "of" results in grammatically incorrect output such
as "2 of". Replace tTable("of") with tTable("protocols") or another appropriate
plural noun translation key that makes grammatical sense in the context of
displaying the count of selected items. Ensure the translation key you choose
exists in the appropriate translation namespace.
src/modules/groups/table/GroupsActionCell.tsx-77-83 (1)

77-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename/Delete tooltip text is not localized.

Dropdown labels are translated, but tooltip bodies still come from GROUP_TOOLTIP_TEXT English constants, so this action menu remains partially untranslated.

Also applies to: 102-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/groups/table/GroupsActionCell.tsx` around lines 77 - 83, The
tooltip content in the FullTooltip component is using hardcoded English
constants from GROUP_TOOLTIP_TEXT (specifically GROUP_TOOLTIP_TEXT.RENAME.JWT
and GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION) instead of localized strings. Replace
all instances of GROUP_TOOLTIP_TEXT constants with the appropriate
localized/translated string values for both the RENAME tooltips (around lines
77-83) and DELETE tooltips (around lines 102-108) to ensure the action menu is
fully translated and matches the localization approach used for the dropdown
labels.
src/app/(dashboard)/groups/page.tsx-46-46 (1)

46-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restricted-access copy is still partially untranslated.

This page passes a localized page label, but src/components/ui/RestrictedAccess.tsx still renders fixed English heading/body text, so denied users get mixed-language UI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/groups/page.tsx at line 46, The RestrictedAccess
component in src/components/ui/RestrictedAccess.tsx is rendering hardcoded
English text for the heading and body content instead of using localized
strings, which conflicts with the translated page label being passed from the
caller. Update the RestrictedAccess component to accept translation keys or
integrate with the translation system so that the heading and body text are
rendered in the user's selected language rather than fixed English strings. This
ensures consistent localization when access is denied.
src/modules/groups/table/GroupsTable.tsx-166-170 (1)

166-170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Policies column tooltip uses the wrong translation key.

The policies header tooltip currently resolves to table.of, which produces incorrect text for this column.

Suggested fix
-								<div className={"text-xs normal-case"}>{tTable("of")}</div>
+								<div className={"text-xs normal-case"}>{t("policies")}</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/groups/table/GroupsTable.tsx` around lines 166 - 170, In the
DataTableHeader component for the policies column in GroupsTable.tsx, the
tooltip is currently using the translation key "of" which is incorrect for this
column context. Replace the translation key passed to tTable in the tooltip prop
from "of" to the appropriate translation key that correctly describes the
policies column header. The key should be relevant to the policies column's
purpose and context.
src/modules/posture-checks/modal/PostureCheckModal.tsx-195-195 (1)

195-195: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Label is still hardcoded in an otherwise localized modal.

Description (optional) remains English-only here, so this segment won’t fully localize.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/posture-checks/modal/PostureCheckModal.tsx` at line 195, The
Label component in PostureCheckModal.tsx contains hardcoded English text
"Description (optional)" instead of using localization strings like the rest of
the modal. Replace this hardcoded string with a localized translation key using
the same i18n/localization approach used elsewhere in the PostureCheckModal
component to ensure consistent internationalization across the entire modal.
src/modules/users/ServiceUsersTable.tsx-45-66 (1)

45-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Core table/filter labels are still hardcoded English.

Name/Role/Status headers and several role filter option labels are fixed strings, so this screen stays partially untranslated under non-English locales.

Also applies to: 132-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/users/ServiceUsersTable.tsx` around lines 45 - 66, Replace the
hardcoded English strings "Name", "Role", and "Status" in the column headers
(found in the header functions for columns with accessorKey values "name",
"role", and "status") with appropriate translation function calls using your
application's i18n/localization system. Additionally, apply the same translation
approach to the role filter option labels referenced in lines 132-135 to ensure
all user-facing text in this table is translatable and respects the user's
locale settings.
src/app/(dashboard)/network-routes/page.tsx-56-58 (1)

56-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize or remove the hardcoded aria-label on the docs link.

Line 56 hardcodes English text. Since aria-label overrides visible content, screen readers will announce English even in zh locale.

🌐 Proposed fix
-								aria-label={
-									"Learn more about routing traffic to private networks"
-								}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/network-routes/page.tsx around lines 56 - 58, The
aria-label attribute on the docs link contains hardcoded English text that will
override any locale-specific translations and be announced in English by screen
readers regardless of the user's language setting. Either localize the
aria-label text using the application's i18n/translation system to match the
current locale (such as Chinese), or remove the aria-label entirely if the
visible link text already provides sufficient accessibility context. Locate the
aria-label with the text "Learn more about routing traffic to private networks"
and apply one of these solutions.
src/modules/reverse-proxy/ReverseProxyModal.tsx-565-570 (1)

565-570: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Modal title still embeds hardcoded English service-mode labels.

Line 569 reads SERVICE_MODES[serviceMode].label, but those labels are English constants in src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx. This yields mixed-language titles (e.g., translated prefix + English mode name).

🌐 Proposed fix
+  const serviceModeLabel = {
+    [ServiceMode.HTTP]: t("httpsService"),
+    [ServiceMode.TLS]: t("tlsPassthrough"),
+    [ServiceMode.TCP]: t("tcpService"),
+    [ServiceMode.UDP]: t("udpService"),
+  }[serviceMode];
+
   const modalTitle = useMemo(() => {
     if (!serviceMode)
       return reverseProxy ? t("editServiceBtn") : t("addServiceBtn");
     const prefix = reverseProxy ? tCommon("edit") : tCommon("create");
-    return `${prefix} ${SERVICE_MODES[serviceMode].label}`;
-  }, [reverseProxy, serviceMode, t, tCommon]);
+    return `${prefix} ${serviceModeLabel}`;
+  }, [reverseProxy, serviceMode, serviceModeLabel, t, tCommon]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 565 - 570, The
modalTitle useMemo hook in ReverseProxyModal.tsx contains a mixed-language issue
where SERVICE_MODES[serviceMode].label provides an English hardcoded label while
the prefix is translated via tCommon. Instead of concatenating the English label
directly, replace SERVICE_MODES[serviceMode].label with a properly translated
service mode label by using the appropriate translation function (likely tCommon
or a similar i18n function) to fetch the translated service mode name, ensuring
both the prefix and the mode name are in the same language throughout the modal
title.
src/app/(dashboard)/peer/page.tsx-683-704 (1)

683-704: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove hardcoded English token in localized timestamp strings

Lines 683 and 701 embed [at] directly in the formatted date string, so Chinese UI still shows English text in peer details.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peer/page.tsx around lines 683 - 704, The hardcoded
English token [at] in both the created_at and last_seen date format strings
prevents proper localization for non-English languages. Remove the [at] string
from both the dayjs().format() calls for peer.created_at and peer.last_seen,
then use the localization function t() to provide a localized translation key
for the word "at" and concatenate it as a separate string between the absolute
timestamp and the relative time expression, similar to how the code already
concatenates the parenthesized relative time.
src/modules/peers/PeerNameCell.tsx-41-41 (1)

41-41: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid concatenating translated fragments in aria-label.

This concatenation hardcodes word order and can break locale grammar. Prefer a single interpolated translation key.

💡 Suggested fix
- aria-label={`${t("viewDetailsOf")} ${peer.name}`}
+ aria-label={t("viewDetailsOf", { name: peer.name })}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/peers/PeerNameCell.tsx` at line 41, The aria-label attribute in
PeerNameCell is concatenating separate translated fragments which hardcodes word
order and breaks locale grammar for different languages. Instead of
concatenating the results of t("viewDetailsOf") with peer.name, use a single
translation key that accepts peer.name as an interpolated parameter, such as
t("viewDetailsOf", { name: peer.name }) or a similar pattern depending on your
i18n library's interpolation syntax. Update the translation key to support
parameter interpolation so that translators can adjust word order appropriately
for each locale.
src/modules/peers/PeerVersionCell.tsx-87-94 (1)

87-94: ⚠️ Potential issue | 🟡 Minor

Add null-check before using latestUrl in the link or ensure both latestVersion and latestUrl are validated together.

The updateAvailable check only validates latestVersion, but the changelog link assumes latestUrl is non-null. The type cast as string at line 89 bypasses TypeScript's safety check. If the API returns a version update without a changelog URL, the link would have an invalid href. Either validate latestUrl in the updateAvailable condition or add a null-check before rendering the link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/peers/PeerVersionCell.tsx` around lines 87 - 94, The InlineLink
component uses latestUrl without validating it is non-null, relying only on the
updateAvailable check which validates latestVersion but not latestUrl. Add a
null-check for latestUrl to ensure it exists before rendering the InlineLink
component, either by wrapping the link in a conditional that checks latestUrl is
truthy or by updating the updateAvailable validation logic to confirm both
latestVersion and latestUrl are present. Remove the unsafe type cast `as string`
after adding proper null-validation, allowing TypeScript to verify the type
safety.
src/components/table/DataTableMultiSelectPopup.tsx-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid double-count rendering in selected label.

defaultLabel already includes {count} via t('selected', { count }), but Line 64 also renders count separately, so the default text becomes duplicated (e.g., 3 3 selected).

Suggested fix
-  const defaultLabel = label || t('selected', { count });
+  const defaultLabel = label ? `${count} ${label}` : t('selected', { count });

...

-                      <span>
-                        <span className={"font-medium text-white"}>
-                          {count}
-                        </span>{" "}
-                        {defaultLabel}
-                      </span>
+                      <span>{defaultLabel}</span>

Also applies to: 63-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/DataTableMultiSelectPopup.tsx` around lines 25 - 26, The
defaultLabel variable on line 25-26 already includes the count via the
translation function t('selected', { count }), but the code on lines 63-67 is
also rendering count separately, causing duplication like "3 3 selected". Remove
the separate count rendering from the label composition in lines 63-67 and use
only defaultLabel, which already contains the interpolated count value, or
restructure the label logic to avoid passing count to the translation function
if you want to render it separately.
src/modules/common-table-rows/GroupsRow.tsx-92-96 (1)

92-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

countOnly now exposes hardcoded English text in localized UI.

At Line 95, forwarding countOnly routes this path through MultipleGroups, where the count badge text is hardcoded as "Groups". This leaks English in zh locales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/common-table-rows/GroupsRow.tsx` around lines 92 - 96, The
`countOnly` prop being passed to the `MultipleGroups` component causes the count
badge text to display as a hardcoded English string "Groups", which breaks
localization for non-English locales. Locate the `MultipleGroups` component and
replace the hardcoded "Groups" text in the count badge with a proper localized
translation key or i18n function call that returns the appropriately translated
text based on the current locale instead of using a hardcoded English string.
🧹 Nitpick comments (4)
src/i18n/request.ts (1)

7-13: 💤 Low value

Consider validating locale against supported values.

If an unsupported locale (e.g., 'fr') is passed, the current code returns locale: 'fr' but falls back to messages.zh, creating a mismatch between the declared locale and the actual messages.

While next-intl middleware should prevent this scenario, adding explicit validation would make the code more defensive.

🛡️ Optional defensive validation
+const supportedLocales = ['en', 'zh'] as const;
+
 export default getRequestConfig(async ({ locale }) => {
-  const resolvedLocale = locale || 'zh';
+  const resolvedLocale = (locale && supportedLocales.includes(locale as any)) 
+    ? locale 
+    : 'zh';
   return {
     locale: resolvedLocale,
     messages: messages[resolvedLocale as keyof typeof messages] || messages.zh
   };
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/request.ts` around lines 7 - 13, The getRequestConfig function
allows unsupported locale values to be set in the returned locale field while
falling back to messages.zh, creating a mismatch between the declared locale and
actual messages. Add validation to ensure the resolvedLocale is a supported
locale (one of the keys in the messages object). If the locale is not supported,
both the locale field and messages should fall back to 'zh' to maintain
consistency between the declared locale and the messages being used.
src/i18n/navigation.ts (1)

1-10: ⚡ Quick win

Eliminate routing configuration duplication.

The routing configuration is defined both here and in src/i18n/routing.ts (lines 3-6). This duplication means any change to the locale configuration must be updated in two places.

♻️ Refactor to import routing from routing.ts
 import { createNavigation } from 'next-intl/navigation';
-import { defineRouting } from 'next-intl/routing';
+import { routing } from './routing';

-export const routing = defineRouting({
-  locales: ['en', 'zh'],
-  defaultLocale: 'zh'
-});
-
 export const { Link, redirect, usePathname, useRouter } =
   createNavigation(routing);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/navigation.ts` around lines 1 - 10, The routing configuration is
duplicated between this file and src/i18n/routing.ts, requiring maintenance in
two places. Remove the routing object definition created by defineRouting (which
contains the locales and defaultLocale configuration) from this file and instead
import the routing object from src/i18n/routing.ts at the top, then pass the
imported routing to createNavigation. This ensures the locale configuration is
maintained in a single location.
src/modules/groups/GroupFilterSelector.tsx (1)

177-179: ⚡ Quick win

Translate the hardcoded "Peer(s)" label.

Line 178 contains a hardcoded English string {item.peers_count} Peer(s) that should be localized. Consider using a parameterized translation key like t('nPeers', { n: item.peers_count }) to match the pattern used elsewhere (e.g., line 71's t('nGroups', { n })).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/groups/GroupFilterSelector.tsx` around lines 177 - 179, The
hardcoded string "{item.peers_count} Peer(s)" in the div element displaying peer
information needs to be localized. Replace this hardcoded English text with a
parameterized translation key using the pattern t('nPeers', { n:
item.peers_count }) to match the existing localization approach used elsewhere
in the file (such as the t('nGroups', { n }) pattern on line 71). This ensures
the peer count label is properly translated based on the user's language
settings.
src/components/NoPeersGettingStarted.tsx (1)

38-49: 💤 Low value

Consider consolidating the split translation for better i18n flexibility.

The current pattern splits the "learn more" sentence across two translation keys (learnMoreInOur and gettingStartedGuide) to insert a link between them. This forces a specific sentence structure that may not work in all languages where word order differs.

Next-intl supports rich text components via the values parameter. Consider refactoring to a single translation key that accepts the link as a component parameter, allowing translators full control over sentence structure and link placement.

Example pattern:

{t.rich('learnMore', {
  link: (chunks) => (
    <InlineLink href="..." target="_blank">
      {chunks}
      <ExternalLinkIcon size={12} />
    </InlineLink>
  )
})}

This allows the translation file to define: "learnMore": "Learn more in our {link}documentation{/link}" with flexible word order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoPeersGettingStarted.tsx` around lines 38 - 49, The current
implementation splits the sentence across two translation keys (learnMoreInOur
and gettingStartedGuide) which restricts translators' ability to reorder words
based on language-specific grammar. Refactor to use t.rich() instead of separate
t() calls, consolidating into a single translation key that accepts the
InlineLink component as a parameter via the values option. Define a link
parameter in the values object that wraps the content with the InlineLink
component (including the ExternalLinkIcon), allowing translators to place the
link anywhere in the sentence structure through the translation definition file.
Update the translation key accordingly to accept the rich text component
placeholder.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker/Dockerfile`:
- Around line 1-13: The Dockerfile runs the Node.js server as root which is a
security vulnerability. Create a non-root user in the Dockerfile using RUN
commands to add a new user and group, then add a USER directive before the CMD
instruction to switch to this non-root user. Additionally, you will need to
adjust the EXPOSE port from 80 to a higher unprivileged port like 8080 (since
port 80 requires root privileges), and update the server.js script to listen on
the new port instead.

In `@docker/server.js`:
- Around line 103-110: The locale fallback logic that checks
!p.startsWith('/zh') and then attempts to serve the Chinese version as a
fallback when English content is missing may unexpectedly serve Chinese content
to English-locale users. Either remove this entire fallback block (the section
checking zh, zhHtml, and zhDirIndex paths) if the fallback is unintended, or
modify it to only apply when the user's Accept-Language header indicates Chinese
preference or when explicitly on a localized path. Alternatively, if this
fallback behavior is intentional (such as when Chinese is the primary locale),
add a comment above the !p.startsWith('/zh') condition explaining the design
decision and when this fallback should occur.

In `@src/app/`(dashboard)/peer/page.tsx:
- Around line 768-815: The form element in the edit peer name modal lacks an
onSubmit handler with preventDefault(), which allows the native form submission
to occur alongside the async save operation triggered by the onClick handler on
the submit button. Add an onSubmit handler to the form element that prevents the
default form submission behavior and then invokes the onSuccess function with
the current name value, ensuring the async operation completes without
triggering an unintended page reload or form submission.

In `@src/app/`(dashboard)/reverse-proxy/logs/page.tsx:
- Around line 56-58: The ReverseProxyEventsTable component uses the
useServerPagination() hook which requires a ServerPaginationProvider ancestor
that is currently missing from the page hierarchy. Wrap the Suspense boundary
containing ReverseProxyEventsTable with a ServerPaginationProvider component and
provide the required url prop to make the pagination context available to the
lazy-loaded table component.

In `@src/layouts/AppLayout.tsx`:
- Around line 45-50: The AppLayout component hardcodes the locale to "zh" in
both the html lang attribute and the NextIntlClientProvider component, while the
en messages import is never used. To fix this, either implement dynamic locale
selection by creating a configurable locale constant (using an environment
variable like NEXT_PUBLIC_LOCALE or accepting it as a prop), then use that
constant in place of the hardcoded "zh" strings and select the corresponding
messages object dynamically, or if this is intentionally a single-locale build,
add a clarifying comment explaining the design decision and remove the unused en
import to avoid confusion about whether it's an oversight.

In `@src/modules/access-control/AccessControlModal.tsx`:
- Around line 277-278: The permission gating logic at line 277-278 and the other
locations (335-336, 370-373, 463-465, 494-495, 507-509) currently disable form
fields when either `policies.create` or `policies.update` is missing, but this
is too restrictive for users with only create-only or update-only permissions.
Instead of checking both permissions together with OR logic, gate the field
disabling based on the current modal mode: if the modal is in create mode, only
check for `permission.policies.create` permission; if in update mode, only check
for `permission.policies.update` permission. You can determine the mode by
checking if a mode variable or similar exists in the component state or props
(look for variables like isCreateMode, mode, or similar). Update all the
disabled conditions across the mentioned locations to use this mode-based gating
approach.

In `@src/modules/access-control/table/AccessControlTable.tsx`:
- Around line 351-370: The accessorFn in the "direction" column definition uses
the logical OR operator with a fallback of true, which causes the bidirectional
property to always evaluate to true even when explicitly set to false. Replace
the OR operator with nullish coalescing operator when accessing
row.rules[0].bidirectional so that false values are properly preserved and only
null or undefined values default to true. This will fix the direction filter to
correctly distinguish between bidirectional and unidirectional policies.

In `@src/modules/posture-checks/modal/PostureCheckModal.tsx`:
- Around line 189-191: The disabled condition for the form field in
PostureCheckModal requires both create and update permissions, but the canCreate
logic elsewhere allows proceeding with either permission. To fix the
inconsistency, update the disabled attribute to match the save eligibility logic
in canCreate. Change the permission gate so that the field is only disabled when
the user has neither create nor update permission, rather than requiring both
permissions to be present.

---

Outside diff comments:
In `@src/app/`(dashboard)/network/page.tsx:
- Around line 261-285: The disabledText and enabledText useMemo hooks are only
localizing the status tokens (inactive/active) while leaving the surrounding
English sentences hardcoded, creating a mixed-language experience for
non-English users. Extract the entire message strings for "High availability is
currently {status} for this network" and "High availability is {status} for this
network" into translation keys, then use tCommon() to retrieve and render the
fully localized messages instead of interpolating English text with localized
tokens.

In `@src/modules/control-center/nodes/SelectGroupNode.tsx`:
- Around line 45-55: The countLabel useMemo hook is missing the translation
function `t` in its dependency array, so locale changes won't trigger
recomputation. Additionally, the hardcoded strings "Resource(s)" and "Peer(s)"
in lines 52 and 54 are not being translated, creating mixed-language output. Add
`t` to the dependency array of the useMemo hook at line 55, and replace the
hardcoded "Resource(s)" and "Peer(s)" strings with appropriate translation key
calls using the `t()` function to ensure consistent translation throughout the
countLabel computation.

In `@src/modules/dns/zones/DNSRecordModal.tsx`:
- Around line 176-181: The modal description in the DNSRecordModal component is
using hardcoded English strings while the title is properly translated using the
t() function. Replace the hardcoded description strings in the conditional logic
(the ternary operator for both the update and add cases) with translation calls
using the t() function, similar to how the title is handled, while keeping the
dynamic zone.domain value interpolated within the translated strings.
- Around line 205-206: The SelectValue component placeholders contain hardcoded
English text ("Select type..." and "Select TTL...") that will not be translated
for non-English locales. Replace the hardcoded placeholder strings in both
SelectValue components (at the location with "Select type..." and the similar
one with "Select TTL...") with appropriate translation function calls to use
localized placeholder text instead of the hardcoded English strings.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 457-463: The confirmation dialog in ReverseProxyModal.tsx contains
hardcoded English strings for the title and description fields while the
confirmText and cancelText use translation functions (t() and tCommon()). Wrap
the title "No Protection Configured" and the description "This service has no
authentication or access control rules configured..." with the appropriate
translation function call (likely t() based on the existing pattern) instead of
keeping them as hardcoded strings, ensuring consistency with the
already-localized button texts.

---

Minor comments:
In `@src/app/`(dashboard)/groups/page.tsx:
- Line 46: The RestrictedAccess component in
src/components/ui/RestrictedAccess.tsx is rendering hardcoded English text for
the heading and body content instead of using localized strings, which conflicts
with the translated page label being passed from the caller. Update the
RestrictedAccess component to accept translation keys or integrate with the
translation system so that the heading and body text are rendered in the user's
selected language rather than fixed English strings. This ensures consistent
localization when access is denied.

In `@src/app/`(dashboard)/network-routes/page.tsx:
- Around line 56-58: The aria-label attribute on the docs link contains
hardcoded English text that will override any locale-specific translations and
be announced in English by screen readers regardless of the user's language
setting. Either localize the aria-label text using the application's
i18n/translation system to match the current locale (such as Chinese), or remove
the aria-label entirely if the visible link text already provides sufficient
accessibility context. Locate the aria-label with the text "Learn more about
routing traffic to private networks" and apply one of these solutions.

In `@src/app/`(dashboard)/peer/page.tsx:
- Around line 683-704: The hardcoded English token [at] in both the created_at
and last_seen date format strings prevents proper localization for non-English
languages. Remove the [at] string from both the dayjs().format() calls for
peer.created_at and peer.last_seen, then use the localization function t() to
provide a localized translation key for the word "at" and concatenate it as a
separate string between the absolute timestamp and the relative time expression,
similar to how the code already concatenates the parenthesized relative time.

In `@src/components/table/DataTableMultiSelectPopup.tsx`:
- Around line 25-26: The defaultLabel variable on line 25-26 already includes
the count via the translation function t('selected', { count }), but the code on
lines 63-67 is also rendering count separately, causing duplication like "3 3
selected". Remove the separate count rendering from the label composition in
lines 63-67 and use only defaultLabel, which already contains the interpolated
count value, or restructure the label logic to avoid passing count to the
translation function if you want to render it separately.

In `@src/i18n/messages/zh.ts`:
- Line 732: The Chinese translation for activePoliciesCount in zh.ts uses simple
string interpolation instead of ICU MessageFormat plural syntax. Update the
activePoliciesCount value in zh.ts to match the English plural format pattern by
replacing the simple string interpolation with the ICU MessageFormat plural
syntax, using the format "{count, plural, other {# 个活跃策略}}" to ensure
consistency with the English version and proper plural handling by next-intl.

In `@src/modules/access-control/table/AccessControlTable.tsx`:
- Around line 258-263: The third parameter passed to the formatCheckboxChip
function call (currently tTable("of")) is used to display text like "${count}
${plural}", and passing the translation key "of" results in grammatically
incorrect output such as "2 of". Replace tTable("of") with tTable("protocols")
or another appropriate plural noun translation key that makes grammatical sense
in the context of displaying the count of selected items. Ensure the translation
key you choose exists in the appropriate translation namespace.

In `@src/modules/common-table-rows/GroupsRow.tsx`:
- Around line 92-96: The `countOnly` prop being passed to the `MultipleGroups`
component causes the count badge text to display as a hardcoded English string
"Groups", which breaks localization for non-English locales. Locate the
`MultipleGroups` component and replace the hardcoded "Groups" text in the count
badge with a proper localized translation key or i18n function call that returns
the appropriately translated text based on the current locale instead of using a
hardcoded English string.

In `@src/modules/groups/table/GroupsActionCell.tsx`:
- Around line 77-83: The tooltip content in the FullTooltip component is using
hardcoded English constants from GROUP_TOOLTIP_TEXT (specifically
GROUP_TOOLTIP_TEXT.RENAME.JWT and GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION) instead
of localized strings. Replace all instances of GROUP_TOOLTIP_TEXT constants with
the appropriate localized/translated string values for both the RENAME tooltips
(around lines 77-83) and DELETE tooltips (around lines 102-108) to ensure the
action menu is fully translated and matches the localization approach used for
the dropdown labels.

In `@src/modules/groups/table/GroupsTable.tsx`:
- Around line 166-170: In the DataTableHeader component for the policies column
in GroupsTable.tsx, the tooltip is currently using the translation key "of"
which is incorrect for this column context. Replace the translation key passed
to tTable in the tooltip prop from "of" to the appropriate translation key that
correctly describes the policies column header. The key should be relevant to
the policies column's purpose and context.

In `@src/modules/peers/PeerNameCell.tsx`:
- Line 41: The aria-label attribute in PeerNameCell is concatenating separate
translated fragments which hardcodes word order and breaks locale grammar for
different languages. Instead of concatenating the results of t("viewDetailsOf")
with peer.name, use a single translation key that accepts peer.name as an
interpolated parameter, such as t("viewDetailsOf", { name: peer.name }) or a
similar pattern depending on your i18n library's interpolation syntax. Update
the translation key to support parameter interpolation so that translators can
adjust word order appropriately for each locale.

In `@src/modules/peers/PeerVersionCell.tsx`:
- Around line 87-94: The InlineLink component uses latestUrl without validating
it is non-null, relying only on the updateAvailable check which validates
latestVersion but not latestUrl. Add a null-check for latestUrl to ensure it
exists before rendering the InlineLink component, either by wrapping the link in
a conditional that checks latestUrl is truthy or by updating the updateAvailable
validation logic to confirm both latestVersion and latestUrl are present. Remove
the unsafe type cast `as string` after adding proper null-validation, allowing
TypeScript to verify the type safety.

In `@src/modules/posture-checks/modal/PostureCheckModal.tsx`:
- Line 195: The Label component in PostureCheckModal.tsx contains hardcoded
English text "Description (optional)" instead of using localization strings like
the rest of the modal. Replace this hardcoded string with a localized
translation key using the same i18n/localization approach used elsewhere in the
PostureCheckModal component to ensure consistent internationalization across the
entire modal.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 565-570: The modalTitle useMemo hook in ReverseProxyModal.tsx
contains a mixed-language issue where SERVICE_MODES[serviceMode].label provides
an English hardcoded label while the prefix is translated via tCommon. Instead
of concatenating the English label directly, replace
SERVICE_MODES[serviceMode].label with a properly translated service mode label
by using the appropriate translation function (likely tCommon or a similar i18n
function) to fetch the translated service mode name, ensuring both the prefix
and the mode name are in the same language throughout the modal title.

In `@src/modules/users/ServiceUsersTable.tsx`:
- Around line 45-66: Replace the hardcoded English strings "Name", "Role", and
"Status" in the column headers (found in the header functions for columns with
accessorKey values "name", "role", and "status") with appropriate translation
function calls using your application's i18n/localization system. Additionally,
apply the same translation approach to the role filter option labels referenced
in lines 132-135 to ensure all user-facing text in this table is translatable
and respects the user's locale settings.

---

Nitpick comments:
In `@src/components/NoPeersGettingStarted.tsx`:
- Around line 38-49: The current implementation splits the sentence across two
translation keys (learnMoreInOur and gettingStartedGuide) which restricts
translators' ability to reorder words based on language-specific grammar.
Refactor to use t.rich() instead of separate t() calls, consolidating into a
single translation key that accepts the InlineLink component as a parameter via
the values option. Define a link parameter in the values object that wraps the
content with the InlineLink component (including the ExternalLinkIcon), allowing
translators to place the link anywhere in the sentence structure through the
translation definition file. Update the translation key accordingly to accept
the rich text component placeholder.

In `@src/i18n/navigation.ts`:
- Around line 1-10: The routing configuration is duplicated between this file
and src/i18n/routing.ts, requiring maintenance in two places. Remove the routing
object definition created by defineRouting (which contains the locales and
defaultLocale configuration) from this file and instead import the routing
object from src/i18n/routing.ts at the top, then pass the imported routing to
createNavigation. This ensures the locale configuration is maintained in a
single location.

In `@src/i18n/request.ts`:
- Around line 7-13: The getRequestConfig function allows unsupported locale
values to be set in the returned locale field while falling back to messages.zh,
creating a mismatch between the declared locale and actual messages. Add
validation to ensure the resolvedLocale is a supported locale (one of the keys
in the messages object). If the locale is not supported, both the locale field
and messages should fall back to 'zh' to maintain consistency between the
declared locale and the messages being used.

In `@src/modules/groups/GroupFilterSelector.tsx`:
- Around line 177-179: The hardcoded string "{item.peers_count} Peer(s)" in the
div element displaying peer information needs to be localized. Replace this
hardcoded English text with a parameterized translation key using the pattern
t('nPeers', { n: item.peers_count }) to match the existing localization approach
used elsewhere in the file (such as the t('nGroups', { n }) pattern on line 71).
This ensures the peer count label is properly translated based on the user's
language settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3541b31-86e1-4158-9b08-89976bc4a92e

📥 Commits

Reviewing files that changed from the base of the PR and between bf66017 and da01b5b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (79)
  • docker/Dockerfile
  • docker/server.js
  • next.config.js
  • package.json
  • src/app/(dashboard)/access-control/page.tsx
  • src/app/(dashboard)/dns/nameservers/page.tsx
  • src/app/(dashboard)/dns/settings/page.tsx
  • src/app/(dashboard)/dns/zones/page.tsx
  • src/app/(dashboard)/events/audit/page.tsx
  • src/app/(dashboard)/group/page.tsx
  • src/app/(dashboard)/groups/page.tsx
  • src/app/(dashboard)/network-routes/page.tsx
  • src/app/(dashboard)/network/page.tsx
  • src/app/(dashboard)/networks/page.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(dashboard)/peers/servers/page.tsx
  • src/app/(dashboard)/peers/users/page.tsx
  • src/app/(dashboard)/posture-checks/page.tsx
  • src/app/(dashboard)/reverse-proxy/clusters/page.tsx
  • src/app/(dashboard)/reverse-proxy/custom-domains/page.tsx
  • src/app/(dashboard)/reverse-proxy/logs/page.tsx
  • src/app/(dashboard)/reverse-proxy/services/page.tsx
  • src/app/(dashboard)/settings/page.tsx
  • src/app/(dashboard)/team/service-users/page.tsx
  • src/app/(dashboard)/team/users/page.tsx
  • src/components/NoPeersGettingStarted.tsx
  • src/components/PeerSelector.tsx
  • src/components/table/DataTable.tsx
  • src/components/table/DataTableMultiSelectPopup.tsx
  • src/components/table/DataTablePagination.tsx
  • src/components/table/DataTableResetFilterButton.tsx
  • src/components/table/DataTableRowsPerPage.tsx
  • src/components/table/filters/GroupsPicker.tsx
  • src/components/ui/AddGroupButton.tsx
  • src/components/ui/AddPeerButton.tsx
  • src/components/ui/CitySelector.tsx
  • src/components/ui/CountrySelector.tsx
  • src/components/ui/NoResults.tsx
  • src/i18n/messages/en.ts
  • src/i18n/messages/zh.ts
  • src/i18n/navigation.ts
  • src/i18n/request.ts
  • src/i18n/routing.ts
  • src/layouts/AppLayout.tsx
  • src/layouts/Navigation.tsx
  • src/modules/access-control/AccessControlModal.tsx
  • src/modules/access-control/table/AccessControlActionCell.tsx
  • src/modules/access-control/table/AccessControlTable.tsx
  • src/modules/activity/ActivityTable.tsx
  • src/modules/common-table-rows/GroupsRow.tsx
  • src/modules/control-center/nodes/SelectGroupNode.tsx
  • src/modules/dns/nameservers/NameserverModal.tsx
  • src/modules/dns/zones/DNSRecordModal.tsx
  • src/modules/dns/zones/DNSZoneModal.tsx
  • src/modules/dns/zones/table/DNSZonesGroupCell.tsx
  • src/modules/groups/GroupFilterSelector.tsx
  • src/modules/groups/SingleGroupSelector.tsx
  • src/modules/groups/table/GroupsActionCell.tsx
  • src/modules/groups/table/GroupsTable.tsx
  • src/modules/networks/NetworkModal.tsx
  • src/modules/networks/misc/NetworkNavigation.tsx
  • src/modules/networks/resources/NetworkResourceModal.tsx
  • src/modules/networks/table/NetworksTable.tsx
  • src/modules/peer/MinimalPeersTable.tsx
  • src/modules/peers/PeerActionCell.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/peers/PeerAddressTooltipContent.tsx
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/peers/PeerGroupCell.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/peers/PeerMultiSelect.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/modules/peers/PeersTable.tsx
  • src/modules/posture-checks/modal/PostureCheckModal.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/users/ServiceUsersTable.tsx
  • src/modules/users/UsersTable.tsx

Comment thread docker/Dockerfile
Comment on lines +1 to +13
FROM node:22-alpine

RUN apk add --no-cache bash curl less ca-certificates git tzdata zip gettext \
nginx curl supervisor certbot-nginx && \
rm -rf /var/cache/apk/* && mkdir -p /run/nginx
WORKDIR /usr/share/nginx/html

STOPSIGNAL SIGINT
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["/usr/bin/supervisord","-c","/etc/supervisord.conf"]
# Copy build files
COPY out/ /usr/share/nginx/html/

WORKDIR /usr/share/nginx/html
# copy configuration files
COPY docker/default.conf /etc/nginx/http.d/default.conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/init_cert.sh /usr/local/init_cert.sh
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
RUN chmod +x /usr/local/init_cert.sh && rm /etc/crontabs/root
RUN chmod +x /usr/local/init_react_envs.sh
# Copy server script
COPY docker/server.js /server.js

EXPOSE 80

# configure supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
# copy build files
COPY out/ /usr/share/nginx/html/ No newline at end of file
CMD ["node", "/server.js"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Container runs as root user, which is a security risk.

The container runs the Node.js server as root by default. If the server is compromised, an attacker gains root privileges inside the container. Add a non-root user and switch to it before running the application.

🔒 Proposed fix to run as non-root user
 FROM node:22-alpine
 
 WORKDIR /usr/share/nginx/html
 
 # Copy build files
 COPY out/ /usr/share/nginx/html/
 
 # Copy server script
 COPY docker/server.js /server.js
 
+# Create non-root user and set ownership
+RUN addgroup -g 1001 -S nodejs && \
+    adduser -S nodejs -u 1001 -G nodejs && \
+    chown -R nodejs:nodejs /usr/share/nginx/html /server.js
+
+USER nodejs
+
 EXPOSE 80
 
 CMD ["node", "/server.js"]

Note: Port 80 is a privileged port (<1024). If you switch to a non-root user, you'll need to either use a higher port (e.g., 8080) or grant CAP_NET_BIND_SERVICE capability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM node:22-alpine
RUN apk add --no-cache bash curl less ca-certificates git tzdata zip gettext \
nginx curl supervisor certbot-nginx && \
rm -rf /var/cache/apk/* && mkdir -p /run/nginx
WORKDIR /usr/share/nginx/html
STOPSIGNAL SIGINT
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["/usr/bin/supervisord","-c","/etc/supervisord.conf"]
# Copy build files
COPY out/ /usr/share/nginx/html/
WORKDIR /usr/share/nginx/html
# copy configuration files
COPY docker/default.conf /etc/nginx/http.d/default.conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/init_cert.sh /usr/local/init_cert.sh
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
RUN chmod +x /usr/local/init_cert.sh && rm /etc/crontabs/root
RUN chmod +x /usr/local/init_react_envs.sh
# Copy server script
COPY docker/server.js /server.js
EXPOSE 80
# configure supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
# copy build files
COPY out/ /usr/share/nginx/html/
\ No newline at end of file
CMD ["node", "/server.js"]
FROM node:22-alpine
WORKDIR /usr/share/nginx/html
# Copy build files
COPY out/ /usr/share/nginx/html/
# Copy server script
COPY docker/server.js /server.js
# Create non-root user and set ownership
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 -G nodejs && \
chown -R nodejs:nodejs /usr/share/nginx/html /server.js
USER nodejs
EXPOSE 80
CMD ["node", "/server.js"]
🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker/Dockerfile` around lines 1 - 13, The Dockerfile runs the Node.js
server as root which is a security vulnerability. Create a non-root user in the
Dockerfile using RUN commands to add a new user and group, then add a USER
directive before the CMD instruction to switch to this non-root user.
Additionally, you will need to adjust the EXPOSE port from 80 to a higher
unprivileged port like 8080 (since port 80 requires root privileges), and update
the server.js script to listen on the new port instead.

Source: Linters/SAST tools

Comment thread docker/server.js
Comment on lines +768 to +815
<form>
<ModalHeader
title={t("editPeerName")}
description={t("editPeerNameDescription")}
color={"blue"}
/>

<div className={"p-default flex flex-col gap-4"}>
<div>
<Input
placeholder={t("peerNamePlaceholder")}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Card className={"w-full px-6 pt-5 pb-4"}>
<Label>
<Globe size={15} />
{t("domainNamePreview")}
</Label>
<HelpText className={"mt-2"}>{t("domainNamePreviewHelp")}</HelpText>
<div className={"text-netbird text-sm break-all whitespace-normal"}>
{domainNamePreview}
</div>
</Card>
</div>

<ModalFooter className={"items-center"} separator={false}>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
{tCommon("cancel")}
</Button>
</ModalClose>

<Button
variant={"primary"}
className={"w-full"}
onClick={() => onSuccess(name)}
disabled={isDisabled}
type={"submit"}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</form>
</ModalContent>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent unintended form submission in the edit-name modal

Line 808 uses type="submit" but the form has no onSubmit handler with preventDefault(). This can trigger a native form submit/reload while the async save is running.

Proposed fix
-			<form>
+			<form
+				onSubmit={(e) => {
+					e.preventDefault();
+					onSuccess(name);
+				}}
+			>
 				<ModalHeader
 					title={t("editPeerName")}
 					description={t("editPeerNameDescription")}
 					color={"blue"}
 				/>
@@
 						<Button
 							variant={"primary"}
 							className={"w-full"}
-							onClick={() => onSuccess(name)}
 							disabled={isDisabled}
 							type={"submit"}
 						>
 							{tCommon("save")}
 						</Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<form>
<ModalHeader
title={t("editPeerName")}
description={t("editPeerNameDescription")}
color={"blue"}
/>
<div className={"p-default flex flex-col gap-4"}>
<div>
<Input
placeholder={t("peerNamePlaceholder")}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Card className={"w-full px-6 pt-5 pb-4"}>
<Label>
<Globe size={15} />
{t("domainNamePreview")}
</Label>
<HelpText className={"mt-2"}>{t("domainNamePreviewHelp")}</HelpText>
<div className={"text-netbird text-sm break-all whitespace-normal"}>
{domainNamePreview}
</div>
</Card>
</div>
<ModalFooter className={"items-center"} separator={false}>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
{tCommon("cancel")}
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
onClick={() => onSuccess(name)}
disabled={isDisabled}
type={"submit"}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</form>
</ModalContent>
<form
onSubmit={(e) => {
e.preventDefault();
onSuccess(name);
}}
>
<ModalHeader
title={t("editPeerName")}
description={t("editPeerNameDescription")}
color={"blue"}
/>
<div className={"p-default flex flex-col gap-4"}>
<div>
<Input
placeholder={t("peerNamePlaceholder")}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Card className={"w-full px-6 pt-5 pb-4"}>
<Label>
<Globe size={15} />
{t("domainNamePreview")}
</Label>
<HelpText className={"mt-2"}>{t("domainNamePreviewHelp")}</HelpText>
<div className={"text-netbird text-sm break-all whitespace-normal"}>
{domainNamePreview}
</div>
</Card>
</div>
<ModalFooter className={"items-center"} separator={false}>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
{tCommon("cancel")}
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
disabled={isDisabled}
type={"submit"}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</form>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peer/page.tsx around lines 768 - 815, The form element
in the edit peer name modal lacks an onSubmit handler with preventDefault(),
which allows the native form submission to occur alongside the async save
operation triggered by the onClick handler on the submit button. Add an onSubmit
handler to the form element that prevents the default form submission behavior
and then invokes the onSuccess function with the current name value, ensuring
the async operation completes without triggering an unintended page reload or
form submission.

Comment on lines +56 to +58
<Suspense fallback={<SkeletonTable />}>
<ReverseProxyEventsTable headingTarget={portalTarget} />
</Suspense>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

TABLE_FILE="$(fd -a 'ReverseProxyEventsTable.tsx$' | head -n1)"
echo "Inspecting table file: $TABLE_FILE"

# Check table props and context dependencies
rg -n -C3 'type Props|interface Props|defaultFilters|headingTarget|useServerPagination|ServerPaginationProvider|usePeers|PeersProvider' "$TABLE_FILE"

PAGE_FILE="$(fd -a 'page.tsx$' | rg 'src/app/\(dashboard\)/reverse-proxy/logs/page.tsx' | head -n1)"
echo "Inspecting page file: $PAGE_FILE"

# Confirm what this page passes/wraps now
rg -n -C3 'ReverseProxyEventsTable|defaultFilters|ServerPaginationProvider|PeersProvider|Suspense' "$PAGE_FILE"

Repository: netbirdio/dashboard

Length of output: 2252


🏁 Script executed:

# Find the dashboard layout
fd -a 'layout.tsx$' | rg '\(dashboard\)' | head -n1

Repository: netbirdio/dashboard

Length of output: 135


🏁 Script executed:

# Check for ServerPaginationProvider and UsersProvider in parent layout
rg -n 'ServerPaginationProvider|UsersProvider' src/app/\(dashboard\)/ --type tsx

Repository: netbirdio/dashboard

Length of output: 91


🏁 Script executed:

# Also check root layout in case providers are there
fd -a 'layout.tsx$' src/app | head -n3

Repository: netbirdio/dashboard

Length of output: 265


🏁 Script executed:

# Search for providers in dashboard layout files
rg 'ServerPaginationProvider|UsersProvider' src/app/\(dashboard\)/

Repository: netbirdio/dashboard

Length of output: 554


🏁 Script executed:

# Check root layout
cat -n src/app/layout.tsx | head -n 100

Repository: netbirdio/dashboard

Length of output: 522


🏁 Script executed:

# Find AppLayout and check if it provides providers
fd -a 'AppLayout.tsx$'

Repository: netbirdio/dashboard

Length of output: 108


🏁 Script executed:

# Search for ServerPaginationProvider and UsersProvider across the entire codebase
rg 'ServerPaginationProvider|UsersProvider' src/layouts/

Repository: netbirdio/dashboard

Length of output: 258


🏁 Script executed:

# Check DashboardLayout to see if it provides ServerPaginationProvider
cat -n src/layouts/DashboardLayout.tsx

Repository: netbirdio/dashboard

Length of output: 6657


🏁 Script executed:

# Search for ServerPaginationProvider location
rg 'ServerPaginationProvider' src/ -l

Repository: netbirdio/dashboard

Length of output: 207


🏁 Script executed:

# Check where ServerPaginationProvider is instantiated/provided
cat -n src/contexts/ServerPaginationProvider.tsx | head -n 80

Repository: netbirdio/dashboard

Length of output: 2720


🏁 Script executed:

# Search for ServerPaginationProvider being used in JSX (as wrapper)
rg -n '<ServerPaginationProvider' src/

Repository: netbirdio/dashboard

Length of output: 45


🏁 Script executed:

# Check the useServerPagination hook to see if it has fallback or requires provider
rg -n -A 10 'export.*useServerPagination|function useServerPagination' src/contexts/ServerPaginationProvider.tsx

Repository: netbirdio/dashboard

Length of output: 438


🏁 Script executed:

# Check if there's any wrapper in the reverse-proxy page or its parent
cat -n src/app/\(dashboard\)/reverse-proxy/logs/page.tsx

Repository: netbirdio/dashboard

Length of output: 2546


🏁 Script executed:

# Check if there's a layout file specifically for reverse-proxy that might provide ServerPaginationProvider
fd -a 'layout.tsx$' src/app/\(dashboard\)/reverse-proxy/

Repository: netbirdio/dashboard

Length of output: 357


🏁 Script executed:

# Double-check if ServerPaginationProvider is provided elsewhere or if there's an optional version
rg -n 'useOptionalServerPagination|ServerPaginationProvider' src/modules/reverse-proxy/events/ReverseProxyEventsTable.tsx

Repository: netbirdio/dashboard

Length of output: 141


🏁 Script executed:

# Check the logs layout for ServerPaginationProvider
cat -n src/app/\(dashboard\)/reverse-proxy/logs/layout.tsx

Repository: netbirdio/dashboard

Length of output: 382


🏁 Script executed:

# Check what BlankLayout is
cat -n src/layouts/BlankLayout.tsx

Repository: netbirdio/dashboard

Length of output: 300


Add ServerPaginationProvider wrapper to page or layout.

The lazy-loaded ReverseProxyEventsTable calls useServerPagination() which requires a ServerPaginationProvider ancestor. This provider is missing from the page hierarchy—DashboardLayout does not provide it, and BlankLayout is just a pass-through. The component will throw "useServerPagination must be used within a ServerPaginationProvider" at runtime. Wrap the table with ServerPaginationProvider (providing the required url prop) or restore it to the layout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/reverse-proxy/logs/page.tsx around lines 56 - 58, The
ReverseProxyEventsTable component uses the useServerPagination() hook which
requires a ServerPaginationProvider ancestor that is currently missing from the
page hierarchy. Wrap the Suspense boundary containing ReverseProxyEventsTable
with a ServerPaginationProvider component and provide the required url prop to
make the pagination context available to the lazy-loaded table component.

Comment thread src/layouts/AppLayout.tsx Outdated
Comment on lines +277 to +278
!permission.policies.update || !permission.policies.create
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Permission gating for form fields is too strict in create/update modes.

These disabled checks currently require both policies.create and policies.update, which blocks legitimate create-only or update-only permission sets. Gate by mode instead (create modal → create permission, update modal → update permission).

💡 Proposed fix
 export function AccessControlModalContent({
   onSuccess,
   policy,
@@
 }: Readonly<ModalProps>) {
   const t = useTranslations("policies");
   const tCommon = useTranslations("common");
   const { permission } = usePermissions();
+  const canEditPolicy = policy
+    ? permission.policies.update
+    : permission.policies.create;
@@
 							<Select
 								value={protocol}
 								onValueChange={(v) => handleProtocolChange(v as Protocol)}
-								disabled={
-									!permission.policies.update || !permission.policies.create
-								}
+								disabled={!canEditPolicy}
 							>
@@
-									disabled={
-										!permission.policies.update || !permission.policies.create
-									}
+									disabled={!canEditPolicy}
 								/>
@@
 									disabled={
 										disableDestinationSelector ||
-										!permission.policies.update ||
-										!permission.policies.create
+										!canEditPolicy
 									}
 								/>
@@
 							disabled={
-								!permission.policies.update || !permission.policies.create
+								!canEditPolicy
 							}
@@
 								disabled={
-									!permission.policies.update || !permission.policies.create
+									!canEditPolicy
 								}
 							/>
@@
 								disabled={
-									!permission.policies.update || !permission.policies.create
+									!canEditPolicy
 								}
 							/>

Also applies to: 335-336, 370-373, 463-465, 494-495, 507-509

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/access-control/AccessControlModal.tsx` around lines 277 - 278,
The permission gating logic at line 277-278 and the other locations (335-336,
370-373, 463-465, 494-495, 507-509) currently disable form fields when either
`policies.create` or `policies.update` is missing, but this is too restrictive
for users with only create-only or update-only permissions. Instead of checking
both permissions together with OR logic, gate the field disabling based on the
current modal mode: if the modal is in create mode, only check for
`permission.policies.create` permission; if in update mode, only check for
`permission.policies.update` permission. You can determine the mode by checking
if a mode variable or similar exists in the component state or props (look for
variables like isCreateMode, mode, or similar). Update all the disabled
conditions across the mentioned locations to use this mode-based gating
approach.

Comment thread src/modules/access-control/table/AccessControlTable.tsx Outdated
Comment on lines 189 to 191
disabled={
!permission.policies.create || !permission.policies.update
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Permission gate is inconsistent with save eligibility logic.

This field is disabled unless the user has both create and update, while canCreate allows proceeding when either one is present. Users with a single permission can reach a blocked form state.

Suggested fix
-  const canCreate =
-    !isEmpty(check?.name) &&
-    isAtLeastOneCheckEnabled &&
-    (permission.policies.create || permission.policies.update);
+  const canEdit = permission.policies.create || permission.policies.update;
+  const canCreate =
+    !isEmpty(check?.name) && isAtLeastOneCheckEnabled && canEdit;

...
-                    disabled={
-                      !permission.policies.create || !permission.policies.update
-                    }
+                    disabled={!canEdit}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/posture-checks/modal/PostureCheckModal.tsx` around lines 189 -
191, The disabled condition for the form field in PostureCheckModal requires
both create and update permissions, but the canCreate logic elsewhere allows
proceeding with either permission. To fix the inconsistency, update the disabled
attribute to match the save eligibility logic in canCreate. Change the
permission gate so that the field is only disabled when the user has neither
create nor update permission, rather than requiring both permissions to be
present.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modules/control-center/nodes/SelectGroupNode.tsx (1)

45-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use peer label key in the zero-resource branch

Line 49 returns t("nGroups", { n: peerCount }), but this branch represents peer count (resources are zero). This renders incorrect text (e.g., “X Group(s)” instead of “X Peer(s)”).

Suggested fix
-    if (resourceCount === 0) {
-      return t("nGroups", { n: peerCount });
-    }
+    if (resourceCount === 0) {
+      return t("nPeers", { n: peerCount });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/control-center/nodes/SelectGroupNode.tsx` around lines 45 - 50,
In the countLabel useMemo hook within SelectGroupNode.tsx, when resourceCount is
zero the code is using the translation key "nGroups" with the peerCount value,
which renders incorrect text for peer counts. Change the translation key from
"nGroups" to "nPeers" (or the appropriate peer-specific translation key) in the
branch where resourceCount === 0 to correctly display peer count labels instead
of group count labels.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ui/RestrictedAccess.tsx`:
- Around line 18-19: The `page` property in the `Props` type definition is
optional, but a fallback default value has been removed from the component
function destructuring (at line 18-19). This causes the heading to render with
an empty second line when `page` is not provided. Either make the `page`
property required in the Props type definition by removing the optional
indicator, or provide a default value in the destructuring of the function
parameter (for example, using nullish coalescing or a default assignment). Apply
the same fix to both instances mentioned, including the one around lines 66-67.

---

Outside diff comments:
In `@src/modules/control-center/nodes/SelectGroupNode.tsx`:
- Around line 45-50: In the countLabel useMemo hook within SelectGroupNode.tsx,
when resourceCount is zero the code is using the translation key "nGroups" with
the peerCount value, which renders incorrect text for peer counts. Change the
translation key from "nGroups" to "nPeers" (or the appropriate peer-specific
translation key) in the branch where resourceCount === 0 to correctly display
peer count labels instead of group count labels.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da4d0cf1-cf94-4d85-bbf7-aac72a51a064

📥 Commits

Reviewing files that changed from the base of the PR and between bb6c304 and 6c363b6.

📒 Files selected for processing (15)
  • docker/server.js
  • src/app/(dashboard)/network-routes/page.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/components/table/DataTableMultiSelectPopup.tsx
  • src/components/ui/MultipleGroups.tsx
  • src/components/ui/RestrictedAccess.tsx
  • src/i18n/messages/en.ts
  • src/i18n/messages/zh.ts
  • src/modules/access-control/table/AccessControlTable.tsx
  • src/modules/control-center/nodes/SelectGroupNode.tsx
  • src/modules/groups/table/GroupsActionCell.tsx
  • src/modules/groups/table/GroupsTable.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/messages/zh.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/modules/groups/table/GroupsActionCell.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/components/table/DataTableMultiSelectPopup.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/app/(dashboard)/network-routes/page.tsx
  • docker/server.js
  • src/modules/groups/table/GroupsTable.tsx
  • src/i18n/messages/en.ts
  • src/app/(dashboard)/peer/page.tsx
  • src/modules/access-control/table/AccessControlTable.tsx

Comment on lines +18 to 19
page,
}: Props) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore a fallback (or make page required) to avoid incomplete restricted-access heading.

page is still optional in Props, but the default was removed. If a caller omits it, the heading renders with an empty second line. Please either reintroduce a default or make page required to keep this UI deterministic.

Suggested minimal fix
 export const RestrictedAccess = ({
   children,
   hasAccess = false,
-  page,
+  page = "this page",
 }: Props) => {

Also applies to: 66-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/RestrictedAccess.tsx` around lines 18 - 19, The `page`
property in the `Props` type definition is optional, but a fallback default
value has been removed from the component function destructuring (at line
18-19). This causes the heading to render with an empty second line when `page`
is not provided. Either make the `page` property required in the Props type
definition by removing the optional indicator, or provide a default value in the
destructuring of the function parameter (for example, using nullish coalescing
or a default assignment). Apply the same fix to both instances mentioned,
including the one around lines 66-67.

…itcher

Previously the locale was hardcoded to "zh" in AppLayout's
NextIntlClientProvider, so every user saw Chinese with no way to switch.

This adds proper client-side locale resolution suitable for the static
export build (`output: "export"` — no server runtime / middleware):

- New single source of truth `src/i18n/config.ts` (locales, defaultLocale,
  message catalog, cookie name, timezones) consumed by request.ts,
  routing.ts, navigation.ts and the new provider.
- New pure detection helpers `src/i18n/detection.ts`:
  cookie (NEXT_LOCALE) -> browser language (navigator.languages) ->
  default. All branches are SSR/build-safe to avoid hydration mismatches.
- New `LocaleProvider` context wraps NextIntlClientProvider, resolves the
  locale on mount, syncs <html lang>, and exposes { locale, setLocale }.
- AppLayout now uses LocaleProvider instead of the hardcoded "zh" wiring.
- New `LanguageSwitcher` in the header and a new `Language` tab in
  Settings, both persisting the choice via the cookie for reloads.

Default locale is now English ("en"); Chinese remains available via the
browser-language fallback or the explicit switcher. Includes a manual
assertion script (detection.test.ts) covering cookie precedence, browser
fallback, persistence and SSR safety.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/ui/LocaleSwitcher.tsx (1)

19-23: 💤 Low value

Consider extracting LOCALE_LABELS to a shared constant.

The same LOCALE_LABELS map is duplicated in src/modules/settings/LanguageTab.tsx (lines 18-21). Extract it to src/i18n/config.ts to maintain a single source of truth.

♻️ Suggested consolidation

In src/i18n/config.ts, add:

export const LOCALE_LABELS: Record<Locale, string> = {
  en: "English",
  zh: "中文",
};

Then import and use it in both LocaleSwitcher and LanguageTab.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/LocaleSwitcher.tsx` around lines 19 - 23, The LOCALE_LABELS
constant is duplicated across two files which violates the single source of
truth principle. Extract the LOCALE_LABELS Record definition from
LocaleSwitcher.tsx and add it as an export in the src/i18n/config.ts file, then
update both LocaleSwitcher.tsx and LanguageTab.tsx to import LOCALE_LABELS from
the i18n config file instead of defining it locally. This ensures the locale
labels are maintained in one place and can be reused consistently across the
application.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/i18n/config.ts`:
- Around line 29-32: The type cast `as unknown as typeof en` on the `zh` object
in the `messages` export bypasses TypeScript's structural validation, hiding the
fact that the Chinese translation catalog contains extra keys that don't exist
in the English catalog. Remove the type cast and instead adjust the zh object's
export type definition to accurately reflect its actual structure, ensuring it
includes the extra keys `dns.nameserversDescription` and `dns.zonesDescription`.
If these extra keys are intentional, document why they exist and adjust the
messages object's type definition to accept translations with different
structures, or create a separate type for the Chinese catalog that explicitly
includes these additional keys.

---

Nitpick comments:
In `@src/components/ui/LocaleSwitcher.tsx`:
- Around line 19-23: The LOCALE_LABELS constant is duplicated across two files
which violates the single source of truth principle. Extract the LOCALE_LABELS
Record definition from LocaleSwitcher.tsx and add it as an export in the
src/i18n/config.ts file, then update both LocaleSwitcher.tsx and LanguageTab.tsx
to import LOCALE_LABELS from the i18n config file instead of defining it
locally. This ensures the locale labels are maintained in one place and can be
reused consistently across the application.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fedd7df8-235d-4fb9-b295-66bd5c424f2e

📥 Commits

Reviewing files that changed from the base of the PR and between 6c363b6 and 98a7923.

📒 Files selected for processing (14)
  • src/app/(dashboard)/settings/page.tsx
  • src/components/ui/LocaleSwitcher.tsx
  • src/contexts/LocaleProvider.tsx
  • src/i18n/config.ts
  • src/i18n/detection.test.ts
  • src/i18n/detection.ts
  • src/i18n/messages/en.ts
  • src/i18n/messages/zh.ts
  • src/i18n/navigation.ts
  • src/i18n/request.ts
  • src/i18n/routing.ts
  • src/layouts/AppLayout.tsx
  • src/layouts/Header.tsx
  • src/modules/settings/LanguageTab.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/messages/en.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/i18n/navigation.ts
  • src/i18n/routing.ts
  • src/app/(dashboard)/settings/page.tsx
  • src/i18n/messages/zh.ts

Comment thread src/i18n/config.ts
…d auth

Replaces all hardcoded English text in 8 settings tab components and the
SessionLost page with useTranslations() calls, so they render in the user's
active locale (English or Chinese):

- Settings tabs: Authentication, Clients, Groups, Networks, Danger Zone,
  Setup Keys, Identity Providers, Permissions — breadcrumbs and h1 headers
  now use the existing settings namespace keys.
- SessionLost: "Session Expired" description + "Login" button localized
  via the auth namespace.
- i18n keys added: auth.sessionExpiredDescription, dns.nameserversDescription,
  dns.zonesDescription (en + zh translations).
…section and onboarding

Covers four high-visibility hardcoded headings and surrounding text:

- User detail page: "Access Tokens" tab label + heading + description
- UserPeersSection: "Peers" heading + "View all peers..." description
- OnboardingIntent: "Get started with NetBird" title
- OnboardingAddResource: "Add your first resource" title

i18n keys added: users.accessTokens, users.accessTokensDescription,
peers.userPeersDescription, onboarding.title, onboarding.addResource
(en + zh translations).
Replaces all hardcoded "Cancel" button text with {t("cancel")} using the
common namespace in every modal component, plus a few adjacent button
texts. Adds import { useTranslations } + useTranslations("common") to
each component that needed it.

Files covered: access-tokens, jobs, networks, peer, posture-checks,
remote-access/ssh, reverse-proxy/auth, reverse-proxy/clusters,
reverse-proxy/domain, routes, settings, setup-keys, users — all modal
dialogs with Cancel buttons now respect the active locale.
Replaces 19 hardcoded aria-label strings with useTranslations() calls:

- "Select all" / "Select row" — group assignments, posture checks
- "Configure policies" / "Configure access control" — network, reverse proxy
- "Configure authentication" / "Remove rule" / "Remove header" — reverse proxy
- "Public listen port" / "Destination port" — reverse proxy L4 config
- "Select language" — locale switcher

Also refactors 5 module-level column arrays into factory functions
to pass the translation function through column definitions.
i18n keys added to common namespace (en + zh translations).
Replaces all hardcoded English strings in DangerZoneTab with
useTranslations("settings") calls: card title, warning paragraph,
confirm/cancel dialog, notify messages, and button label.
i18n keys added to settings namespace (en + zh).
…t URL)

Resolve merge conflict in IdentityProviderModal.tsx by adopting upstream
main's new 'Endpoint URLs' section (Redirect/Callback + Logout URLs from
PR netbirdio#657), then re-applying i18n: useTranslations import, Cancel button
localization, and new translation keys for the added endpoint/logout text
(endpointUrls, redirectCallback, logoutLabel, notAllProvidersLogout,
learnMore) in en + zh.

Also brings in upstream DNS Zones & Setup modal improvements (netbirdio#669),
banner IPv6 link (netbirdio#662), remote jobs docs link (netbirdio#664).
…text)

Replaces hardcoded English text inside all 7 settings tabs with
useTranslations("settings") calls. Covers:

- AuthenticationTab: Session Expiration, User Approval, Local MFA,
  Peer Session Expiration, Days/Hours, Require login after disconnect
- ClientSettingsTab: Automatic Updates, Force Updates, Expose Services
  from CLI, Enable Peer Expose, Allowed peer groups, Lazy Connections
- GroupsSettings: Group propagation, JWT group sync, JWT claim,
  JWT allow groups, group access warning
- NetworkSettingsTab: DNS Domain, Network Range, IPv6 ranges,
  IPv6 enabled groups, DNS Wildcard Routing, validation errors
- IdentityProvidersTab: table headers (Name/Type), description
- SetupKeysTab: description text
- PermissionsTab: Restrict dashboard toggle, Save Changes

40+ new translation keys added to settings namespace (en + zh).
…ttings

NetworkResourceAccessControl: "Access Control Policies" label + help text
PeerExpirationToggle: session expiration description, tooltip text (setup
key disabled, no permission, global setting disabled, go to settings)
PeerExpirationSettings: notification texts, inactivity expiration text

New keys in peers + networks namespaces (en + zh).
@mlsmaycon

Copy link
Copy Markdown
Contributor

@sakuradairong we are about to merge a huge change where we sync a private fork into this repo. This will cause lots of changes for your PR.

I advise you to wait for a bit as this may break things for you.

@sakuradairong

Copy link
Copy Markdown
Author

Thank you for the heads up! I will pause further changes and wait for the private fork merge to land first. After that I will rebase this PR and resolve any conflicts.

For context, this PR adds:

  • Cookie-based locale detection (enabled by default, switchable to zh via cookie)
  • Language switcher in the header and Settings page
  • Comprehensive Chinese localization (~900 translation keys)

Please let me know when the fork merge is complete and I will update the PR accordingly.

@sakuradairong

Copy link
Copy Markdown
Author

Good catch on the LOCALE_LABELS duplication. I will address this after rebasing on the upcoming fork merge.

# Conflicts:
#	package-lock.json
#	src/app/(dashboard)/dns/settings/page.tsx
#	src/app/(dashboard)/events/audit/page.tsx
#	src/app/(dashboard)/peer/page.tsx
#	src/app/(dashboard)/peers/servers/page.tsx
#	src/app/(dashboard)/peers/users/page.tsx
#	src/app/(dashboard)/reverse-proxy/services/page.tsx
#	src/app/(dashboard)/settings/page.tsx
#	src/app/(dashboard)/team/user/page.tsx
#	src/app/(dashboard)/team/users/page.tsx
#	src/components/ui/AddGroupButton.tsx
#	src/components/ui/AddPeerButton.tsx
#	src/layouts/Navigation.tsx
#	src/modules/access-control/AccessControlModal.tsx
#	src/modules/access-control/table/AccessControlActionCell.tsx
#	src/modules/access-control/table/AccessControlTable.tsx
#	src/modules/common-table-rows/GroupsRow.tsx
#	src/modules/groups/table/GroupsActionCell.tsx
#	src/modules/networks/NetworkModal.tsx
#	src/modules/networks/resources/NetworkResourceAccessControl.tsx
#	src/modules/networks/resources/NetworkResourceModal.tsx
#	src/modules/peers/PeerActionCell.tsx
#	src/modules/peers/PeerMultiSelect.tsx
#	src/modules/peers/PeersTable.tsx
#	src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx
#	src/modules/reverse-proxy/ReverseProxyLayer4Content.tsx
#	src/modules/reverse-proxy/ReverseProxyModal.tsx
#	src/modules/reverse-proxy/auth/AuthHeaderModal.tsx
#	src/modules/reverse-proxy/clusters/ClustersModal.tsx
#	src/modules/reverse-proxy/domain/CustomDomainModal.tsx
#	src/modules/reverse-proxy/targets/ReverseProxyTargetCustomHeaders.tsx
#	src/modules/settings/AuthenticationTab.tsx
#	src/modules/settings/ClientSettingsTab.tsx
#	src/modules/settings/DangerZoneTab.tsx
#	src/modules/settings/GroupsSettings.tsx
#	src/modules/settings/IdentityProviderModal.tsx
#	src/modules/settings/NetworkSettingsTab.tsx
#	src/modules/settings/PermissionsTab.tsx
#	src/modules/users/ServiceUserModal.tsx
#	src/modules/users/ServiceUsersTable.tsx
#	src/modules/users/UsersTable.tsx
- AccessiblePeersSection, PeerNetworkRoutesSection, PeerRemoteJobsSection
- PostureCheckGeoLocation, PostureCheckNetBirdVersion, PostureCheckOperatingSystem
- PostureCheckPeerNetworkRange, PostureCheckProcess, PostureCheckNoChecksInfo
- Update en.ts and zh.ts messages with new translation keys
- RouteModal: routeType, networkRange, domains, distributionGroups, metric
- SetupKeyModal: createTitle, nameHelp, usageLimitHelp, expiresIn
- CreateAccessTokenModal: tokenName, tokenNameHelp, tokenExpiresIn
- Add corresponding keys to en.ts and zh.ts
sakuradairong and others added 13 commits June 23, 2026 22:03
# Conflicts:
#	src/app/(dashboard)/team/users/page.tsx
- Refactor PeersTableColumns from const to function with t() param
- Add useTranslations('peers') to PeersTable component
- Replace hardcoded Name, Address, Groups, OS, Serial number, Version headers
- Add selectAll, selectRow, unknown keys
- Refactor UsersTableColumns from const to function with t() param
- Add useTranslations('users') to UsersTable component
- Replace hardcoded Name, Role, Status, Groups, Last Login headers
- Refactor both table column defs from const to function with t() param
- Add lastUsed, lastUsedOn, nameAndKey, expires keys to common/setupKeys namespaces
- PeerSSHToggle: sshAccess, enableSSH
- PeerEditIPModal: changesTakeEffect, cancel
- Add sshAccess, enableSSH, changesTakeEffect keys
…p 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
- Add translations for PageNotFound, NoResultsCard, Dialog close button
- Localize Navigation labels (Integrations, Traffic Events)
- Translate error page states (blocked, pending, access error)
- Localize UserDropdown, LoginExpiredBadge, DarkModeToggle, LocaleSwitcher
- Add missing keys to en.ts and zh.ts
sakuradairong added 3 commits June 24, 2026 07:00
- Add namePlaceholder, emailPlaceholder, userActions keys to users namespace
- Add Chinese translations for the new keys
- Fix remaining hardcoded placeholders in UserInviteModal (John Doe, hello@netbird.io)
- Fix hardcoded placeholder in ServiceUserModal (use serviceUserNamePlaceholder)
- Fix hardcoded aria-label in UserActionCell (use t('userActions'))
- PeerActionCell.tsx: replace ~20 hardcoded strings with t() calls
  (approve, bypass compliance, session expiration, SSH, delete actions)
- Activity module: internationalize 5 files including ActivityDescription.tsx
  with ~104 activity event description templates using t.rich()
- Setup Keys: replace ~18 hardcoded strings in SetupKeyActionCell
  and SetupKeyGroupsCell with translation keys
- Add Chinese translations with natural grammar and consistent terminology
- Routes: 12 files internationalized (98 keys), covering route table,
  modal, action cells, and route-group sub-module
- DNS: 14 files internationalized (158 keys), covering nameserver
  and DNS zone management
- Networks: 28 files internationalized (184+ keys), covering network
  resources, routing peers, and provider-level notifications
- Access Control: 7 files internationalized (112 keys), covering
  SSH settings, posture checks, port/protocol filters, and table
- Add natural Chinese translations with consistent terminology
@sakuradairong

Copy link
Copy Markdown
Author

Update: the Chinese translation work is now mostly complete.

I have pushed the latest localization updates covering the shared UI and the main dashboard modules, including Peers, Users, Activity, Setup Keys, Routes, DNS, Networks, and Access Control. At this point the remaining work should mainly be review, final polish, and addressing any feedback or conflicts from upstream changes.

Thanks!

sakuradairong and others added 3 commits June 24, 2026 08:57
- Extract 485 remaining hardcoded English UI strings across
  app, cloud, components, contexts, hooks, and modules
- Append them to a new 'untranslated' namespace in en.ts as baseline
- Mirror same English values in zh.ts as placeholders to maintain key alignment
- No Chinese translations added yet - to be translated in future pass
@sakuradairong

Copy link
Copy Markdown
Author

Hi team, I''ve synced the latest origin/main into this branch and fixed the blockers that were preventing a green build. Here is a summary:

Fixes applied

  • Merged origin/main (non-fast-forward merge) to resolve the API drift.
  • Replaced removed export isNetBirdHostedisNetBirdCloud in:
    • src/modules/settings/GroupsSettings.tsx
    • src/modules/settings/NetworkSettingsTab.tsx
  • Restored missing imports IdentityProviderCard and AccountMfaCard in src/app/(dashboard)/team/users/page.tsx.
  • Fixed PeerMultiSelect scope bug where the inner component used t without calling useTranslations("peers").
  • Added dataCy prop support to PeerGroupSelector so callers that pass dataCy type-check correctly.
  • Fixed translator type mismatch in AccessControlTable column factory.
  • Fixed next-intl value type errors in:
    • ActivityDescription.tsx (nested t.rich returning ReactNode used as interpolation value)
    • UserResendInviteButton.tsx (user.email possibly undefined)
  • Addressed CodeRabbit feedback:
    • Extracted shared LOCALE_LABELS into src/i18n/config.ts and updated both LocaleSwitcher and LanguageTab to import it.
    • Removed the unsafe zh: zh as unknown as typeof en cast from src/i18n/config.ts.

Verification

  • npx tsc --noEmit ✅ 0 errors
  • npm run build ✅ static export completed successfully
  • i18n key parity check: en.ts and zh.ts both have 2483 keys, missingInZh = 0, extraInZh = 0
  • Changed-file translation usage check: missingCount = 0

Notes

  • The npm run lint / ESLint 9 setup issue is unrelated to this localization work and is still tracked separately.
  • Branch localize-zh-v2.39.0 has been pushed to both fork and rainycy remotes.

Please re-run the checks when convenient. Let me know if anything else is needed!

sakuradairong and others added 8 commits June 26, 2026 15:14
Remove dead i18n files (navigation.ts, routing.ts) and clean up the stale JSDoc link in config.ts.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Declare the global IntlMessages interface from the English message catalog so translation keys are type-checked.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Convert static metadata objects to async generateMetadata using getTranslations for six dashboard route groups.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add English and Chinese strings for onboarding demo-call variants, posture-check labels/actions, and route/network interpolation messages.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Replace hard-coded English in OnboardingIntent, OnboardingEnd and OnboardingDemoCall with useTranslations calls, including rich-text demo-call variants.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Convert hard-coded labels, tooltips, notifications and dialog text in posture-checks components to use the postureChecks namespace.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Use ICU message placeholders for router removal, route/network deletion notifications and validation errors to support variable ordering in translations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The admin key lives in the users namespace, not common. Remove the type-unsafe tCommon call and its fallback string.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants