* Edit Farsi translations (#788)

* Add a Ukrainian translation (#786)

* Add a Ukrainian translation

* Clarify some strings in the Ukrainian translation

* feat: change dock position (#774)

* feat: change dock position

* fix grid row and column

* add top position

* fix responsive for the top position

* change content side

* fix overflowing menu

* [improvement] theme on body (#790)

* Update Tldraw.tsx

* Add theme on body, adjust dark page options dialog

* fix test

* Preparing for global integration (#775)

* Update translations.ts

* Create en.json

* Make main translation default

* Remove unused locale property of translation

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* Fix language menu

* Update ar.json (#793)

* feature/add Hebrew translations (#792)

* hebrew translations

* pr fixes

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* fix toolspanel item position (#791)

* fix toolspanel item position

* add translation

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* Add remote caching

* Adds link to translation guide (#794)

Co-authored-by: Baahar Ebrahimi <108254874+Baahaarmast@users.noreply.github.com>
Co-authored-by: walking-octopus <46994949+walking-octopus@users.noreply.github.com>
Co-authored-by: Judicael <46365844+judicaelandria@users.noreply.github.com>
Co-authored-by: Ali Alhaidary <75235623+ali-alhaidary@users.noreply.github.com>
Co-authored-by: gadi246 <gadi246@gmail.com>
This commit is contained in:
Steve Ruiz 2022-07-07 11:59:47 +01:00 committed by GitHub
parent 4d900fb7fd
commit 88fbdacaea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 505 additions and 97 deletions

View file

@ -1 +1 @@
{}
{"teamId":"team_MjXkk3MAaGtBFaHcssVUiMgd","apiUrl":"https://api.vercel.com"}

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@
],
"scripts": {
"build": "turbo run build",
"build:www": "turbo run build:www --force",
"build:www": "turbo run build:www",
"build:core": "turbo run build:core",
"build:packages": "turbo run build:packages --stream",
"build:apps": "turbo run build:apps",

View file

@ -50,8 +50,8 @@
},
"devDependencies": {
"@swc-node/jest": "^1.4.3",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@tldraw/intersect": "*",
"@tldraw/vec": "*",
"@types/node": "^17.0.14",

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { mockDocument, mockUtils } from '~test'
import { render } from '@testing-library/react'
import { act, render } from '@testing-library/react'
import { Renderer } from './Renderer'
import { action, makeAutoObservable } from 'mobx'
import type { TLBinding, TLBounds, TLPage, TLPageState } from '~types'
@ -83,14 +83,15 @@ describe('When passing observables', () => {
const wrapper = render(<Renderer shapeUtils={mockUtils} page={page} pageState={pageState} />)
// PageState
expect(wrapper.getByTestId('layer')).toHaveProperty(
'style.transform',
`scale(1) translateX(0px) translateY(0px)`
)
act(() => {
// PageState
pageState.pan([10, 10])
})
expect(wrapper.getByTestId('layer')).toHaveProperty(
'style.transform',
@ -109,7 +110,9 @@ describe('When passing observables', () => {
rotate(0rad)`
)
act(() => {
page.moveShape('box1', [10, 10])
})
expect(wrapper.getByTestId('container')).toHaveProperty(
'style.transform',

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { Renderer } from '@tldraw/core'
import { defineMessages, IntlConfig, IntlProvider } from 'react-intl'
import { IntlProvider } from 'react-intl'
import { styled, dark } from '~styles'
import { TDDocument, TDStatus } from '~types'
import { TldrawApp, TDCallbacks } from '~state'
@ -443,9 +443,19 @@ const InnerTldraw = React.memo(function InnerTldraw({
const translation = useTranslation(settings.language)
// Put the theme on the body. This means that components with
// multiple editors cannot have different themes.
React.useLayoutEffect(() => {
if (settings.isDarkMode) {
window.document.body.classList.add(dark)
} else {
window.document.body.classList.remove(dark)
}
}, [settings.isDarkMode])
return (
<IntlProvider locale={translation.locale} messages={translation.messages}>
<StyledLayout ref={rWrapper} tabIndex={-0} className={settings.isDarkMode ? dark : ''}>
<StyledLayout ref={rWrapper} tabIndex={-0}>
<Loading />
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
<ContextMenu>

View file

@ -10,9 +10,17 @@ export interface DMContentProps {
sideOffset?: number
children: React.ReactNode
id?: string
side?: 'top' | 'left' | 'right' | 'bottom' | undefined
}
export function DMContent({ sideOffset = 8, children, align, variant, id }: DMContentProps) {
export function DMContent({
sideOffset = 8,
children,
align,
variant,
id,
side = 'bottom',
}: DMContentProps) {
return (
<DropdownMenu.Content
dir="ltr"
@ -21,6 +29,7 @@ export function DMContent({ sideOffset = 8, children, align, variant, id }: DMCo
onEscapeKeyDown={stopPropagation}
asChild
id={id}
side={side}
>
<StyledContent variant={variant}>{children}</StyledContent>
</DropdownMenu.Content>

View file

@ -20,8 +20,8 @@ export function DMSubMenu({ children, size, disabled = false, label, id }: DMSub
{label}
</RowButton>
</TriggerItem>
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
<MenuContent size={size}>
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2} align="start">
<MenuContent size={size} overflow>
{children}
<Arrow offset={13} />
</MenuContent>

View file

@ -20,5 +20,17 @@ export const MenuContent = styled('div', {
minWidth: 72,
},
},
overflow: {
true: {
maxHeight: '60vh',
overflowY: 'auto',
overflowX: 'hidden',
},
},
},
'-ms-overflow-style': 'none' /* for Internet Explorer, Edge */,
scrollbarWidth: 'none',
'&::webkit-scrollbar': {
display: 'none',
},
})

View file

@ -99,7 +99,8 @@ export const StyledRowButton = styled('button', {
background: 'none',
border: 'none',
cursor: 'pointer',
height: '32px',
height: 32,
minHeight: 32,
outline: 'none',
color: '$text',
fontFamily: '$ui',

View file

@ -30,6 +30,7 @@ const StyledInput = styled('input', {
width: '100%',
paddingLeft: '$3',
paddingRight: '$6',
backgroundColor: '$background',
height: '32px',
outline: 'none',
@ -48,4 +49,5 @@ const StyledInputIcon = styled(SmallIcon, {
paddingLeft: '$3',
paddingRight: '$3',
pointerEvents: 'none',
color: '$text',
})

View file

@ -34,6 +34,8 @@ import { Divider } from '~components/Primitives/Divider'
import { ToolButton } from '~components/Primitives/ToolButton'
import { useIntl } from 'react-intl'
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
const selectedShapesCountSelector = (s: TDSnapshot) =>
s.document.pageStates[s.appState.currentPageId].selectedIds.length
@ -77,8 +79,6 @@ export function ActionButton() {
const app = useTldrawApp()
const intl = useIntl()
const isFrenchLang = navigator.language === 'fr'
const isAllLocked = app.useStore(isAllLockedSelector)
const isAllAspectLocked = app.useStore(isAllAspectLockedSelector)
@ -91,6 +91,8 @@ export function ActionButton() {
const selectedShapesCount = app.useStore(selectedShapesCountSelector)
const dockPosition = app.useStore(dockPositionState)
const hasTwoOrMore = selectedShapesCount > 1
const hasThreeOrMore = selectedShapesCount > 2
@ -182,6 +184,8 @@ export function ActionButton() {
[app]
)
const contentSide = dockPosition === 'bottom' || dockPosition === 'top' ? 'top' : dockPosition
return (
<DropdownMenu.Root dir="ltr" onOpenChange={handleMenuOpenChange}>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-Tools-Dots">
@ -189,7 +193,7 @@ export function ActionButton() {
<DotsHorizontalIcon />
</ToolButton>
</DropdownMenu.Trigger>
<DMContent sideOffset={16}>
<DMContent sideOffset={16} side={contentSide}>
<>
<ButtonsRow>
<ToolButton variant="icon" disabled={!hasSelection} onClick={handleDuplicate}>

View file

@ -12,15 +12,33 @@ const isEmptyCanvasSelector = (s: TDSnapshot) => {
)
}
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
export const BackToContent = React.memo(function BackToContent() {
const app = useTldrawApp()
const isEmptyCanvas = app.useStore(isEmptyCanvasSelector)
const dockPosition = app.useStore(dockPositionState)
const isDebugMode = app.useStore(isDebugModeSelector)
const style = {
bottom:
dockPosition === 'bottom' && isDebugMode
? 120
: dockPosition === 'bottom'
? 80
: isDebugMode
? 60
: 20,
left: '50%',
transform: 'translate(-50%,0)',
}
if (!isEmptyCanvas) return null
return (
<BackToContentContainer id="TD-Tools-Back_to_content">
<BackToContentContainer id="TD-Tools-Back_to_content" style={{ ...style }}>
<RowButton onClick={app.zoomToContent}>Back to content</RowButton>
</BackToContentContainer>
)
@ -30,7 +48,9 @@ const BackToContentContainer = styled(MenuContent, {
pointerEvents: 'all',
width: 'fit-content',
minWidth: 0,
gridRow: 1,
flexGrow: 2,
display: 'block',
// gridRow: 1,
// flexGrow: 2,
// display: 'block',
position: 'fixed',
bottom: 0,
})

View file

@ -16,6 +16,7 @@ import { EraserIcon } from '~components/Primitives/icons'
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
const toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
export const PrimaryTools = React.memo(function PrimaryTools() {
const app = useTldrawApp()
@ -24,6 +25,7 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
const activeTool = app.useStore(activeToolSelector)
const isToolLocked = app.useStore(toolLockedSelector)
const dockPosition = app.useStore(dockPositionState)
const selectSelectTool = React.useCallback(() => {
app.selectTool('select')
@ -49,8 +51,10 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
app.selectTool(TDShapeType.Sticky)
}, [app])
const panelStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
return (
<Panel side="center" id="TD-PrimaryTools">
<Panel side="center" id="TD-PrimaryTools" style={{ flexDirection: panelStyle }}>
<ToolButtonWithTooltip
kbd={'1'}
label={intl.formatMessage({ id: 'select' })}

View file

@ -34,12 +34,12 @@ const shapeShapeIcons = {
[TDShapeType.Line]: <LineIcon />,
}
const statusSelector = (s: TDSnapshot) => s.appState.status
enum Status {
SpacePanning = 'spacePanning',
}
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
export const ShapesMenu = React.memo(function ShapesMenu({
activeTool,
isToolLocked,
@ -47,7 +47,7 @@ export const ShapesMenu = React.memo(function ShapesMenu({
const app = useTldrawApp()
const intl = useIntl()
const status = app.useStore(statusSelector)
const dockPosition = app.useStore(dockPositionState)
const [lastActiveTool, setLastActiveTool] = React.useState<ShapeShape>(TDShapeType.Rectangle)
@ -74,6 +74,9 @@ export const ShapesMenu = React.memo(function ShapesMenu({
}, [])
const isActive = shapeShapes.includes(activeTool as ShapeShape)
const contentSide = dockPosition === 'bottom' || dockPosition === 'top' ? 'top' : dockPosition
const panelStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
return (
<DropdownMenu.Root dir="ltr" onOpenChange={selectShapeTool}>
@ -89,8 +92,8 @@ export const ShapesMenu = React.memo(function ShapesMenu({
{shapeShapeIcons[lastActiveTool]}
</ToolButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content asChild dir="ltr" side="top" sideOffset={12}>
<Panel side="center">
<DropdownMenu.Content asChild dir="ltr" side={contentSide} sideOffset={12}>
<Panel side="center" style={{ flexDirection: panelStyle }}>
{shapeShapes.map((shape, i) => (
<Tooltip
key={shape}

View file

@ -1,7 +1,7 @@
import * as React from 'react'
import { styled } from '~styles'
import type { TDSnapshot } from '~types'
import { useTldrawApp } from '~hooks'
import { useMediaQuery, useTldrawApp } from '~hooks'
import { StatusBar } from './StatusBar'
import { BackToContent } from './BackToContent'
import { PrimaryTools } from './PrimaryTools'
@ -9,6 +9,7 @@ import { ActionButton } from './ActionButton'
import { DeleteButton } from './DeleteButton'
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
interface ToolsPanelProps {
onBlur?: React.FocusEventHandler
@ -17,12 +18,62 @@ interface ToolsPanelProps {
export const ToolsPanel = React.memo(function ToolsPanel({ onBlur }: ToolsPanelProps) {
const app = useTldrawApp()
const isDebugMode = app.useStore(isDebugModeSelector)
const dockPosition = app.useStore(dockPositionState)
const isMobile = useMediaQuery('(max-width: 900px)')
const bottomStyle = {
width: '100%',
height: 'min-content',
left: 0,
right: 0,
bottom: isDebugMode ? 40 : 0,
}
const topStyle = {
width: '100%',
height: 'min-content',
left: 0,
right: 0,
top: isMobile ? 60 : 10,
}
const rightStyle = { width: 'min-content', height: '100%', right: 0 }
const leftStyle = { width: 'min-content', height: '100%', left: 10 }
const toolStyle = () => {
switch (dockPosition) {
case 'bottom':
return bottomStyle
case 'left':
return leftStyle
case 'right':
return rightStyle
case 'top':
return topStyle
default:
return bottomStyle
}
}
const style = toolStyle()
const centerWrapStyle =
dockPosition === 'bottom' || dockPosition === 'top'
? { gridRow: 1, gridColumn: 2 }
: { gridRow: 2, gridColumn: 1 }
const primaryToolStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
return (
<StyledToolsPanelContainer onBlur={onBlur}>
<StyledCenterWrap id="TD-Tools">
<StyledToolsPanelContainer
style={{
...style,
}}
onBlur={onBlur}
>
<StyledCenterWrap
id="TD-Tools"
style={{
...centerWrapStyle,
}}
>
<BackToContent />
<StyledPrimaryTools>
<StyledPrimaryTools style={{ flexDirection: primaryToolStyle }}>
<ActionButton />
<PrimaryTools />
<DeleteButton />
@ -39,9 +90,6 @@ export const ToolsPanel = React.memo(function ToolsPanel({ onBlur }: ToolsPanelP
const StyledToolsPanelContainer = styled('div', {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
width: '100%',
minWidth: 0,
maxWidth: '100%',
@ -70,8 +118,12 @@ const StyledCenterWrap = styled('div', {
})
const StyledStatusWrap = styled('div', {
gridRow: 2,
gridColumn: '1 / span 3',
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
width: '100%',
maxWidth: '100%',
})
const StyledPrimaryTools = styled('div', {

View file

@ -1,36 +1,52 @@
import { ExternalLinkIcon } from '@radix-ui/react-icons'
import * as React from 'react'
import { useIntl } from 'react-intl'
import { DMCheckboxItem, DMSubMenu } from '~components/Primitives/DropdownMenu'
import { FormattedMessage, useIntl } from 'react-intl'
import { DMCheckboxItem, DMDivider, DMItem, DMSubMenu } from '~components/Primitives/DropdownMenu'
import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
import { SmallIcon } from '~components/Primitives/SmallIcon'
import { useTldrawApp } from '~hooks'
import { TDLanguage, TRANSLATIONS } from '~translations'
import { TDSnapshot } from '~types'
const settingsSelector = (s: TDSnapshot) => s.settings
const languageSelector = (s: TDSnapshot) => s.settings.language
export function LanguageMenu() {
const app = useTldrawApp()
const setting = app.useStore(settingsSelector)
const language = app.useStore(languageSelector)
const intl = useIntl()
const handleChangeLanguage = React.useCallback(
(code: TDLanguage) => {
app.setSetting('language', code)
(locale: TDLanguage) => {
app.setSetting('language', locale)
},
[app]
)
return (
<DMSubMenu label={intl.formatMessage({ id: 'language' })}>
{TRANSLATIONS.map(({ code, label }) => (
{TRANSLATIONS.map(({ locale, label }) => (
<DMCheckboxItem
key={code}
checked={setting.language === code}
onCheckedChange={() => handleChangeLanguage(code)}
id={`TD-MenuItem-Language-${code}`}
key={locale}
checked={language === locale}
onCheckedChange={() => handleChangeLanguage(locale)}
id={`TD-MenuItem-Language-${locale}`}
>
{label}
</DMCheckboxItem>
))}
<DMDivider />
<a
href="https://github.com/tldraw/tldraw/blob/develop/guides/translation.md"
target="_blank"
rel="nofollow"
>
<DMItem id="TD-MenuItem-Translation-Link">
<FormattedMessage id="translation.link" />
<SmallIcon>
<ExternalLinkIcon />
</SmallIcon>
</DMItem>
</a>
</DMSubMenu>
)
}

View file

@ -191,7 +191,7 @@ export const StyledDialogContent = styled(Dialog.Content, {
marginTop: '-5vh',
pointerEvents: 'all',
backgroundColor: '$panel',
padding: '$0',
padding: '$1',
borderRadius: '$2',
font: '$ui',
'&:focus': {

View file

@ -2,10 +2,13 @@ import * as React from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/Primitives/DropdownMenu'
import { useTldrawApp } from '~hooks'
import { TDSnapshot } from '~types'
import { TDDockPosition, TDSnapshot } from '~types'
import { styled } from '~styles'
const settingsSelector = (s: TDSnapshot) => s.settings
const DockPosition = ['bottom', 'left', 'right', 'top']
export function PreferencesMenu() {
const app = useTldrawApp()
const intl = useIntl()
@ -52,6 +55,13 @@ export function PreferencesMenu() {
app.setSetting('isCadSelectMode', (v) => !v)
}, [app])
const handleChangeDockPosition = React.useCallback(
(position: TDDockPosition) => {
app.setSetting('dockPosition', position)
},
[app]
)
return (
<DMSubMenu label={intl.formatMessage({ id: 'menu.preferences' })} id="TD-MenuItem-Preferences">
<DMCheckboxItem
@ -128,6 +138,24 @@ export function PreferencesMenu() {
>
<FormattedMessage id="preferences.clone.handles" />
</DMCheckboxItem>
<DMSubMenu label={intl.formatMessage({ id: 'dock.position' })}>
{DockPosition.map((position) => (
<DMCheckboxItem
key={position}
checked={settings.dockPosition === position}
onCheckedChange={() => handleChangeDockPosition(position as TDDockPosition)}
id={`TD-MenuItem-DockPosition-${position}`}
>
<StyledText>
<FormattedMessage id={position} />
</StyledText>
</DMCheckboxItem>
))}
</DMSubMenu>
</DMSubMenu>
)
}
const StyledText = styled('span', {
textTransform: 'capitalize',
})

View file

@ -5,3 +5,4 @@ export * from './useStylesheet'
export * from './useFileSystemHandlers'
export * from './useFileSystem'
export * from './useTranslation'
export * from './useMediaQuery'

View file

@ -0,0 +1,19 @@
import * as React from 'react'
export function useMediaQuery(query: string) {
const [matches, setMatches] = React.useState(false)
React.useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => setMatches(media.matches)
window.addEventListener('resize', listener)
return () => window.removeEventListener('resize', listener)
}, [matches, query])
return matches
}
export default useMediaQuery

View file

@ -1,10 +1,8 @@
import * as React from 'react'
import { getTranslation, TDLanguage } from '../translations/translations'
export function useTranslation(code?: TDLanguage) {
export function useTranslation(locale?: TDLanguage) {
return React.useMemo(() => {
const locale = code ?? navigator.language.split(/[-_]/)[0]
return getTranslation(locale)
}, [code])
return getTranslation(locale ?? navigator.language.split(/[-_]/)[0])
}, [locale])
}

View file

@ -4168,6 +4168,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
showCloneHandles: false,
showGrid: false,
language: 'en',
dockPosition: 'bottom',
},
appState: {
status: TDStatus.Idle,

View file

@ -53,6 +53,7 @@ TldrawTestApp {
"version": 15.3,
},
"settings": Object {
"dockPosition": "bottom",
"isCadSelectMode": false,
"isDarkMode": false,
"isDebugMode": false,
@ -203,6 +204,7 @@ TldrawTestApp {
"version": 15.3,
},
"settings": Object {
"dockPosition": "bottom",
"isCadSelectMode": false,
"isDarkMode": false,
"isDebugMode": false,
@ -374,6 +376,7 @@ TldrawTestApp {
"version": 15.3,
},
"settings": Object {
"dockPosition": "bottom",
"isCadSelectMode": false,
"isDarkMode": false,
"isDebugMode": false,

View file

@ -86,5 +86,10 @@
"forward": "للخلف",
"backward": "للوراء",
"back": "خلف",
"language": "لغة"
"language": "لغة",
"dock.position": "موقع الادوات",
"bottom": "اسفل",
"left": "يسار",
"right": "يمين",
"top": "أعلى"
}

View file

@ -0,0 +1 @@
{}

View file

@ -18,7 +18,7 @@
"menu.preferences": "تنظیم‌ها",
"menu.sign.in": "ورود",
"menu.sign.out": "خروج",
"sponsored": مایت‌شده",
"sponsored": امیان",
"become.a.sponsor": " حامی شو",
"zoom.to.selection": "نمایش انتخاب‌شده‌ها",
"zoom.to.fit": "نمایش کل صفحه",

View file

@ -87,5 +87,10 @@
"forward": "Au premier plan",
"backward": "En arrière plan",
"back": "À l'arrière",
"language": "Langage"
"language": "Langage",
"dock.position": "Position du dock",
"bottom": "En bas",
"left": "À Gauche",
"right": "À Droite",
"top": "En Haut"
}

View file

@ -0,0 +1,90 @@
{
"style.menu.color": "צבע",
"style.menu.fill": "מלא",
"style.menu.dash": "גבול",
"style.menu.size": "גודל",
"style.menu.keep.open": "השאר פתוח",
"style.menu.font": "גופן",
"style.menu.align": "יישור",
"styles": "עיצוב",
"zoom.in": "הגדל תצוגה",
"zoom.out": "הקטן תצוגה",
"to": "ל",
"to.selection": "לסימון",
"to.fit": "להתאמה",
"menu.file": "קובץ",
"menu.edit": "עריכה",
"menu.view": "תצוגה",
"menu.preferences": "מאפיינים",
"menu.sign.in": "הירשם",
"menu.sign.out": "התנתק",
"sponsored": "חסות",
"become.a.sponsor": "מתן חסות",
"zoom.to.selection": "זום לבחירה",
"zoom.to.fit": "זום להתאמה",
"zoom.to": "זום ל",
"preferences.dark.mode": "מצב חשוך",
"preferences.focus.mode": "מצב פוקוס",
"preferences.debug.mode": "מצב דיבאג",
"preferences.show.grid": "(גריד)הראה רשת עימוד",
"preferences.use.cad.selection": "סימון CAD",
"preferences.keep.stylemenu.open": "השאר תפריט עיצוב פתוח",
"preferences.always.show.snaps": "הראה קווי מתאר",
"preferences.rotate.handles": "הראה ידיות סיבוב",
"preferences.binding.handles": "הראה ידיות קשירה",
"preferences.clone.handles": "הראה ידיות שיכפול",
"undo": "בטל",
"redo": "עשה מחדש",
"cut": "גזור",
"copy": "העתק",
"paste": "הדבק",
"copy.as": "העתק כ",
"export.as": "ייצא כ",
"select.all": "בחר הכל",
"select.none": "בטל בחירה",
"delete": "מחק",
"new.project": "פרויקט חדש",
"open": "פתח",
"save": "שמור",
"save.as": "שמור כ",
"upload.media": "העלאת מדיה",
"create.page": "צור דף",
"new.page": "דף חדש",
"page.name": "שם הדף",
"duplicate": "שכפל",
"cancel": "בטל",
"copy.invite.link": "העתק קישור הזמנה",
"create.multiplayer.project": "צור פרויקט רב משתתפים",
"copy.multiplayer.project": "העתק לפרויקט רב משתתפים",
"select": "סמן",
"eraser": "מחק",
"draw": "צייר",
"arrow": "חץ",
"text": "טקסט",
"sticky": "דביקי",
"Rectangle": "מרובע",
"Ellipse": "אליפסה",
"Triangle": "משולש",
"Line": "קו",
"rotate": "סובב",
"lock.aspect.ratio": "נעל יחס רוחב-גובה",
"unlock.aspect.ratio": "שחרר נעילת יחס רוחב-גובה",
"group": "קבץ",
"ungroup": "בטל קיבוץ",
"move.to.back": "הבא לתחתית",
"move.backward": "הזז אחורה",
"move.forward": "הזז קדימה",
"move.to.front": "הבא לחזית",
"reset.angle": "אפס זווית",
"lock": "נעל",
"unlock": "שחרר נעילה",
"move.to.page": "הזז לדף",
"flip.horizontal": "הפוך אופקית",
"flip.vertical": "הפוך אנכית",
"move": "הזז",
"to.front": "הבא לקדימה",
"forward": "קדימה",
"backward": "אחורה",
"back": "בחזרה",
"language": "שפה"
}

View file

@ -87,5 +87,10 @@
"forward": "Sposta avanti",
"backward": "Sposta indietro",
"back": "In fondo",
"language": "Lingua"
"language": "Lingua",
"dock.position": "Posizione dock",
"bottom": "In basso",
"left": "Sinistra",
"right": "Destra",
"top": "In Alto"
}

View file

@ -51,7 +51,6 @@
"create.page": "Create Page",
"new.page": "New Page",
"page.name": "Page Name",
"page": "Page",
"duplicate": "Duplicate",
"cancel": "Cancel",
"copy.invite.link": "Copy Invite Link",
@ -87,5 +86,11 @@
"forward": "Forward",
"backward": "Backward",
"back": "Back",
"language": "Language"
"language": "Language",
"translation.link": "Learn More",
"dock.position": "Dock Position",
"bottom": "Bottom",
"left": "Left",
"right": "Right",
"top": "Top"
}

View file

@ -1,19 +1,22 @@
import ar from './ar.json'
import da from './da.json'
import de from './de.json'
import en from './main.json'
import en from './en.json'
import es from './es.json'
import fa from './fa.json'
import fr from './fr.json'
import he from './he.json'
import it from './it.json'
import ja from './ja.json'
import ko_kr from './ko-kr.json'
import main from './main.json'
import ne from './ne.json'
import no from './no.json'
import pl from './pl.json'
import pt_br from './pt-br.json'
import ru from './ru.json'
import tr from './tr.json'
import uk from './uk.json'
import zh_cn from './zh-cn.json'
// The default language (english) must have a value for every message.
@ -22,49 +25,50 @@ import zh_cn from './zh-cn.json'
// translation instead.
export const TRANSLATIONS: TDTranslations = [
{ code: 'ar', locale: 'ar', label: 'عربي', messages: ar },
{ code: 'en', locale: 'en', label: 'English', messages: en },
{ code: 'es', locale: 'es', label: 'Español', messages: es },
{ code: 'fr', locale: 'fr', label: 'Français', messages: fr },
{ code: 'fa', locale:'fa', label: 'فارسی', messages: fa },
{ code: 'it', locale: 'it', label: 'Italiano', messages: it },
{ code: 'ja', locale: 'ja', label: '日本語', messages: ja },
{ code: 'ko-kr', locale: 'ko-kr', label: '한국어', messages: ko_kr },
{ code: 'ne', locale: 'ne', label: 'नेपाली', messages: ne },
{ code: 'no', locale: 'no', label: 'Norwegian', messages: no },
{ code: 'pl', locale: 'pl', label: 'Polski', messages: pl },
{ code: 'pt-br', locale: 'pt-br', label: 'Português - Brasil', messages: pt_br },
{ code: 'tr', locale: 'tr', label: 'Türkçe', messages: tr },
{ code: 'zh-cn', locale: 'zh-ch', label: 'Chinese - Simplified', messages: zh_cn },
{ code: 'da', locale: 'da', label: 'Danish', messages: da },
{ code: 'de', locale: 'de', label: 'Deutsch', messages: de},
{ code: 'ru', locale: 'ru', label: 'Russian', messages: ru },
{ locale: 'ar', label: 'عربي', messages: ar },
{ locale: 'da', label: 'Danish', messages: da },
{ locale: 'de', label: 'Deutsch', messages: de },
{ locale: 'en', label: 'English', messages: en },
{ locale: 'es', label: 'Español', messages: es },
{ locale: 'fa', label: 'فارسی', messages: fa },
{ locale: 'fr', label: 'Français', messages: fr },
{ locale: 'he', label: 'עברית', messages: he },
{ locale: 'it', label: 'Italiano', messages: it },
{ locale: 'ja', label: '日本語', messages: ja },
{ locale: 'ko-kr', label: '한국어', messages: ko_kr },
{ locale: 'ne', label: 'नेपाली', messages: ne },
{ locale: 'no', label: 'Norwegian', messages: no },
{ locale: 'pl', label: 'Polski', messages: pl },
{ locale: 'pt-br', label: 'Português - Brasil', messages: pt_br },
{ locale: 'ru', label: 'Russian', messages: ru },
{ locale: 'tr', label: 'Türkçe', messages: tr },
{ locale: 'uk', label: 'Ukrainian', messages: uk },
{ locale: 'zh-ch', label: 'Chinese - Simplified', messages: zh_cn },
]
/* ----------------- (do not change) ---------------- */
TRANSLATIONS.sort((a, b) => (a.code < b.code ? -1 : 1))
TRANSLATIONS.sort((a, b) => (a.locale < b.locale ? -1 : 1))
export type TDTranslation = {
readonly code: string
readonly label: string
readonly locale: string
readonly label: string
readonly messages: Partial<typeof en>
}
export type TDTranslations = TDTranslation[]
export type TDLanguage = TDTranslations[number]['code']
export type TDLanguage = TDTranslations[number]['locale']
export function getTranslation(code: TDLanguage): TDTranslation {
const translation = TRANSLATIONS.find((t) => t.code === code)
export function getTranslation(locale: TDLanguage): TDTranslation {
const translation = TRANSLATIONS.find((t) => t.locale === locale)
const defaultTranslation = TRANSLATIONS.find((t) => t.code === 'en')!
const messages = {
...defaultTranslation.messages,
return {
locale,
label: translation?.label ?? locale,
messages: {
...main,
...translation?.messages,
},
}
return { code, messages, locale: code, label: translation?.label ?? code }
}

View file

@ -0,0 +1,90 @@
{
"style.menu.color": "Колір",
"style.menu.fill": "Заповнювати",
"style.menu.dash": "Штрих",
"style.menu.size": "Розмір",
"style.menu.keep.open": "Тримати відкритим",
"style.menu.font": "Шрифт",
"style.menu.align": "Вирівняти",
"styles": "Стиль",
"zoom.in": "Збільшити",
"zoom.out": "Зменшити",
"to": "до",
"to.selection": "До виділення",
"to.fit": "За розміром екрану",
"menu.file": "Файл",
"menu.edit": "Редагування",
"menu.view": "Вигляд",
"menu.preferences": "Налаштування",
"menu.sign.in": "Увійти",
"menu.sign.out": "Вийти",
"sponsored": "Спонсовано",
"become.a.sponsor": "Стати спонсором",
"zoom.to.selection": "Наблизити до виділення",
"zoom.to.fit": "Збільшити за розміром екрану",
"zoom.to": "Наблизити до",
"preferences.dark.mode": "Темна тема",
"preferences.focus.mode": "Мінімалістичний режим",
"preferences.debug.mode": "Режим налагодження",
"preferences.show.grid": "Показати сітку",
"preferences.use.cad.selection": "Використовувати CAD виділення",
"preferences.keep.stylemenu.open": "Тримати меню стилів відкритим",
"preferences.always.show.snaps": "Завжди показувати прив'язки",
"preferences.rotate.handles": "Ручки обертання",
"preferences.binding.handles": "Ручки прив'язки",
"preferences.clone.handles": "Ручки клонування",
"undo": "Скасувати",
"redo": "Повторити",
"cut": "Вирізати",
"copy": "Скопіювати",
"paste": "Вставити",
"copy.as": "Скопіювати як",
"export.as": "Експортувати як",
"select.all": "Обрати все",
"select.none": "Зняти виділення",
"delete": "Видалити",
"new.project": "Новий проект",
"open": "Відкрити",
"save": "Зберегти",
"save.as": "Зберегти як",
"upload.media": "Завантажити медіа",
"create.page": "Створити сторінку",
"new.page": "Нова сторінка",
"page.name": "Назва сторінки",
"duplicate": "Дублювати",
"cancel": "Скасувати",
"copy.invite.link": "Скопіювати посилання на запрошення",
"create.multiplayer.project": "Створити багатокористувацький проект",
"copy.multiplayer.project": "Скопіювати в багатокористувацький проект",
"select": "Вибирати",
"eraser": "Ластик",
"draw": "Малювати",
"arrow": "Стрілка",
"text": "Текст",
"sticky": "Нотатка",
"Rectangle": "Прямокутник",
"Ellipse": "Еліпс",
"Triangle": "Трикутник",
"Line": " Лінія",
"rotate": "Повернути",
"lock.aspect.ratio": "Заблокувати співвідношення сторін",
"unlock.aspect.ratio": " Розблокувати співвідношення сторін",
"group": "Згрупувати",
"ungroup": " Розгрупувати",
"move.to.back": "Перемістити назад",
"move.backward": "Перемістити на задній план",
"move.forward": "Перемістити вперед",
"move.to.front": "Перемістити на передній план",
"reset.angle": "Скидання кута",
"lock": "Блокування",
"unlock": " Розблокування",
"move.to.page": "Перейти на сторінку",
"flip.horizontal": "Перевернути горизонтально",
"flip.vertical": "Перевернути вертикально",
"move": "Перемістити",
"to.front": "На передній план",
"forward": " Вперед",
"backward": "На задній план",
"back": "Назад",
"language": "Мова"
}

View file

@ -77,6 +77,8 @@ export class TDEventHandler {
onShapeClone?: TLShapeCloneHandler
}
export type TDDockPosition = 'bottom' | 'left' | 'right' | 'top'
// The shape of the TldrawApp's React (zustand) store
export interface TDSnapshot {
settings: {
@ -96,6 +98,7 @@ export interface TDSnapshot {
showCloneHandles: boolean
showGrid: boolean
language: TDLanguage
dockPosition: TDDockPosition
}
appState: {
currentStyle: ShapeStyles

View file

@ -1,3 +1,17 @@
import '@testing-library/jest-dom/extend-expect'
import 'fake-indexeddb/auto'
global.ResizeObserver = require('resize-observer-polyfill')
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})