🌐 Introduce i18n

Only translate dashboard page for now

Closes #322
This commit is contained in:
Baptiste Arnaud
2023-03-11 11:05:07 +01:00
parent 8df830721c
commit 138f3f8b07
24 changed files with 237 additions and 183 deletions

View File

@@ -1,37 +0,0 @@
import { CloseButton, Flex, HStack, StackProps } from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
type VerifyEmailBannerProps = { id: string } & StackProps
export const Banner = ({ id, ...props }: VerifyEmailBannerProps) => {
const [show, setShow] = useState(false)
const localStorageKey = `banner-${id}`
useEffect(() => {
if (!localStorage.getItem(localStorageKey)) setShow(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleCloseClick = () => {
localStorage.setItem(localStorageKey, 'hide')
setShow(false)
}
if (!show) return <></>
return (
<HStack
h="50px"
bgColor="blue.400"
color="white"
justifyContent="center"
align="center"
w="full"
{...props}
>
<Flex maxW="1000px" justifyContent="space-between" w="full">
<HStack>{props.children}</HStack>
<CloseButton rounded="full" onClick={handleCloseClick} />
</Flex>
</HStack>
)
}

View File

@@ -1,70 +0,0 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Text,
Stack,
Link,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
type Props = {
isOpen: boolean
onClose: () => void
}
const localStorageKey = 'typebot-20-modal'
export const AnnoucementModal = ({ isOpen, onClose }: Props) => {
const [show, setShow] = useState(false)
useEffect(() => {
if (!localStorage.getItem(localStorageKey)) setShow(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleCloseClick = () => {
localStorage.setItem(localStorageKey, 'hide')
setShow(false)
onClose()
}
if (!show) return <></>
return (
<Modal isOpen={isOpen} onClose={handleCloseClick} size="2xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>What&apos;s new in Typebot 2.0?</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6" pb="10">
<Text>Typebo 2.0 has been launched February the 15th 🎉.</Text>
<iframe
width="620"
height="315"
src="https://www.youtube.com/embed/u8FZHvlYviw"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ borderRadius: '5px' }}
/>
<Text>
Most questions are answered in this{' '}
<Link
href="https://docs.typebot.io"
color="blue.500"
textDecor="underline"
>
FAQ
</Link>
. If you have other questions, open up the bot on the bottom right
corner. 😃
</Text>
<Text>Baptiste.</Text>
</ModalBody>
</ModalContent>
</Modal>
)
}

View File

@@ -8,8 +8,10 @@ import { isNotDefined } from 'utils'
import Link from 'next/link'
import { WorkspaceSettingsModal } from '@/features/workspace'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
import { useScopedI18n } from '@/locales'
export const DashboardHeader = () => {
const scopedT = useScopedI18n('dashboard.header')
const { user } = useUser()
const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
@@ -52,7 +54,7 @@ export const DashboardHeader = () => {
onClick={onOpen}
isLoading={isNotDefined(workspace)}
>
Settings & Members
{scopedT('settingsButton.label')}
</Button>
<WorkspaceDropdown
currentWorkspace={workspace}

View File

@@ -7,6 +7,7 @@ import {
import { TypebotDndProvider, FolderContent } from '@/features/folders'
import { ParentModalProvider } from '@/features/graph'
import { useWorkspace } from '@/features/workspace'
import { useScopedI18n } from '@/locales'
import { Stack, VStack, Spinner, Text } from '@chakra-ui/react'
import { Plan } from 'db'
import { useRouter } from 'next/router'
@@ -15,6 +16,7 @@ import { guessIfUserIsEuropean } from 'utils/pricing'
import { DashboardHeader } from './DashboardHeader'
export const DashboardPage = () => {
const scopedT = useScopedI18n('dashboard')
const [isLoading, setIsLoading] = useState(false)
const { query } = useRouter()
const { user } = useUser()
@@ -42,7 +44,7 @@ export const DashboardPage = () => {
return (
<Stack minH="100vh">
<Seo title={workspace?.name ?? 'My typebots'} />
<Seo title={workspace?.name ?? scopedT('title')} />
<DashboardHeader />
{!workspace?.stripeId && (
<ParentModalProvider>
@@ -57,7 +59,7 @@ export const DashboardPage = () => {
<TypebotDndProvider>
{isLoading ? (
<VStack w="full" justifyContent="center" pt="10" spacing={6}>
<Text>You are being redirected...</Text>
<Text>{scopedT('redirectionMessage')}</Text>
<Spinner />
</VStack>
) : (

View File

@@ -40,7 +40,7 @@ test('folders and typebots should be deletable', async ({ page }) => {
await page.click('li:has-text("Folder #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Folder #1"')).not.toBeVisible()
await page.click('button[aria-label="Show Typebot #1 menu"]')
await page.click('button[aria-label="Show more options"]')
await page.click('li:has-text("Typebot #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Typebot #1"')).not.toBeVisible()

View File

@@ -3,8 +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'
export const BackButton = ({ id }: { id: string | null }) => {
const t = useI18n()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()
@@ -26,7 +28,7 @@ export const BackButton = ({ id }: { id: string | null }) => {
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
Back
{t('back')}
</Button>
)
}

View File

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

View File

@@ -9,10 +9,12 @@ import {
import { useWorkspace } from '@/features/workspace'
import { Plan } from 'db'
import React from 'react'
import { useScopedI18n } from '@/locales'
type Props = { isLoading: boolean; onClick: () => void }
export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
const scopedT = useScopedI18n('folders.createFolderButton')
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()
@@ -27,7 +29,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
isLoading={isLoading}
>
<HStack>
<Text>Create a folder</Text>
<Text>{scopedT('label')}</Text>
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
<ChangePlanModal

View File

@@ -25,6 +25,7 @@ import React, { useMemo } from 'react'
import { deleteFolderQuery } from '../queries/deleteFolderQuery'
import { useToast } from '@/hooks/useToast'
import { updateFolderQuery } from '../queries/updateFolderQuery'
import { useI18n, useScopedI18n } from '@/locales'
export const FolderButton = ({
folder,
@@ -35,6 +36,8 @@ export const FolderButton = ({
onFolderDeleted: () => void
onFolderRenamed: (newName: string) => void
}) => {
const t = useI18n()
const scopedT = useScopedI18n('folders.folderButton')
const router = useRouter()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()
@@ -49,7 +52,6 @@ export const FolderButton = ({
const { error } = await deleteFolderQuery(folder.id)
return error
? showToast({
title: "Couldn't delete the folder",
description: error.message,
})
: onFolderDeleted()
@@ -59,7 +61,7 @@ export const FolderButton = ({
if (newName === '' || newName === folder.name) return
const { error } = await updateFolderQuery(folder.id, { name: newName })
return error
? showToast({ title: 'An error occured', description: error.message })
? showToast({ title: t('errorMessage'), description: error.message })
: onFolderRenamed(newName)
}
@@ -106,7 +108,7 @@ export const FolderButton = ({
onOpen()
}}
>
Delete
{t('delete')}
</MenuItem>
</MenuList>
</Menu>
@@ -138,8 +140,9 @@ export const FolderButton = ({
confirmButtonLabel={'Delete'}
message={
<Text>
Are you sure you want to delete <strong>{folder.name}</strong>{' '}
folder? (Everything inside will be move to your dashboard)
{scopedT('deleteConfirmationMessage', {
folderName: <strong>{folder.name}</strong>,
})}
</Text>
}
title={`Delete ${folder.name}?`}

View File

@@ -24,12 +24,14 @@ import { CreateFolderButton } from './CreateFolderButton'
import { ButtonSkeleton, FolderButton } from './FolderButton'
import { TypebotButton } from './TypebotButton'
import { TypebotCardOverlay } from './TypebotButtonOverlay'
import { useI18n } from '@/locales'
type Props = { folder: DashboardFolder | null }
const dragDistanceTolerance = 20
export const FolderContent = ({ folder }: Props) => {
const t = useI18n()
const { workspace, currentRole } = useWorkspace()
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
const {
@@ -57,7 +59,9 @@ export const FolderContent = ({ folder }: Props) => {
workspaceId: workspace?.id,
parentId: folder?.id,
onError: (error) => {
showToast({ title: "Couldn't fetch folders", description: error.message })
showToast({
description: error.message,
})
},
})
@@ -70,7 +74,6 @@ export const FolderContent = ({ folder }: Props) => {
folderId: folder === null ? 'root' : folder.id,
onError: (error) => {
showToast({
title: "Couldn't fetch typebots",
description: error.message,
})
},
@@ -94,7 +97,7 @@ export const FolderContent = ({ folder }: Props) => {
setIsCreatingFolder(false)
if (error)
return showToast({
title: 'An error occured',
title: t('errorMessage'),
description: error.message,
})
if (newFolder) mutateFolders({ folders: [...folders, newFolder] })

View File

@@ -1,6 +1,7 @@
import { Seo } from '@/components/Seo'
import { DashboardHeader } from '@/features/dashboard'
import { useToast } from '@/hooks/useToast'
import { useI18n } from '@/locales'
import { Stack, Flex, Spinner } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useFolder } from '../hooks/useFolder'
@@ -8,6 +9,7 @@ import { TypebotDndProvider } from '../TypebotDndProvider'
import { FolderContent } from './FolderContent'
export const FolderPage = () => {
const t = useI18n()
const router = useRouter()
const { showToast } = useToast()
@@ -16,7 +18,6 @@ export const FolderPage = () => {
folderId: router.query.id?.toString(),
onError: (error) => {
showToast({
title: "Couldn't fetch folder content",
description: error.message,
})
},
@@ -24,7 +25,7 @@ export const FolderPage = () => {
return (
<Stack minH="100vh">
<Seo title="My typebots" />
<Seo title={t('dashboard.title')} />
<DashboardHeader />
<TypebotDndProvider>
{!folder ? (

View File

@@ -1,9 +1,12 @@
import React from 'react'
import {
Alert,
AlertIcon,
Button,
Flex,
IconButton,
MenuItem,
Stack,
Tag,
Text,
useDisclosure,
@@ -28,6 +31,7 @@ import {
import { MoreButton } from './MoreButton'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
import { deletePublishedTypebotQuery } from '@/features/publish/queries/deletePublishedTypebotQuery'
import { useScopedI18n } from '@/locales'
type Props = {
typebot: TypebotInDashboard
@@ -42,6 +46,7 @@ export const TypebotButton = ({
onTypebotUpdated,
onMouseDown,
}: Props) => {
const scopedT = useScopedI18n('folders.typebotButton')
const router = useRouter()
const { workspace } = useWorkspace()
const { draggedTypebot } = useTypebotDnd()
@@ -68,7 +73,6 @@ export const TypebotButton = ({
const { error } = await deleteTypebotQuery(typebot.id)
if (error)
return showToast({
title: "Couldn't delete typebot",
description: error.message,
})
onTypebotUpdated()
@@ -78,14 +82,13 @@ export const TypebotButton = ({
e.stopPropagation()
const { data } = await getTypebotQuery(typebot.id)
const typebotToDuplicate = data?.typebot
if (!typebotToDuplicate) return { error: new Error('Typebot not found') }
if (!typebotToDuplicate) return
const { data: createdTypebot, error } = await importTypebotQuery(
data.typebot,
workspace?.plan ?? Plan.FREE
)
if (error)
return showToast({
title: "Couldn't duplicate typebot",
description: error.message,
})
if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}/edit`)
@@ -153,14 +156,18 @@ export const TypebotButton = ({
pos="absolute"
top="20px"
right="20px"
aria-label={`Show ${typebot.name} menu`}
aria-label={scopedT('showMoreOptions')}
>
{typebot.publishedTypebotId && (
<MenuItem onClick={handleUnpublishClick}>Unpublish</MenuItem>
<MenuItem onClick={handleUnpublishClick}>
{scopedT('unpublish')}
</MenuItem>
)}
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
<MenuItem onClick={handleDuplicateClick}>
{scopedT('duplicate')}
</MenuItem>
<MenuItem color="red.400" onClick={handleDeleteClick}>
Delete
{scopedT('delete')}
</MenuItem>
</MoreButton>
</>
@@ -181,12 +188,17 @@ export const TypebotButton = ({
{!isReadOnly && (
<ConfirmModal
message={
<Text>
Are you sure you want to delete your Typebot &quot;{typebot.name}
&quot;.
<br />
All associated data will be lost.
</Text>
<Stack spacing="4">
<Text>
{scopedT('deleteConfirmationMessage', {
typebotName: <strong>{typebot.name}</strong>,
})}
</Text>
<Alert status="warning">
<AlertIcon />
{scopedT('deleteConfirmationMessageWarning')}
</Alert>
</Stack>
}
confirmButtonLabel="Delete"
onConfirm={handleDeleteTypebotClick}