feat(i18n): expand Chinese localization and fix env substitution#671
feat(i18n): expand Chinese localization and fix env substitution#671sakuradairong wants to merge 54 commits into
Conversation
- 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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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. ChangesDashboard i18n and runtime migration
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
635603d to
da01b5b
Compare
There was a problem hiding this comment.
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 winLocalize 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 winLocalize 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 winTranslate 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 winLocalize 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
countLabelmemo misses locale dependency and returns untranslated strings.At line 45,
useMemocallst(...)but the dependency list at line 55 omitst, 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 winInconsistent 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 winTranslation key
"of"likely incorrect for thepluralparameter.
formatCheckboxChipuses the third parameter to display selected count as"${count} ${plural}". PassingtTable("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 winRename/Delete tooltip text is not localized.
Dropdown labels are translated, but tooltip bodies still come from
GROUP_TOOLTIP_TEXTEnglish 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 winRestricted-access copy is still partially untranslated.
This page passes a localized
pagelabel, butsrc/components/ui/RestrictedAccess.tsxstill 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 winPolicies 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 winLabel 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 winCore table/filter labels are still hardcoded English.
Name/Role/Statusheaders 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 winLocalize or remove the hardcoded
aria-labelon the docs link.Line 56 hardcodes English text. Since
aria-labeloverrides 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 winModal title still embeds hardcoded English service-mode labels.
Line 569 reads
SERVICE_MODES[serviceMode].label, but those labels are English constants insrc/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 winRemove 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 winAvoid 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 | 🟡 MinorAdd null-check before using
latestUrlin the link or ensure bothlatestVersionandlatestUrlare validated together.The
updateAvailablecheck only validateslatestVersion, but the changelog link assumeslatestUrlis non-null. The type castas stringat line 89 bypasses TypeScript's safety check. If the API returns a version update without a changelog URL, the link would have an invalidhref. Either validatelatestUrlin theupdateAvailablecondition 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 winAvoid double-count rendering in selected label.
defaultLabelalready includes{count}viat('selected', { count }), but Line 64 also renderscountseparately, 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
countOnlynow exposes hardcoded English text in localized UI.At Line 95, forwarding
countOnlyroutes this path throughMultipleGroups, 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 valueConsider validating locale against supported values.
If an unsupported locale (e.g.,
'fr') is passed, the current code returnslocale: 'fr'but falls back tomessages.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 winEliminate 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 winTranslate 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 liket('nPeers', { n: item.peers_count })to match the pattern used elsewhere (e.g., line 71'st('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 valueConsider consolidating the split translation for better i18n flexibility.
The current pattern splits the "learn more" sentence across two translation keys (
learnMoreInOurandgettingStartedGuide) 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
valuesparameter. 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (79)
docker/Dockerfiledocker/server.jsnext.config.jspackage.jsonsrc/app/(dashboard)/access-control/page.tsxsrc/app/(dashboard)/dns/nameservers/page.tsxsrc/app/(dashboard)/dns/settings/page.tsxsrc/app/(dashboard)/dns/zones/page.tsxsrc/app/(dashboard)/events/audit/page.tsxsrc/app/(dashboard)/group/page.tsxsrc/app/(dashboard)/groups/page.tsxsrc/app/(dashboard)/network-routes/page.tsxsrc/app/(dashboard)/network/page.tsxsrc/app/(dashboard)/networks/page.tsxsrc/app/(dashboard)/peer/page.tsxsrc/app/(dashboard)/peers/servers/page.tsxsrc/app/(dashboard)/peers/users/page.tsxsrc/app/(dashboard)/posture-checks/page.tsxsrc/app/(dashboard)/reverse-proxy/clusters/page.tsxsrc/app/(dashboard)/reverse-proxy/custom-domains/page.tsxsrc/app/(dashboard)/reverse-proxy/logs/page.tsxsrc/app/(dashboard)/reverse-proxy/services/page.tsxsrc/app/(dashboard)/settings/page.tsxsrc/app/(dashboard)/team/service-users/page.tsxsrc/app/(dashboard)/team/users/page.tsxsrc/components/NoPeersGettingStarted.tsxsrc/components/PeerSelector.tsxsrc/components/table/DataTable.tsxsrc/components/table/DataTableMultiSelectPopup.tsxsrc/components/table/DataTablePagination.tsxsrc/components/table/DataTableResetFilterButton.tsxsrc/components/table/DataTableRowsPerPage.tsxsrc/components/table/filters/GroupsPicker.tsxsrc/components/ui/AddGroupButton.tsxsrc/components/ui/AddPeerButton.tsxsrc/components/ui/CitySelector.tsxsrc/components/ui/CountrySelector.tsxsrc/components/ui/NoResults.tsxsrc/i18n/messages/en.tssrc/i18n/messages/zh.tssrc/i18n/navigation.tssrc/i18n/request.tssrc/i18n/routing.tssrc/layouts/AppLayout.tsxsrc/layouts/Navigation.tsxsrc/modules/access-control/AccessControlModal.tsxsrc/modules/access-control/table/AccessControlActionCell.tsxsrc/modules/access-control/table/AccessControlTable.tsxsrc/modules/activity/ActivityTable.tsxsrc/modules/common-table-rows/GroupsRow.tsxsrc/modules/control-center/nodes/SelectGroupNode.tsxsrc/modules/dns/nameservers/NameserverModal.tsxsrc/modules/dns/zones/DNSRecordModal.tsxsrc/modules/dns/zones/DNSZoneModal.tsxsrc/modules/dns/zones/table/DNSZonesGroupCell.tsxsrc/modules/groups/GroupFilterSelector.tsxsrc/modules/groups/SingleGroupSelector.tsxsrc/modules/groups/table/GroupsActionCell.tsxsrc/modules/groups/table/GroupsTable.tsxsrc/modules/networks/NetworkModal.tsxsrc/modules/networks/misc/NetworkNavigation.tsxsrc/modules/networks/resources/NetworkResourceModal.tsxsrc/modules/networks/table/NetworksTable.tsxsrc/modules/peer/MinimalPeersTable.tsxsrc/modules/peers/PeerActionCell.tsxsrc/modules/peers/PeerAddressCell.tsxsrc/modules/peers/PeerAddressTooltipContent.tsxsrc/modules/peers/PeerConnectButton.tsxsrc/modules/peers/PeerGroupCell.tsxsrc/modules/peers/PeerLastSeenCell.tsxsrc/modules/peers/PeerMultiSelect.tsxsrc/modules/peers/PeerNameCell.tsxsrc/modules/peers/PeerOSCell.tsxsrc/modules/peers/PeerVersionCell.tsxsrc/modules/peers/PeersTable.tsxsrc/modules/posture-checks/modal/PostureCheckModal.tsxsrc/modules/reverse-proxy/ReverseProxyModal.tsxsrc/modules/users/ServiceUsersTable.tsxsrc/modules/users/UsersTable.tsx
| 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"] |
There was a problem hiding this comment.
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.
| 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
(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
| <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> |
There was a problem hiding this comment.
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.
| <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.
| <Suspense fallback={<SkeletonTable />}> | ||
| <ReverseProxyEventsTable headingTarget={portalTarget} /> | ||
| </Suspense> |
There was a problem hiding this comment.
🧩 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 -n1Repository: netbirdio/dashboard
Length of output: 135
🏁 Script executed:
# Check for ServerPaginationProvider and UsersProvider in parent layout
rg -n 'ServerPaginationProvider|UsersProvider' src/app/\(dashboard\)/ --type tsxRepository: netbirdio/dashboard
Length of output: 91
🏁 Script executed:
# Also check root layout in case providers are there
fd -a 'layout.tsx$' src/app | head -n3Repository: 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 100Repository: 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.tsxRepository: netbirdio/dashboard
Length of output: 6657
🏁 Script executed:
# Search for ServerPaginationProvider location
rg 'ServerPaginationProvider' src/ -lRepository: netbirdio/dashboard
Length of output: 207
🏁 Script executed:
# Check where ServerPaginationProvider is instantiated/provided
cat -n src/contexts/ServerPaginationProvider.tsx | head -n 80Repository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: netbirdio/dashboard
Length of output: 141
🏁 Script executed:
# Check the logs layout for ServerPaginationProvider
cat -n src/app/\(dashboard\)/reverse-proxy/logs/layout.tsxRepository: netbirdio/dashboard
Length of output: 382
🏁 Script executed:
# Check what BlankLayout is
cat -n src/layouts/BlankLayout.tsxRepository: 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.
| !permission.policies.update || !permission.policies.create | ||
| } |
There was a problem hiding this comment.
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.
| disabled={ | ||
| !permission.policies.create || !permission.policies.update | ||
| } |
There was a problem hiding this comment.
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.
… tooltips, table headers, timestamps, and fix plural/aria-label issues
…ss keys to common namespace
There was a problem hiding this comment.
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 winUse 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
📒 Files selected for processing (15)
docker/server.jssrc/app/(dashboard)/network-routes/page.tsxsrc/app/(dashboard)/peer/page.tsxsrc/components/table/DataTableMultiSelectPopup.tsxsrc/components/ui/MultipleGroups.tsxsrc/components/ui/RestrictedAccess.tsxsrc/i18n/messages/en.tssrc/i18n/messages/zh.tssrc/modules/access-control/table/AccessControlTable.tsxsrc/modules/control-center/nodes/SelectGroupNode.tsxsrc/modules/groups/table/GroupsActionCell.tsxsrc/modules/groups/table/GroupsTable.tsxsrc/modules/peers/PeerNameCell.tsxsrc/modules/peers/PeerVersionCell.tsxsrc/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
| page, | ||
| }: Props) => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/components/ui/LocaleSwitcher.tsx (1)
19-23: 💤 Low valueConsider extracting LOCALE_LABELS to a shared constant.
The same
LOCALE_LABELSmap is duplicated insrc/modules/settings/LanguageTab.tsx(lines 18-21). Extract it tosrc/i18n/config.tsto 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
📒 Files selected for processing (14)
src/app/(dashboard)/settings/page.tsxsrc/components/ui/LocaleSwitcher.tsxsrc/contexts/LocaleProvider.tsxsrc/i18n/config.tssrc/i18n/detection.test.tssrc/i18n/detection.tssrc/i18n/messages/en.tssrc/i18n/messages/zh.tssrc/i18n/navigation.tssrc/i18n/request.tssrc/i18n/routing.tssrc/layouts/AppLayout.tsxsrc/layouts/Header.tsxsrc/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
…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).
|
@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. |
|
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:
Please let me know when the fork merge is complete and I will update the PR accordingly. |
|
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
# Conflicts: # src/app/(dashboard)/team/users/page.tsx
…lect, AuthenticationTab, UserInviteModal
- 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
f912c53 to
a6f46b1
Compare
- 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
|
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! |
- 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
|
Hi team, I''ve synced the latest Fixes applied
Verification
Notes
Please re-run the checks when convenient. Let me know if anything else is needed! |
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>
Summary
docker/server.jsto substitute both$VARand$$VARplaceholders at startup, applyno-storeto.jsassets, and renderOidcTrustedDomains.jsfrom the template.safePathguard in the static server so request paths cannot escape the served web root.Networks,MinimalPeers,ServiceUsers, andActivitytables intouseMemohooks so they can consumeuseTranslations(column headers, code, role, status, address, last seen, etc.).dns.selectType/selectTTL,activity.code,policies.all/protocols,postureChecks.descriptionOptional,serviceUsers.{role,status,learnMoreAbout},networks.policies, and the fullreverseProxymodal 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).noProtectionTitle/Description), service mode labels (httpsService,tlsPassthrough,tcpService,udpService), modal descriptions, tooltip text (GROUP_TOOLTIP_TEXT→ translation keys), timestamp[at]token,RestrictedAccessheading/description,MultipleGroupsbadge,PeerNameCellaria-label interpolation,PeerVersionCelllatestUrl null-check,DataTableMultiSelectPopupdouble-count fix,AccessControlTablebidirectional||→??, andzh.tsICU plural format foractivePoliciesCount.Verification
npx tsc --noEmitnpm run buildgit diff --check.tsxfilesOut of scope
next,js-cookie, etc.) — should be handled in a separate PR.npm run lint— the project still calls the removednext lintscript; this is a repo-wide config issue, not a regression introduced here.policies.learnMore,peers.operatingSystem.linux); these are inherited from earlier work and tracked separately.Summary by CodeRabbit
Release Notes
New Features
Refactor
Deployment