diff --git a/docs/public/stack-auth-cli-template.py b/docs/public/stack-auth-cli-template.py index 126d19b359..22ff7ff49a 100644 --- a/docs/public/stack-auth-cli-template.py +++ b/docs/public/stack-auth-cli-template.py @@ -3,54 +3,67 @@ import webbrowser import urllib.parse + def prompt_cli_login( - *, - base_url: str = "https://api.stack-auth.com", - app_url: str, - project_id: str, - publishable_client_key: str, + *, + base_url: str = "https://api.stack-auth.com", + app_url: str, + project_id: str, + publishable_client_key: str, ): - if not app_url: - raise Exception("app_url is required and must be set to the URL of the app you're authenticating with") - if not project_id: - raise Exception("project_id is required") - if not publishable_client_key: - raise Exception("publishable_client_key is required") + if not app_url: + raise Exception( + "app_url is required and must be set to the URL of the app you're authenticating with" + ) + if not project_id: + raise Exception("project_id is required") + if not publishable_client_key: + raise Exception("publishable_client_key is required") - def post(endpoint, json): - return requests.request( - 'POST', - f'{base_url}{endpoint}', - headers={ - 'Content-Type': 'application/json', - 'x-stack-project-id': project_id, - 'x-stack-access-type': 'client', - 'x-stack-publishable-client-key': publishable_client_key, - }, - json=json, - ) + def post(endpoint, json): + return requests.request( + "POST", + f"{base_url}{endpoint}", + headers={ + "Content-Type": "application/json", + "x-stack-project-id": project_id, + "x-stack-access-type": "client", + "x-stack-publishable-client-key": publishable_client_key, + }, + json=json, + ) - # Step 1: Initiate the CLI auth process - init = post('/api/v1/auth/cli', { - 'expires_in_millis': 10 * 60 * 1000, - }) - if init.status_code != 200: - raise Exception(f"Failed to initiate CLI auth: {init.status_code} {init.text}") - polling_code = init.json()['polling_code'] - login_code = init.json()['login_code'] + # Step 1: Initiate the CLI auth process + init = post( + "/api/v1/auth/cli", + { + "expires_in_millis": 10 * 60 * 1000, + }, + ) + if init.status_code != 200: + raise Exception(f"Failed to initiate CLI auth: {init.status_code} {init.text}") + polling_code = init.json()["polling_code"] + login_code = init.json()["login_code"] - # Step 2: Open the browser for the user to authenticate - url = f'{app_url}/handler/cli-auth-confirm?login_code={urllib.parse.quote(login_code)}' - print(f"Opening browser to authenticate. If it doesn't open automatically, please visit:\n{url}") - webbrowser.open(url) + # Step 2: Open the browser for the user to authenticate + url = f"{app_url}/handler/cli-auth-confirm?login_code={urllib.parse.quote(login_code)}" + print( + f"Opening browser to authenticate. If it doesn't open automatically, please visit:\n{url}" + ) + webbrowser.open(url) - # Step 3: Retrieve the token - while True: - status = post('/api/v1/auth/cli/poll', { - 'polling_code': polling_code, - }) - if status.status_code != 200 and status.status_code != 201: - raise Exception(f"Failed to get CLI auth status: {status.status_code} {status.text}") - if status.json()['status'] == 'success': - return status.json()['refresh_token'] - time.sleep(2) + # Step 3: Retrieve the token + while True: + status = post( + "/api/v1/auth/cli/poll", + { + "polling_code": polling_code, + }, + ) + if status.status_code != 200 and status.status_code != 201: + raise Exception( + f"Failed to get CLI auth status: {status.status_code} {status.text}" + ) + if status.json()["status"] == "success": + return status.json()["refresh_token"] + time.sleep(2) diff --git a/package.json b/package.json index 82accaa0c9..3ac4b68281 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "eslint-plugin-react": "^7.37.2", "jsdom": "^24.1.3", "only-allow": "^1.2.1", + "pluralize": "^8.0.0", "rimraf": "^5.0.10", "tsup": "^8.3.5", "turbo": "^2.2.3", diff --git a/packages/template/src/components-page/account-settings.tsx b/packages/template/src/components-page/account-settings.tsx index 02f3258f81..0c6199c5a4 100644 --- a/packages/template/src/components-page/account-settings.tsx +++ b/packages/template/src/components-page/account-settings.tsx @@ -16,6 +16,7 @@ import { ProfilePage } from "./account-settings/profile-page/profile-page"; import { SettingsPage } from './account-settings/settings/settings-page'; import { TeamCreationPage } from './account-settings/teams/team-creation-page'; import { TeamPage } from './account-settings/teams/team-page'; +import { pluralize } from "pluralize"; const Icon = ({ name }: { name: keyof typeof icons }) => { const LucideIcon = icons[name]; @@ -24,6 +25,7 @@ const Icon = ({ name }: { name: keyof typeof icons }) => { export function AccountSettings(props: { fullPage?: boolean, + entityName?: string, extraItems?: ({ title: string, content: React.ReactNode, @@ -66,6 +68,8 @@ export function AccountSettings(props: { const userFromHook = useUser({ or: props.mockUser ? 'return-null' : 'redirect' }); const stackApp = useStackApp(); const projectFromHook = stackApp.useProject(); + const entityName = props.entityName ?? "Team"; + const entityPlural = pluralize(entityName); // Use mock data if provided, otherwise use real data const user = props.mockUser ? { @@ -151,7 +155,7 @@ export function AccountSettings(props: { content: item.content, } as const)) || []), ...(teams.length > 0 || project.config.clientTeamCreationEnabled) ? [{ - title: t('Teams'), + title: t('{entityPlural}', { entityPlural: entityPlural }), type: 'divider', }] as const : [], ...teams.map(team => ({ @@ -166,7 +170,7 @@ export function AccountSettings(props: { , } as const)), ...project.config.clientTeamCreationEnabled ? [{ - title: t('Create a team'), + title: t('Create a {entity}', { entity: entityName.toLowerCase() }), icon: , type: 'item', id: 'team-creation', diff --git a/packages/template/src/components-page/team-creation.tsx b/packages/template/src/components-page/team-creation.tsx index 6e81944138..f55b9c5df5 100644 --- a/packages/template/src/components-page/team-creation.tsx +++ b/packages/template/src/components-page/team-creation.tsx @@ -12,11 +12,15 @@ import { FormWarningText } from "../components/elements/form-warning"; import { MaybeFullPage } from "../components/elements/maybe-full-page"; import { useTranslation } from "../lib/translations"; -export function TeamCreation(props: { fullPage?: boolean }) { +export function TeamCreation(props: { + fullPage?: boolean, + entityName?: string, +}) { const { t } = useTranslation(); + const entityName = props.entityName ?? "Team"; const schema = yupObject({ - displayName: yupString().defined().nonEmpty(t('Please enter a team name')), + displayName: yupString().defined().nonEmpty(t('Please enter {entity} name', { entity: entityName.toLowerCase() })) }); const { register, handleSubmit, formState: { errors } } = useForm({ @@ -29,7 +33,7 @@ export function TeamCreation(props: { fullPage?: boolean }) { const navigate = app.useNavigate(); if (!project.config.clientTeamCreationEnabled) { - return ; + return ; } const onSubmit = async (data: yup.InferType) => { @@ -48,7 +52,7 @@ export function TeamCreation(props: { fullPage?: boolean }) { - {t('Create a Team')} + {t('Create {entity}', {entity: entityName})} }) { +function TeamInvitationInner(props: { + fullPage?: boolean, + searchParams: Record, + entityName?: string, +}) { const { t } = useTranslation(); const stackApp = useStackApp(); const [success, setSuccess] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(null); const details = React.use(cachedGetInvitationDetails(stackApp, props.searchParams.code || '')); + const entityName = props.entityName ?? "Team" if (errorMessage || details.status === 'error') { return ( @@ -33,7 +38,7 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record stackApp.redirectToHome()} @@ -46,7 +51,7 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record runAsynchronouslyWithAlert(async () => { @@ -65,26 +70,33 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record }) { +export function TeamInvitation(props: { + fullPage?: boolean, + searchParams: Record + entityName?: string, +}) { const { t } = useTranslation(); const user = useUser(); const stackApp = useStackApp(); + const fullPage = props.fullPage ?? false; + const searchParams = props.searchParams; + const entityName = props.entityName ?? "Team"; const invalidJsx = ( - - {t('Please double check if you have the correct team invitation link.')} + + {t('Please double check if you have the correct {entity} invitation link.', {entity: entityName.toLowerCase()})} ); const expiredJsx = ( - - {t('Your team invitation link has expired. Please request a new team invitation link ')} + + {t('Your {entity} invitation link has expired. Please request a new {entity} invitation link ', {entity: entityName.toLowerCase()})} ); const usedJsx = ( - - {t('This team invitation link has already been used.')} + + {t('This {entity} invitation link has already been used.', {entity: entityName.toLowerCase()})} ); @@ -96,14 +108,14 @@ export function TeamInvitation({ fullPage=false, searchParams }: { fullPage?: bo if (!user) { return ( stackApp.redirectToSignIn()} secondaryButtonText={t('Cancel')} secondaryAction={() => stackApp.redirectToHome()} > - {t('Sign in or create an account to join the team.')} + {t('Sign in or create an account to join the {entity}.', {entity: entityName.toLowerCase()})} ); } @@ -123,5 +135,5 @@ export function TeamInvitation({ fullPage=false, searchParams }: { fullPage?: bo } } - return ; + return ; }; diff --git a/packages/template/src/components/selected-team-switcher.tsx b/packages/template/src/components/selected-team-switcher.tsx index 18168f79a8..3f2c3d86ec 100644 --- a/packages/template/src/components/selected-team-switcher.tsx +++ b/packages/template/src/components/selected-team-switcher.tsx @@ -19,17 +19,20 @@ import { Suspense, useEffect, useMemo } from "react"; import { Team, useStackApp, useUser } from ".."; import { useTranslation } from "../lib/translations"; import { TeamIcon } from "./team-icon"; +import { pluralize } from 'pluralize'; type MockTeam = { id: string, displayName: string, profileImageUrl?: string | null, + entityName?: string, }; type SelectedTeamSwitcherProps = { urlMap?: (team: AllowNull extends true ? Team | null : Team) => string, selectedTeam?: Team, noUpdateSelectedTeam?: boolean, + entityName?: string, allowNull?: AllowNull, nullLabel?: string, onChange?: (team: AllowNull extends true ? Team | null : Team) => void, @@ -78,6 +81,8 @@ function Inner(props: SelectedTeamSwitcherProps rawTeams?.sort((a, b) => b.id === selectedTeam?.id ? 1 : -1), [rawTeams, selectedTeam]); + const entityName = props.entityName ?? "Team"; + const pluralEntity = pluralize(entityName) useEffect(() => { if (!props.noUpdateSelectedTeam && props.selectedTeam && !props.mockUser) { @@ -94,7 +99,7 @@ function Inner(props: SelectedTeamSwitcherProps team.id === value) || null; if (!team) { - throw new StackAssertionError('Team not found, this should not happen'); + throw new StackAssertionError('{entity} not found, this should not happen', {entity: entityName}); } } else { team = null; @@ -118,14 +123,14 @@ function Inner(props: SelectedTeamSwitcherProps - + {user?.selectedTeam ? - {t('Current team')} + {t('Current {entity}', {entity: entityName.toLowerCase()})} (props: SelectedTeamSwitcherProps - {props.nullLabel || t('No team')} + {props.nullLabel || t('No {entity}', {entity: entityName.toLowerCase()})} } {teams?.length ? - {t('Other teams')} + {t('Other {pluralEntity}', {pluralEntity: pluralEntity})} {teams.filter(team => team.id !== user?.selectedTeam?.id) .map(team => ( @@ -175,7 +180,7 @@ function Inner(props: SelectedTeamSwitcherProps - {t('No teams yet')} + {t('No {pluralEntity} yet', {pluralEntity: pluralEntity})} : null} {project.config.clientTeamCreationEnabled && <> @@ -191,7 +196,7 @@ function Inner(props: SelectedTeamSwitcherProps - {t('Create a team')} + {t('Create {entity}', {entity: entityName.toLowerCase()})} >}