diff --git a/docs/docs/sdk/web/admin/utilities.md b/docs/docs/sdk/web/admin/utilities.md
index 20f168aa7..ab2a3521c 100644
--- a/docs/docs/sdk/web/admin/utilities.md
+++ b/docs/docs/sdk/web/admin/utilities.md
@@ -128,6 +128,69 @@ const query = transformDataTableQueryToRQLRequest(tableQuery, {
---
+## `useTerminology`
+
+Hook that returns customizable entity labels based on the terminology config provided via `AdminConfigProvider`. Allows deployments to rename entities (e.g. "Organization" to "Workspace") across the UI without code changes.
+
+The terminology is configured at runtime via the `/configs` endpoint:
+
+```json
+{
+ "terminology": {
+ "organization": { "singular": "Workspace", "plural": "Workspaces" },
+ "user": { "singular": "Person", "plural": "People" }
+ }
+}
+```
+
+### Usage
+
+```tsx
+import { useTerminology } from "@raystack/frontier/admin";
+
+const MyView = () => {
+ const t = useTerminology();
+
+ return (
+
+
{t.organization({ plural: true, case: "capital" })}
+
Create a new {t.organization({ case: "lower" })}
+
+ );
+};
+```
+
+### Options
+
+Each entity function accepts an optional options object:
+
+| Option | Type | Description |
+| --- | --- | --- |
+| `plural` | `boolean` | Use plural form. Defaults to `false`. |
+| `case` | `"lower" \| "upper" \| "capital"` | Text casing. Returns the configured value as-is when omitted. |
+
+### Available entities
+
+`t.organization()`, `t.project()`, `t.team()`, `t.member()`, `t.user()`, `t.appName()`
+
+---
+
+## `useAdminPaths`
+
+Hook that returns URL-safe slugs derived from terminology config, for use in dynamic routing.
+
+```tsx
+import { useAdminPaths } from "@raystack/frontier/admin";
+
+const paths = useAdminPaths();
+// Default: { organizations: "organizations", users: "users", projects: "projects", ... }
+// With custom terminology: { organizations: "workspaces", users: "people", ... }
+
+navigate(`/${paths.organizations}/${id}`);
+```
+
+---
+
## `ConnectRPCPaginatedResponse`
Type for paginated API responses.
diff --git a/web/apps/admin/configs.dev.json b/web/apps/admin/configs.dev.json
new file mode 100644
index 000000000..d858368e7
--- /dev/null
+++ b/web/apps/admin/configs.dev.json
@@ -0,0 +1,32 @@
+{
+ "title": "Frontier Admin",
+ "app_url": "localhost:5173",
+ "token_product_id": "token",
+ "organization_types": [],
+ "webhooks": {
+ "enable_delete": false
+ },
+ "terminology": {
+ "organization": {
+ "singular": "Organization",
+ "plural": "Organizations"
+ },
+ "project": {
+ "singular": "Project",
+ "plural": "Projects"
+ },
+ "team": {
+ "singular": "Team",
+ "plural": "Teams"
+ },
+ "member": {
+ "singular": "Member",
+ "plural": "Members"
+ },
+ "user": {
+ "singular": "User",
+ "plural": "Users"
+ },
+ "appName": "Frontier Admin"
+ }
+}
diff --git a/web/apps/admin/src/components/Sidebar/index.tsx b/web/apps/admin/src/components/Sidebar/index.tsx
index f735b3c64..e40eb8dec 100644
--- a/web/apps/admin/src/components/Sidebar/index.tsx
+++ b/web/apps/admin/src/components/Sidebar/index.tsx
@@ -26,6 +26,7 @@ import CpuChipIcon from "~/assets/icons/cpu-chip.svg?react";
import { AppContext } from "~/contexts/App";
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { Link, useLocation } from "react-router-dom";
+import { useTerminology, useAdminPaths } from "@raystack/frontier/admin";
export type NavigationItemsTypes = {
to?: string;
@@ -36,90 +37,96 @@ export type NavigationItemsTypes = {
const BRAND_NAME = "Frontier";
-const navigationItems: NavigationItemsTypes[] = [
- {
- name: "Organizations",
- to: `/organizations`,
- icon: ,
- },
- {
- name: "Users",
- to: `/users`,
- icon: ,
- },
- {
- name: "Audit Logs",
- to: `/audit-logs`,
- icon: ,
- },
- {
- name: "Invoices",
- to: `/invoices`,
- icon: ,
- },
- {
- name: "Authorization",
- subItems: [
- {
- name: "Roles",
- to: `/roles`,
- icon: ,
- },
- ],
- },
- {
- name: "Billing",
- subItems: [
- {
- name: "Products",
- to: `/products`,
- icon: ,
- },
- {
- name: "Plans",
- to: `/plans`,
- icon: ,
- },
- ],
- },
- {
- name: "Features",
- subItems: [
- {
- name: "Webhooks",
- to: `/webhooks`,
- icon: ,
- },
- ],
- },
- {
- name: "Settings",
- subItems: [
- {
- name: "Preferences",
- to: `/preferences`,
- icon: ,
- },
- {
- name: "Admins",
- to: `/super-admins`,
- icon: ,
- },
- ],
- },
- // {
- // name: "Projects",
- // to: `/projects`,
- // },
+const useNavigationItems = (): NavigationItemsTypes[] => {
+ const t = useTerminology();
+ const paths = useAdminPaths();
- // {
- // name: "Groups",
- // to: `/groups`,
- // },
-];
+ return [
+ {
+ name: t.organization({ plural: true, case: "capital" }),
+ to: `/${paths.organizations}`,
+ icon: ,
+ },
+ {
+ name: t.user({ plural: true, case: "capital" }),
+ to: `/${paths.users}`,
+ icon: ,
+ },
+ {
+ name: "Audit Logs",
+ to: `/audit-logs`,
+ icon: ,
+ },
+ {
+ name: "Invoices",
+ to: `/invoices`,
+ icon: ,
+ },
+ {
+ name: "Authorization",
+ subItems: [
+ {
+ name: "Roles",
+ to: `/roles`,
+ icon: ,
+ },
+ ],
+ },
+ {
+ name: "Billing",
+ subItems: [
+ {
+ name: "Products",
+ to: `/products`,
+ icon: ,
+ },
+ {
+ name: "Plans",
+ to: `/plans`,
+ icon: ,
+ },
+ ],
+ },
+ {
+ name: "Features",
+ subItems: [
+ {
+ name: "Webhooks",
+ to: `/webhooks`,
+ icon: ,
+ },
+ ],
+ },
+ {
+ name: "Settings",
+ subItems: [
+ {
+ name: "Preferences",
+ to: `/preferences`,
+ icon: ,
+ },
+ {
+ name: "Admins",
+ to: `/super-admins`,
+ icon: ,
+ },
+ ],
+ },
+ // {
+ // name: t.project({ plural: true, case: "capital" }),
+ // to: `/projects`,
+ // },
+
+ // {
+ // name: t.team({ plural: true, case: "capital" }),
+ // to: `/groups`,
+ // },
+ ];
+};
export default function IAMSidebar() {
const location = useLocation();
+ const navigationItems = useNavigationItems();
const isActive = (navlink?: string) => {
const firstPathPart = location.pathname.split("/")[1];
diff --git a/web/apps/admin/src/contexts/App.tsx b/web/apps/admin/src/contexts/App.tsx
index 0492b1f4f..e62880d02 100644
--- a/web/apps/admin/src/contexts/App.tsx
+++ b/web/apps/admin/src/contexts/App.tsx
@@ -11,6 +11,7 @@ import {
type User,
} from "@raystack/proton/frontier";
import { Config, defaultConfig } from "~/utils/constants";
+import { AdminConfigProvider } from "@raystack/frontier/admin";
interface AppContextValue {
isAdmin: boolean;
@@ -62,7 +63,9 @@ export const AppContextProvider: React.FC = function ({
config,
user,
}}>
- {children}
+
+ {children}
+
);
};
diff --git a/web/apps/admin/src/pages/admins/AdminsPage.tsx b/web/apps/admin/src/pages/admins/AdminsPage.tsx
index 8bc110cec..c82e4d79b 100644
--- a/web/apps/admin/src/pages/admins/AdminsPage.tsx
+++ b/web/apps/admin/src/pages/admins/AdminsPage.tsx
@@ -1,12 +1,13 @@
-import { AdminsView } from "@raystack/frontier/admin";
+import { AdminsView, useAdminPaths } from "@raystack/frontier/admin";
import { useNavigate } from "react-router-dom";
export function AdminsPage() {
const navigate = useNavigate();
+ const paths = useAdminPaths();
return (
navigate(`/organizations/${orgId}`)}
+ onNavigateToOrg={(orgId: string) => navigate(`/${paths.organizations}/${orgId}`)}
/>
);
}
diff --git a/web/apps/admin/src/pages/organizations/list/index.tsx b/web/apps/admin/src/pages/organizations/list/index.tsx
index 2ccbcb8c7..6d5e0079d 100644
--- a/web/apps/admin/src/pages/organizations/list/index.tsx
+++ b/web/apps/admin/src/pages/organizations/list/index.tsx
@@ -1,5 +1,6 @@
import {
OrganizationListView,
+ useAdminPaths,
} from "@raystack/frontier/admin";
import { useCallback, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
@@ -17,6 +18,7 @@ async function loadCountries(): Promise {
export function OrganizationListPage() {
const navigate = useNavigate();
const { config } = useContext(AppContext);
+ const paths = useAdminPaths();
const [countries, setCountries] = useState([]);
useEffect(() => {
@@ -24,8 +26,8 @@ export function OrganizationListPage() {
}, []);
const onNavigateToOrg = useCallback(
- (id: string) => navigate(`/organizations/${id}`),
- [navigate],
+ (id: string) => navigate(`/${paths.organizations}/${id}`),
+ [navigate, paths.organizations],
);
const onExportCsv = useCallback(async () => {
diff --git a/web/apps/admin/src/pages/users/UsersPage.tsx b/web/apps/admin/src/pages/users/UsersPage.tsx
index 31fc92f4d..b82a684fe 100644
--- a/web/apps/admin/src/pages/users/UsersPage.tsx
+++ b/web/apps/admin/src/pages/users/UsersPage.tsx
@@ -1,4 +1,4 @@
-import { UsersView } from "@raystack/frontier/admin";
+import { UsersView, useAdminPaths } from "@raystack/frontier/admin";
import { useCallback } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom";
import { clients } from "~/connect/clients";
@@ -10,6 +10,7 @@ export function UsersPage() {
const { userId } = useParams();
const navigate = useNavigate();
const location = useLocation();
+ const paths = useAdminPaths();
const onExportUsers = useCallback(async () => {
await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv");
@@ -17,15 +18,15 @@ export function UsersPage() {
const onNavigateToUser = useCallback(
(id: string) => {
- navigate(`/users/${id}/security`);
+ navigate(`/${paths.users}/${id}/security`);
},
- [navigate],
+ [navigate, paths.users],
);
return (
navigate("/users")}
+ onCloseDetail={() => navigate(`/${paths.users}`)}
onExportUsers={onExportUsers}
onNavigateToUser={onNavigateToUser}
currentPath={location.pathname}
diff --git a/web/apps/admin/src/routes.tsx b/web/apps/admin/src/routes.tsx
index 8519c9a15..ef93a0478 100644
--- a/web/apps/admin/src/routes.tsx
+++ b/web/apps/admin/src/routes.tsx
@@ -31,6 +31,7 @@ import {
OrganizationInvoicesView,
OrganizationTokensView,
OrganizationApisView,
+ useAdminPaths,
} from "@raystack/frontier/admin";
import { UsersPage } from "./pages/users/UsersPage";
@@ -40,6 +41,7 @@ import { AuditLogsPage } from "./pages/audit-logs/AuditLogsPage";
export default memo(function AppRoutes() {
const { isAdmin, isLoading, user } = useContext(AppContext);
+ const paths = useAdminPaths();
const isUserEmpty = R.either(R.isEmpty, R.isNil)(user);
@@ -58,20 +60,20 @@ export default memo(function AppRoutes() {
) : isAdmin ? (
}>
- } />
- } />
+ } />
+ } />
}>
- } />
- } />
+ } />
+ } />
} />
- } />
+ } />
} />
} />
} />
- }>
+ }>
} />
} />
@@ -85,7 +87,7 @@ export default memo(function AppRoutes() {
}>
} />
-
+
}>
} />
@@ -102,7 +104,7 @@ export default memo(function AppRoutes() {
} />
} />
- } />
+ } />
) : (
diff --git a/web/apps/admin/src/utils/constants.ts b/web/apps/admin/src/utils/constants.ts
index c3b6d473a..ded60ed1a 100644
--- a/web/apps/admin/src/utils/constants.ts
+++ b/web/apps/admin/src/utils/constants.ts
@@ -17,6 +17,20 @@ export interface WebhooksConfig {
enable_delete: boolean;
}
+export interface EntityTerminologies {
+ singular: string;
+ plural: string;
+}
+
+export interface AdminTerminologyConfig {
+ organization?: EntityTerminologies;
+ project?: EntityTerminologies;
+ team?: EntityTerminologies;
+ member?: EntityTerminologies;
+ user?: EntityTerminologies;
+ appName?: string;
+}
+
export interface Config {
title: string;
logo?: string;
@@ -24,8 +38,18 @@ export interface Config {
token_product_id?: string;
organization_types?: string[];
webhooks?: WebhooksConfig;
+ terminology?: AdminTerminologyConfig;
}
+export const defaultTerminology: Required = {
+ organization: { singular: "Organization", plural: "Organizations" },
+ project: { singular: "Project", plural: "Projects" },
+ team: { singular: "Team", plural: "Teams" },
+ member: { singular: "Member", plural: "Members" },
+ user: { singular: "User", plural: "Users" },
+ appName: "Frontier Admin",
+};
+
export const defaultConfig: Config = {
title: "Frontier Admin",
app_url: "example.com",
@@ -34,6 +58,7 @@ export const defaultConfig: Config = {
webhooks: {
enable_delete: false,
},
+ terminology: defaultTerminology,
};
export const NULL_DATE = "0001-01-01T00:00:00Z";
diff --git a/web/apps/admin/vite.config.ts b/web/apps/admin/vite.config.ts
index a3113598e..1052d8835 100644
--- a/web/apps/admin/vite.config.ts
+++ b/web/apps/admin/vite.config.ts
@@ -1,10 +1,37 @@
import react from "@vitejs/plugin-react-swc";
import dotenv from "dotenv";
-import { defineConfig } from "vite";
+import { defineConfig, type Plugin } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import svgr from "vite-plugin-svgr";
+import fs from "node:fs";
+import path from "node:path";
dotenv.config();
+/**
+ * Vite plugin that serves a local JSON file at `/configs` during development.
+ * Edit `configs.dev.json` in the project root to change the config
+ * (including terminology overrides like organization → workspace).
+ */
+function devConfigsPlugin(): Plugin {
+ return {
+ name: "dev-configs",
+ configureServer(server) {
+ server.middlewares.use("/configs", (_req, res) => {
+ const configPath = path.resolve(__dirname, "configs.dev.json");
+ try {
+ const content = fs.readFileSync(configPath, "utf-8");
+ // Re-read on every request so changes are picked up without restart
+ JSON.parse(content); // validate JSON
+ res.setHeader("Content-Type", "application/json");
+ res.end(content);
+ } catch {
+ res.statusCode = 500;
+ res.end(JSON.stringify({ error: "Failed to read configs.dev.json" }));
+ }
+ });
+ },
+ };
+}
// https://vitejs.dev/config/
export default defineConfig(() => {
@@ -31,7 +58,7 @@ export default defineConfig(() => {
allow: [".."],
},
},
- plugins: [react(), svgr(), tsconfigPaths()],
+ plugins: [devConfigsPlugin(), react(), svgr(), tsconfigPaths()],
define: {
"process.env": process.env,
},
diff --git a/web/sdk/admin/contexts/AdminConfigContext.tsx b/web/sdk/admin/contexts/AdminConfigContext.tsx
new file mode 100644
index 000000000..830986ddb
--- /dev/null
+++ b/web/sdk/admin/contexts/AdminConfigContext.tsx
@@ -0,0 +1,39 @@
+import React, { createContext, ReactNode, useContext } from "react";
+import { merge } from "lodash";
+import { Config, defaultConfig, defaultTerminology } from "../utils/constants";
+import { TerminologyProvider } from "../../shared/terminology";
+
+const AdminConfigContext = createContext(defaultConfig);
+
+export interface AdminConfigProviderProps {
+ children: ReactNode;
+ config?: Config;
+}
+
+export const AdminConfigProvider: React.FC = ({
+ children,
+ config = {},
+}) => {
+ const mergedConfig: Config = merge({}, defaultConfig, config);
+
+ // Ensure terminology is always present with defaults
+ mergedConfig.terminology = merge({}, defaultTerminology, config.terminology);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const useAdminConfig = () => {
+ const context = useContext(AdminConfigContext);
+ return context || defaultConfig;
+};
+
+export { AdminConfigContext };
diff --git a/web/sdk/admin/hooks/useAdminPaths.ts b/web/sdk/admin/hooks/useAdminPaths.ts
new file mode 100644
index 000000000..adb07d0e8
--- /dev/null
+++ b/web/sdk/admin/hooks/useAdminPaths.ts
@@ -0,0 +1,49 @@
+import { useAdminConfig } from "../contexts/AdminConfigContext";
+import { defaultTerminology } from "../utils/constants";
+
+/**
+ * Converts a terminology plural label into a URL-safe slug.
+ * e.g. "Organizations" → "organizations", "Workspaces" → "workspaces"
+ */
+function toSlug(text: string): string {
+ return text
+ .toLowerCase()
+ .replace(/\s+/g, "-")
+ .replace(/[^a-z0-9-]/g, "");
+}
+
+export interface AdminPaths {
+ /** URL slug for the organization entity, e.g. "organizations" or "workspaces" */
+ organizations: string;
+ /** URL slug for the user entity, e.g. "users" or "people" */
+ users: string;
+ /** URL slug for the project entity, e.g. "projects" or "repos" */
+ projects: string;
+ /** URL slug for the member entity, e.g. "members" or "participants" */
+ members: string;
+ /** URL slug for the team entity, e.g. "teams" or "groups" */
+ teams: string;
+}
+
+export const useAdminPaths = (): AdminPaths => {
+ const config = useAdminConfig();
+ const terminology = config.terminology || defaultTerminology;
+
+ return {
+ organizations: toSlug(
+ terminology.organization?.plural || defaultTerminology.organization.plural
+ ),
+ users: toSlug(
+ terminology.user?.plural || defaultTerminology.user.plural
+ ),
+ projects: toSlug(
+ terminology.project?.plural || defaultTerminology.project.plural
+ ),
+ members: toSlug(
+ terminology.member?.plural || defaultTerminology.member.plural
+ ),
+ teams: toSlug(
+ terminology.team?.plural || defaultTerminology.team.plural
+ ),
+ };
+};
diff --git a/web/sdk/admin/hooks/useTerminology.ts b/web/sdk/admin/hooks/useTerminology.ts
new file mode 100644
index 000000000..4c9eb143d
--- /dev/null
+++ b/web/sdk/admin/hooks/useTerminology.ts
@@ -0,0 +1,6 @@
+export {
+ useTerminology,
+ type TerminologyOptions,
+ type TerminologyEntity,
+ type TerminologyMap,
+} from "../../shared/terminology";
diff --git a/web/sdk/admin/index.ts b/web/sdk/admin/index.ts
index 4746ee360..5d9e4cf1b 100644
--- a/web/sdk/admin/index.ts
+++ b/web/sdk/admin/index.ts
@@ -22,6 +22,17 @@ export { OrganizationInvoicesView } from "./views/organizations/details/invoices
export { OrganizationTokensView } from "./views/organizations/details/tokens";
export { OrganizationApisView } from "./views/organizations/details/apis";
+// context exports
+export {
+ AdminConfigProvider,
+ useAdminConfig,
+ type AdminConfigProviderProps,
+} from "./contexts/AdminConfigContext";
+
+// hook exports
+export { useTerminology } from "./hooks/useTerminology";
+export { useAdminPaths, type AdminPaths } from "./hooks/useAdminPaths";
+
// utils exports
export {
getConnectNextPageParam,
@@ -33,3 +44,9 @@ export {
transformDataTableQueryToRQLRequest,
type TransformOptions,
} from "./utils/transform-query";
+export {
+ type Config,
+ type AdminTerminologyConfig,
+ defaultConfig,
+ defaultTerminology,
+} from "./utils/constants";
diff --git a/web/sdk/admin/utils/constants.ts b/web/sdk/admin/utils/constants.ts
index 46c670d27..5f3fde2d6 100644
--- a/web/sdk/admin/utils/constants.ts
+++ b/web/sdk/admin/utils/constants.ts
@@ -1,3 +1,5 @@
+import { EntityTerminologies } from "../../shared/types";
+
export const SCOPES = {
ORG: "app/organization",
PROJECT: "app/project",
@@ -15,16 +17,36 @@ export const DEFAULT_ROLES = {
export const NULL_DATE = "0001-01-01T00:00:00Z";
+export interface AdminTerminologyConfig {
+ organization?: EntityTerminologies;
+ project?: EntityTerminologies;
+ team?: EntityTerminologies;
+ member?: EntityTerminologies;
+ user?: EntityTerminologies;
+ appName?: string;
+}
+
export interface Config {
title?: string;
app_url?: string;
token_product_id?: string;
organization_types?: string[];
+ terminology?: AdminTerminologyConfig;
}
+export const defaultTerminology: Required = {
+ organization: { singular: "Organization", plural: "Organizations" },
+ project: { singular: "Project", plural: "Projects" },
+ team: { singular: "Team", plural: "Teams" },
+ member: { singular: "Member", plural: "Members" },
+ user: { singular: "User", plural: "Users" },
+ appName: "Frontier Admin",
+};
+
export const defaultConfig: Config = {
title: "Frontier Admin",
app_url: "example.com",
token_product_id: "token",
organization_types: [],
+ terminology: defaultTerminology,
};
diff --git a/web/sdk/admin/views/admins/columns.tsx b/web/sdk/admin/views/admins/columns.tsx
index 400b881ac..297d8b5e5 100644
--- a/web/sdk/admin/views/admins/columns.tsx
+++ b/web/sdk/admin/views/admins/columns.tsx
@@ -1,12 +1,16 @@
import { Text, type DataTableColumnDef } from "@raystack/apsara";
import type { ServiceUser, User } from "@raystack/proton/frontier";
+import { TerminologyEntity } from "../../hooks/useTerminology";
export const getColumns: (options?: {
onNavigateToOrg?: (orgId: string) => void;
+ t?: {
+ organization: TerminologyEntity;
+ };
}) => DataTableColumnDef<
User | ServiceUser,
unknown
->[] = ({ onNavigateToOrg } = {}) => {
+>[] = ({ onNavigateToOrg, t } = {}) => {
return [
{
header: "Title",
@@ -36,7 +40,7 @@ export const getColumns: (options?: {
},
},
{
- header: "Organization",
+ header: t?.organization({ case: "capital" }) || "Organization",
accessorKey: "orgId",
cell: (info) => {
const org_id = info.getValue() as string;
diff --git a/web/sdk/admin/views/admins/index.tsx b/web/sdk/admin/views/admins/index.tsx
index c334e61db..aebe1edbc 100644
--- a/web/sdk/admin/views/admins/index.tsx
+++ b/web/sdk/admin/views/admins/index.tsx
@@ -5,6 +5,7 @@ import { useQuery } from "@connectrpc/connect-query";
import { AdminServiceQueries } from "@raystack/proton/frontier";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { PageHeader } from "../../components/PageHeader";
+import { useTerminology } from "../../hooks/useTerminology";
const pageHeader = {
title: "Super Admins",
@@ -26,6 +27,7 @@ export type AdminsViewProps = {
};
export default function AdminsView({ onNavigateToOrg }: AdminsViewProps = {}) {
+ const t = useTerminology();
const {
data: platformUsersData,
isLoading,
@@ -35,7 +37,7 @@ export default function AdminsView({ onNavigateToOrg }: AdminsViewProps = {}) {
staleTime: Infinity,
});
- const columns = getColumns({ onNavigateToOrg });
+ const columns = getColumns({ onNavigateToOrg, t });
const data = [
...(platformUsersData?.users || []),
...(platformUsersData?.serviceusers || []),
diff --git a/web/sdk/admin/views/audit-logs/columns.tsx b/web/sdk/admin/views/audit-logs/columns.tsx
index 559cc1241..600d82c51 100644
--- a/web/sdk/admin/views/audit-logs/columns.tsx
+++ b/web/sdk/admin/views/audit-logs/columns.tsx
@@ -14,13 +14,19 @@ import {
import { ACTOR_TYPES, getActionBadgeColor } from "./util";
import { ComponentPropsWithoutRef } from "react";
import ActorCell from "./actor-cell";
+import { TerminologyEntity } from "../../hooks/useTerminology";
interface getColumnsOptions {
groupCountMap: Record>;
+ t: {
+ organization: TerminologyEntity;
+ user: TerminologyEntity;
+ };
}
export const getColumns = ({
groupCountMap,
+ t,
}: getColumnsOptions): DataTableColumnDef[] => {
return [
{
@@ -43,14 +49,14 @@ export const getColumns = ({
cell: () => null,
filterType: "multiselect",
filterOptions: [
- { label: "User", value: ACTOR_TYPES.USER },
- { label: "Service User", value: ACTOR_TYPES.SERVICE_USER },
+ { label: t.user({ case: "capital" }), value: ACTOR_TYPES.USER },
+ { label: `Service ${t.user({ case: "capital" })}`, value: ACTOR_TYPES.SERVICE_USER },
{ label: "System", value: ACTOR_TYPES.SYSTEM },
],
},
{
accessorKey: "orgName",
- header: "Organization",
+ header: t.organization({ case: "capital" }),
classNames: {
cell: styles["org-column"],
header: styles["org-column"],
diff --git a/web/sdk/admin/views/audit-logs/index.tsx b/web/sdk/admin/views/audit-logs/index.tsx
index b1e2ef087..79b5fc727 100644
--- a/web/sdk/admin/views/audit-logs/index.tsx
+++ b/web/sdk/admin/views/audit-logs/index.tsx
@@ -28,6 +28,7 @@ import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import SidePanelDetails from "./sidepanel-details";
import { useQueryClient } from "@tanstack/react-query";
import { AUDIT_LOG_QUERY_KEY } from "./util";
+import { useTerminology } from "../../hooks/useTerminology";
const NoAuditLogs = () => {
return (
@@ -70,6 +71,7 @@ export type AuditLogsViewProps = {
};
export default function AuditLogsView({ appName, onExportCsv, onNavigate }: AuditLogsViewProps = {}) {
+ const t = useTerminology();
const queryClient = useQueryClient();
const [tableQuery, setTableQuery] = useDebouncedState<{
query: DataTableQuery;
@@ -153,8 +155,9 @@ export default function AuditLogsView({ appName, onExportCsv, onNavigate }: Audi
groupCountMap: infiniteData
? getGroupCountMapFromFirstPage(infiniteData)
: {},
+ t,
}),
- [infiniteData],
+ [infiniteData, t],
);
const loading = isLoading || isFetchingNextPage;
diff --git a/web/sdk/admin/views/audit-logs/sidepanel-details.tsx b/web/sdk/admin/views/audit-logs/sidepanel-details.tsx
index 261912603..2f6fe46d5 100644
--- a/web/sdk/admin/views/audit-logs/sidepanel-details.tsx
+++ b/web/sdk/admin/views/audit-logs/sidepanel-details.tsx
@@ -17,6 +17,8 @@ import ActorCell from "./actor-cell";
import SidepanelListItemLink from "./sidepanel-list-link";
import { isZeroUUID } from "../../utils/helper";
import SidepanelListId from "./sidepanel-list-id";
+import { useTerminology } from "../../hooks/useTerminology";
+import { useAdminPaths } from "../../hooks/useAdminPaths";
type SidePanelDetailsProps = Partial & {
onClose: () => void;
@@ -38,6 +40,8 @@ export default function SidePanelDetails({
onNavigate,
...rest
}: SidePanelDetailsProps) {
+ const t = useTerminology();
+ const paths = useAdminPaths();
const { actor, event, resource, occurredAt, id, orgId, orgName, target } =
rest;
const date = dayjs(timestampToDate(occurredAt));
@@ -68,7 +72,7 @@ export default function SidePanelDetails({
Overview
@@ -76,8 +80,8 @@ export default function SidePanelDetails({
{orgName || "-"}
diff --git a/web/sdk/admin/views/invoices/columns.tsx b/web/sdk/admin/views/invoices/columns.tsx
index c5f3e94c8..838520608 100644
--- a/web/sdk/admin/views/invoices/columns.tsx
+++ b/web/sdk/admin/views/invoices/columns.tsx
@@ -6,8 +6,15 @@ import {
TimeStamp,
timestampToDate,
} from "../../utils/connect-timestamp";
+import { TerminologyEntity } from "../../hooks/useTerminology";
-export const getColumns = (): DataTableColumnDef<
+interface GetColumnsOptions {
+ t: {
+ organization: TerminologyEntity;
+ };
+}
+
+export const getColumns = ({ t }: GetColumnsOptions): DataTableColumnDef<
SearchInvoicesResponse_Invoice,
unknown
>[] => {
@@ -42,7 +49,7 @@ export const getColumns = (): DataTableColumnDef<
},
{
accessorKey: "orgTitle",
- header: "Organization",
+ header: t.organization({ case: "capital" }),
cell: ({ row, getValue }) => {
return getValue() as string;
},
diff --git a/web/sdk/admin/views/invoices/index.tsx b/web/sdk/admin/views/invoices/index.tsx
index cdc5e264c..812b3700c 100644
--- a/web/sdk/admin/views/invoices/index.tsx
+++ b/web/sdk/admin/views/invoices/index.tsx
@@ -19,8 +19,10 @@ import {
} from "../../utils/connect-pagination";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { transformDataTableQueryToRQLRequest } from "../../utils/transform-query";
+import { useTerminology } from "../../hooks/useTerminology";
const NoInvoices = () => {
+ const t = useTerminology();
return (
{
subHeading: styles["empty-state-subheading"],
}}
heading="No invoices found"
- subHeading="Start billing to organizations to populate the table"
+ subHeading={`Start billing to ${t.organization({ plural: true, case: "lower" })} to populate the table`}
icon={}
/>
);
@@ -46,6 +48,7 @@ export type InvoicesViewProps = {
};
export default function InvoicesView({ appName }: InvoicesViewProps = {}) {
+ const t = useTerminology();
const [tableQuery, setTableQuery] = useState(INITIAL_QUERY);
const query = transformDataTableQueryToRQLRequest(tableQuery, {
@@ -95,7 +98,7 @@ export default function InvoicesView({ appName }: InvoicesViewProps = {}) {
}
};
- const columns = getColumns();
+ const columns = getColumns({ t });
const loading = isLoading || isFetchingNextPage;
diff --git a/web/sdk/admin/views/organizations/details/apis/columns.tsx b/web/sdk/admin/views/organizations/details/apis/columns.tsx
index 95b39e355..402ba34e8 100644
--- a/web/sdk/admin/views/organizations/details/apis/columns.tsx
+++ b/web/sdk/admin/views/organizations/details/apis/columns.tsx
@@ -3,12 +3,16 @@ import dayjs from "dayjs";
import { NULL_DATE } from "../../../../utils/constants";
import styles from "./apis.module.css";
import type {
- SearchOrganizationServiceUsersResponse_OrganizationServiceUser,
- SearchOrganizationServiceUsersResponse_Project
+ SearchOrganizationServiceUsersResponse_OrganizationServiceUser,
+ SearchOrganizationServiceUsersResponse_Project
} from "@raystack/proton/frontier";
+import { TerminologyEntity } from "../../../../hooks/useTerminology";
interface ColumnOptions {
groupCountMap: Record>;
+ t: {
+ project: TerminologyEntity;
+ };
}
export function getColumns(
@@ -17,6 +21,7 @@ export function getColumns(
SearchOrganizationServiceUsersResponse_OrganizationServiceUser,
unknown
>[] {
+ const { t } = options;
return [
{
accessorKey: "title",
@@ -33,7 +38,7 @@ SearchOrganizationServiceUsersResponse_OrganizationServiceUser,
},
{
accessorKey: "projects",
- header: "Projects",
+ header: t.project({ plural: true, case: "capital" }),
cell: ({ getValue }) => {
const value =
getValue() as SearchOrganizationServiceUsersResponse_Project[];
diff --git a/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx b/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx
index a490d2731..77ef3f58c 100644
--- a/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx
+++ b/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx
@@ -11,6 +11,7 @@ import {
} from "@raystack/proton/frontier";
import { create } from "@bufbuild/protobuf";
import { timestampToDayjs } from "../../../../utils/connect-timestamp";
+import { useTerminology } from "../../../../hooks/useTerminology";
interface ServiceUserDetailsDialogProps {
onClose: () => void;
@@ -21,6 +22,7 @@ export const ServiceUserDetailsDialog = ({
serviceUser,
onClose,
}: ServiceUserDetailsDialogProps) => {
+ const t = useTerminology();
const { id = "", orgId = "", title = "" } = serviceUser || {};
const projectsRequest = useMemo(
@@ -100,7 +102,7 @@ export const ServiceUserDetailsDialog = ({
: ""}
- Projects{" "}
+ {t.project({ plural: true, case: "capital" })}{" "}
{!isProjectLoading && projects.length > 0
? `(${projects.length})`
: ""}
diff --git a/web/sdk/admin/views/organizations/details/apis/index.tsx b/web/sdk/admin/views/organizations/details/apis/index.tsx
index f1eaa9ae9..523169e08 100644
--- a/web/sdk/admin/views/organizations/details/apis/index.tsx
+++ b/web/sdk/admin/views/organizations/details/apis/index.tsx
@@ -19,6 +19,7 @@ import {
} from "../../../../utils/connect-pagination";
import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query";
import { useDebounceValue } from "usehooks-ts";
+import { useTerminology } from "../../../../hooks/useTerminology";
const NoCredentials = () => {
return (
@@ -61,6 +62,7 @@ const TRANSFORM_OPTIONS = {
};
export function OrganizationApisView() {
+ const t = useTerminology();
const { organization, search } = useContext(OrganizationContext);
const organizationId = organization?.id || "";
const {
@@ -86,7 +88,7 @@ export function OrganizationApisView() {
null,
);
- const title = `API | ${organization?.title} | Organizations`;
+ const title = `API | ${organization?.title} | ${t.organization({ plural: true, case: "capital" })}`;
useEffect(() => {
setSearchVisibility(true);
@@ -162,8 +164,9 @@ export function OrganizationApisView() {
groupCountMap: infiniteData
? getGroupCountMapFromFirstPage(infiniteData)
: {},
+ t,
}),
- [infiniteData],
+ [infiniteData, t],
);
return (
diff --git a/web/sdk/admin/views/organizations/details/edit/organization.tsx b/web/sdk/admin/views/organizations/details/edit/organization.tsx
index fbfedf193..ed39789b4 100644
--- a/web/sdk/admin/views/organizations/details/edit/organization.tsx
+++ b/web/sdk/admin/views/organizations/details/edit/organization.tsx
@@ -21,6 +21,7 @@ import { useMutation, createConnectQueryKey, useTransport } from "@connectrpc/co
import { useQueryClient } from "@tanstack/react-query";
import { FrontierServiceQueries, UpdateOrganizationRequestSchema, type Organization, OrganizationSchema } from "@raystack/proton/frontier";
import { create, type JsonObject } from "@bufbuild/protobuf";
+import { useTerminology } from "../../../../hooks/useTerminology";
const orgUpdateSchema = z
.object({
@@ -77,6 +78,7 @@ function getDefaultValue(organization: Organization, industries: string[]) {
}
export function EditOrganizationPanel({ onClose }: { onClose: () => void }) {
+ const t = useTerminology();
const { organization, appUrl, countries: countriesFromContext = [], organizationTypes: industries = [] } = useContext(OrganizationContext);
const [countries, setCountries] = useState(countriesFromContext);
const queryClient = useQueryClient();
@@ -122,12 +124,12 @@ export function EditOrganizationPanel({ onClose }: { onClose: () => void }) {
useEffect(() => {
if (mutationError) {
if (mutationError.message?.includes("already exists")) {
- setError("name", { message: "Organization name already exists" });
+ setError("name", { message: `${t.organization({ case: "capital" })} name already exists` });
} else {
console.error("Unable to update organization:", mutationError);
}
}
- }, [mutationError, setError]);
+ }, [mutationError, setError, t]);
async function onSubmit(data: OrgUpdateSchema) {
try {
@@ -164,7 +166,7 @@ export function EditOrganizationPanel({ onClose }: { onClose: () => void }) {
className={styles["side-panel"]}
>
void }) {
style={{ width: "100%" }}
>
- Pick a logo for your organization
+ Pick a logo for your {t.organization({ case: "lower" })}
>
);
}}
/>
-
+
void }) {
return (