🧑‍💻 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

@@ -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: {