Skip to content

feat(admin-ui): runtime terminology customization for admin UI#1454

Open
rohilsurana wants to merge 11 commits intomainfrom
feat/admin-terminology-customization
Open

feat(admin-ui): runtime terminology customization for admin UI#1454
rohilsurana wants to merge 11 commits intomainfrom
feat/admin-terminology-customization

Conversation

@rohilsurana
Copy link
Member

@rohilsurana rohilsurana commented Mar 17, 2026

Summary

  • Adds a runtime terminology customization system for the admin UI, allowing deployments to rename entities (e.g. "Organization" → "Workspace", "User" → "Person") via the /configs endpoint
  • All user-visible labels and browser URL paths update dynamically based on config — no rebuild needed
  • Uses a single shared useTerminology hook across both the react SDK and admin SDK via a shared TerminologyProvider

What changed

Shared infrastructure (web/sdk/shared/terminology.tsx):

  • TerminologyProvider context + useTerminology hook — single source of truth for both SDKs
  • createTerminologyMap utility, TerminologyEntity types

Admin SDK (web/sdk/admin/):

  • AdminConfigProvider wraps children with TerminologyProvider
  • useAdminPaths hook — generates URL-safe slugs from terminology for dynamic routing
  • All 44 view files updated: hardcoded entity labels → useTerminology()

React SDK (web/sdk/react/):

  • CustomizationProvider wraps children with TerminologyProvider
  • useTerminology re-exported from shared (existing imports unchanged)

Admin app (web/apps/admin/):

  • Route paths, sidebar links, breadcrumbs, and navigation callbacks all use dynamic useAdminPaths
  • Vite dev server middleware serves configs.dev.json at /configs for local development
  • Edit the JSON file and refresh to test different terminology without restart

Docs:

  • docs/docs/sdk/web/admin/utilities.md updated with useTerminology and useAdminPaths usage

Usage

Configure via /configs endpoint response:

{
  "terminology": {
    "organization": { "singular": "Workspace", "plural": "Workspaces" },
    "user": { "singular": "Person", "plural": "People" }
  }
}

In views:

import { useTerminology, useAdminPaths } from "@raystack/frontier/admin";

const t = useTerminology();
const paths = useAdminPaths();

// Labels
t.organization({ plural: true, case: "capital" }) // → "Workspaces"
t.user({ case: "lower" })                         // → "person"

// URL paths
paths.organizations // → "workspaces"
paths.users         // → "people"

Test plan

  • pnpm run build in web/sdk — passes
  • pnpm run build in web/apps/admin — passes
  • Start dev server, visit http://localhost:5173/configs — returns JSON from configs.dev.json
  • Edit configs.dev.json to use "Workspace" terminology, refresh — sidebar, page titles, breadcrumbs, table headers, URLs all show "Workspaces"
  • Verify default config (no terminology override) still shows "Organizations", "Users", etc.
  • Verify react SDK useTerminology still works unchanged

@vercel
Copy link

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Mar 17, 2026 1:09pm

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a dev JSON config and a Vite dev plugin; introduces AdminConfigProvider, terminology and path hooks (useAdminTerminology, useAdminPaths) in the SDK; wires these into the admin app, replacing many hard-coded labels and route segments with dynamic terminology and path slugs.

Changes

Cohort / File(s) Summary
Dev config
web/apps/admin/configs.dev.json
New development JSON config with title, app_url, token_product_id, webhooks, and terminology mappings.
Vite dev tooling
web/apps/admin/vite.config.ts
Adds devConfigsPlugin to serve and validate configs.dev.json at /configs during local dev.
App bootstrap & routing
web/apps/admin/src/contexts/App.tsx, web/apps/admin/src/routes.tsx
Wraps app with AdminConfigProvider and replaces hard-coded route literals with values from useAdminPaths().
Navigation / Sidebar
web/apps/admin/src/components/Sidebar/index.tsx
Replaces static nav items with useNavigationItems() that uses useAdminTerminology() and useAdminPaths() to build labels and paths.
App pages (path updates)
web/apps/admin/src/pages/...
web/apps/admin/src/pages/admins/AdminsPage.tsx, .../organizations/list/index.tsx, .../users/UsersPage.tsx
Pages now consume useAdminPaths() and use dynamic path prefixes instead of hard-coded route strings; dependency arrays adjusted.
SDK config & defaults
web/sdk/admin/utils/constants.ts, web/apps/admin/src/utils/constants.ts
Adds EntityTerminologies, AdminTerminologyConfig, defaultTerminology, and exposes terminology on Config/defaultConfig.
SDK admin config context
web/sdk/admin/contexts/AdminConfigContext.tsx, web/sdk/admin/index.ts
New AdminConfigContext, AdminConfigProvider, useAdminConfig and related re-exports added to the SDK public surface.
SDK hooks: terminology & paths
web/sdk/admin/hooks/useAdminTerminology.ts, web/sdk/admin/hooks/useAdminPaths.ts
New hooks: useAdminTerminology() (callable entities with plural/case options) and useAdminPaths() (slug generation for admin route prefixes) with types exported.
SDK: views — terminology & path adoption
web/sdk/admin/views/... (admins, audit-logs, invoices, organizations/, users/, tokens, projects, members, etc.)
Widespread replacement of hard-coded labels and hrefs with terminology/path hooks; many column builders and getColumns signatures updated to accept a t parameter for localized headers.
App callsites updated
web/apps/admin/... (pages/components consuming SDK views)
Updated to provide AdminConfigProvider and to use/forward useAdminPaths()/useAdminTerminology() where required by SDK view changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rsbh
  • rohanchkrabrty
  • paanSinghCoder
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

@rohilsurana rohilsurana changed the title feat(web): runtime terminology customization for admin UI feat(admin-ui): runtime terminology customization for admin UI Mar 17, 2026
@rohilsurana rohilsurana requested a review from rsbh March 17, 2026 08:14
Add a context-based terminology system that allows deployments to
customize entity labels (e.g. Organization → Workspace) and URL
paths across the entire admin UI via the /configs endpoint.

- Add AdminConfigContext, useAdminTerminology hook, and useAdminPaths hook
- Replace all hardcoded entity labels in 44 admin view files
- Make route paths dynamic based on terminology config
- Add Vite dev server middleware to serve configs.dev.json locally
@rohilsurana rohilsurana force-pushed the feat/admin-terminology-customization branch from 7fe0837 to 7ad2980 Compare March 17, 2026 08:15
@rohilsurana rohilsurana marked this pull request as ready for review March 17, 2026 08:15
@coveralls
Copy link

coveralls commented Mar 17, 2026

Pull Request Test Coverage Report for Build 23194563756

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • 150 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.09%) to 40.775%

Files with Coverage Reduction New Missed Lines %
core/userpat/service.go 22 84.67%
internal/api/v1beta1connect/user_pat.go 31 70.66%
pkg/server/connect_interceptors/authorization.go 97 0.0%
Totals Coverage Status
Change from base Build 23184527020: 0.09%
Covered Lines: 14321
Relevant Lines: 35122

💛 - Coveralls

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (3)
web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx (1)

53-77: ⚠️ Potential issue | 🟡 Minor

Include terminology in addMember dependencies to avoid stale toast text.

addMember captures terminology, but its dependency list does not include it. After runtime /configs changes, toast copy can remain stale until remount.

Suggested fix
 export function useAddProjectMembers({ projectId }: useAddProjectMembersProps) {
   const t = useAdminTerminology();
+  const memberLabel = t.member({ case: "capital" });
   const { orgMembersMap } = useContext(OrganizationContext);
@@
-        toast.success(`${t.member({ case: "capital" })} added`);
+        toast.success(`${memberLabel} added`);
@@
-    [projectId, createPolicy, refetch, projectMembers],
+    [projectId, createPolicy, refetch, projectMembers, memberLabel],
   );
web/sdk/admin/views/organizations/details/layout/index.tsx (1)

35-70: ⚠️ Potential issue | 🟡 Minor

Found-state page title still bypasses runtime terminology.

Line 69 hardcodes "Organizations", so this page title won’t reflect customized terminology (e.g., “Workspaces”).

Suggested fix
-  const title = `${organization?.title} | Organizations`;
+  const title = `${organization?.title} | ${t.organization({ plural: true, case: "capital" })}`;
web/sdk/admin/views/users/list/invite-users.tsx (1)

206-224: ⚠️ Potential issue | 🟠 Major

Organization field placeholder is still hardcoded.

The label is terminology-aware, but the placeholder still says "Select an Organization", so this dialog is only partially customizable.

Suggested fix
-                            <Select.Value placeholder="Select an Organization" />
+                            <Select.Value
+                              placeholder={`Select ${t.organization({ case: "capital" })}`}
+                            />
🧹 Nitpick comments (4)
web/sdk/admin/views/organizations/details/layout/invite-users-dialog.tsx (1)

69-69: Use plural terminology when multiple invitations succeed

Line 69 always uses singular user terminology, but this flow accepts multiple emails. Consider pluralizing based on response count for better UX consistency.

Proposed refactor
-      onSuccess: () => {
+      onSuccess: (data) => {
         queryClient.invalidateQueries({
           queryKey: createConnectQueryKey({
             schema: FrontierServiceQueries.listOrganizationInvitations,
             transport,
             input: { orgId: organizationId },
             cardinality: "finite",
           }),
         });
-        toast.success(`${t.user({ case: "capital" })} invited`);
+        const inviteCount = data?.invitations?.length ?? 0;
+        toast.success(
+          `${t.user({ case: "capital", plural: inviteCount > 1 })} invited`,
+        );
         onOpenChange(false);
       },
web/apps/admin/src/utils/constants.ts (1)

44-51: Avoid duplicating terminology defaults across app and sdk constants.

This defaultTerminology can drift from web/sdk/admin/utils/constants.ts, leading to inconsistent behavior. Prefer a shared source for terminology defaults/types.

Also applies to: 61-61

web/sdk/admin/views/organizations/details/apis/columns.tsx (1)

9-9: Use a type-only import for TerminologyEntity.

TerminologyEntity is used only as a type annotation (in the ColumnOptions interface), so import type is more appropriate and makes the intent explicit.

Suggested patch
-import { TerminologyEntity } from "../../../../hooks/useAdminTerminology";
+import type { TerminologyEntity } from "../../../../hooks/useAdminTerminology";
web/sdk/admin/hooks/useAdminTerminology.ts (1)

22-23: The "capital" case lowercases subsequent characters, which may not be intended for multi-word terms.

If a deployment customizes terminology with multi-word values like "Service Account", applying case: "capital" will produce "Service account" instead of preserving "Service Account". Consider whether this is the desired behavior.

♻️ Alternative: preserve original casing after first character
     case "capital":
-      return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
+      return text.charAt(0).toUpperCase() + text.slice(1);

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a551206-c73a-4949-9209-8c6b23aae0f5

📥 Commits

Reviewing files that changed from the base of the PR and between d530d58 and 7ad2980.

📒 Files selected for processing (48)
  • web/apps/admin/configs.dev.json
  • web/apps/admin/src/components/Sidebar/index.tsx
  • web/apps/admin/src/contexts/App.tsx
  • web/apps/admin/src/pages/admins/AdminsPage.tsx
  • web/apps/admin/src/pages/organizations/list/index.tsx
  • web/apps/admin/src/pages/users/UsersPage.tsx
  • web/apps/admin/src/routes.tsx
  • web/apps/admin/src/utils/constants.ts
  • web/apps/admin/vite.config.ts
  • web/sdk/admin/contexts/AdminConfigContext.tsx
  • web/sdk/admin/hooks/useAdminPaths.ts
  • web/sdk/admin/hooks/useAdminTerminology.ts
  • web/sdk/admin/index.ts
  • web/sdk/admin/utils/constants.ts
  • web/sdk/admin/views/admins/columns.tsx
  • web/sdk/admin/views/admins/index.tsx
  • web/sdk/admin/views/audit-logs/columns.tsx
  • web/sdk/admin/views/audit-logs/index.tsx
  • web/sdk/admin/views/audit-logs/sidepanel-details.tsx
  • web/sdk/admin/views/invoices/columns.tsx
  • web/sdk/admin/views/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/apis/columns.tsx
  • web/sdk/admin/views/organizations/details/apis/details-dialog.tsx
  • web/sdk/admin/views/organizations/details/apis/index.tsx
  • web/sdk/admin/views/organizations/details/edit/organization.tsx
  • web/sdk/admin/views/organizations/details/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/layout/index.tsx
  • web/sdk/admin/views/organizations/details/layout/invite-users-dialog.tsx
  • web/sdk/admin/views/organizations/details/layout/navbar.tsx
  • web/sdk/admin/views/organizations/details/members/remove-member.tsx
  • web/sdk/admin/views/organizations/details/projects/columns.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
  • web/sdk/admin/views/organizations/details/projects/members/remove-member.tsx
  • web/sdk/admin/views/organizations/details/projects/rename-project.tsx
  • web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx
  • web/sdk/admin/views/organizations/details/security/block-organization.tsx
  • web/sdk/admin/views/organizations/details/side-panel/index.tsx
  • web/sdk/admin/views/organizations/details/tokens/columns.tsx
  • web/sdk/admin/views/organizations/details/tokens/index.tsx
  • web/sdk/admin/views/organizations/list/create.tsx
  • web/sdk/admin/views/organizations/list/index.tsx
  • web/sdk/admin/views/organizations/list/navbar.tsx
  • web/sdk/admin/views/users/details/layout/navbar.tsx
  • web/sdk/admin/views/users/details/layout/suspend-user.tsx
  • web/sdk/admin/views/users/details/security/block-user.tsx
  • web/sdk/admin/views/users/details/user-details.tsx
  • web/sdk/admin/views/users/list/invite-users.tsx
  • web/sdk/admin/views/users/list/list.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
web/sdk/admin/hooks/useAdminTerminology.ts (1)

44-69: ⚠️ Potential issue | 🟠 Major

Memoization is still tied to unstable object identity.

Line 69 uses [terminology] as the dependency. In web/sdk/admin/contexts/AdminConfigContext.tsx (Line 12-20), mergedConfig.terminology is reconstructed, so this hook can still return a fresh t object on provider re-renders even when terminology values are unchanged.

🔧 Proposed fix (depend on primitive terminology values)
 export const useAdminTerminology = () => {
   const config = useAdminConfig();
   const terminology = config.terminology || defaultTerminology;
+  const organizationSingular =
+    terminology.organization?.singular ?? defaultTerminology.organization.singular;
+  const organizationPlural =
+    terminology.organization?.plural ?? defaultTerminology.organization.plural;
+  const projectSingular =
+    terminology.project?.singular ?? defaultTerminology.project.singular;
+  const projectPlural =
+    terminology.project?.plural ?? defaultTerminology.project.plural;
+  const teamSingular =
+    terminology.team?.singular ?? defaultTerminology.team.singular;
+  const teamPlural =
+    terminology.team?.plural ?? defaultTerminology.team.plural;
+  const memberSingular =
+    terminology.member?.singular ?? defaultTerminology.member.singular;
+  const memberPlural =
+    terminology.member?.plural ?? defaultTerminology.member.plural;
+  const userSingular =
+    terminology.user?.singular ?? defaultTerminology.user.singular;
+  const userPlural =
+    terminology.user?.plural ?? defaultTerminology.user.plural;
+  const appName = terminology.appName ?? defaultTerminology.appName;
 
   return useMemo(() => ({
-    organization: createEntity(
-      terminology.organization?.singular || defaultTerminology.organization.singular,
-      terminology.organization?.plural || defaultTerminology.organization.plural
-    ),
-    project: createEntity(
-      terminology.project?.singular || defaultTerminology.project.singular,
-      terminology.project?.plural || defaultTerminology.project.plural
-    ),
-    team: createEntity(
-      terminology.team?.singular || defaultTerminology.team.singular,
-      terminology.team?.plural || defaultTerminology.team.plural
-    ),
-    member: createEntity(
-      terminology.member?.singular || defaultTerminology.member.singular,
-      terminology.member?.plural || defaultTerminology.member.plural
-    ),
-    user: createEntity(
-      terminology.user?.singular || defaultTerminology.user.singular,
-      terminology.user?.plural || defaultTerminology.user.plural
-    ),
-    appName: createEntity(
-      terminology.appName || defaultTerminology.appName,
-      terminology.appName || defaultTerminology.appName
-    ),
-  }),[terminology]);
+    organization: createEntity(organizationSingular, organizationPlural),
+    project: createEntity(projectSingular, projectPlural),
+    team: createEntity(teamSingular, teamPlural),
+    member: createEntity(memberSingular, memberPlural),
+    user: createEntity(userSingular, userPlural),
+    appName: createEntity(appName, appName),
+  }), [
+    organizationSingular,
+    organizationPlural,
+    projectSingular,
+    projectPlural,
+    teamSingular,
+    teamPlural,
+    memberSingular,
+    memberPlural,
+    userSingular,
+    userPlural,
+    appName,
+  ]);
 };

Verification script (read-only) to confirm current dependency behavior and provider reconstruction pattern:

#!/bin/bash
set -euo pipefail

echo "== Checking AdminConfigProvider terminology reconstruction =="
fd 'AdminConfigContext.tsx$' -t f | while read -r f; do
  echo "-- $f"
  rg -n -C2 'merge\(\{\}, defaultTerminology, config\.terminology\)|AdminConfigContext\.Provider value' "$f"
done

echo
echo "== Checking useAdminTerminology memo dependencies =="
fd 'useAdminTerminology.ts$' -t f | while read -r f; do
  echo "-- $f"
  rg -n -C2 'return useMemo\(|\]\s*,\s*\[terminology\]|\),\s*\[terminology\]' "$f"
done

Expected result: provider rebuilds terminology object and hook memo depends on that object identity.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03f37194-74da-486d-bc9c-2f5fe67608f9

📥 Commits

Reviewing files that changed from the base of the PR and between 42dcda4 and 189aa2f.

📒 Files selected for processing (1)
  • web/sdk/admin/hooks/useAdminTerminology.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web/sdk/admin/views/organizations/details/layout/index.tsx (1)

69-69: Consider moving title computation inside the conditional branch.

The title variable is computed unconditionally but only used when organization is truthy (line 77). While not a bug (the value is never rendered when undefined), moving this inside the branch improves clarity and avoids evaluating organization?.title unnecessarily.

♻️ Optional: Move title computation

Move the title computation inside the organization ? branch, or compute it inline at line 77:

-  const title = `${organization?.title} | ${t.organization({ case: "capital", plural: true })}`;
-
   return isLoading ? (
     <Flex align="center" justify="center" style={{ minHeight: "200px", width: "100%" }}>
       <Spinner size={6} />
     </Flex>
   ) : organization ? (
     <Flex direction="column" className={styles.page}>
-      <PageTitle title={title} />
+      <PageTitle title={`${organization.title} | ${t.organization({ case: "capital", plural: true })}`} />

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f0743a3-ad29-4f99-bf9e-e7a910bbbb3d

📥 Commits

Reviewing files that changed from the base of the PR and between d049992 and 48b2e1b.

📒 Files selected for processing (3)
  • web/sdk/admin/views/organizations/details/layout/index.tsx
  • web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx
  • web/sdk/admin/views/users/list/invite-users.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx

…SDK and admin

Move applyCase, createEntity, types, and createTerminologyMap into
shared/terminology.ts. Both useTerminology (react SDK) and
useAdminTerminology (admin SDK) now delegate to the same core logic.
Existing imports are preserved via re-exports.
Replace useAdminTerminology with a single shared useTerminology hook.
Add TerminologyProvider to shared/terminology.tsx — both
CustomizationProvider (react SDK) and AdminConfigProvider (admin)
wrap their children with it, so useTerminology works everywhere.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants