1.20.0 (#797)
* 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) * Update ar.json (#795) * [feature] readonly link (#796) * Copy readonly link * Update [id].tsx * Add readonly label * update psuedohash * Update utils.ts 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:
parent
5ac091339a
commit
315112459c
11 changed files with 343 additions and 24 deletions
71
apps/www/components/ReadOnlyMultiplayerEditor.tsx
Normal file
71
apps/www/components/ReadOnlyMultiplayerEditor.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { RoomProvider } from '../utils/liveblocks'
|
||||||
|
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||||
|
import { useAccountHandlers } from 'hooks/useAccountHandlers'
|
||||||
|
import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets'
|
||||||
|
import { useMultiplayerState } from 'hooks/useMultiplayerState'
|
||||||
|
import { useUploadAssets } from 'hooks/useUploadAssets'
|
||||||
|
import React, { FC } from 'react'
|
||||||
|
import { styled } from 'styles'
|
||||||
|
import { useReadOnlyMultiplayerState } from 'hooks/useReadOnlyMultiplayerState'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
roomId: string
|
||||||
|
isUser: boolean
|
||||||
|
isSponsor: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReadOnlyMultiplayerEditor: FC<Props> = ({
|
||||||
|
roomId,
|
||||||
|
isUser = false,
|
||||||
|
isSponsor = false,
|
||||||
|
}: {
|
||||||
|
roomId: string
|
||||||
|
isUser: boolean
|
||||||
|
isSponsor: boolean
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<RoomProvider id={roomId}>
|
||||||
|
<ReadOnlyEditor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
|
||||||
|
</RoomProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner Editor
|
||||||
|
|
||||||
|
function ReadOnlyEditor({ roomId, isUser, isSponsor }: Props) {
|
||||||
|
const { onSaveProjectAs, onSaveProject } = useFileSystem()
|
||||||
|
const { onSignIn, onSignOut } = useAccountHandlers()
|
||||||
|
const { error, ...events } = useReadOnlyMultiplayerState(roomId)
|
||||||
|
|
||||||
|
if (error) return <LoadingScreen>Error: {error.message}</LoadingScreen>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tldraw">
|
||||||
|
<Tldraw
|
||||||
|
autofocus
|
||||||
|
disableAssets={false}
|
||||||
|
showPages={false}
|
||||||
|
showSponsorLink={!isSponsor}
|
||||||
|
onSignIn={isSponsor ? undefined : onSignIn}
|
||||||
|
onSignOut={isUser ? onSignOut : undefined}
|
||||||
|
onSaveProjectAs={onSaveProjectAs}
|
||||||
|
onSaveProject={onSaveProject}
|
||||||
|
readOnly
|
||||||
|
{...events}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadOnlyMultiplayerEditor
|
||||||
|
|
||||||
|
const LoadingScreen = styled('div', {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
})
|
151
apps/www/hooks/useReadOnlyMultiplayerState.ts
Normal file
151
apps/www/hooks/useReadOnlyMultiplayerState.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import React, { useState, useRef, useCallback } from 'react'
|
||||||
|
import type { TldrawApp, TDUser, TDShape, TDBinding, TDAsset } from '@tldraw/tldraw'
|
||||||
|
import { Storage, useRedo, useUndo, useRoom, useUpdateMyPresence } from '../utils/liveblocks'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { LiveMap } from '@liveblocks/client'
|
||||||
|
|
||||||
|
declare const window: Window & { app: TldrawApp }
|
||||||
|
|
||||||
|
export function useReadOnlyMultiplayerState(roomId: string) {
|
||||||
|
const [app, setApp] = useState<TldrawApp>()
|
||||||
|
const [error, setError] = useState<Error>()
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const room = useRoom()
|
||||||
|
const onUndo = useUndo()
|
||||||
|
const onRedo = useRedo()
|
||||||
|
const updateMyPresence = useUpdateMyPresence()
|
||||||
|
|
||||||
|
const rIsPaused = useRef(false)
|
||||||
|
|
||||||
|
const rLiveShapes = useRef<Storage['shapes']>()
|
||||||
|
const rLiveBindings = useRef<Storage['bindings']>()
|
||||||
|
const rLiveAssets = useRef<Storage['assets']>()
|
||||||
|
|
||||||
|
// Callbacks --------------
|
||||||
|
|
||||||
|
// Put the state into the window, for debugging.
|
||||||
|
const onMount = useCallback(
|
||||||
|
(app: TldrawApp) => {
|
||||||
|
app.loadRoom(roomId)
|
||||||
|
app.pause() // Turn off the app's own undo / redo stack
|
||||||
|
window.app = app
|
||||||
|
setApp(app)
|
||||||
|
},
|
||||||
|
[roomId]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle presence updates when the user's pointer / selection changes
|
||||||
|
const onChangePresence = useCallback(
|
||||||
|
(app: TldrawApp, user: TDUser) => {
|
||||||
|
updateMyPresence({ id: app.room?.userId, user })
|
||||||
|
},
|
||||||
|
[updateMyPresence]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document Changes --------
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubs: (() => void)[] = []
|
||||||
|
if (!(app && room)) return
|
||||||
|
// Handle errors
|
||||||
|
unsubs.push(room.subscribe('error', (error) => setError(error)))
|
||||||
|
|
||||||
|
// Handle changes to other users' presence
|
||||||
|
unsubs.push(
|
||||||
|
room.subscribe('others', (others, event) => {
|
||||||
|
if (event.type === 'leave') {
|
||||||
|
if (event.user.presence) {
|
||||||
|
app?.removeUser(event.user.presence.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.updateUsers(
|
||||||
|
others
|
||||||
|
.toArray()
|
||||||
|
.filter((other) => other.presence)
|
||||||
|
.map((other) => other.presence!.user)
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
let stillAlive = true
|
||||||
|
|
||||||
|
// Setup the document's storage and subscriptions
|
||||||
|
async function setupDocument() {
|
||||||
|
const storage = await room.getStorage()
|
||||||
|
|
||||||
|
// Migrate previous versions
|
||||||
|
const version = storage.root.get('version')
|
||||||
|
|
||||||
|
// Initialize (get or create) maps for shapes/bindings/assets
|
||||||
|
|
||||||
|
let lShapes = storage.root.get('shapes')
|
||||||
|
if (!lShapes || !('_serialize' in lShapes)) {
|
||||||
|
storage.root.set('shapes', new LiveMap())
|
||||||
|
lShapes = storage.root.get('shapes')
|
||||||
|
}
|
||||||
|
rLiveShapes.current = lShapes
|
||||||
|
|
||||||
|
let lBindings = storage.root.get('bindings')
|
||||||
|
if (!lBindings || !('_serialize' in lBindings)) {
|
||||||
|
storage.root.set('bindings', new LiveMap())
|
||||||
|
lBindings = storage.root.get('bindings')
|
||||||
|
}
|
||||||
|
rLiveBindings.current = lBindings
|
||||||
|
|
||||||
|
let lAssets = storage.root.get('assets')
|
||||||
|
if (!lAssets || !('_serialize' in lAssets)) {
|
||||||
|
storage.root.set('assets', new LiveMap())
|
||||||
|
lAssets = storage.root.get('assets')
|
||||||
|
}
|
||||||
|
rLiveAssets.current = lAssets
|
||||||
|
|
||||||
|
// Save the version number for future migrations
|
||||||
|
storage.root.set('version', 2.1)
|
||||||
|
|
||||||
|
// Subscribe to changes
|
||||||
|
const handleChanges = () => {
|
||||||
|
app?.replacePageContent(
|
||||||
|
Object.fromEntries(lShapes.entries()),
|
||||||
|
Object.fromEntries(lBindings.entries()),
|
||||||
|
Object.fromEntries(lAssets.entries())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stillAlive) {
|
||||||
|
unsubs.push(room.subscribe(lShapes, handleChanges))
|
||||||
|
|
||||||
|
// Update the document with initial content
|
||||||
|
handleChanges()
|
||||||
|
|
||||||
|
// Zoom to fit the content
|
||||||
|
app.zoomToFit()
|
||||||
|
if (app.zoom > 1) {
|
||||||
|
app.resetZoom()
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDocument()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stillAlive = false
|
||||||
|
unsubs.forEach((unsub) => unsub())
|
||||||
|
}
|
||||||
|
}, [room, app])
|
||||||
|
|
||||||
|
return {
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
onMount,
|
||||||
|
onChangePresence,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
}
|
||||||
|
}
|
31
apps/www/pages/v/[id].tsx
Normal file
31
apps/www/pages/v/[id].tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import type { GetServerSideProps } from 'next'
|
||||||
|
import { getSession } from 'next-auth/react'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { Utils } from '@tldraw/core'
|
||||||
|
const ReadOnlyMultiplayerEditor = dynamic(() => import('components/ReadOnlyMultiplayerEditor'), {
|
||||||
|
ssr: false,
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
interface RoomProps {
|
||||||
|
id: string
|
||||||
|
isSponsor: boolean
|
||||||
|
isUser: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Room({ id, isUser, isSponsor }: RoomProps) {
|
||||||
|
return <ReadOnlyMultiplayerEditor isUser={isUser} isSponsor={isSponsor} roomId={id} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
const session = await getSession(context)
|
||||||
|
const id = context.query.id?.toString()
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
id: Utils.lns(id),
|
||||||
|
isUser: session?.user ? true : false,
|
||||||
|
isSponsor: session?.isSponsor ?? false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1485,6 +1485,21 @@ left past the initial left edge) then swap points on that axis.
|
||||||
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
|
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
|
||||||
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
|
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reversable psuedo hash.
|
||||||
|
* @param str string
|
||||||
|
*/
|
||||||
|
static lns(str: string) {
|
||||||
|
const result = str
|
||||||
|
.split('')
|
||||||
|
.map((n) => (Number.isNaN(+n) ? n : +n < 5 ? 5 + +n : +n > 5 ? +n - 5 : +n))
|
||||||
|
result.push(...result.splice(0, Math.round(result.length / 5)))
|
||||||
|
result.push(...result.splice(0, Math.round(result.length / 4)))
|
||||||
|
result.push(...result.splice(0, Math.round(result.length / 3)))
|
||||||
|
result.push(...result.splice(0, Math.round(result.length / 2)))
|
||||||
|
return result.reverse().join('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Utils
|
export default Utils
|
||||||
|
|
|
@ -19,10 +19,23 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
|
||||||
|
|
||||||
const [copied, setCopied] = React.useState(false)
|
const [copied, setCopied] = React.useState(false)
|
||||||
|
|
||||||
|
const rTimeout = React.useRef<any>(0)
|
||||||
|
|
||||||
const handleCopySelect = React.useCallback(() => {
|
const handleCopySelect = React.useCallback(() => {
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
TLDR.copyStringToClipboard(window.location.href)
|
TLDR.copyStringToClipboard(window.location.href)
|
||||||
setTimeout(() => setCopied(false), 1200)
|
clearTimeout(rTimeout.current)
|
||||||
|
rTimeout.current = setTimeout(() => setCopied(false), 1200)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCopyReadOnlySelect = React.useCallback(() => {
|
||||||
|
setCopied(true)
|
||||||
|
const segs = window.location.href.split('/')
|
||||||
|
segs[segs.length - 2] = 'v'
|
||||||
|
segs[segs.length - 1] = Utils.lns(segs[segs.length - 1])
|
||||||
|
TLDR.copyStringToClipboard(segs.join('/'))
|
||||||
|
clearTimeout(rTimeout.current)
|
||||||
|
rTimeout.current = setTimeout(() => setCopied(false), 1200)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleCreateMultiplayerProject = React.useCallback(async () => {
|
const handleCreateMultiplayerProject = React.useCallback(async () => {
|
||||||
|
@ -103,6 +116,14 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
|
||||||
<FormattedMessage id="copy.invite.link" />
|
<FormattedMessage id="copy.invite.link" />
|
||||||
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
|
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
|
||||||
</DMItem>
|
</DMItem>
|
||||||
|
<DMItem
|
||||||
|
id="TD-Multiplayer-CopyReadOnlyLink"
|
||||||
|
onClick={handleCopyReadOnlySelect}
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="copy.readonly.link" />
|
||||||
|
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
|
||||||
|
</DMItem>
|
||||||
<DMDivider id="TD-Multiplayer-CopyInviteLinkDivider" />
|
<DMDivider id="TD-Multiplayer-CopyInviteLinkDivider" />
|
||||||
<DMItem
|
<DMItem
|
||||||
id="TD-Multiplayer-CreateMultiplayerProject"
|
id="TD-Multiplayer-CreateMultiplayerProject"
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { StyleMenu } from './StyleMenu'
|
||||||
import { Panel } from '~components/Primitives/Panel'
|
import { Panel } from '~components/Primitives/Panel'
|
||||||
import { ToolButton } from '~components/Primitives/ToolButton'
|
import { ToolButton } from '~components/Primitives/ToolButton'
|
||||||
import { RedoIcon, UndoIcon } from '~components/Primitives/icons'
|
import { RedoIcon, UndoIcon } from '~components/Primitives/icons'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
|
||||||
import { useTldrawApp } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { MultiplayerMenu } from './MultiplayerMenu'
|
import { MultiplayerMenu } from './MultiplayerMenu'
|
||||||
|
|
||||||
|
@ -44,12 +43,19 @@ export function TopPanel({
|
||||||
<StyledSpacer />
|
<StyledSpacer />
|
||||||
{(showStyles || showZoom) && (
|
{(showStyles || showZoom) && (
|
||||||
<Panel side="right">
|
<Panel side="right">
|
||||||
<ToolButton>
|
{app.readOnly ? (
|
||||||
<UndoIcon onClick={app.undo} />
|
<ReadOnlyLabel>Read Only</ReadOnlyLabel>
|
||||||
</ToolButton>
|
) : (
|
||||||
<ToolButton>
|
<>
|
||||||
<RedoIcon onClick={app.redo} />
|
{' '}
|
||||||
</ToolButton>
|
<ToolButton>
|
||||||
|
<UndoIcon onClick={app.undo} />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton>
|
||||||
|
<RedoIcon onClick={app.redo} />
|
||||||
|
</ToolButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{showZoom && <ZoomMenu />}
|
{showZoom && <ZoomMenu />}
|
||||||
{showStyles && !readOnly && <StyleMenu />}
|
{showStyles && !readOnly && <StyleMenu />}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
@ -76,3 +82,15 @@ const StyledSpacer = styled('div', {
|
||||||
flexGrow: 2,
|
flexGrow: 2,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ReadOnlyLabel = styled('div', {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontFamily: '$ui',
|
||||||
|
fontSize: '$1',
|
||||||
|
paddingLeft: '$4',
|
||||||
|
paddingRight: '$1',
|
||||||
|
userSelect: 'none',
|
||||||
|
})
|
||||||
|
|
|
@ -1230,8 +1230,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
const doc = TldrawApp.defaultDocument
|
const doc = TldrawApp.defaultDocument
|
||||||
|
|
||||||
// Set the default page name to the localized version of "Page"
|
// Set the default page name to the localized version of "Page"
|
||||||
const translation = getTranslation(this.settings.language)
|
doc.pages['page'].name = 'Page 1'
|
||||||
doc.pages['page'].name = translation.messages['page'] + ' 1' ?? 'Page 1'
|
|
||||||
|
|
||||||
this.resetHistory()
|
this.resetHistory()
|
||||||
.clearSelectHistory()
|
.clearSelectHistory()
|
||||||
|
|
|
@ -244,9 +244,20 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
// Pointer Events (generic)
|
// Pointer Events (generic)
|
||||||
|
|
||||||
onPointerMove: TLPointerEventHandler = () => {
|
onPointerMove: TLPointerEventHandler = () => {
|
||||||
if (this.app.readOnly) return
|
|
||||||
const { originPoint, currentPoint } = this.app
|
const { originPoint, currentPoint } = this.app
|
||||||
|
|
||||||
|
if (this.app.readOnly && this.app.isPointing) {
|
||||||
|
if (this.app.session) {
|
||||||
|
this.app.updateSession()
|
||||||
|
} else {
|
||||||
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
||||||
|
this.app.startSession(SessionType.Brush)
|
||||||
|
this.setStatus(Status.Brushing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch (this.status) {
|
switch (this.status) {
|
||||||
case Status.PointingBoundsHandle: {
|
case Status.PointingBoundsHandle: {
|
||||||
if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')
|
if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"backward": "للوراء",
|
"backward": "للوراء",
|
||||||
"back": "خلف",
|
"back": "خلف",
|
||||||
"language": "لغة",
|
"language": "لغة",
|
||||||
|
"translation.link": "للمزيد",
|
||||||
"dock.position": "موقع الادوات",
|
"dock.position": "موقع الادوات",
|
||||||
"bottom": "اسفل",
|
"bottom": "اسفل",
|
||||||
"left": "يسار",
|
"left": "يسار",
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
"style.menu.fill": "Fill",
|
"style.menu.fill": "Fill",
|
||||||
"style.menu.dash": "Dash",
|
"style.menu.dash": "Dash",
|
||||||
"style.menu.size": "Size",
|
"style.menu.size": "Size",
|
||||||
"style.menu.keep.open": "Keep open",
|
"style.menu.keep.open": "Keep Open",
|
||||||
"style.menu.font": "Font",
|
"style.menu.font": "Font",
|
||||||
"style.menu.align": "Align",
|
"style.menu.align": "Align",
|
||||||
"styles": "Styles",
|
"styles": "Styles",
|
||||||
"zoom.in": "Zoom in",
|
"zoom.in": "Zoom In",
|
||||||
"zoom.out": "Zoom out",
|
"zoom.out": "Zoom Out",
|
||||||
"to": "to",
|
"to": "To",
|
||||||
"to.selection": "To selection",
|
"to.selection": "To Selection",
|
||||||
"to.fit": "To fit",
|
"to.fit": "To Fit",
|
||||||
"menu.file": "File",
|
"menu.file": "File",
|
||||||
"menu.edit": "Edit",
|
"menu.edit": "Edit",
|
||||||
"menu.view": "View",
|
"menu.view": "View",
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
"menu.sign.in": "Sign In",
|
"menu.sign.in": "Sign In",
|
||||||
"menu.sign.out": "Sign Out",
|
"menu.sign.out": "Sign Out",
|
||||||
"sponsored": "Sponsored",
|
"sponsored": "Sponsored",
|
||||||
"become.a.sponsor": "Become a sponsor",
|
"become.a.sponsor": "Become a Sponsor",
|
||||||
"zoom.to.selection": "Zoom to Selection",
|
"zoom.to.selection": "Zoom to Selection",
|
||||||
"zoom.to.fit": "Zoom to Fit",
|
"zoom.to.fit": "Zoom to Fit",
|
||||||
"zoom.to": "Zoom to",
|
"zoom.to": "Zoom to",
|
||||||
|
@ -38,10 +38,10 @@
|
||||||
"cut": "Cut",
|
"cut": "Cut",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"copy.as": "Copy as",
|
"copy.as": "Copy As",
|
||||||
"export.as": "Export as",
|
"export.as": "Export As",
|
||||||
"select.all": "Select all",
|
"select.all": "Select All",
|
||||||
"select.none": "Select none",
|
"select.none": "Select None",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"new.project": "New Project",
|
"new.project": "New Project",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"copy.invite.link": "Copy Invite Link",
|
"copy.invite.link": "Copy Invite Link",
|
||||||
|
"copy.readonly.link": "Copy ReadOnly Link",
|
||||||
"create.multiplayer.project": "Create a Multiplayer Project",
|
"create.multiplayer.project": "Create a Multiplayer Project",
|
||||||
"copy.multiplayer.project": "Copy to Multiplayer Project",
|
"copy.multiplayer.project": "Copy to Multiplayer Project",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
"to.front": "To Front",
|
"to.front": "To Front",
|
||||||
"forward": "Forward",
|
"forward": "Forward",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
"back": "Back",
|
"back": "To Back",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"translation.link": "Learn More",
|
"translation.link": "Learn More",
|
||||||
"dock.position": "Dock Position",
|
"dock.position": "Dock Position",
|
||||||
|
|
|
@ -53,7 +53,7 @@ TRANSLATIONS.sort((a, b) => (a.locale < b.locale ? -1 : 1))
|
||||||
export type TDTranslation = {
|
export type TDTranslation = {
|
||||||
readonly locale: string
|
readonly locale: string
|
||||||
readonly label: string
|
readonly label: string
|
||||||
readonly messages: Partial<typeof en>
|
readonly messages: Partial<typeof main>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TDTranslations = TDTranslation[]
|
export type TDTranslations = TDTranslation[]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue