🧑‍💻 Migrate to Tolgee (#976)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

### Summary by CodeRabbit

- Refactor: Transitioned to a new translation library (`@tolgee/react`)
across the application, enhancing the localization capabilities and
consistency.
- New Feature: Introduced a JSON configuration file for application
settings, improving customization and flexibility.
- Refactor: Updated SVG attribute naming convention in the
`WhatsAppLogo` component to align with React standards.
- Chore: Adjusted the `.gitignore` file and added a new line at the end.
- Documentation: Added instructions for setting up environment variables
for the Tolgee i18n contribution dev tool, improving the self-hosting
configuration guide.
- Style: Updated the `CollaborationMenuButton` to hide the
`PopoverContent` component by scaling it down to zero.
- Refactor: Simplified error handling logic for fetching and updating
typebots in `TypebotProvider.tsx`, improving code readability and
maintenance.
- Refactor: Removed the dependency on the `parseGroupTitle` function,
simplifying the code in several components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-10-27 09:23:50 +02:00
committed by GitHub
parent 31b3fc311e
commit bed8b42a2e
101 changed files with 2141 additions and 2210 deletions

View File

@@ -25,12 +25,12 @@ import { useApiTokens } from '../hooks/useApiTokens'
import { ApiTokenFromServer } from '../types'
import { parseTimeSince } from '@/helpers/parseTimeSince'
import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery'
import { useScopedI18n } from '@/locales'
import { T, useTranslate } from '@tolgee/react'
type Props = { user: User }
export const ApiTokensList = ({ user }: Props) => {
const scopedT = useScopedI18n('account.apiTokens')
const { t } = useTranslate()
const { showToast } = useToast()
const { apiTokens, isLoading, mutate } = useApiTokens({
userId: user.id,
@@ -57,10 +57,12 @@ export const ApiTokensList = ({ user }: Props) => {
return (
<Stack spacing={4}>
<Heading fontSize="2xl">{scopedT('heading')}</Heading>
<Text>{scopedT('description')}</Text>
<Heading fontSize="2xl">{t('account.apiTokens.heading')}</Heading>
<Text>{t('account.apiTokens.description')}</Text>
<Flex justifyContent="flex-end">
<Button onClick={onCreateOpen}>{scopedT('createButton.label')}</Button>
<Button onClick={onCreateOpen}>
{t('account.apiTokens.createButton.label')}
</Button>
<CreateTokenModal
userId={user.id}
isOpen={isCreateOpen}
@@ -73,8 +75,8 @@ export const ApiTokensList = ({ user }: Props) => {
<Table>
<Thead>
<Tr>
<Th>{scopedT('table.nameHeader')}</Th>
<Th w="130px">{scopedT('table.createdHeader')}</Th>
<Th>{t('account.apiTokens.table.nameHeader')}</Th>
<Th w="130px">{t('account.apiTokens.table.createdHeader')}</Th>
<Th w="0" />
</Tr>
</Thead>
@@ -90,7 +92,7 @@ export const ApiTokensList = ({ user }: Props) => {
variant="outline"
onClick={() => setDeletingId(token.id)}
>
{scopedT('deleteButton.label')}
{t('account.apiTokens.deleteButton.label')}
</Button>
</Td>
</Tr>
@@ -118,14 +120,17 @@ export const ApiTokensList = ({ user }: Props) => {
onClose={() => setDeletingId(undefined)}
message={
<Text>
{scopedT('deleteConfirmationMessage', {
tokenName: (
<strong>{apiTokens?.find(byId(deletingId))?.name}</strong>
),
})}
<T
keyName="account.apiTokens.deleteConfirmationMessage"
params={{
strong: (
<strong>{apiTokens?.find(byId(deletingId))?.name}</strong>
),
}}
/>
</Text>
}
confirmButtonLabel={scopedT('deleteButton.label')}
confirmButtonLabel={t('account.apiTokens.deleteButton.label')}
/>
</Stack>
)

View File

@@ -10,7 +10,7 @@ import Image from 'next/image'
import lightModeIllustration from 'public/images/light-mode.png'
import darkModeIllustration from 'public/images/dark-mode.png'
import systemModeIllustration from 'public/images/system-mode.png'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
defaultValue: string
@@ -18,22 +18,22 @@ type Props = {
}
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
const scopedT = useScopedI18n('account.preferences.appearance')
const { t } = useTranslate()
const appearanceData = [
{
value: 'light',
label: scopedT('lightLabel'),
label: t('account.preferences.appearance.lightLabel'),
image: lightModeIllustration,
},
{
value: 'dark',
label: scopedT('darkLabel'),
label: t('account.preferences.appearance.darkLabel'),
image: darkModeIllustration,
},
{
value: 'system',
label: scopedT('systemLabel'),
label: t('account.preferences.appearance.systemLabel'),
image: systemModeIllustration,
},
]

View File

@@ -1,5 +1,5 @@
import { CopyButton } from '@/components/CopyButton'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import {
Modal,
ModalOverlay,
@@ -33,7 +33,7 @@ export const CreateTokenModal = ({
onNewToken,
}: Props) => {
const inputRef = useRef<HTMLInputElement>(null)
const scopedT = useScopedI18n('account.apiTokens.createModal')
const { t } = useTranslate()
const [name, setName] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [newTokenValue, setNewTokenValue] = useState<string>()
@@ -54,14 +54,18 @@ export const CreateTokenModal = ({
<ModalOverlay />
<ModalContent>
<ModalHeader>
{newTokenValue ? scopedT('createdHeading') : scopedT('createHeading')}
{newTokenValue
? t('account.apiTokens.createModal.createdHeading')
: t('account.apiTokens.createModal.createHeading')}
</ModalHeader>
<ModalCloseButton />
{newTokenValue ? (
<ModalBody as={Stack} spacing="4">
<Text>
{scopedT('copyInstruction')}{' '}
<strong>{scopedT('securityWarning')}</strong>
{t('account.apiTokens.createModal.copyInstruction')}{' '}
<strong>
{t('account.apiTokens.createModal.securityWarning')}
</strong>
</Text>
<InputGroup size="md">
<Input readOnly pr="4.5rem" value={newTokenValue} />
@@ -72,10 +76,14 @@ export const CreateTokenModal = ({
</ModalBody>
) : (
<ModalBody as="form" onSubmit={createToken}>
<Text mb="4">{scopedT('nameInput.label')}</Text>
<Text mb="4">
{t('account.apiTokens.createModal.nameInput.label')}
</Text>
<Input
ref={inputRef}
placeholder={scopedT('nameInput.placeholder')}
placeholder={t(
'account.apiTokens.createModal.nameInput.placeholder'
)}
onChange={(e) => setName(e.target.value)}
/>
</ModalBody>
@@ -84,7 +92,7 @@ export const CreateTokenModal = ({
<ModalFooter>
{newTokenValue ? (
<Button onClick={onClose} colorScheme="blue">
{scopedT('doneButton.label')}
{t('account.apiTokens.createModal.doneButton.label')}
</Button>
) : (
<Button
@@ -93,7 +101,7 @@ export const CreateTokenModal = ({
isLoading={isSubmitting}
onClick={createToken}
>
{scopedT('createButton.label')}
{t('account.apiTokens.createModal.createButton.label')}
</Button>
)}
</ModalFooter>

View File

@@ -1,5 +1,5 @@
import { MouseIcon, LaptopIcon } from '@/components/icons'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import {
HStack,
Radio,
@@ -18,18 +18,20 @@ export const GraphNavigationRadioGroup = ({
defaultValue,
onChange,
}: Props) => {
const scopedT = useScopedI18n('account.preferences.graphNavigation')
const { t } = useTranslate()
const graphNavigationData = [
{
value: GraphNavigation.MOUSE,
label: scopedT('mouse.label'),
description: scopedT('mouse.description'),
label: t('account.preferences.graphNavigation.mouse.label'),
description: t('account.preferences.graphNavigation.mouse.description'),
icon: <MouseIcon boxSize="35px" />,
},
{
value: GraphNavigation.TRACKPAD,
label: scopedT('trackpad.label'),
description: scopedT('trackpad.description'),
label: t('account.preferences.graphNavigation.trackpad.label'),
description: t(
'account.preferences.graphNavigation.trackpad.description'
),
icon: <LaptopIcon boxSize="35px" />,
},
]

View File

@@ -5,10 +5,10 @@ import { ApiTokensList } from './ApiTokensList'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { useUser } from '../hooks/useUser'
import { TextInput } from '@/components/inputs/TextInput'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const MyAccountForm = () => {
const scopedT = useScopedI18n('account.myAccount')
const { t } = useTranslate()
const { user, updateUser } = useUser()
const [name, setName] = useState(user?.name ?? '')
const [email, setEmail] = useState(user?.email ?? '')
@@ -47,11 +47,11 @@ export const MyAccountForm = () => {
leftIcon={<UploadIcon />}
onFileUploaded={handleFileUploaded}
>
{scopedT('changePhotoButton.label')}
{t('account.myAccount.changePhotoButton.label')}
</UploadButton>
)}
<Text color="gray.500" fontSize="sm">
{scopedT('changePhotoButton.specification')}
{t('account.myAccount.changePhotoButton.specification')}
</Text>
</Stack>
</HStack>
@@ -59,17 +59,17 @@ export const MyAccountForm = () => {
<TextInput
defaultValue={name}
onChange={handleNameChange}
label={scopedT('nameInput.label')}
label={t('account.myAccount.nameInput.label')}
withVariableButton={false}
debounceTimeout={0}
/>
<Tooltip label={scopedT('emailInput.disabledTooltip')}>
<Tooltip label={t('account.myAccount.emailInput.disabledTooltip')}>
<span>
<TextInput
type="email"
defaultValue={email}
onChange={handleEmailChange}
label={scopedT('emailInput.label')}
label={t('account.myAccount.emailInput.label')}
withVariableButton={false}
debounceTimeout={0}
isDisabled

View File

@@ -14,9 +14,10 @@ import React, { useEffect } from 'react'
import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
import { AppearanceRadioGroup } from './AppearanceRadioGroup'
import { useUser } from '../hooks/useUser'
import { useChangeLocale, useCurrentLocale, useScopedI18n } from '@/locales'
import { ChevronDownIcon } from '@/components/icons'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { useTranslate, useTolgee } from '@tolgee/react'
import { useRouter } from 'next/router'
const localeHumanReadable = {
en: 'English',
@@ -27,11 +28,11 @@ const localeHumanReadable = {
} as const
export const UserPreferencesForm = () => {
const scopedT = useScopedI18n('account.preferences')
const { getLanguage } = useTolgee()
const router = useRouter()
const { t } = useTranslate()
const { colorMode } = useColorMode()
const { user, updateUser } = useUser()
const changeLocale = useChangeLocale()
const currentLocale = useCurrentLocale()
useEffect(() => {
if (!user?.graphNavigation)
@@ -47,17 +48,23 @@ export const UserPreferencesForm = () => {
}
const updateLocale = (locale: keyof typeof localeHumanReadable) => () => {
changeLocale(locale)
document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000`
router.replace(router.pathname, undefined, { locale })
}
const currentLanguage = getLanguage()
return (
<Stack spacing={12}>
<HStack spacing={4}>
<Heading size="md">{scopedT('language.heading')}</Heading>
<Heading size="md">{t('account.preferences.language.heading')}</Heading>
<Menu>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
{localeHumanReadable[currentLocale]}
{currentLanguage
? localeHumanReadable[
currentLanguage as keyof typeof localeHumanReadable
]
: 'Loading...'}
</MenuButton>
<MenuList>
{Object.keys(localeHumanReadable).map((locale) => (
@@ -76,19 +83,25 @@ export const UserPreferencesForm = () => {
))}
</MenuList>
</Menu>
{currentLocale !== 'en' && (
<MoreInfoTooltip>{scopedT('language.tooltip')}</MoreInfoTooltip>
{currentLanguage !== 'en' && (
<MoreInfoTooltip>
{t('account.preferences.language.tooltip')}
</MoreInfoTooltip>
)}
</HStack>
<Stack spacing={6}>
<Heading size="md">{scopedT('graphNavigation.heading')}</Heading>
<Heading size="md">
{t('account.preferences.graphNavigation.heading')}
</Heading>
<GraphNavigationRadioGroup
defaultValue={user?.graphNavigation ?? GraphNavigation.TRACKPAD}
onChange={changeGraphNavigation}
/>
</Stack>
<Stack spacing={6}>
<Heading size="md">{scopedT('appearance.heading')}</Heading>
<Heading size="md">
{t('account.preferences.appearance.heading')}
</Heading>
<AppearanceRadioGroup
defaultValue={
user?.preferredAppAppearance

View File

@@ -12,12 +12,12 @@ import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { Graph } from '@/features/graph/components/Graph'
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib'
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
const t = useI18n()
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot()
const { data } = trpc.analytics.getTotalAnswersInBlocks.useQuery(

View File

@@ -1,4 +1,4 @@
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import {
GridProps,
SimpleGrid,
@@ -22,13 +22,13 @@ export const StatsCards = ({
stats,
...props
}: { stats?: Stats } & GridProps) => {
const scopedT = useScopedI18n('analytics')
const { t } = useTranslate()
const bg = useColorModeValue('white', 'gray.900')
return (
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
<StatLabel>{scopedT('viewsLabel')}</StatLabel>
<StatLabel>{t('analytics.viewsLabel')}</StatLabel>
{stats ? (
<StatNumber>{stats.totalViews}</StatNumber>
) : (
@@ -36,7 +36,7 @@ export const StatsCards = ({
)}
</Stat>
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
<StatLabel>{scopedT('startsLabel')}</StatLabel>
<StatLabel>{t('analytics.startsLabel')}</StatLabel>
{stats ? (
<StatNumber>{stats.totalStarts}</StatNumber>
) : (
@@ -44,10 +44,10 @@ export const StatsCards = ({
)}
</Stat>
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
<StatLabel>{scopedT('completionRateLabel')}</StatLabel>
<StatLabel>{t('analytics.completionRateLabel')}</StatLabel>
{stats ? (
<StatNumber>
{computeCompletionRate(scopedT('notAvailableLabel'))(
{computeCompletionRate(t('analytics.notAvailableLabel'))(
stats.totalCompleted,
stats.totalStarts
)}

View File

@@ -13,13 +13,13 @@ import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useUser } from '@/features/account/hooks/useUser'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { env } from '@typebot.io/env'
const totalSteps = 5
export const OnboardingPage = () => {
const t = useI18n()
const { t } = useTranslate()
const { push, replace } = useRouter()
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
const confettiCanon = useRef<confetti.CreateTypes>()

View File

@@ -1,4 +1,4 @@
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Alert } from '@chakra-ui/react'
type Props = {
@@ -6,16 +6,16 @@ type Props = {
}
export const SignInError = ({ error }: Props) => {
const scopedT = useScopedI18n('auth.error')
const { t } = useTranslate()
const errors: Record<string, string> = {
Signin: scopedT('default'),
OAuthSignin: scopedT('default'),
OAuthCallback: scopedT('default'),
OAuthCreateAccount: scopedT('email'),
EmailCreateAccount: scopedT('default'),
Callback: scopedT('default'),
OAuthAccountNotLinked: scopedT('oauthNotLinked'),
default: scopedT('unknown'),
Signin: t('auth.error.default'),
OAuthSignin: t('auth.error.default'),
OAuthCallback: t('auth.error.default'),
OAuthCreateAccount: t('auth.error.email'),
EmailCreateAccount: t('auth.error.default'),
Callback: t('auth.error.default'),
OAuthAccountNotLinked: t('auth.error.oauthNotLinked'),
default: t('auth.error.unknown'),
}
return (
<Alert status="error" variant="solid" rounded="md">

View File

@@ -27,7 +27,7 @@ import { BuiltInProviderType } from 'next-auth/providers'
import { useToast } from '@/hooks/useToast'
import { TextLink } from '@/components/TextLink'
import { SignInError } from './SignInError'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
defaultEmail?: string
@@ -35,7 +35,7 @@ type Props = {
export const SignInForm = ({
defaultEmail,
}: Props & HTMLChakraProps<'form'>) => {
const scopedT = useScopedI18n('auth')
const { t } = useTranslate()
const router = useRouter()
const { status } = useSession()
const [authLoading, setAuthLoading] = useState(false)
@@ -79,8 +79,8 @@ export const SignInForm = ({
})
if (response?.error) {
showToast({
title: scopedT('signinErrorToast.title'),
description: scopedT('signinErrorToast.description'),
title: t('auth.signinErrorToast.title'),
description: t('auth.signinErrorToast.description'),
})
} else {
setIsMagicLinkSent(true)
@@ -88,7 +88,7 @@ export const SignInForm = ({
} catch {
showToast({
status: 'info',
description: scopedT('signinErrorToast.tooManyRequests'),
description: t('auth.signinErrorToast.tooManyRequests'),
})
}
setAuthLoading(false)
@@ -98,12 +98,12 @@ export const SignInForm = ({
if (hasNoAuthProvider)
return (
<Text>
{scopedT('noProvider.preLink')}{' '}
{t('auth.noProvider.preLink')}{' '}
<TextLink
href="https://docs.typebot.io/self-hosting/configuration"
isExternal
>
{scopedT('noProvider.link')}
{t('auth.noProvider.link')}
</TextLink>
</Text>
)
@@ -114,9 +114,7 @@ export const SignInForm = ({
<SocialLoginButtons providers={providers} />
{providers?.email && (
<>
<DividerWithText mt="6">
{scopedT('orEmailLabel')}
</DividerWithText>
<DividerWithText mt="6">{t('auth.orEmailLabel')}</DividerWithText>
<HStack as="form" onSubmit={handleEmailSubmit}>
<Input
name="email"
@@ -134,7 +132,7 @@ export const SignInForm = ({
}
isDisabled={isMagicLinkSent}
>
{scopedT('emailSubmitButton.label')}
{t('auth.emailSubmitButton.label')}
</Button>
</HStack>
</>
@@ -150,8 +148,8 @@ export const SignInForm = ({
<HStack>
<AlertIcon />
<Stack spacing={1}>
<Text fontWeight="semibold">{scopedT('magicLink.title')}</Text>
<Text fontSize="sm">{scopedT('magicLink.description')}</Text>
<Text fontWeight="semibold">{t('auth.magicLink.title')}</Text>
<Text fontSize="sm">{t('auth.magicLink.description')}</Text>
</Stack>
</HStack>
</Alert>

View File

@@ -1,6 +1,6 @@
import { Seo } from '@/components/Seo'
import { TextLink } from '@/components/TextLink'
import { useScopedI18n } from '@/locales'
import { T, useTranslate } from '@tolgee/react'
import { VStack, Heading, Text } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { SignInForm } from './SignInForm'
@@ -11,7 +11,7 @@ type Props = {
}
export const SignInPage = ({ type }: Props) => {
const scopedT = useScopedI18n('auth')
const { t } = useTranslate()
const { query } = useRouter()
return (
@@ -19,8 +19,8 @@ export const SignInPage = ({ type }: Props) => {
<Seo
title={
type === 'signin'
? scopedT('signin.heading')
: scopedT('register.heading')
? t('auth.signin.heading')
: t('auth.register.heading')
}
/>
<Heading
@@ -29,39 +29,36 @@ export const SignInPage = ({ type }: Props) => {
}}
>
{type === 'signin'
? scopedT('signin.heading')
: scopedT('register.heading')}
? t('auth.signin.heading')
: t('auth.register.heading')}
</Heading>
{type === 'signin' ? (
<Text>
{scopedT('signin.noAccountLabel.preLink')}{' '}
{t('auth.signin.noAccountLabel.preLink')}{' '}
<TextLink href="/register">
{scopedT('signin.noAccountLabel.link')}
{t('auth.signin.noAccountLabel.link')}
</TextLink>
</Text>
) : (
<Text>
{scopedT('register.alreadyHaveAccountLabel.preLink')}{' '}
{t('auth.register.alreadyHaveAccountLabel.preLink')}{' '}
<TextLink href="/signin">
{scopedT('register.alreadyHaveAccountLabel.link')}
{t('auth.register.alreadyHaveAccountLabel.link')}
</TextLink>
</Text>
)}
<SignInForm defaultEmail={query.g?.toString()} />
{type === 'signup' ? (
<Text fontSize="sm" maxW="400px" textAlign="center">
{scopedT('register.aggreeToTerms', {
termsOfService: (
<TextLink href={'https://typebot.io/terms-of-service'}>
{scopedT('register.termsOfService')}
</TextLink>
),
privacyPolicy: (
<TextLink href={'https://typebot.io/privacy-policies'}>
{scopedT('register.privacyPolicy')}
</TextLink>
),
})}
<T
keyName="auth.register.aggreeToTerms"
params={{
terms: <TextLink href={'https://typebot.io/terms-of-service'} />,
privacy: (
<TextLink href={'https://typebot.io/privacy-policies'} />
),
}}
/>
</Text>
) : null}
</VStack>

View File

@@ -15,7 +15,7 @@ import { omit } from '@typebot.io/lib'
import { AzureAdLogo } from '@/components/logos/AzureAdLogo'
import { FacebookLogo } from '@/components/logos/FacebookLogo'
import { GitlabLogo } from '@/components/logos/GitlabLogo'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
providers:
@@ -24,7 +24,7 @@ type Props = {
}
export const SocialLoginButtons = ({ providers }: Props) => {
const scopedT = useScopedI18n('auth.socialLogin')
const { t } = useTranslate()
const { query } = useRouter()
const { status } = useSession()
const [authLoading, setAuthLoading] =
@@ -65,7 +65,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('githubButton.label')}
{t('auth.socialLogin.githubButton.label')}
</Button>
)}
{providers?.google && (
@@ -79,7 +79,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('googleButton.label')}
{t('auth.socialLogin.googleButton.label')}
</Button>
)}
{providers?.facebook && (
@@ -93,7 +93,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('facebookButton.label')}
{t('auth.socialLogin.facebookButton.label')}
</Button>
)}
{providers?.gitlab && (
@@ -107,7 +107,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('gitlabButton.label', {
{t('auth.socialLogin.gitlabButton.label', {
gitlabProviderName: providers.gitlab.name,
})}
</Button>
@@ -123,7 +123,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('azureButton.label', {
{t('auth.socialLogin.azureButton.label', {
azureProviderName: providers['azure-ad'].name,
})}
</Button>
@@ -137,7 +137,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
}
variant="outline"
>
{scopedT('customButton.label', {
{t('auth.socialLogin.customButton.label', {
customProviderName: providers['custom-oauth'].name,
})}
</Button>

View File

@@ -1,6 +1,6 @@
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Button, ButtonProps, Link } from '@chakra-ui/react'
type Props = {
@@ -8,7 +8,7 @@ type Props = {
} & Pick<ButtonProps, 'colorScheme'>
export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => {
const scopedT = useScopedI18n('billing')
const { t } = useTranslate()
const { showToast } = useToast()
const { data } = trpc.billing.getBillingPortalUrl.useQuery(
{
@@ -29,7 +29,7 @@ export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => {
isLoading={!data}
colorScheme={colorScheme}
>
{scopedT('billingPortalButton.label')}
{t('billing.billingPortalButton.label')}
</Button>
)
}

View File

@@ -10,7 +10,7 @@ import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvi
import { useUser } from '@/features/account/hooks/useUser'
import { StarterPlanPricingCard } from './StarterPlanPricingCard'
import { ProPlanPricingCard } from './ProPlanPricingCard'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { StripeClimateLogo } from './StripeClimateLogo'
import { guessIfUserIsEuropean } from '@typebot.io/lib/billing/guessIfUserIsEuropean'
@@ -20,7 +20,7 @@ type Props = {
}
export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
const scopedT = useScopedI18n('billing')
const { t } = useTranslate()
const { user } = useUser()
const { showToast } = useToast()
@@ -49,7 +49,7 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
trpcContext.workspace.getWorkspace.invalidate()
showToast({
status: 'success',
description: scopedT('updateSuccessToast.description', {
description: t('billing.updateSuccessToast.description', {
plan: workspace?.plan,
}),
})
@@ -87,9 +87,9 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
<HStack maxW="500px">
<StripeClimateLogo />
<Text fontSize="xs" color="gray.500">
{scopedT('contribution.preLink')}{' '}
{t('billing.contribution.preLink')}{' '}
<TextLink href="https://climate.stripe.com/5VCRAq" isExternal>
{scopedT('contribution.link')}
{t('billing.contribution.link')}
</TextLink>
</Text>
</HStack>
@@ -128,9 +128,9 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
)}
<Text color="gray.500">
{scopedT('customLimit.preLink')}{' '}
{t('billing.customLimit.preLink')}{' '}
<TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal>
{scopedT('customLimit.link')}
{t('billing.customLimit.link')}
</TextLink>
</Text>
</Stack>

View File

@@ -1,6 +1,6 @@
import { AlertInfo } from '@/components/AlertInfo'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import {
Modal,
ModalBody,
@@ -26,7 +26,7 @@ export const ChangePlanModal = ({
type,
excludedPlans,
}: ChangePlanModalProps) => {
const t = useI18n()
const { t } = useTranslate()
const { workspace } = useWorkspace()
return (
<Modal

View File

@@ -16,6 +16,7 @@ import {
Thead,
Tr,
} from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { proChatTiers } from '@typebot.io/lib/billing/constants'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
@@ -25,12 +26,14 @@ type Props = {
}
export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => {
const { t } = useTranslate()
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Heading size="lg">Chats pricing table</Heading>
<Heading size="lg">{t('billing.tiersModal.heading')}</Heading>
</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6">

View File

@@ -12,14 +12,14 @@ import { PlanTag } from './PlanTag'
import { BillingPortalButton } from './BillingPortalButton'
import { trpc } from '@/lib/trpc'
import { Workspace } from '@typebot.io/schemas'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
workspace: Pick<Workspace, 'id' | 'plan' | 'stripeId'>
}
export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
const scopedT = useScopedI18n('billing.currentSubscription')
const { t } = useTranslate()
const { data } = trpc.billing.getSubscription.useQuery({
workspaceId: workspace.id,
@@ -31,13 +31,15 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
return (
<Stack spacing="4">
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
<Heading fontSize="3xl">
{t('billing.currentSubscription.heading')}
</Heading>
<HStack data-testid="current-subscription">
<Text>{scopedT('subheading')} </Text>
<Text>{t('billing.currentSubscription.subheading')} </Text>
<PlanTag plan={workspace.plan} />
{data?.subscription?.cancelDate && (
<Text fontSize="sm">
({scopedT('cancelDate')}{' '}
({t('billing.currentSubscription.cancelDate')}{' '}
{data.subscription.cancelDate.toDateString()})
</Text>
)}
@@ -45,7 +47,7 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
{data?.subscription?.status === 'past_due' && (
<Alert fontSize="sm" status="error">
<AlertIcon />
{scopedT('pastDueAlert')}
{t('billing.currentSubscription.pastDueAlert')}
</Alert>
)}

View File

@@ -18,14 +18,14 @@ import Link from 'next/link'
import React from 'react'
import { trpc } from '@/lib/trpc'
import { useToast } from '@/hooks/useToast'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
workspaceId: string
}
export const InvoicesList = ({ workspaceId }: Props) => {
const scopedT = useScopedI18n('billing.invoices')
const { t } = useTranslate()
const { showToast } = useToast()
const { data, status } = trpc.billing.listInvoices.useQuery(
{
@@ -40,9 +40,9 @@ export const InvoicesList = ({ workspaceId }: Props) => {
return (
<Stack spacing={6}>
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
<Heading fontSize="3xl">{t('billing.invoices.heading')}</Heading>
{data?.invoices.length === 0 && status !== 'loading' ? (
<Text>{scopedT('empty')}</Text>
<Text>{t('billing.invoices.empty')}</Text>
) : (
<TableContainer>
<Table>
@@ -50,8 +50,8 @@ export const InvoicesList = ({ workspaceId }: Props) => {
<Tr>
<Th w="0" />
<Th>#</Th>
<Th>{scopedT('paidAt')}</Th>
<Th>{scopedT('subtotal')}</Th>
<Th>{t('billing.invoices.paidAt')}</Th>
<Th>{t('billing.invoices.subtotal')}</Th>
<Th w="0" />
</Tr>
</Thead>

View File

@@ -18,7 +18,7 @@ import { useRouter } from 'next/router'
import React, { FormEvent, useState } from 'react'
import { isDefined } from '@typebot.io/lib'
import { taxIdTypes } from '../taxIdTypes'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export type PreCheckoutModalProps = {
selectedSubscription:
@@ -47,7 +47,7 @@ export const PreCheckoutModal = ({
existingEmail,
onClose,
}: PreCheckoutModalProps) => {
const scopedT = useScopedI18n('billing.preCheckoutModal')
const { t } = useTranslate()
const { ref } = useParentModal()
const vatValueInputRef = React.useRef<HTMLInputElement>(null)
const router = useRouter()
@@ -131,7 +131,7 @@ export const PreCheckoutModal = ({
<Stack as="form" spacing="4" onSubmit={goToCheckout}>
<TextInput
isRequired
label={scopedT('companyInput.label')}
label={t('billing.preCheckoutModal.companyInput.label')}
defaultValue={customer.company}
onChange={updateCustomerCompany}
withVariableButton={false}
@@ -140,17 +140,17 @@ export const PreCheckoutModal = ({
<TextInput
isRequired
type="email"
label={scopedT('emailInput.label')}
label={t('billing.preCheckoutModal.emailInput.label')}
defaultValue={customer.email}
onChange={updateCustomerEmail}
withVariableButton={false}
debounceTimeout={0}
/>
<FormControl>
<FormLabel>{scopedT('taxId.label')}</FormLabel>
<FormLabel>{t('billing.preCheckoutModal.taxId.label')}</FormLabel>
<HStack>
<Select
placeholder={scopedT('taxId.placeholder')}
placeholder={t('billing.preCheckoutModal.taxId.placeholder')}
items={vatCodeLabels}
isPopoverMatchingInputWidth={false}
onSelect={updateVatType}
@@ -171,7 +171,7 @@ export const PreCheckoutModal = ({
colorScheme="blue"
isDisabled={customer.company === '' || customer.email === ''}
>
{scopedT('submitButton.label')}
{t('billing.preCheckoutModal.submitButton.label')}
</Button>
</Stack>
</ModalBody>

View File

@@ -14,10 +14,10 @@ import {
import { Plan } from '@typebot.io/prisma'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { useI18n, useScopedI18n } from '@/locales'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
import { ChatsProTiersModal } from './ChatsProTiersModal'
import { prices } from '@typebot.io/lib/billing/constants'
import { T, useTranslate } from '@tolgee/react'
type Props = {
currentPlan: Plan
@@ -32,12 +32,12 @@ export const ProPlanPricingCard = ({
isLoading,
onPayClick,
}: Props) => {
const t = useI18n()
const scopedT = useScopedI18n('billing.pricingCard')
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslate()
const getButtonLabel = () => {
if (currentPlan === Plan.PRO) return scopedT('upgradeButton.current')
if (currentPlan === Plan.PRO)
return t('billing.pricingCard.upgradeButton.current')
return t('upgrade')
}
@@ -65,38 +65,41 @@ export const ProPlanPricingCard = ({
fontWeight="semibold"
style={{ marginTop: 0 }}
>
{scopedT('pro.mostPopularLabel')}
{t('billing.pricingCard.pro.mostPopularLabel')}
</Tag>
</Flex>
<Stack justifyContent="space-between" h="full">
<Stack spacing="4" mt={2}>
<Heading fontSize="2xl">
{scopedT('heading', {
plan: (
<chakra.span
color={useColorModeValue('blue.400', 'blue.300')}
>
Pro
</chakra.span>
),
})}
<T
keyName="billing.pricingCard.heading"
params={{
strong: (
<chakra.span
color={useColorModeValue('blue.400', 'blue.300')}
/>
),
}}
/>
</Heading>
<Text>{scopedT('pro.description')}</Text>
<Text>{t('billing.pricingCard.pro.description')}</Text>
</Stack>
<Stack spacing="8">
<Stack spacing="4">
<Heading>
{formatPrice(prices.PRO, { currency })}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
<chakra.span fontSize="md">
{t('billing.pricingCard.perMonth')}
</chakra.span>
</Heading>
<Text fontWeight="bold">
<Tooltip
label={
<FeaturesList
features={[
scopedT('starter.brandingRemoved'),
scopedT('starter.fileUploadBlock'),
scopedT('starter.createFolders'),
t('billing.pricingCard.starter.brandingRemoved'),
t('billing.pricingCard.starter.fileUploadBlock'),
t('billing.pricingCard.starter.createFolders'),
]}
spacing="0"
/>
@@ -105,19 +108,21 @@ export const ProPlanPricingCard = ({
placement="top"
>
<chakra.span textDecoration="underline" cursor="pointer">
{scopedT('pro.everythingFromStarter')}
{t('billing.pricingCard.pro.everythingFromStarter')}
</chakra.span>
</Tooltip>
{scopedT('plus')}
{t('billing.pricingCard.plus')}
</Text>
<FeaturesList
features={[
scopedT('pro.includedSeats'),
t('billing.pricingCard.pro.includedSeats'),
<Stack key="starter-chats" spacing={1}>
<HStack key="test">
<Text>10,000 {scopedT('chatsPerMonth')}</Text>
<Text>
10,000 {t('billing.pricingCard.chatsPerMonth')}
</Text>
<MoreInfoTooltip>
{scopedT('chatsTooltip')}
{t('billing.pricingCard.chatsTooltip')}
</MoreInfoTooltip>
</HStack>
<Text
@@ -130,9 +135,9 @@ export const ProPlanPricingCard = ({
</Button>
</Text>
</Stack>,
scopedT('pro.whatsAppIntegration'),
scopedT('pro.customDomains'),
scopedT('pro.analytics'),
t('billing.pricingCard.pro.whatsAppIntegration'),
t('billing.pricingCard.pro.customDomains'),
t('billing.pricingCard.pro.analytics'),
]}
/>
</Stack>

View File

@@ -10,9 +10,9 @@ import {
import { Plan } from '@typebot.io/prisma'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { useI18n, useScopedI18n } from '@/locales'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
import { prices } from '@typebot.io/lib/billing/constants'
import { T, useTranslate } from '@tolgee/react'
type Props = {
currentPlan: Plan
@@ -27,12 +27,12 @@ export const StarterPlanPricingCard = ({
currency,
onPayClick,
}: Props) => {
const t = useI18n()
const scopedT = useScopedI18n('billing.pricingCard')
const { t } = useTranslate()
const getButtonLabel = () => {
if (currentPlan === Plan.PRO) return t('downgrade')
if (currentPlan === Plan.STARTER) return scopedT('upgradeButton.current')
if (currentPlan === Plan.STARTER)
return t('billing.pricingCard.upgradeButton.current')
return t('upgrade')
}
@@ -51,25 +51,30 @@ export const StarterPlanPricingCard = ({
<Stack>
<Stack spacing="4">
<Heading fontSize="2xl">
{scopedT('heading', {
plan: <chakra.span color="orange.400">Starter</chakra.span>,
})}
<T
keyName="billing.pricingCard.heading"
params={{ strong: <chakra.span color="orange.400" /> }}
/>
</Heading>
<Text>{scopedT('starter.description')}</Text>
<Text>{t('billing.pricingCard.starter.description')}</Text>
</Stack>
<Heading>
{formatPrice(prices.STARTER, { currency })}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
<chakra.span fontSize="md">
{t('billing.pricingCard.perMonth')}
</chakra.span>
</Heading>
</Stack>
<FeaturesList
features={[
scopedT('starter.includedSeats'),
t('billing.pricingCard.starter.includedSeats'),
<Stack key="starter-chats" spacing={0}>
<HStack>
<Text>2,000 {scopedT('chatsPerMonth')}</Text>
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
<Text>2,000 {t('billing.pricingCard.chatsPerMonth')}</Text>
<MoreInfoTooltip>
{t('billing.pricingCard.chatsTooltip')}
</MoreInfoTooltip>
</HStack>
<Text
fontSize="sm"
@@ -78,9 +83,9 @@ export const StarterPlanPricingCard = ({
Extra chats: $10 per 500
</Text>
</Stack>,
scopedT('starter.brandingRemoved'),
scopedT('starter.fileUploadBlock'),
scopedT('starter.createFolders'),
t('billing.pricingCard.starter.brandingRemoved'),
t('billing.pricingCard.starter.fileUploadBlock'),
t('billing.pricingCard.starter.createFolders'),
]}
/>
</Stack>

View File

@@ -3,7 +3,7 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import React from 'react'
import { isNotDefined } from '@typebot.io/lib'
import { ChangePlanModal } from './ChangePlanModal'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
limitReachedType?: string
@@ -15,7 +15,7 @@ export const UpgradeButton = ({
excludedPlans,
...props
}: Props) => {
const t = useI18n()
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace()
return (

View File

@@ -13,15 +13,15 @@ import { Workspace } from '@typebot.io/prisma'
import React from 'react'
import { parseNumberWithCommas } from '@typebot.io/lib'
import { defaultQueryOptions, trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { getChatsLimit } from '@typebot.io/lib/billing/getChatsLimit'
import { useTranslate } from '@tolgee/react'
type Props = {
workspace: Workspace
}
export const UsageProgressBars = ({ workspace }: Props) => {
const scopedT = useScopedI18n('billing.usage')
const { t } = useTranslate()
const { data, isLoading } = trpc.billing.getUsage.useQuery(
{
workspaceId: workspace.id,
@@ -39,12 +39,12 @@ export const UsageProgressBars = ({ workspace }: Props) => {
return (
<Stack spacing={6}>
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
<Heading fontSize="3xl">{t('billing.usage.heading')}</Heading>
<Stack spacing={3}>
<Flex justifyContent="space-between">
<HStack>
<Heading fontSize="xl" as="h3">
{scopedT('chats.heading')}
{t('billing.usage.chats.heading')}
</Heading>
{chatsPercentage >= 80 && (
<Tooltip
@@ -53,10 +53,10 @@ export const UsageProgressBars = ({ workspace }: Props) => {
p="3"
label={
<Text>
{scopedT('chats.alert.soonReach')}
{t('billing.usage.chats.alert.soonReach')}
<br />
<br />
{scopedT('chats.alert.updatePlan')}
{t('billing.usage.chats.alert.updatePlan')}
</Text>
}
>
@@ -81,7 +81,7 @@ export const UsageProgressBars = ({ workspace }: Props) => {
<Text>
/{' '}
{workspaceChatsLimit === 'inf'
? scopedT('unlimited')
? t('billing.usage.unlimited')
: parseNumberWithCommas(workspaceChatsLimit)}
</Text>
</HStack>

View File

@@ -4,7 +4,7 @@ import { TextInput } from '@/components/inputs'
import { useState } from 'react'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
type Props = {
@@ -18,7 +18,7 @@ export const AudioBubbleForm = ({
content,
onContentChange,
}: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.audio.settings')
const { t } = useTranslate()
const [currentTab, setCurrentTab] = useState<'link' | 'upload'>('link')
const updateUrl = (url: string) => onContentChange({ ...content, url })
@@ -34,14 +34,14 @@ export const AudioBubbleForm = ({
onClick={() => setCurrentTab('upload')}
size="sm"
>
{scopedT('upload.label')}
{t('editor.blocks.bubbles.audio.settings.upload.label')}
</Button>
<Button
variant={currentTab === 'link' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('link')}
size="sm"
>
{scopedT('embedLink.label')}
{t('editor.blocks.bubbles.audio.settings.embedLink.label')}
</Button>
</HStack>
<Stack p="2" spacing={4}>
@@ -54,25 +54,27 @@ export const AudioBubbleForm = ({
onFileUploaded={updateUrl}
colorScheme="blue"
>
{scopedT('chooseFile.label')}
{t('editor.blocks.bubbles.audio.settings.chooseFile.label')}
</UploadButton>
</Flex>
)}
{currentTab === 'link' && (
<>
<TextInput
placeholder={scopedT('worksWith.placeholder')}
placeholder={t(
'editor.blocks.bubbles.audio.settings.worksWith.placeholder'
)}
defaultValue={content.url ?? ''}
onChange={updateUrl}
/>
<Text fontSize="sm" color="gray.400" textAlign="center">
{scopedT('worksWith.text')}
{t('editor.blocks.bubbles.audio.settings.worksWith.text')}
</Text>
</>
)}
</Stack>
<SwitchWithLabel
label={scopedT('autoplay.label')}
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
initialValue={content.isAutoplayEnabled ?? true}
onCheckChange={updateAutoPlay}
/>

View File

@@ -1,17 +1,19 @@
import { Text } from '@chakra-ui/react'
import { AudioBubbleContent } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
url: AudioBubbleContent['url']
}
export const AudioBubbleNode = ({ url }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.audio.node')
const { t } = useTranslate()
return isDefined(url) ? (
<audio src={url} controls />
) : (
<Text color={'gray.500'}>{scopedT('clickToEdit.text')}</Text>
<Text color={'gray.500'}>
{t('editor.blocks.bubbles.audio.node.clickToEdit.text')}
</Text>
)
}

View File

@@ -1,4 +1,4 @@
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Text } from '@chakra-ui/react'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
@@ -7,8 +7,12 @@ type Props = {
}
export const EmbedBubbleContent = ({ block }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.embed.node')
const { t } = useTranslate()
if (!block.content?.url)
return <Text color="gray.500">{scopedT('clickToEdit.text')}</Text>
return <Text>{scopedT('show.text')}</Text>
return (
<Text color="gray.500">
{t('editor.blocks.bubbles.embed.node.clickToEdit.text')}
</Text>
)
return <Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text>
}

View File

@@ -2,7 +2,7 @@ import { TextInput, NumberInput } from '@/components/inputs'
import { Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleContent } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
content: EmbedBubbleContent
@@ -10,7 +10,7 @@ type Props = {
}
export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.embed.settings')
const { t } = useTranslate()
const handleUrlChange = (url: string) => {
const iframeUrl = sanitizeUrl(
url.trim().startsWith('<iframe') ? extractUrlFromIframe(url) : url
@@ -25,12 +25,14 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
<Stack p="2" spacing={6}>
<Stack>
<TextInput
placeholder={scopedT('worksWith.placeholder')}
placeholder={t(
'editor.blocks.bubbles.embed.settings.worksWith.placeholder'
)}
defaultValue={content?.url ?? ''}
onChange={handleUrlChange}
/>
<Text fontSize="sm" color="gray.400" textAlign="center">
{scopedT('worksWith.text')}
{t('editor.blocks.bubbles.embed.settings.worksWith.text')}
</Text>
</Stack>
@@ -38,7 +40,7 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
label="Height:"
defaultValue={content?.height}
onValueChange={handleHeightChange}
suffix={scopedT('numberInput.unit')}
suffix={t('editor.blocks.bubbles.embed.settings.numberInput.unit')}
width="150px"
/>
</Stack>

View File

@@ -1,4 +1,4 @@
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Box, Text, Image } from '@chakra-ui/react'
import { ImageBubbleBlock } from '@typebot.io/schemas'
@@ -7,11 +7,13 @@ type Props = {
}
export const ImageBubbleContent = ({ block }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.image.node')
const { t } = useTranslate()
const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}')
return !block.content?.url ? (
<Text color={'gray.500'}>{scopedT('clickToEdit.text')}</Text>
<Text color={'gray.500'}>
{t('editor.blocks.bubbles.image.node.clickToEdit.text')}
</Text>
) : (
<Box w="full">
<Image

View File

@@ -2,7 +2,7 @@ import { ImageUploadContent } from '@/components/ImageUploadContent'
import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Stack } from '@chakra-ui/react'
import { isDefined, isNotEmpty } from '@typebot.io/lib'
import { ImageBubbleBlock } from '@typebot.io/schemas'
@@ -19,9 +19,7 @@ export const ImageBubbleSettings = ({
block,
onContentChange,
}: Props) => {
const scopedT = useScopedI18n(
'editor.blocks.bubbles.image.switchWithLabel.onClick'
)
const { t } = useTranslate()
const [showClickLinkInput, setShowClickLinkInput] = useState(
isNotEmpty(block.content.clickLink?.url)
)
@@ -60,7 +58,7 @@ export const ImageBubbleSettings = ({
/>
<Stack>
<SwitchWithLabel
label={scopedT('label')}
label={t('editor.blocks.bubbles.image.switchWithLabel.onClick.label')}
initialValue={showClickLinkInput}
onCheckChange={toggleClickLink}
/>
@@ -73,7 +71,9 @@ export const ImageBubbleSettings = ({
defaultValue={block.content.clickLink?.url}
/>
<TextInput
placeholder={scopedT('placeholder')}
placeholder={t(
'editor.blocks.bubbles.image.switchWithLabel.onClick.placeholder'
)}
onChange={updateClickLinkAltText}
defaultValue={block.content.clickLink?.alt}
/>

View File

@@ -18,7 +18,7 @@ import { colors } from '@/lib/theme'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { selectEditor, TElement } from '@udecode/plate-common'
import { TextEditorToolBar } from './TextEditorToolBar'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type TextBubbleEditorContentProps = {
id: string
@@ -31,7 +31,7 @@ const TextBubbleEditorContent = ({
textEditorValue,
onClose,
}: TextBubbleEditorContentProps) => {
const scopedT = useScopedI18n('editor.blocks.bubbles')
const { t } = useTranslate()
const editor = usePlateEditorRef()
const varDropdownRef = useRef<HTMLDivElement | null>(null)
const rememberedSelection = useRef<BaseSelection | null>(null)
@@ -137,7 +137,7 @@ const TextBubbleEditorContent = ({
})
setIsFirstFocus(false)
},
'aria-label': `${scopedT('textEditor.plate.label')}`,
'aria-label': `${t('editor.blocks.bubbles.textEditor.plate.label')}`,
onBlur: () => {
rememberedSelection.current = editor?.selection
},
@@ -156,7 +156,9 @@ const TextBubbleEditorContent = ({
<VariableSearchInput
initialVariableId={undefined}
onSelectVariable={handleVariableSelected}
placeholder={scopedT('textEditor.searchVariable.placeholder')}
placeholder={t(
'editor.blocks.bubbles.textEditor.searchVariable.placeholder'
)}
autoFocus
/>
</PopoverContent>

View File

@@ -1,4 +1,4 @@
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Box, Text, Image } from '@chakra-ui/react'
import { VideoBubbleBlock, VideoBubbleContentType } from '@typebot.io/schemas'
@@ -7,9 +7,13 @@ type Props = {
}
export const VideoBubbleContent = ({ block }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.video.node')
const { t } = useTranslate()
if (!block.content?.url || !block.content.type)
return <Text color="gray.500">{scopedT('clickToEdit.text')}</Text>
return (
<Text color="gray.500">
{t('editor.blocks.bubbles.video.node.clickToEdit.text')}
</Text>
)
const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}')
switch (block.content.type) {

View File

@@ -5,7 +5,7 @@ import {
VideoBubbleContentType,
} from '@typebot.io/schemas'
import { NumberInput, TextInput } from '@/components/inputs'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
type Props = {
@@ -14,7 +14,7 @@ type Props = {
}
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
const scopedT = useScopedI18n('editor.blocks.bubbles.video.settings')
const { t } = useTranslate()
const updateUrl = (url: string) => {
const info = parseVideoUrl(url)
return onSubmit({
@@ -33,12 +33,14 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
<Stack p="2" spacing={4}>
<Stack>
<TextInput
placeholder={scopedT('worksWith.placeholder')}
placeholder={t(
'editor.blocks.bubbles.video.settings.worksWith.placeholder'
)}
defaultValue={content?.url ?? ''}
onChange={updateUrl}
/>
<Text fontSize="sm" color="gray.400" textAlign="center">
{scopedT('worksWith.text')}
{t('editor.blocks.bubbles.video.settings.worksWith.text')}
</Text>
</Stack>
@@ -47,7 +49,7 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
label="Height:"
defaultValue={content?.height ?? 400}
onValueChange={updateHeight}
suffix={scopedT('numberInput.unit')}
suffix={t('editor.blocks.bubbles.video.settings.numberInput.unit')}
width="150px"
/>
)}

View File

@@ -3,7 +3,7 @@ import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Group, IntegrationBlockType, Typebot } from '@typebot.io/schemas'
import { byId, isWebhookBlock, parseGroupTitle } from '@typebot.io/lib'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod'
import { Webhook } from '@typebot.io/prisma'
@@ -70,7 +70,7 @@ export const listWebhookBlocks = authenticatedProcedure
...blocks.map((block) => ({
id: block.id,
type: block.type,
label: `${parseGroupTitle(group.title)} > ${block.id}`,
label: `${group.title} > ${block.id}`,
url: block.options.webhook
? block.options.webhook.url
: typebot?.webhooks.find(byId(block.webhookId))?.url ?? undefined,

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { byId, isDefined, parseGroupTitle } from '@typebot.io/lib'
import { byId, isDefined } from '@typebot.io/lib'
import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump'
type Props = {
@@ -15,8 +15,7 @@ export const JumpNodeBody = ({ options }: Props) => {
if (!selectedGroup) return <Text color="gray.500">Configure...</Text>
return (
<Text>
Jump to{' '}
<Tag colorScheme="blue">{parseGroupTitle(selectedGroup.title)}</Tag>{' '}
Jump to <Tag colorScheme="blue">{selectedGroup.title}</Tag>{' '}
{isDefined(blockIndex) && blockIndex >= 0 ? (
<>
at block <Tag colorScheme="blue">{blockIndex + 1}</Tag>

View File

@@ -3,7 +3,7 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { Stack } from '@chakra-ui/react'
import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump'
import React from 'react'
import { byId, parseGroupTitle } from '@typebot.io/lib'
import { byId } from '@typebot.io/lib'
type Props = {
groupId: string
@@ -32,7 +32,7 @@ export const JumpSettings = ({ groupId, options, onOptionsChange }: Props) => {
items={typebot.groups
.filter((group) => group.id !== currentGroupId)
.map((group) => ({
label: parseGroupTitle(group.title),
label: group.title,
value: group.id,
}))}
selectedItem={selectedGroup?.id}

View File

@@ -1,7 +1,6 @@
import { Select } from '@/components/inputs/Select'
import { Input } from '@chakra-ui/react'
import { Group } from '@typebot.io/schemas'
import { parseGroupTitle } from '@typebot.io/lib'
type Props = {
groups: Group[]
@@ -23,7 +22,7 @@ export const GroupsDropdown = ({
<Select
selectedItem={groupId}
items={(groups ?? []).map((group) => ({
label: parseGroupTitle(group.title),
label: group.title,
value: group.id,
}))}
onSelect={onGroupIdSelected}

View File

@@ -28,7 +28,11 @@ export const CollaborationMenuButton = ({
</Tooltip>
</span>
</PopoverTrigger>
<PopoverContent shadow="lg" width="430px">
<PopoverContent
shadow="lg"
width="430px"
rootProps={{ style: { transform: 'scale(0)' } }}
>
<CollaborationList />
</PopoverContent>
</Popover>

View File

@@ -5,13 +5,13 @@ import { useUser } from '@/features/account/hooks/useUser'
import { isNotDefined } from '@typebot.io/lib'
import Link from 'next/link'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown'
import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
export const DashboardHeader = () => {
const scopedT = useScopedI18n('dashboard.header')
const { t } = useTranslate()
const { user, logOut } = useUser()
const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
@@ -50,7 +50,7 @@ export const DashboardHeader = () => {
onClick={onOpen}
isLoading={isNotDefined(workspace)}
>
{scopedT('settingsButton.label')}
{t('dashboard.header.settingsButton.label')}
</Button>
<WorkspaceDropdown
currentWorkspace={workspace}

View File

@@ -5,7 +5,6 @@ import {
PreCheckoutModalProps,
} from '@/features/billing/components/PreCheckoutModal'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useScopedI18n } from '@/locales'
import { Stack, VStack, Spinner, Text } from '@chakra-ui/react'
import { Plan } from '@typebot.io/prisma'
import { useRouter } from 'next/router'
@@ -16,9 +15,10 @@ import { TypebotDndProvider } from '@/features/folders/TypebotDndProvider'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
import { trpc } from '@/lib/trpc'
import { guessIfUserIsEuropean } from '@typebot.io/lib/billing/guessIfUserIsEuropean'
import { useTranslate } from '@tolgee/react'
export const DashboardPage = () => {
const scopedT = useScopedI18n('dashboard')
const { t } = useTranslate()
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { user } = useUser()
@@ -58,7 +58,7 @@ export const DashboardPage = () => {
return (
<Stack minH="100vh">
<Seo title={workspace?.name ?? scopedT('title')} />
<Seo title={workspace?.name ?? t('dashboard.title')} />
<DashboardHeader />
{!workspace?.stripeId && (
<ParentModalProvider>
@@ -73,7 +73,7 @@ export const DashboardPage = () => {
<TypebotDndProvider>
{isLoading ? (
<VStack w="full" justifyContent="center" pt="10" spacing={6}>
<Text>{scopedT('redirectionMessage')}</Text>
<Text>{t('dashboard.redirectionMessage')}</Text>
<Spinner />
</VStack>
) : (

View File

@@ -14,7 +14,7 @@ import { Plan } from '@typebot.io/prisma'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { BlockLabel } from './BlockLabel'
import { LockTag } from '@/features/billing/components/LockTag'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
type: DraggableBlockType
@@ -27,7 +27,7 @@ type Props = {
export const BlockCard = (
props: Pick<Props, 'type' | 'onMouseDown'>
): JSX.Element => {
const scopedT = useScopedI18n('editor.blockCard')
const { t } = useTranslate()
const { workspace } = useWorkspace()
switch (props.type) {
@@ -35,7 +35,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('bubbleBlock.tooltip.label')}
tooltip={t('blocks.bubbles.embed.blockCard.tooltip')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
@@ -45,7 +45,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('inputBlock.tooltip.files.label')}
tooltip={t('blocks.inputs.fileUpload.blockCard.tooltip')}
>
<BlockIcon type={props.type} />
<HStack>
@@ -58,7 +58,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('logicBlock.tooltip.code.label')}
tooltip={t('editor.blockCard.logicBlock.tooltip.code.label')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
@@ -68,7 +68,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('logicBlock.tooltip.typebotLink.label')}
tooltip={t('editor.blockCard.logicBlock.tooltip.typebotLink.label')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
@@ -78,7 +78,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('logicBlock.tooltip.jump.label')}
tooltip={t('editor.blockCard.logicBlock.tooltip.jump.label')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
@@ -88,7 +88,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('integrationBlock.tooltip.googleSheets.label')}
tooltip={t('blocks.integrations.googleSheets.blockCard.tooltip')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
@@ -98,7 +98,7 @@ export const BlockCard = (
return (
<BlockCardLayout
{...props}
tooltip={scopedT('integrationBlock.tooltip.googleAnalytics.label')}
tooltip={t('blocks.integrations.googleAnalytics.blockCard.tooltip')}
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />

View File

@@ -7,84 +7,98 @@ import {
BlockType,
} from '@typebot.io/schemas'
import React from 'react'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = { type: BlockType }
export const BlockLabel = ({ type }: Props): JSX.Element => {
const scopedT = useScopedI18n('editor.sidebarBlock')
const { t } = useTranslate()
switch (type) {
case 'start':
return <Text fontSize="sm">{scopedT('start.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.start.label')}</Text>
case BubbleBlockType.TEXT:
case InputBlockType.TEXT:
return <Text fontSize="sm">{scopedT('text.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.text.label')}</Text>
case BubbleBlockType.IMAGE:
return <Text fontSize="sm">{scopedT('image.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.image.label')}</Text>
case BubbleBlockType.VIDEO:
return <Text fontSize="sm">{scopedT('video.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.video.label')}</Text>
case BubbleBlockType.EMBED:
return <Text fontSize="sm">{scopedT('embed.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.embed.label')}</Text>
case BubbleBlockType.AUDIO:
return <Text fontSize="sm">{scopedT('audio.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.audio.label')}</Text>
case InputBlockType.NUMBER:
return <Text fontSize="sm">{scopedT('number.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.number.label')}</Text>
case InputBlockType.EMAIL:
return <Text fontSize="sm">{scopedT('email.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.email.label')}</Text>
case InputBlockType.URL:
return <Text fontSize="sm">{scopedT('website.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.website.label')}</Text>
case InputBlockType.DATE:
return <Text fontSize="sm">{scopedT('date.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.date.label')}</Text>
case InputBlockType.PHONE:
return <Text fontSize="sm">{scopedT('phone.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.phone.label')}</Text>
case InputBlockType.CHOICE:
return <Text fontSize="sm">{scopedT('button.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.button.label')}</Text>
case InputBlockType.PICTURE_CHOICE:
return <Text fontSize="sm">{scopedT('picChoice.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.picChoice.label')}</Text>
)
case InputBlockType.PAYMENT:
return <Text fontSize="sm">{scopedT('payment.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.payment.label')}</Text>
case InputBlockType.RATING:
return <Text fontSize="sm">{scopedT('rating.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.rating.label')}</Text>
case InputBlockType.FILE:
return <Text fontSize="sm">{scopedT('file.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.file.label')}</Text>
case LogicBlockType.SET_VARIABLE:
return <Text fontSize="sm">{scopedT('setVariable.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.setVariable.label')}</Text>
)
case LogicBlockType.CONDITION:
return <Text fontSize="sm">{scopedT('condition.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.condition.label')}</Text>
)
case LogicBlockType.REDIRECT:
return <Text fontSize="sm">{scopedT('redirect.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.redirect.label')}</Text>
)
case LogicBlockType.SCRIPT:
return <Text fontSize="sm">{scopedT('script.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.script.label')}</Text>
case LogicBlockType.TYPEBOT_LINK:
return <Text fontSize="sm">{scopedT('typebot.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.typebot.label')}</Text>
case LogicBlockType.WAIT:
return <Text fontSize="sm">{scopedT('wait.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.wait.label')}</Text>
case LogicBlockType.JUMP:
return <Text fontSize="sm">{scopedT('jump.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.jump.label')}</Text>
case LogicBlockType.AB_TEST:
return <Text fontSize="sm">{scopedT('abTest.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.abTest.label')}</Text>
case IntegrationBlockType.GOOGLE_SHEETS:
return <Text fontSize="sm">{scopedT('sheets.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.sheets.label')}</Text>
case IntegrationBlockType.GOOGLE_ANALYTICS:
return <Text fontSize="sm">{scopedT('analytics.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.analytics.label')}</Text>
)
case IntegrationBlockType.WEBHOOK:
return <Text fontSize="sm">{scopedT('webhook.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.webhook.label')}</Text>
case IntegrationBlockType.ZAPIER:
return <Text fontSize="sm">{scopedT('zapier.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.zapier.label')}</Text>
case IntegrationBlockType.MAKE_COM:
return <Text fontSize="sm">{scopedT('makecom.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.makecom.label')}</Text>
case IntegrationBlockType.PABBLY_CONNECT:
return <Text fontSize="sm">{scopedT('pabbly.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.pabbly.label')}</Text>
case IntegrationBlockType.EMAIL:
return <Text fontSize="sm">{scopedT('email.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.email.label')}</Text>
case IntegrationBlockType.CHATWOOT:
return <Text fontSize="sm">{scopedT('chatwoot.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.chatwoot.label')}</Text>
)
case IntegrationBlockType.OPEN_AI:
return <Text fontSize="sm">{scopedT('openai.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.openai.label')}</Text>
case IntegrationBlockType.PIXEL:
return <Text fontSize="sm">{scopedT('pixel.label')}</Text>
return <Text fontSize="sm">{t('editor.sidebarBlock.pixel.label')}</Text>
case IntegrationBlockType.ZEMANTIC_AI:
return <Text fontSize="sm">{scopedT('zemanticAi.label')}</Text>
return (
<Text fontSize="sm">{t('editor.sidebarBlock.zemanticAi.label')}</Text>
)
}
}

View File

@@ -23,10 +23,10 @@ import { BlockCard } from './BlockCard'
import { LockedIcon, UnlockedIcon } from '@/components/icons'
import { BlockCardOverlay } from './BlockCardOverlay'
import { headerHeight } from '../constants'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const BlocksSideBar = () => {
const scopedT = useScopedI18n('editor.sidebarBlocks')
const { t } = useTranslate()
const { setDraggedBlockType, draggedBlockType } = useBlockDnd()
const [position, setPosition] = useState({
x: 0,
@@ -107,16 +107,16 @@ export const BlocksSideBar = () => {
<Tooltip
label={
isLocked
? scopedT('sidebar.unlock.label')
: scopedT('sidebar.lock.label')
? t('editor.sidebarBlocks.sidebar.unlock.label')
: t('editor.sidebarBlocks.sidebar.lock.label')
}
>
<IconButton
icon={isLocked ? <LockedIcon /> : <UnlockedIcon />}
aria-label={
isLocked
? scopedT('sidebar.icon.unlock.label')
: scopedT('sidebar.icon.lock.label')
? t('editor.sidebarBlocks.sidebar.icon.unlock.label')
: t('editor.sidebarBlocks.sidebar.icon.lock.label')
}
size="sm"
onClick={handleLockClick}
@@ -126,7 +126,7 @@ export const BlocksSideBar = () => {
<Stack>
<Text fontSize="sm" fontWeight="semibold">
{scopedT('blockType.bubbles.heading')}
{t('editor.sidebarBlocks.blockType.bubbles.heading')}
</Text>
<SimpleGrid columns={2} spacing="3">
{Object.values(BubbleBlockType).map((type) => (
@@ -137,7 +137,7 @@ export const BlocksSideBar = () => {
<Stack>
<Text fontSize="sm" fontWeight="semibold">
{scopedT('blockType.inputs.heading')}
{t('editor.sidebarBlocks.blockType.inputs.heading')}
</Text>
<SimpleGrid columns={2} spacing="3">
{Object.values(InputBlockType).map((type) => (
@@ -148,7 +148,7 @@ export const BlocksSideBar = () => {
<Stack>
<Text fontSize="sm" fontWeight="semibold">
{scopedT('blockType.logic.heading')}
{t('editor.sidebarBlocks.blockType.logic.heading')}
</Text>
<SimpleGrid columns={2} spacing="3">
{Object.values(LogicBlockType).map((type) => (
@@ -159,7 +159,7 @@ export const BlocksSideBar = () => {
<Stack>
<Text fontSize="sm" fontWeight="semibold">
{scopedT('blockType.integrations.heading')}
{t('editor.sidebarBlocks.blockType.integrations.heading')}
</Text>
<SimpleGrid columns={2} spacing="3">
{Object.values(IntegrationBlockType).map((type) => (

View File

@@ -6,7 +6,7 @@ import {
useColorModeValue,
} from '@chakra-ui/react'
import React, { useState } from 'react'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type EditableProps = {
defaultName: string
@@ -16,7 +16,7 @@ export const EditableTypebotName = ({
defaultName,
onNewName,
}: EditableProps) => {
const scopedT = useScopedI18n('editor.editableTypebotName')
const { t } = useTranslate()
const emptyNameBg = useColorModeValue('gray.100', 'gray.700')
const [currentName, setCurrentName] = useState(defaultName)
@@ -27,7 +27,7 @@ export const EditableTypebotName = ({
}
return (
<Tooltip label={scopedT('tooltip.rename.label')}>
<Tooltip label={t('editor.editableTypebotName.tooltip.rename.label')}>
<Editable
value={currentName}
onChange={setCurrentName}

View File

@@ -21,10 +21,10 @@ import {
} from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const GettingStartedModal = () => {
const scopedT = useScopedI18n('editor.gettingStartedModal')
const { t } = useTranslate()
const { query } = useRouter()
const { isOpen, onOpen, onClose } = useDisclosure()
@@ -40,7 +40,9 @@ export const GettingStartedModal = () => {
<ModalCloseButton />
<ModalBody as={Stack} spacing="8" py="10">
<Stack spacing={4}>
<Heading fontSize="xl">{scopedT('editorBasics.heading')}</Heading>
<Heading fontSize="xl">
{t('editor.gettingStartedModal.editorBasics.heading')}
</Heading>
<List spacing={4}>
<HStack as={ListItem}>
<Flex
@@ -56,7 +58,9 @@ export const GettingStartedModal = () => {
>
1
</Flex>
<Text>{scopedT('editorBasics.list.one.label')}</Text>
<Text>
{t('editor.gettingStartedModal.editorBasics.list.one.label')}
</Text>
</HStack>
<HStack as={ListItem}>
<Flex
@@ -72,7 +76,9 @@ export const GettingStartedModal = () => {
>
2
</Flex>
<Text>{scopedT('editorBasics.list.two.label')}</Text>
<Text>
{t('editor.gettingStartedModal.editorBasics.list.two.label')}
</Text>
</HStack>
<HStack as={ListItem}>
<Flex
@@ -88,7 +94,11 @@ export const GettingStartedModal = () => {
>
3
</Flex>
<Text>{scopedT('editorBasics.list.three.label')}</Text>
<Text>
{t(
'editor.gettingStartedModal.editorBasics.list.three.label'
)}
</Text>
</HStack>
<HStack as={ListItem}>
<Flex
@@ -104,15 +114,18 @@ export const GettingStartedModal = () => {
>
4
</Flex>
<Text>{scopedT('editorBasics.list.four.label')}</Text>
<Text>
{t('editor.gettingStartedModal.editorBasics.list.four.label')}
</Text>
</HStack>
</List>
</Stack>
<Text>{scopedT('editorBasics.list.label')}</Text>
<Text>{t('editor.gettingStartedModal.editorBasics.list.label')}</Text>
<Stack spacing={4}>
<Heading fontSize="xl">
{scopedT('seeAction.label')} ({`<`} {scopedT('seeAction.time')})
{t('editor.gettingStartedModal.seeAction.label')} ({`<`}{' '}
{t('editor.gettingStartedModal.seeAction.time')})
</Heading>
<iframe
width="100%"
@@ -127,7 +140,7 @@ export const GettingStartedModal = () => {
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
{scopedT('seeAction.item.label')}
{t('editor.gettingStartedModal.seeAction.item.label')}
</Box>
<AccordionIcon />
</AccordionButton>

View File

@@ -30,10 +30,10 @@ import { RightPanel, useEditor } from '../providers/EditorProvider'
import { useTypebot } from '../providers/TypebotProvider'
import { SupportBubble } from '@/components/SupportBubble'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const TypebotHeader = () => {
const scopedT = useScopedI18n('editor.headers')
const { t } = useTranslate()
const router = useRouter()
const {
typebot,
@@ -105,7 +105,7 @@ export const TypebotHeader = () => {
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
size="sm"
>
{scopedT('flowButton.label')}
{t('editor.headers.flowButton.label')}
</Button>
<Button
as={Link}
@@ -114,7 +114,7 @@ export const TypebotHeader = () => {
variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
size="sm"
>
{scopedT('themeButton.label')}
{t('editor.headers.themeButton.label')}
</Button>
<Button
as={Link}
@@ -123,7 +123,7 @@ export const TypebotHeader = () => {
variant={router.pathname.endsWith('settings') ? 'outline' : 'ghost'}
size="sm"
>
{scopedT('settingsButton.label')}
{t('editor.headers.settingsButton.label')}
</Button>
<Button
as={Link}
@@ -132,7 +132,7 @@ export const TypebotHeader = () => {
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
size="sm"
>
{scopedT('shareButton.label')}
{t('editor.headers.shareButton.label')}
</Button>
{isDefined(publishedTypebot) && (
<Button
@@ -142,7 +142,7 @@ export const TypebotHeader = () => {
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
size="sm"
>
{scopedT('resultsButton.label')}
{t('editor.headers.resultsButton.label')}
</Button>
)}
</HStack>
@@ -225,21 +225,23 @@ export const TypebotHeader = () => {
</Tooltip>
</HStack>
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
{scopedT('helpButton.label')}
{t('editor.headers.helpButton.label')}
</Button>
</HStack>
{isSavingLoading && (
<HStack>
<Spinner speed="0.7s" size="sm" color="gray.400" />
<Text fontSize="sm" color="gray.400">
{scopedT('savingSpinner.label')}
{t('editor.headers.savingSpinner.label')}
</Text>
</HStack>
)}
</HStack>
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
<CollaborationMenuButton isLoading={isNotDefined(typebot)} />
<Flex pos="relative">
<CollaborationMenuButton isLoading={isNotDefined(typebot)} />
</Flex>
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
<Button
colorScheme="gray"
@@ -247,7 +249,7 @@ export const TypebotHeader = () => {
isLoading={isNotDefined(typebot)}
size="sm"
>
{scopedT('previewButton.label')}
{t('editor.headers.previewButton.label')}
</Button>
)}
<PublishButton size="sm" />

View File

@@ -23,7 +23,6 @@ import { areTypebotsEqual } from '@/features/publish/helpers/areTypebotsEqual'
import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished'
import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot'
import { trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
const autoSaveTimeout = 10000
@@ -80,7 +79,6 @@ export const TypebotProvider = ({
children: ReactNode
typebotId?: string
}) => {
const scopedT = useScopedI18n('editor.provider')
const { push } = useRouter()
const { showToast } = useToast()
@@ -96,15 +94,11 @@ export const TypebotProvider = ({
if (error.data?.httpStatus === 404) {
showToast({
status: 'info',
description: scopedT('messages.getTypebotError.description'),
description: "Couldn't find typebot.",
})
push('/typebots')
return
}
showToast({
title: scopedT('messages.getTypebotError.title'),
description: error.message,
})
},
}
)
@@ -114,13 +108,6 @@ export const TypebotProvider = ({
{ typebotId: typebotId as string },
{
enabled: isDefined(typebotId),
onError: (error) => {
if (error.data?.httpStatus === 404) return
showToast({
title: scopedT('messages.publishedTypebotError.title'),
description: error.message,
})
},
}
)
@@ -128,7 +115,7 @@ export const TypebotProvider = ({
trpc.typebot.updateTypebot.useMutation({
onError: (error) =>
showToast({
title: scopedT('messages.updateTypebotError.title'),
title: 'Error while updating typebot',
description: error.message,
}),
onSuccess: () => {
@@ -264,10 +251,7 @@ export const TypebotProvider = ({
isPublished,
updateTypebot: updateLocalTypebot,
restorePublishedTypebot,
...groupsActions(
setLocalTypebot as SetTypebot,
scopedT('groups.copy.title')
),
...groupsActions(setLocalTypebot as SetTypebot),
...blocksAction(setLocalTypebot as SetTypebot),
...variablesAction(setLocalTypebot as SetTypebot),
...edgesAction(setLocalTypebot as SetTypebot),

View File

@@ -12,7 +12,7 @@ import {
createBlockDraft,
duplicateBlockDraft,
} from './blocks'
import { isEmpty, parseGroupTitle } from '@typebot.io/lib'
import { isEmpty } from '@typebot.io/lib'
import { Coordinates } from '@/features/graph/types'
export type GroupsActions = {
@@ -28,10 +28,7 @@ export type GroupsActions = {
deleteGroup: (groupIndex: number) => void
}
const groupsActions = (
setTypebot: SetTypebot,
groupCopyLabel: string
): GroupsActions => ({
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
createGroup: ({
id,
block,
@@ -67,11 +64,19 @@ const groupsActions = (
const group = typebot.groups[groupIndex]
const id = createId()
const totalGroupsWithSameTitle = typebot.groups.filter(
(group) => group.title === group.title
).length
const newGroup: Group = {
...group,
title: isEmpty(group.title)
? ''
: `${parseGroupTitle(group.title)} ${groupCopyLabel}`,
: `${group.title}${
totalGroupsWithSameTitle > 0
? ` (${totalGroupsWithSameTitle})`
: ''
}}`,
id,
blocks: group.blocks.map((block) => duplicateBlockDraft(id)(block)),
graphCoordinates: {

View File

@@ -3,10 +3,10 @@ import { ChevronLeftIcon } from '@/components/icons'
import { useTypebotDnd } from '../TypebotDndProvider'
import Link from 'next/link'
import React, { useMemo } from 'react'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const BackButton = ({ id }: { id: string | null }) => {
const t = useI18n()
const { t } = useTranslate()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()

View File

@@ -3,14 +3,14 @@ import { PlusIcon } from '@/components/icons'
import { useRouter } from 'next/router'
import { stringify } from 'qs'
import React from 'react'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const CreateBotButton = ({
folderId,
isFirstBot,
...props
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
const scopedT = useScopedI18n('folders.createTypebotButton')
const { t } = useTranslate()
const router = useRouter()
const handleClick = () =>
@@ -39,7 +39,7 @@ export const CreateBotButton = ({
textAlign="center"
mt="6"
>
{scopedT('label')}
{t('folders.createTypebotButton.label')}
</Text>
</VStack>
</Button>

View File

@@ -3,16 +3,15 @@ import { FolderPlusIcon } from '@/components/icons'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { Plan } from '@typebot.io/prisma'
import React from 'react'
import { useI18n, useScopedI18n } from '@/locales'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { LockTag } from '@/features/billing/components/LockTag'
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
import { useTranslate } from '@tolgee/react'
type Props = { isLoading: boolean; onClick: () => void }
export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
const t = useI18n()
const scopedT = useScopedI18n('folders.createFolderButton')
const { t } = useTranslate()
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()
@@ -27,7 +26,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
isLoading={isLoading}
>
<HStack>
<Text>{scopedT('label')}</Text>
<Text>{t('folders.createFolderButton.label')}</Text>
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
<ChangePlanModal

View File

@@ -24,8 +24,8 @@ import { useRouter } from 'next/router'
import React, { useMemo } from 'react'
import { deleteFolderQuery } from '../queries/deleteFolderQuery'
import { useToast } from '@/hooks/useToast'
import { useI18n, useScopedI18n } from '@/locales'
import { updateFolderQuery } from '../queries/updateFolderQuery'
import { T, useTranslate } from '@tolgee/react'
export const FolderButton = ({
folder,
@@ -36,8 +36,7 @@ export const FolderButton = ({
onFolderDeleted: () => void
onFolderRenamed: (newName: string) => void
}) => {
const t = useI18n()
const scopedT = useScopedI18n('folders.folderButton')
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()
@@ -140,9 +139,12 @@ export const FolderButton = ({
confirmButtonLabel={'Delete'}
message={
<Text>
{scopedT('deleteConfirmationMessage', {
folderName: <strong>{folder.name}</strong>,
})}
<T
keyName="folders.folderButton.deleteConfirmationMessage"
params={{
strong: <strong>{folder.name}</strong>,
}}
/>
</Text>
}
title={`Delete ${folder.name}?`}

View File

@@ -21,7 +21,7 @@ import { CreateFolderButton } from './CreateFolderButton'
import { ButtonSkeleton, FolderButton } from './FolderButton'
import { TypebotButton } from './TypebotButton'
import { TypebotCardOverlay } from './TypebotButtonOverlay'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
import { TypebotInDashboard } from '@/features/dashboard/types'
import { trpc } from '@/lib/trpc'
@@ -31,7 +31,7 @@ type Props = { folder: DashboardFolder | null }
const dragDistanceTolerance = 20
export const FolderContent = ({ folder }: Props) => {
const t = useI18n()
const { t } = useTranslate()
const { workspace, currentRole } = useWorkspace()
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
const {

View File

@@ -1,7 +1,7 @@
import { Seo } from '@/components/Seo'
import { DashboardHeader } from '@/features/dashboard/components/DashboardHeader'
import { useToast } from '@/hooks/useToast'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { Stack, Flex, Spinner } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useFolder } from '../hooks/useFolder'
@@ -9,7 +9,7 @@ import { TypebotDndProvider } from '../TypebotDndProvider'
import { FolderContent } from './FolderContent'
export const FolderPage = () => {
const t = useI18n()
const { t } = useTranslate()
const router = useRouter()
const { showToast } = useToast()

View File

@@ -21,7 +21,7 @@ import { useDebounce } from 'use-debounce'
import { useToast } from '@/hooks/useToast'
import { MoreButton } from './MoreButton'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
import { useScopedI18n } from '@/locales'
import { T, useTranslate } from '@tolgee/react'
import { TypebotInDashboard } from '@/features/dashboard/types'
import { isMobile } from '@/helpers/isMobile'
import { trpc, trpcVanilla } from '@/lib/trpc'
@@ -40,7 +40,7 @@ export const TypebotButton = ({
onTypebotUpdated,
onMouseDown,
}: Props) => {
const scopedT = useScopedI18n('folders.typebotButton')
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot } = useTypebotDnd()
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
@@ -149,7 +149,7 @@ export const TypebotButton = ({
top="27px"
size="sm"
>
{scopedT('live')}
{t('folders.typebotButton.live')}
</Tag>
)}
{!isReadOnly && (
@@ -169,18 +169,18 @@ export const TypebotButton = ({
pos="absolute"
top="20px"
right="20px"
aria-label={scopedT('showMoreOptions')}
aria-label={t('folders.typebotButton.showMoreOptions')}
>
{typebot.publishedTypebotId && (
<MenuItem onClick={handleUnpublishClick}>
{scopedT('unpublish')}
{t('folders.typebotButton.unpublish')}
</MenuItem>
)}
<MenuItem onClick={handleDuplicateClick}>
{scopedT('duplicate')}
{t('folders.typebotButton.duplicate')}
</MenuItem>
<MenuItem color="red.400" onClick={handleDeleteClick}>
{scopedT('delete')}
{t('folders.typebotButton.delete')}
</MenuItem>
</MoreButton>
</>
@@ -203,13 +203,16 @@ export const TypebotButton = ({
message={
<Stack spacing="4">
<Text>
{scopedT('deleteConfirmationMessage', {
typebotName: <strong>{typebot.name}</strong>,
})}
<T
keyName="folders.typebotButton.deleteConfirmationMessage"
params={{
strong: <strong>{typebot.name}</strong>,
}}
/>
</Text>
<Alert status="warning">
<AlertIcon />
{scopedT('deleteConfirmationMessageWarning')}
{t('folders.typebotButton.deleteConfirmationMessageWarning')}
</Alert>
</Stack>
}

View File

@@ -42,7 +42,7 @@ import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/compon
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
import { PixelNodeBody } from '@/features/blocks/integrations/pixel/components/PixelNodeBody'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { ZemanticAiNodeBody } from '@/features/blocks/integrations/zemanticAi/ZemanticAiNodeBody'
type Props = {
@@ -50,7 +50,7 @@ type Props = {
indices: BlockIndices
}
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
const scopedT = useScopedI18n('editor.blocks.start')
const { t } = useTranslate()
switch (block.type) {
case BubbleBlockType.TEXT: {
return <TextBubbleContent block={block} />
@@ -201,7 +201,7 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
return <ZemanticAiNodeBody options={block.options} />
}
case 'start': {
return <Text>{scopedT('text')}</Text>
return <Text>{t('editor.blocks.start.text')}</Text>
}
}
}

View File

@@ -27,7 +27,7 @@ import { isNotDefined } from '@typebot.io/lib'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
import { parseTimeSince } from '@/helpers/parseTimeSince'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'
import { useToast } from '@/hooks/useToast'
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
@@ -39,7 +39,7 @@ export const PublishButton = ({
isMoreMenuDisabled = false,
...props
}: Props) => {
const t = useI18n()
const { t } = useTranslate()
const warningTextColor = useColorModeValue('red.300', 'red.600')
const { workspace } = useWorkspace()
const { push, query, pathname } = useRouter()

View File

@@ -24,12 +24,12 @@ import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { CustomDomainsDropdown } from '@/features/customDomains/components/CustomDomainsDropdown'
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import { env } from '@typebot.io/env'
import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon'
export const SharePage = () => {
const t = useI18n()
const { t } = useTranslate()
const { workspace } = useWorkspace()
const { typebot, updateTypebot, publishedTypebot } = useTypebot()
const { showToast } = useToast()

View File

@@ -16,10 +16,10 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useUser } from '@/features/account/hooks/useUser'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const CreateNewTypebotButtons = () => {
const scopedT = useScopedI18n('templates.buttons')
const { t } = useTranslate()
const { workspace } = useWorkspace()
const { user } = useUser()
const router = useRouter()
@@ -72,7 +72,7 @@ export const CreateNewTypebotButtons = () => {
return (
<VStack maxW="600px" w="full" flex="1" pt="20" spacing={10}>
<Heading>{scopedT('heading')}</Heading>
<Heading>{t('templates.buttons.heading')}</Heading>
<Stack w="full" spacing={6}>
<Button
variant="outline"
@@ -89,7 +89,7 @@ export const CreateNewTypebotButtons = () => {
onClick={() => handleCreateSubmit()}
isLoading={isLoading}
>
{scopedT('fromScratchButton.label')}
{t('templates.buttons.fromScratchButton.label')}
</Button>
<Button
variant="outline"
@@ -106,7 +106,7 @@ export const CreateNewTypebotButtons = () => {
onClick={onOpen}
isLoading={isLoading}
>
{scopedT('fromTemplateButton.label')}
{t('templates.buttons.fromTemplateButton.label')}
</Button>
<ImportTypebotFromFileButton
variant="outline"
@@ -123,7 +123,7 @@ export const CreateNewTypebotButtons = () => {
isLoading={isLoading}
onNewTypebot={handleCreateSubmit}
>
{scopedT('importFileButton.label')}
{t('templates.buttons.importFileButton.label')}
</ImportTypebotFromFileButton>
</Stack>
<TemplatesModal

View File

@@ -4,7 +4,7 @@ import { Typebot, typebotCreateSchema } from '@typebot.io/schemas'
import { preprocessTypebot } from '@typebot.io/schemas/features/typebot/helpers/preprocessTypebot'
import React, { ChangeEvent } from 'react'
import { z } from 'zod'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
onNewTypebot: (typebot: Typebot) => void
@@ -14,7 +14,7 @@ export const ImportTypebotFromFileButton = ({
onNewTypebot,
...props
}: Props) => {
const scopedT = useScopedI18n('templates.importFromFileButon')
const { t } = useTranslate()
const { showToast } = useToast()
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
@@ -29,7 +29,7 @@ export const ImportTypebotFromFileButton = ({
} catch (err) {
console.error(err)
showToast({
description: scopedT('toastError.description'),
description: t('templates.importFromFileButon.toastError.description'),
details: {
content: JSON.stringify(err, null, 2),
lang: 'json',

View File

@@ -19,7 +19,7 @@ import { templates } from '../data'
import { TemplateProps } from '../types'
import { useToast } from '@/hooks/useToast'
import { sendRequest } from '@typebot.io/lib'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
isOpen: boolean
@@ -28,7 +28,7 @@ type Props = {
}
export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
const scopedT = useScopedI18n('templates.modal')
const { t } = useTranslate()
const templateCardBackgroundColor = useColorModeValue('white', 'gray.800')
const [typebot, setTypebot] = useState<Typebot>()
const [selectedTemplate, setSelectedTemplate] = useState<TemplateProps>(
@@ -90,7 +90,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
pl="1"
color="gray.500"
>
{scopedT('menuHeading.marketing')}
{t('templates.modal.menuHeading.marketing')}
</Text>
{templates
.filter((template) => template.category === 'marketing')
@@ -112,7 +112,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
<Text>{template.name}</Text>
{template.isNew && (
<Tag colorScheme="orange" size="sm" flexShrink={0}>
{scopedT('menuHeading.new.tag')}
{t('templates.modal.menuHeading.new.tag')}
</Tag>
)}
</HStack>
@@ -126,7 +126,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
pl="1"
color="gray.500"
>
{scopedT('menuHeading.product')}
{t('templates.modal.menuHeading.product')}
</Text>
{templates
.filter((template) => template.category === 'product')
@@ -148,7 +148,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
<Text>{template.name}</Text>
{template.isNew && (
<Tag colorScheme="orange" size="sm" flexShrink={0}>
{scopedT('menuHeading.new.tag')}
{t('templates.modal.menuHeading.new.tag')}
</Tag>
)}
</HStack>
@@ -162,7 +162,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
pl="1"
color="gray.500"
>
{scopedT('menuHeading.other')}
{t('templates.modal.menuHeading.other')}
</Text>
{templates
.filter((template) => template.category === undefined)
@@ -184,7 +184,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
<Text>{template.name}</Text>
{template.isNew && (
<Tag colorScheme="orange" size="sm" flexShrink={0}>
{scopedT('menuHeading.new.tag')}
{t('templates.modal.menuHeading.new.tag')}
</Tag>
)}
</HStack>
@@ -231,7 +231,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
onClick={onUseThisTemplateClick}
isLoading={isLoading}
>
{scopedT('useTemplateButton.label')}
{t('templates.modal.useTemplateButton.label')}
</Button>
</HStack>
</Stack>

View File

@@ -8,7 +8,7 @@ import { Plan } from '@typebot.io/prisma'
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
isBrandingEnabled: boolean
@@ -23,7 +23,7 @@ export const GeneralSettings = ({
onGeneralThemeChange,
onBrandingChange,
}: Props) => {
const t = useI18n()
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace()
const isWorkspaceFreePlan = isFreePlan(workspace)

View File

@@ -13,7 +13,7 @@ import { WorkspaceInvitation, WorkspaceRole } from '@typebot.io/prisma'
import { FormEvent, useState } from 'react'
import { Member } from '../types'
import { sendInvitationQuery } from '../queries/sendInvitationQuery'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
workspaceId: string
@@ -29,7 +29,7 @@ export const AddMemberForm = ({
isLoading,
isLocked,
}: Props) => {
const scopedT = useScopedI18n('workspace.membersList')
const { t } = useTranslate()
const [invitationEmail, setInvitationEmail] = useState('')
const [invitationRole, setInvitationRole] = useState<WorkspaceRole>(
WorkspaceRole.MEMBER
@@ -54,7 +54,7 @@ export const AddMemberForm = ({
return (
<HStack as="form" onSubmit={handleInvitationSubmit}>
<Input
placeholder={scopedT('inviteInput.placeholder')}
placeholder={t('workspace.membersList.inviteInput.placeholder')}
name="inviteEmail"
value={invitationEmail}
onChange={(e) => setInvitationEmail(e.target.value)}
@@ -75,7 +75,7 @@ export const AddMemberForm = ({
type="submit"
isDisabled={isLoading || isLocked || invitationEmail === ''}
>
{scopedT('inviteButton.label')}
{t('workspace.membersList.inviteButton.label')}
</Button>
</HStack>
)

View File

@@ -13,7 +13,7 @@ import {
import { WorkspaceRole } from '@typebot.io/prisma'
import React from 'react'
import { convertWorkspaceRoleToReadable } from './AddMemberForm'
import { useI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
image?: string
@@ -38,7 +38,7 @@ export const MemberItem = ({
onDeleteClick,
onSelectNewRole,
}: Props) => {
const t = useI18n()
const { t } = useTranslate()
const handleAdminClick = () => onSelectNewRole(WorkspaceRole.ADMIN)
const handleMemberClick = () => onSelectNewRole(WorkspaceRole.MEMBER)
@@ -88,7 +88,7 @@ export const MemberIdentityContent = ({
isGuest?: boolean
email: string
}) => {
const t = useI18n()
const { t } = useTranslate()
return (
<HStack justifyContent="space-between" maxW="full" p="2">

View File

@@ -19,11 +19,11 @@ import { updateInvitationQuery } from '../queries/updateInvitationQuery'
import { updateMemberQuery } from '../queries/updateMemberQuery'
import { Member } from '../types'
import { useWorkspace } from '../WorkspaceProvider'
import { useScopedI18n } from '@/locales'
import { getSeatsLimit } from '@typebot.io/lib/billing/getSeatsLimit'
import { useTranslate } from '@tolgee/react'
export const MembersList = () => {
const scopedT = useScopedI18n('workspace.membersList')
const { t } = useTranslate()
const { user } = useUser()
const { workspace, currentRole } = useWorkspace()
const { members, invitations, isLoading, mutate } = useMembers({
@@ -103,12 +103,12 @@ export const MembersList = () => {
<Stack w="full" spacing={3}>
{!canInviteNewMember && (
<UnlockPlanAlertInfo>
{scopedT('unlockBanner.label')}
{t('workspace.membersList.unlockBanner.label')}
</UnlockPlanAlertInfo>
)}
{isDefined(seatsLimit) && (
<Heading fontSize="2xl">
{scopedT('title')}{' '}
{t('workspace.membersList.title')}{' '}
{seatsLimit === -1 ? '' : `(${currentMembersCount}/${seatsLimit})`}
</Heading>
)}

View File

@@ -7,7 +7,7 @@ import {
} from '@/components/icons'
import { PlanTag } from '@/features/billing/components/PlanTag'
import { trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
import {
Menu,
MenuButton,
@@ -32,7 +32,7 @@ export const WorkspaceDropdown = ({
onLogoutClick,
onCreateNewWorkspaceClick,
}: Props) => {
const scopedT = useScopedI18n('workspace.dropdown')
const { t } = useTranslate()
const { data } = trpc.workspace.listWorkspaces.useQuery()
const workspaces = data?.workspaces ?? []
@@ -72,14 +72,14 @@ export const WorkspaceDropdown = ({
</MenuItem>
))}
<MenuItem onClick={onCreateNewWorkspaceClick} icon={<PlusIcon />}>
{scopedT('newButton.label')}
{t('workspace.dropdown.newButton.label')}
</MenuItem>
<MenuItem
onClick={onLogoutClick}
icon={<LogOutIcon />}
color="orange.500"
>
{scopedT('logoutButton.label')}
{t('workspace.dropdown.logoutButton.label')}
</MenuItem>
</MenuList>
</Menu>

View File

@@ -12,10 +12,10 @@ import React from 'react'
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
import { useWorkspace } from '../WorkspaceProvider'
import { TextInput } from '@/components/inputs'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
const scopedT = useScopedI18n('workspace.settings')
const { t } = useTranslate()
const { workspace, workspaces, updateWorkspace, deleteCurrentWorkspace } =
useWorkspace()
@@ -34,7 +34,7 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
return (
<Stack spacing="6" w="full">
<FormControl>
<FormLabel>{scopedT('icon.title')}</FormLabel>
<FormLabel>{t('workspace.settings.icon.title')}</FormLabel>
<Flex>
{workspace && (
<EditableEmojiOrImageIcon
@@ -51,7 +51,7 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
</FormControl>
{workspace && (
<TextInput
label={scopedT('name.label')}
label={t('workspace.settings.name.label')}
withVariableButton={false}
defaultValue={workspace?.name}
onChange={handleNameChange}
@@ -74,12 +74,12 @@ const DeleteWorkspaceButton = ({
workspaceName: string
onConfirm: () => Promise<void>
}) => {
const scopedT = useScopedI18n('workspace.settings')
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<>
<Button colorScheme="red" variant="outline" onClick={onOpen}>
{scopedT('deleteButton.label')}
{t('workspace.settings.deleteButton.label')}
</Button>
<ConfirmModal
isOpen={isOpen}
@@ -87,7 +87,7 @@ const DeleteWorkspaceButton = ({
onClose={onClose}
message={
<Text>
{scopedT('deleteButton.confirmMessage', {
{t('workspace.settings.deleteButton.confirmMessage', {
workspaceName,
})}
</Text>

View File

@@ -24,7 +24,7 @@ import packageJson from '../../../../../../package.json'
import { UserPreferencesForm } from '@/features/account/components/UserPreferencesForm'
import { MyAccountForm } from '@/features/account/components/MyAccountForm'
import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout'
import { useScopedI18n } from '@/locales'
import { useTranslate } from '@tolgee/react'
type Props = {
isOpen: boolean
@@ -46,7 +46,7 @@ export const WorkspaceSettingsModal = ({
workspace,
onClose,
}: Props) => {
const scopedT = useScopedI18n('workspace.settings.modal')
const { t } = useTranslate()
const { currentRole } = useWorkspace()
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
@@ -82,7 +82,7 @@ export const WorkspaceSettingsModal = ({
justifyContent="flex-start"
pl="4"
>
{scopedT('menu.myAccount.label')}
{t('workspace.settings.modal.menu.myAccount.label')}
</Button>
<Button
variant={selectedTab === 'user-settings' ? 'solid' : 'ghost'}
@@ -92,12 +92,12 @@ export const WorkspaceSettingsModal = ({
justifyContent="flex-start"
pl="4"
>
{scopedT('menu.preferences.label')}
{t('workspace.settings.modal.menu.preferences.label')}
</Button>
</Stack>
<Stack>
<Text pl="4" color="gray.500">
{scopedT('menu.workspace.label')}
{t('workspace.settings.modal.menu.workspace.label')}
</Text>
{canEditWorkspace && (
<Button
@@ -116,7 +116,7 @@ export const WorkspaceSettingsModal = ({
justifyContent="flex-start"
pl="4"
>
{scopedT('menu.settings.label')}
{t('workspace.settings.modal.menu.settings.label')}
</Button>
)}
<Button
@@ -127,7 +127,7 @@ export const WorkspaceSettingsModal = ({
justifyContent="flex-start"
pl="4"
>
{scopedT('menu.members.label')}
{t('workspace.settings.modal.menu.members.label')}
</Button>
{canEditWorkspace && (
<Button
@@ -140,7 +140,7 @@ export const WorkspaceSettingsModal = ({
overflow="scroll"
className="hide-scrollbar"
>
{scopedT('menu.billingAndUsage.label')}
{t('workspace.settings.modal.menu.billingAndUsage.label')}
</Button>
)}
</Stack>
@@ -148,7 +148,9 @@ export const WorkspaceSettingsModal = ({
<Flex justify="center" pt="10">
<Text color="gray.500" fontSize="xs">
{scopedT('menu.version.label', { version: packageJson.version })}
{t('workspace.settings.modal.menu.version.label', {
version: packageJson.version,
})}
</Text>
</Flex>
</Stack>