From afcf59562623d42248e4b09cc12ff95167c9ee86 Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Thu, 31 Oct 2024 04:09:43 +0000 Subject: [PATCH] feat: add import modal (#8599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [BS-1471](https://linear.app/affine-design/issue/BS-1471/新的-import-dialog-ui) --- .../components/affine/import-modal/index.tsx | 359 ++++++++++++++++++ .../affine/import-modal/style.css.ts | 110 ++++++ .../core/src/components/atoms/index.ts | 2 + .../block-suite-header/menu/index.tsx | 27 +- .../components/providers/modal-provider.tsx | 25 +- .../src/components/root-app-sidebar/index.tsx | 21 +- packages/frontend/i18n/src/resources/en.json | 16 + .../frontend/i18n/src/resources/zh-Hans.json | 16 + .../frontend/i18n/src/resources/zh-Hant.json | 16 + 9 files changed, 569 insertions(+), 23 deletions(-) create mode 100644 packages/frontend/core/src/components/affine/import-modal/index.tsx create mode 100644 packages/frontend/core/src/components/affine/import-modal/style.css.ts diff --git a/packages/frontend/core/src/components/affine/import-modal/index.tsx b/packages/frontend/core/src/components/affine/import-modal/index.tsx new file mode 100644 index 000000000..61f19719f --- /dev/null +++ b/packages/frontend/core/src/components/affine/import-modal/index.tsx @@ -0,0 +1,359 @@ +import { Button, IconButton, Modal } from '@affine/component'; +import { UrlService } from '@affine/core/modules/url'; +import { WorkbenchService } from '@affine/core/modules/workbench'; +import { useI18n } from '@affine/i18n'; +import { + MarkdownTransformer, + NotionHtmlTransformer, + openFileOrFiles, +} from '@blocksuite/affine/blocks'; +import type { DocCollection } from '@blocksuite/affine/store'; +import { + ExportToMarkdownIcon, + HelpIcon, + NotionIcon, +} from '@blocksuite/icons/rc'; +import { useService, WorkspaceService } from '@toeverything/infra'; +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { useSetAtom } from 'jotai'; +import { type ReactElement, useCallback, useState } from 'react'; + +import { openImportModalAtom } from '../../atoms'; +import { useAsyncCallback } from '../../hooks/affine-async-hooks'; +import * as style from './style.css'; + +type ImportType = 'markdown' | 'markdownZip' | 'notion'; +type AcceptType = 'Markdown' | 'Zip'; +type Status = 'idle' | 'importing' | 'success' | 'error'; + +type ImportConfig = { + fileOptions: { acceptType: AcceptType; multiple: boolean }; + importFunction: ( + docCollection: DocCollection, + file: File | File[] + ) => Promise; +}; + +const DISCORD_URL = 'https://discord.gg/whd5mjYqVw'; + +const importOptions = [ + { + label: 'com.affine.import.markdown-files', + prefixIcon: ( + + ), + testId: 'editor-option-menu-import-markdown-files', + type: 'markdown' as ImportType, + }, + { + label: 'com.affine.import.markdown-with-media-files', + prefixIcon: ( + + ), + testId: 'editor-option-menu-import-markdown-with-media', + type: 'markdownZip' as ImportType, + }, + { + label: 'com.affine.import.notion', + prefixIcon: , + suffixIcon: ( + + ), + suffixTooltip: 'com.affine.import.notion.tooltip', + testId: 'editor-option-menu-import-notion', + type: 'notion' as ImportType, + }, +]; + +const importConfigs: Record = { + markdown: { + fileOptions: { acceptType: 'Markdown', multiple: true }, + importFunction: async (docCollection, files) => { + if (!Array.isArray(files)) { + throw new Error('Expected an array of files for markdown files import'); + } + const pageIds: string[] = []; + for (const file of files) { + const text = await file.text(); + const fileName = file.name.split('.').slice(0, -1).join('.'); + const pageId = await MarkdownTransformer.importMarkdownToDoc({ + collection: docCollection, + markdown: text, + fileName, + }); + if (pageId) pageIds.push(pageId); + } + return pageIds; + }, + }, + markdownZip: { + fileOptions: { acceptType: 'Zip', multiple: false }, + importFunction: async (docCollection, file) => { + if (Array.isArray(file)) { + throw new Error('Expected a single zip file for markdownZip import'); + } + return MarkdownTransformer.importMarkdownZip({ + collection: docCollection, + imported: file, + }); + }, + }, + notion: { + fileOptions: { acceptType: 'Zip', multiple: false }, + importFunction: async (docCollection, file) => { + if (Array.isArray(file)) { + throw new Error('Expected a single zip file for notion import'); + } + const { pageIds } = await NotionHtmlTransformer.importNotionZip({ + collection: docCollection, + imported: file, + }); + return pageIds; + }, + }, +}; + +const ImportOptionItem = ({ + label, + prefixIcon, + suffixIcon, + suffixTooltip, + type, + onImport, +}: { + label: string; + prefixIcon: ReactElement; + suffixIcon?: ReactElement; + suffixTooltip?: string; + type: ImportType; + onImport: (type: ImportType) => void; +}) => { + const t = useI18n(); + return ( +
onImport(type)}> + {prefixIcon} +
{t[label]()}
+ {suffixIcon && ( + + )} +
+ ); +}; + +const ImportOptions = ({ + onImport, +}: { + onImport: (type: ImportType) => void; +}) => { + const t = useI18n(); + return ( + <> +
{t['Import']()}
+
+ {importOptions.map( + ({ label, prefixIcon, suffixIcon, suffixTooltip, testId, type }) => ( + + ) + )} +
+
+ {t['com.affine.import.modal.tip']()}{' '} + + Discord + {' '} + . +
+ + ); +}; + +const ImportingStatus = () => { + const t = useI18n(); + return ( + <> +
+ {t['com.affine.import.status.importing.title']()} +
+

+ {t['com.affine.import.status.importing.message']()} +

+ + ); +}; + +const SuccessStatus = ({ onComplete }: { onComplete: () => void }) => { + const t = useI18n(); + return ( + <> +
+ {t['com.affine.import.status.success.title']()} +
+

+ {t['com.affine.import.status.success.message']()}{' '} + + Discord + + . +

+
+ +
+ + ); +}; + +const ErrorStatus = ({ + error, + onRetry, +}: { + error: string | null; + onRetry: () => void; +}) => { + const t = useI18n(); + const urlService = useService(UrlService); + return ( + <> +
+ {t['com.affine.import.status.failed.title']()} +
+

+ {error || 'Unknown error occurred'} +

+
+ + +
+ + ); +}; + +export const ImportModal = ({ ...modalProps }) => { + const t = useI18n(); + const [status, setStatus] = useState('idle'); + const [importError, setImportError] = useState(null); + const [pageIds, setPageIds] = useState([]); + const setOpenImportModalAtom = useSetAtom(openImportModalAtom); + const workspace = useService(WorkspaceService).workspace; + const workbench = useService(WorkbenchService).workbench; + const docCollection = workspace.docCollection; + + const handleImport = useAsyncCallback( + async (type: ImportType) => { + setImportError(null); + try { + const importConfig = importConfigs[type]; + const file = await openFileOrFiles(importConfig.fileOptions); + + if (!file || (Array.isArray(file) && file.length === 0)) { + throw new Error( + t['com.affine.import.status.failed.message.no-file-selected']() + ); + } + + setStatus('importing'); + + const pageIds = await importConfig.importFunction(docCollection, file); + + setPageIds(pageIds); + setStatus('success'); + } catch (error) { + setImportError( + error instanceof Error ? error.message : 'Unknown error occurred' + ); + setStatus('error'); + } + }, + [docCollection, t] + ); + + const handleComplete = useCallback(() => { + if (pageIds.length > 1) { + workbench.openAll(); + } else if (pageIds.length === 1) { + workbench.openDoc(pageIds[0]); + } + setOpenImportModalAtom(false); + }, [pageIds, workbench, setOpenImportModalAtom]); + + const handleRetry = () => { + setStatus('idle'); + }; + + const statusComponents = { + idle: , + importing: , + success: , + error: , + }; + + return ( + +
+ {statusComponents[status]} +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/import-modal/style.css.ts b/packages/frontend/core/src/components/affine/import-modal/style.css.ts new file mode 100644 index 000000000..3876a60bb --- /dev/null +++ b/packages/frontend/core/src/components/affine/import-modal/style.css.ts @@ -0,0 +1,110 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const importModalContainer = style({ + width: '100%', + height: '100%', + display: 'flex', + boxSizing: 'border-box', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '20px 24px', + gap: '12px', +}); + +export const importModalTitle = style({ + width: '100%', + height: 'auto', + fontSize: cssVar('fontH6'), + fontWeight: '600', + lineHeight: cssVar('lineHeight'), +}); + +export const importModalContent = style({ + width: '100%', + flex: 1, + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +export const closeButton = style({ + top: '24px', + right: '24px', +}); + +export const importModalTip = style({ + width: '100%', + height: 'auto', + fontSize: cssVar('fontSm'), + lineHeight: cssVar('lineHeight'), + fontWeight: '400', + color: cssVar('textSecondaryColor'), +}); + +export const link = style({ + color: cssVar('linkColor'), + cursor: 'pointer', +}); + +export const importStatusContent = style({ + width: '100%', + fontSize: cssVar('fontBase'), + lineHeight: cssVar('lineHeight'), + fontWeight: '400', + color: cssVar('textPrimaryColor'), +}); + +export const importModalButtonContainer = style({ + width: '100%', + display: 'flex', + flexDirection: 'row', + gap: '20px', + justifyContent: 'end', + marginTop: '20px', +}); + +export const importItem = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + height: 'auto', + gap: '4px', + padding: '8px 12px', + borderRadius: '8px', + border: `1px solid ${cssVarV2('layer/insideBorder/border')}`, + background: cssVarV2('button/secondary'), + selectors: { + '&:hover': { + background: cssVarV2('layer/background/hoverOverlay'), + cursor: 'pointer', + transition: 'background .30s', + }, + }, +}); + +export const importItemLabel = style({ + display: 'flex', + alignItems: 'center', + padding: '0 4px', + textAlign: 'left', + flex: 1, + color: cssVar('textPrimaryColor'), + fontSize: cssVar('fontBase'), + lineHeight: cssVar('lineHeight'), + fontWeight: '500', + whiteSpace: 'nowrap', + overflow: 'hidden', +}); + +export const importItemPrefix = style({ + marginRight: 'auto', +}); + +export const importItemSuffix = style({ + marginLeft: 'auto', +}); diff --git a/packages/frontend/core/src/components/atoms/index.ts b/packages/frontend/core/src/components/atoms/index.ts index 7ffbf3f36..c018dd4f1 100644 --- a/packages/frontend/core/src/components/atoms/index.ts +++ b/packages/frontend/core/src/components/atoms/index.ts @@ -35,6 +35,8 @@ export const openSettingModalAtom = atom({ open: false, }); +export const openImportModalAtom = atom(false); + export type AuthAtomData = | { state: 'signIn' } | { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 69ae69f11..27bd190b4 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -7,12 +7,14 @@ import { } from '@affine/component/ui/menu'; import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal'; import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu'; -import { openHistoryTipsModalAtom } from '@affine/core/components/atoms'; +import { + openHistoryTipsModalAtom, + openImportModalAtom, +} from '@affine/core/components/atoms'; import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper'; import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud'; import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page'; import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper'; -import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta'; import { Export, MoveToTrash } from '@affine/core/components/page-list'; import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; @@ -47,7 +49,6 @@ import { useSetAtom } from 'jotai'; import { useCallback, useState } from 'react'; import { HeaderDropDownButton } from '../../../pure/header-drop-down-button'; -import { usePageHelper } from '../../block-suite-page-list/utils'; import { useFavorite } from '../favorite'; type PageMenuProps = { @@ -69,7 +70,6 @@ export const PageHeaderMenuButton = ({ const confirmEnableCloud = useEnableCloud(); const workspace = useService(WorkspaceService).workspace; - const docCollection = workspace.docCollection; const editorService = useService(EditorService); const isInTrash = useLiveData( @@ -83,7 +83,6 @@ export const PageHeaderMenuButton = ({ const { favorite, toggleFavorite } = useFavorite(pageId); const { duplicate } = useBlockSuiteMetaHelper(); - const { importFile } = usePageHelper(docCollection); const { setTrashModal } = useTrashModalHelper(); const [isEditing, setEditing] = useState(!page.readonly); @@ -109,6 +108,7 @@ export const PageHeaderMenuButton = ({ const [historyModalOpen, setHistoryModalOpen] = useState(false); const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom); + const setOpenImportModalAtom = useSetAtom(openImportModalAtom); const openHistoryModal = useCallback(() => { track.$.header.history.open(); @@ -184,19 +184,10 @@ export const PageHeaderMenuButton = ({ }); }, [duplicate, pageId]); - const onImportFile = useAsyncCallback(async () => { - const options = await importFile(); + const handleOpenImportModal = useCallback(() => { track.$.header.docOptions.import(); - if (options.isWorkspaceFile) { - track.$.header.actions.createWorkspace({ - control: 'import', - }); - } else { - track.$.header.actions.createDoc({ - control: 'import', - }); - } - }, [importFile]); + setOpenImportModalAtom(true); + }, [setOpenImportModalAtom]); const handleShareMenuOpenChange = useCallback((open: boolean) => { if (open) { @@ -361,7 +352,7 @@ export const PageHeaderMenuButton = ({ } data-testid="editor-option-menu-import" - onSelect={onImportFile} + onSelect={handleOpenImportModal} > {t['Import']()} diff --git a/packages/frontend/core/src/components/providers/modal-provider.tsx b/packages/frontend/core/src/components/providers/modal-provider.tsx index 3ce0e5eb7..1e254bf8a 100644 --- a/packages/frontend/core/src/components/providers/modal-provider.tsx +++ b/packages/frontend/core/src/components/providers/modal-provider.tsx @@ -21,6 +21,7 @@ import { PeekViewManagerModal } from '../../modules/peek-view'; import { AuthModal } from '../affine/auth'; import { AiLoginRequiredModal } from '../affine/auth/ai-login-required'; import { HistoryTipsModal } from '../affine/history-tips-modal'; +import { ImportModal } from '../affine/import-modal'; import { IssueFeedbackModal } from '../affine/issue-feedback-modal'; import { CloudQuotaModal, @@ -30,7 +31,11 @@ import { SettingModal } from '../affine/setting-modal'; import { SignOutModal } from '../affine/sign-out-modal'; import { StarAFFiNEModal } from '../affine/star-affine-modal'; import type { SettingAtom } from '../atoms'; -import { openSettingModalAtom, openSignOutModalAtom } from '../atoms'; +import { + openImportModalAtom, + openSettingModalAtom, + openSignOutModalAtom, +} from '../atoms'; import { InfoModal } from '../doc-properties/info-modal/info-modal'; import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper'; import { useAsyncCallback } from '../hooks/affine-async-hooks'; @@ -128,6 +133,7 @@ export function CurrentWorkspaceModals() { titles={deletePageTitles} /> {currentWorkspace ? : null} + ); } @@ -193,3 +199,20 @@ export const AllWorkspaceModals = (): ReactElement => { ); }; + +export const Import = () => { + const [open, setOpenImportModalAtom] = useAtom(openImportModalAtom); + + const onOpenChange = useCallback( + (open: boolean) => { + setOpenImportModalAtom(open); + }, + [setOpenImportModalAtom] + ); + + if (!open) { + return null; + } + + return ; +}; diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 6dc5b8c52..36e6b2320 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -1,4 +1,7 @@ -import { openSettingModalAtom } from '@affine/core/components/atoms'; +import { + openImportModalAtom, + openSettingModalAtom, +} from '@affine/core/components/atoms'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { AddPageButton, @@ -27,6 +30,7 @@ import type { Doc } from '@blocksuite/affine/store'; import { AllDocsIcon, GithubIcon, + ImportIcon, JournalIcon, SettingsIcon, } from '@blocksuite/icons/rc'; @@ -43,7 +47,6 @@ import { useCallback } from 'react'; import { WorkbenchService } from '../../modules/workbench'; import { usePageHelper } from '../blocksuite/block-suite-page-list/utils'; import { WorkspaceNavigator } from '../workspace-selector'; -import ImportPage from './import-page'; import { quickSearch, quickSearchAndNewPage, @@ -82,7 +85,6 @@ export const RootAppSidebar = (): ReactElement => { CMDKQuickSearchService, }); const currentWorkspace = workspaceService.workspace; - const docCollection = currentWorkspace.docCollection; const t = useI18n(); const workbench = workbenchService.workbench; const currentPath = useLiveData( @@ -105,6 +107,7 @@ export const RootAppSidebar = (): ReactElement => { ); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); + const setOpenImportModalAtom = useSetAtom(openImportModalAtom); const onOpenSettingModal = useCallback(() => { setOpenSettingModalAtom({ @@ -114,6 +117,10 @@ export const RootAppSidebar = (): ReactElement => { track.$.navigationPanel.$.openSettings(); }, [setOpenSettingModalAtom]); + const onOpenImportModal = useCallback(() => { + setOpenImportModalAtom(true); + }, [setOpenImportModalAtom]); + return ( @@ -163,7 +170,13 @@ export const RootAppSidebar = (): ReactElement => {
- + } + onClick={onOpenImportModal} + > + {t['Import']()} + } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 9337fdac1..be2664f91 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -10,6 +10,7 @@ "Cancel": "Cancel", "Click to replace photo": "Click to replace photo", "Collections": "Collections", + "Complete": "Complete", "Confirm": "Confirm", "Continue": "Continue", "Convert to ": "Convert to ", @@ -46,6 +47,7 @@ "Favorite": "Favourite", "Favorited": "Favourited", "Favorites": "Favourites", + "Feedback": "Feedback", "Find 0 result": "Found 0 results", "Go Back": "Go back", "Go Forward": "Go forward", @@ -82,6 +84,7 @@ "Remove special filter": "Remove special filter", "Removed successfully": "Removed successfully", "Rename": "Rename", + "Retry": "Retry", "Save": "Save", "Select": "Select", "Sign in": "Sign in AFFiNE Cloud", @@ -508,6 +511,19 @@ "com.affine.import-template.dialog.errorImport": "Failed to import template, please try again.", "com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.", "com.affine.import_file": "Support Markdown/Notion", + "com.affine.import.affine-workspace-data": "AFFiNE workspace data", + "com.affine.import.markdown-files": "Markdown files (.md)", + "com.affine.import.markdown-with-media-files": "Markdown with media files (.zip)", + "com.affine.import.modal.tip": "If you'd like to request support for additional file types, feel free to let us know on", + "com.affine.import.notion": "Notion", + "com.affine.import.notion.tooltip": "Import your Notion data. Supported import formats: HTML with subpages.", + "com.affine.import.status.failed.message": "Import failed, please try again.", + "com.affine.import.status.failed.message.no-file-selected": "No file selected", + "com.affine.import.status.failed.title": "Import failure", + "com.affine.import.status.importing.message": "Importing your workspace data, please wait patiently.", + "com.affine.import.status.importing.title": "Importing...", + "com.affine.import.status.success.message": "Your document has been imported successfully, thank you for choosing AFFiNE. Any questions please feel free to feedback to us", + "com.affine.import.status.success.title": "Import completed", "com.affine.inviteModal.button.cancel": "Cancel", "com.affine.issue-feedback.cancel": "Maybe later", "com.affine.issue-feedback.confirm": "Create issue on GitHub", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index 3467162ad..fb2c63b26 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -10,6 +10,7 @@ "Cancel": "取消", "Click to replace photo": "点击以更换照片", "Collections": "精选", + "Complete": "完成", "Confirm": "确认", "Continue": "继续", "Convert to ": "转换为", @@ -46,6 +47,7 @@ "Favorite": "收藏", "Favorited": "已收藏", "Favorites": "收藏夹", + "Feedback": "反馈", "Find 0 result": "找到 0 个结果", "Go Back": "返回", "Go Forward": "前进", @@ -82,6 +84,7 @@ "Remove special filter": "移除特殊筛选", "Removed successfully": "成功移除", "Rename": "重命名", + "Retry": "重试", "Save": "保存", "Select": "选择", "Sign in": "登录 AFFiNE Cloud", @@ -507,6 +510,19 @@ "com.affine.import-template.dialog.errorImport": "插入模板失败,请重试。", "com.affine.import-template.dialog.errorLoad": "读取模版失败,请重试。", "com.affine.import_file": "支持 Markdown/Notion", + "com.affine.import.affine-workspace-data": "AFFiNE 工作区数据", + "com.affine.import.markdown-files": "Markdown 文件 (.md)", + "com.affine.import.markdown-with-media-files": "Markdown 文件(带媒体文件) (.zip)", + "com.affine.import.modal.tip": "如果您希望请求支持其他文件类型,请随时告诉我们", + "com.affine.import.notion": "Notion", + "com.affine.import.notion.tooltip": "导入您的 Notion 数据。支持导入格式:HTML 带子页面。", + "com.affine.import.status.failed.message": "导入失败,请重试。", + "com.affine.import.status.failed.message.no-file-selected": "未选择文件", + "com.affine.import.status.failed.title": "导入失败", + "com.affine.import.status.importing.message": "正在导入您的数据,请耐心等待。", + "com.affine.import.status.importing.title": "导入中...", + "com.affine.import.status.success.message": "您的文档已成功导入,感谢选择 AFFiNE。有任何问题请随时反馈给我们", + "com.affine.import.status.success.title": "导入完成", "com.affine.inviteModal.button.cancel": "取消", "com.affine.issue-feedback.cancel": "稍后", "com.affine.issue-feedback.confirm": "在 GitHUb 分享反馈", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index fea3bb635..364306096 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -10,6 +10,7 @@ "Cancel": "取消", "Click to replace photo": "点击以更换照片", "Collections": "收藏集", + "Complete": "完成", "Confirm": "確認", "Continue": "繼續", "Convert to ": "轉換為", @@ -46,6 +47,7 @@ "Favorite": "收藏", "Favorited": "已收藏", "Favorites": "我的收藏", + "Feedback": "反饋", "Find 0 result": "發現 0 個結果", "Go Back": "返回", "Go Forward": "前進", @@ -82,6 +84,7 @@ "Remove special filter": "移除特殊篩選器", "Removed successfully": "成功移除", "Rename": "重新命名", + "Retry": "重試", "Save": "保存", "Select": "選擇", "Sign in": "登入 AFFiNE Cloud", @@ -504,6 +507,19 @@ "com.affine.import-template.dialog.errorImport": "導入模板失敗,請重試。", "com.affine.import-template.dialog.errorLoad": "加載模板失敗,請重試。", "com.affine.import_file": "支援 Markdown/Notion", + "com.affine.import.affine-workspace-data": "AFFiNE 工作區數據", + "com.affine.import.markdown-files": "Markdown 文件 (.md)", + "com.affine.import.markdown-with-media-files": "Markdown 文件(帶媒體文件) (.zip)", + "com.affine.import.modal.tip": "如果您希望請求支持其他文件類型,請隨時告訴我們", + "com.affine.import.notion": "Notion", + "com.affine.import.notion.tooltip": "導入您的 Notion 數據。支持導入格式:HTML 帶子頁面。", + "com.affine.import.status.failed.message": "導入失敗,請重試。", + "com.affine.import.status.failed.message.no-file-selected": "未選擇文件", + "com.affine.import.status.failed.title": "導入失敗", + "com.affine.import.status.importing.message": "正在導入您的數據,請耐心等待。", + "com.affine.import.status.importing.title": "導入中...", + "com.affine.import.status.success.message": "您的文檔已成功導入,感謝選擇 AFFiNE。有任何問題請隨時反饋給我們", + "com.affine.import.status.success.title": "導入完成", "com.affine.inviteModal.button.cancel": "取消", "com.affine.issue-feedback.cancel": "稍後", "com.affine.issue-feedback.confirm": "在 GitHub 分享反饋",