diff --git a/src/App.tsx b/src/App.tsx index ff8d05b..4044cf0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,14 +28,21 @@ import Embeddable from './Embeddable' import type { ResolvablePromise } from '@excalidraw/excalidraw/types/utils' import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types' import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js' +import { useExcalidrawLang } from './hooks/useExcalidrawLang' + interface WhiteboardAppProps { - fileId: number; - fileName: string; - isEmbedded: boolean; - publicSharingToken: string | null; + fileId: number + fileName: string + isEmbedded: boolean + publicSharingToken: string | null } -export default function App({ fileId, isEmbedded, fileName, publicSharingToken }: WhiteboardAppProps) { +export default function App({ + fileId, + isEmbedded, + fileName, + publicSharingToken, +}: WhiteboardAppProps) { const fileNameWithoutExtension = fileName.split('.').slice(0, -1).join('.') const [viewModeEnabled] = useState(isEmbedded) @@ -44,13 +51,19 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } const isDarkMode = () => { const ncThemes = document.body.dataset?.themes - return (window.matchMedia('(prefers-color-scheme: dark)').matches && ncThemes?.indexOf('light') === -1) + return ( + (window.matchMedia('(prefers-color-scheme: dark)').matches + && ncThemes?.indexOf('light') === -1) || ncThemes?.indexOf('dark') > -1 + ) } const [theme, setTheme] = useState(isDarkMode() ? 'dark' : 'light') + const lang = useExcalidrawLang() + useEffect(() => { - const themeChangeListener = () => setTheme(isDarkMode() ? 'dark' : 'light') + const themeChangeListener = () => + setTheme(isDarkMode() ? 'dark' : 'light') const mq = window.matchMedia('(prefers-color-scheme: dark)') mq.addEventListener('change', themeChangeListener) return () => { @@ -69,26 +82,29 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } } const initialStatePromiseRef = useRef<{ - promise: ResolvablePromise; + promise: ResolvablePromise }>({ promise: null! }) if (!initialStatePromiseRef.current.promise) { initialStatePromiseRef.current.promise = resolvablePromise() } - const [ - excalidrawAPI, - setExcalidrawAPI, - ] = useState(null) + const [excalidrawAPI, setExcalidrawAPI] + = useState(null) const [collab, setCollab] = useState(null) - if (excalidrawAPI && !collab) setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken)) + if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken)) } if (collab && !collab.portal.socket) collab.startCollab() useEffect(() => { - const extraTools = document.getElementsByClassName('App-toolbar__extra-tools-trigger')[0] + const extraTools = document.getElementsByClassName( + 'App-toolbar__extra-tools-trigger', + )[0] const smartPick = document.createElement('label') smartPick.classList.add(...['ToolIcon', 'Shape']) if (extraTools) { - extraTools.parentNode?.insertBefore(smartPick, extraTools.previousSibling) + extraTools.parentNode?.insertBefore( + smartPick, + extraTools.previousSibling, + ) const root = createRoot(smartPick) root.render(renderSmartPicker()) } @@ -130,7 +146,7 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } ( element: NonDeletedExcalidrawElement, event: CustomEvent<{ - nativeEvent: MouseEvent | React.PointerEvent; + nativeEvent: MouseEvent | React.PointerEvent }>, ) => { const link = element.link! @@ -151,11 +167,16 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } const addWebEmbed = (link: string) => { let cords: { x: any; y: any } if (excalidrawAPI) { - cords = viewportCoordsToSceneCoords({ clientX: 100, clientY: 100 }, excalidrawAPI.getAppState()) + cords = viewportCoordsToSceneCoords( + { clientX: 100, clientY: 100 }, + excalidrawAPI.getAppState(), + ) } else { cords = { x: 0, y: 0 } } - const elements = excalidrawAPI?.getSceneElementsIncludingDeleted().slice() + const elements = excalidrawAPI + ?.getSceneElementsIncludingDeleted() + .slice() elements?.push({ link, id: (Math.random() + 1).toString(36).substring(7), @@ -204,7 +225,12 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } const renderSmartPicker = () => { return ( - ) @@ -234,7 +260,7 @@ export default function App({ fileId, isEmbedded, fileName, publicSharingToken } }, }} onLinkOpen={onLinkOpen} - > + langCode={lang}> {renderMenu()} diff --git a/src/hooks/useExcalidrawLang.ts b/src/hooks/useExcalidrawLang.ts new file mode 100644 index 0000000..4dd375f --- /dev/null +++ b/src/hooks/useExcalidrawLang.ts @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { useState, useEffect } from 'react' +import { getLanguage } from '@nextcloud/l10n' +import { languages } from '@excalidraw/excalidraw' + +const languageMap = new Map(languages.map(lang => [lang.code.toLowerCase(), lang.code])) + +function mapNextcloudToExcalidrawLang(nextcloudLang: string): string { + const lowerNextcloudLang = nextcloudLang.toLowerCase() + + if (languageMap.has(lowerNextcloudLang)) { + return languageMap.get(lowerNextcloudLang)! + } + + const hyphenatedLang = lowerNextcloudLang.replace('_', '-') + if (languageMap.has(hyphenatedLang)) { + return languageMap.get(hyphenatedLang)! + } + + for (const [excalidrawLang, originalCode] of languageMap) { + if (excalidrawLang.startsWith(lowerNextcloudLang) + || lowerNextcloudLang.startsWith(excalidrawLang.split('-')[0])) { + return originalCode + } + } + + return 'en' +} + +export function useExcalidrawLang() { + const [lang, setLang] = useState(() => mapNextcloudToExcalidrawLang(getLanguage())) + + useEffect(() => { + const nextcloudLang = getLanguage() + setLang(mapNextcloudToExcalidrawLang(nextcloudLang)) + }, []) + + return lang +}