Merge pull request #344 from JimmFly/dev

feat:add i18n for layout
This commit is contained in:
DarkSky
2022-09-02 10:16:41 +08:00
committed by GitHub
15 changed files with 194 additions and 32 deletions

View File

@@ -1,5 +1,7 @@
{ {
"translation": { "translation": {
"Blog": "Blog",
"AboutUs": "About AboutUs",
"Open Source": "Open Source", "Open Source": "Open Source",
"Privacy First": "Privacy First", "Privacy First": "Privacy First",
"Alternative": "Alternative", "Alternative": "Alternative",

View File

@@ -1,5 +1,7 @@
{ {
"translation": { "translation": {
"Blog": "博客",
"AboutUs": "关于我们",
"Open Source": "开源", "Open Source": "开源",
"Privacy First": "隐私第一", "Privacy First": "隐私第一",
"Alternative": "的另一种选择", "Alternative": "的另一种选择",

View File

@@ -11,7 +11,9 @@
"@mui/icons-material": "^5.8.4", "@mui/icons-material": "^5.8.4",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"date-fns": "^2.29.2", "date-fns": "^2.29.2",
"i18next": "^21.9.1",
"jotai": "^1.8.1", "jotai": "^1.8.1",
"react-i18next": "^11.18.4",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"turndown": "7.1.1" "turndown": "7.1.1"
}, },

View File

@@ -4,7 +4,7 @@ import { CloseIcon } from '@toeverything/components/common';
import { IconButton, MuiSnackbar, styled } from '@toeverything/components/ui'; import { IconButton, MuiSnackbar, styled } from '@toeverything/components/ui';
import { services } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service';
import { useLocalTrigger } from '@toeverything/datasource/state'; import { useLocalTrigger } from '@toeverything/datasource/state';
import { useTranslation } from 'react-i18next';
const cleanupWorkspace = (workspace: string) => const cleanupWorkspace = (workspace: string) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const req = indexedDB.deleteDatabase(workspace); const req = indexedDB.deleteDatabase(workspace);
@@ -75,7 +75,7 @@ export const FileSystem = () => {
setError(true); setError(true);
setTimeout(() => setError(false), 3000); setTimeout(() => setError(false), 3000);
}, []); }, []);
const { t } = useTranslation();
const apiSupported = useMemo(() => fsApiSupported(), []); const apiSupported = useMemo(() => fsApiSupported(), []);
if (apiSupported && !selected) { if (apiSupported && !selected) {
@@ -105,7 +105,7 @@ export const FileSystem = () => {
} }
}} }}
> >
Sync to Disk {t('Sync to Disk')}
</StyledFileSystem> </StyledFileSystem>
</> </>
); );

View File

@@ -12,6 +12,7 @@ import {
useShowSettingsSidebar, useShowSettingsSidebar,
} from '@toeverything/datasource/state'; } from '@toeverything/datasource/state';
import { useTranslation } from 'react-i18next';
import { EditorBoardSwitcher } from './EditorBoardSwitcher'; import { EditorBoardSwitcher } from './EditorBoardSwitcher';
import { FileSystem, fsApiSupported } from './FileSystem'; import { FileSystem, fsApiSupported } from './FileSystem';
import { CurrentPageTitle } from './Title'; import { CurrentPageTitle } from './Title';
@@ -20,16 +21,17 @@ export const LayoutHeader = () => {
const [isLocalWorkspace] = useLocalTrigger(); const [isLocalWorkspace] = useLocalTrigger();
const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } = const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } =
useShowSettingsSidebar(); useShowSettingsSidebar();
const { t } = useTranslation();
const warningTips = useMemo(() => { const warningTips = useMemo(() => {
if (!fsApiSupported()) { if (!fsApiSupported()) {
return 'Welcome to the AFFiNE demo. To begin saving changes you can SYNC DATA TO DISK with the latest version of Chromium based browser like Chrome/Edge'; return t('WarningTips.IsNotfsApiSupported');
} else if (!isLocalWorkspace) { } else if (!isLocalWorkspace) {
return 'Welcome to the AFFiNE demo. To begin saving changes you can SYNC TO DISK.'; return t('WarningTips.IsNotLocalWorkspace');
} else { } else {
return 'AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data'; return t('WarningTips.DoNotStore');
} }
}, [isLocalWorkspace]); }, [isLocalWorkspace, t]);
return ( return (
<StyledContainerForHeaderRoot> <StyledContainerForHeaderRoot>
@@ -43,7 +45,7 @@ export const LayoutHeader = () => {
<FlexContainer> <FlexContainer>
<StyledHelper> <StyledHelper>
<FileSystem /> <FileSystem />
<StyledShare disabled={true}>Share</StyledShare> <StyledShare disabled={true}>{t('Share')}</StyledShare>
<div style={{ margin: '0px 12px' }}> <div style={{ margin: '0px 12px' }}>
<IconButton <IconButton
size="large" size="large"

View File

@@ -0,0 +1,40 @@
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import en_US from './resources/en.json';
import zh_CN from './resources/zh.json';
// See https://react.i18next.com/latest/typescript
declare module 'react-i18next' {
// and extend them!
interface CustomTypeOptions {
// custom namespace type if you changed it
defaultNS: 'ns1';
// custom resources type
resources: {
en: typeof en_US.translation;
zh: typeof zh_CN.translation;
};
}
}
const resources = {
en: en_US,
zh: zh_CN,
} as const;
i18next.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
resources,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export const options = [
{ value: 'en', text: 'English' },
{ value: 'zh', text: '简体中文' },
] as const;
export { i18next };

View File

@@ -0,0 +1,26 @@
{
"translation": {
"Sync to Disk": "Sync to Disk",
"Share": "Share",
"WarningTips": {
"IsNotfsApiSupported": "Welcome to the AFFiNE demo. To begin saving changes you can SYNC DATA TO DISK with the latest version of Chromium based browser like Chrome/Edge",
"IsNotLocalWorkspace": "Welcome to the AFFiNE demo. To begin saving changes you can SYNC TO DISK.",
"DoNotStore": "AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data"
},
"Layout": "Layout",
"Comment": "Comment",
"Settings": "Settings",
"ComingSoon": "Layout Settings Coming Soon...",
"Duplicate Page": "Duplicate Page",
"Copy Page Link": "Copy Page Link",
"Language": "Language",
"Clear Workspace": "Clear Workspace",
"Export As Markdown": "Export As Markdown",
"Export As HTML": "Export As HTML",
"Export As PDF (Unsupported)": "Export As PDF (Unsupported)",
"Import Workspace": "Import Workspace",
"Export Workspace": "Export Workspace",
"Last edited by": "Last edited by {{name}}",
"Logout": "Logout"
}
}

View File

@@ -0,0 +1,26 @@
{
"translation": {
"Sync to Disk": "同步到磁盘",
"Share": "分享",
"WarningTips": {
"IsNotfsApiSupported": "欢迎来到AFFiNE 的演示界面。您可以使用最新版本的基于Chrome的浏览器(如Chrome/Edge)将数据同步到磁盘来进行保存",
"IsNotLocalWorkspace": "欢迎来到AFFiNE 的演示界面,您可以同步到磁盘来进行保存操作。",
"DoNotStore": "AFFINE 正在积极开发中,当前版本不稳定。请不要存储信息或数据。"
},
"ComingSoon": "布局设置即将到来",
"Layout": "布局",
"Comment": "评论",
"Settings": "设置",
"Duplicate Page": "复制页面",
"Copy Page Link": "复制页面链接",
"Language": "当前语言",
"Clear Workspace": "清空工作区域",
"Export As Markdown": "导出 markdown",
"Export As HTML": "导出 HTML",
"Export As PDF (Unsupported)": "导出 PDF (暂不支持)",
"Import Workspace": "导入 Workspace",
"Export Workspace": "导出 Workspace",
"Last edited by": "最后编辑者为 {{name}}",
"Logout": "退出登录"
}
}

View File

@@ -1,3 +1,4 @@
export * from './header'; export * from './header';
export * from './i18n';
export * from './settings-sidebar'; export * from './settings-sidebar';
export * from './workspace-sidebar'; export * from './workspace-sidebar';

View File

@@ -5,6 +5,7 @@ import {
} from '@toeverything/components/icons'; } from '@toeverything/components/icons';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import { cloneElement, useCallback, useMemo, type ReactElement } from 'react'; import { cloneElement, useCallback, useMemo, type ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Comments } from '../Comments'; import { Comments } from '../Comments';
import { useActiveComment } from '../Comments/use-comments'; import { useActiveComment } from '../Comments/use-comments';
import { LayoutSettings } from '../Layout'; import { LayoutSettings } from '../Layout';
@@ -16,12 +17,13 @@ const _defaultTabsKeys = ['layout', 'comment', 'settings'] as const;
export const ContainerTabs = () => { export const ContainerTabs = () => {
const { activeCommentId, resolveComment } = useActiveComment(); const { activeCommentId, resolveComment } = useActiveComment();
const { t } = useTranslation();
const getSettingsTabsData = useCallback((): SettingsTabItemType[] => { const getSettingsTabsData = useCallback((): SettingsTabItemType[] => {
return [ return [
{ {
type: 'layout', type: 'layout',
text: 'Layout', text: t('Layout'),
icon: ( icon: (
<IconWrapper> <IconWrapper>
<LayoutIcon /> <LayoutIcon />
@@ -31,7 +33,7 @@ export const ContainerTabs = () => {
}, },
{ {
type: 'comment', type: 'comment',
text: 'Comment', text: t('Comment'),
icon: ( icon: (
<IconWrapper> <IconWrapper>
<CommentIcon /> <CommentIcon />
@@ -48,7 +50,7 @@ export const ContainerTabs = () => {
}, },
{ {
type: 'settings', type: 'settings',
text: 'Settings', text: t('Settings'),
icon: ( icon: (
<IconWrapper> <IconWrapper>
<SettingsIcon /> <SettingsIcon />
@@ -57,7 +59,7 @@ export const ContainerTabs = () => {
panel: <SettingsPanel />, panel: <SettingsPanel />,
}, },
]; ];
}, [activeCommentId, resolveComment]); }, [activeCommentId, resolveComment, t]);
const settingsTabsData = useMemo(() => { const settingsTabsData = useMemo(() => {
return getSettingsTabsData(); return getSettingsTabsData();
@@ -67,7 +69,6 @@ export const ContainerTabs = () => {
_defaultTabsKeys as unknown as string[], _defaultTabsKeys as unknown as string[],
'settings' 'settings'
); );
return ( return (
<> <>
<StyledTabsTitlesContainer> <StyledTabsTitlesContainer>

View File

@@ -1,8 +1,21 @@
import { Divider, ListItem, styled, Switch } from '@toeverything/components/ui'; import {
Divider,
ListItem,
Option,
Select,
styled,
Switch,
} from '@toeverything/components/ui';
import { useTranslation } from 'react-i18next';
import { options } from '../../i18n';
import { useSettings } from './use-settings'; import { useSettings } from './use-settings';
export const SettingsList = () => { export const SettingsList = () => {
const settings = useSettings(); const settings = useSettings();
const { i18n } = useTranslation();
const changeLanguage = event => {
i18n.changeLanguage(event);
};
return ( return (
<StyledSettingsList> <StyledSettingsList>
@@ -15,7 +28,7 @@ export const SettingsList = () => {
if (type === 'switch') { if (type === 'switch') {
return ( return (
<SwitchItemContainer <SwitchItemContainer
key={item.name} key={item.key}
onClick={() => { onClick={() => {
item.onChange(!item.value); item.onChange(!item.value);
}} }}
@@ -31,8 +44,25 @@ export const SettingsList = () => {
} }
return ( return (
<ListItem key={item.name} onClick={() => item.onClick()}> <ListItem key={item.key} onClick={() => item.onClick()}>
{item.name} {item.name}
{item.key === 'Language' ? (
<div style={{ marginLeft: '12em' }}>
<Select
defaultValue={options[0].value}
onChange={changeLanguage}
>
{options.map(option => (
<Option
key={option.value}
value={option.value}
>
{option.text}
</Option>
))}
</Select>
</div>
) : null}
</ListItem> </ListItem>
); );
})} })}

View File

@@ -1,6 +1,7 @@
import { styled, Typography } from '@toeverything/components/ui'; import { styled, Typography } from '@toeverything/components/ui';
import { useUserAndSpaces } from '@toeverything/datasource/state'; import { useUserAndSpaces } from '@toeverything/datasource/state';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { useTranslation } from 'react-i18next';
import { usePageLastUpdated, useWorkspaceAndPageId } from '../util'; import { usePageLastUpdated, useWorkspaceAndPageId } from '../util';
export const LastModified = () => { export const LastModified = () => {
@@ -9,12 +10,12 @@ export const LastModified = () => {
const { workspaceId, pageId } = useWorkspaceAndPageId(); const { workspaceId, pageId } = useWorkspaceAndPageId();
const lastModified = usePageLastUpdated({ workspaceId, pageId }); const lastModified = usePageLastUpdated({ workspaceId, pageId });
const formatLastModified = format(lastModified, 'MM/dd/yyyy hh:mm'); const formatLastModified = format(lastModified, 'MM/dd/yyyy hh:mm');
const { t } = useTranslation();
return ( return (
<div> <div>
<div> <div>
<ContentText type="xs"> <ContentText type="xs">
<span>Last edited by </span> <span>{t('Last edited by', { name: username })}</span>
<span>{username}</span>
</ContentText> </ContentText>
</div> </div>
<div> <div>

View File

@@ -2,6 +2,7 @@ import { MoveToIcon } from '@toeverything/components/icons';
import { ListItem, styled, Typography } from '@toeverything/components/ui'; import { ListItem, styled, Typography } from '@toeverything/components/ui';
import { LOGOUT_COOKIES, LOGOUT_LOCAL_STORAGE } from '@toeverything/utils'; import { LOGOUT_COOKIES, LOGOUT_LOCAL_STORAGE } from '@toeverything/utils';
import { getAuth, signOut } from 'firebase/auth'; import { getAuth, signOut } from 'firebase/auth';
import { useTranslation } from 'react-i18next';
const logout = () => { const logout = () => {
LOGOUT_LOCAL_STORAGE.forEach(name => localStorage.removeItem(name)); LOGOUT_LOCAL_STORAGE.forEach(name => localStorage.removeItem(name));
@@ -16,10 +17,11 @@ const logout = () => {
}; };
export const Logout = () => { export const Logout = () => {
const { t } = useTranslation();
return ( return (
<ListItem onClick={logout}> <ListItem onClick={logout}>
<StyledIcon /> <StyledIcon />
<ContentText type="base">Logout</ContentText> <ContentText type="base">{t('Logout')}</ContentText>
</ListItem> </ListItem>
); );
}; };

View File

@@ -1,9 +1,10 @@
import { message } from '@toeverything/components/ui'; import { message } from '@toeverything/components/ui';
import { copyToClipboard } from '@toeverything/utils'; import { copyToClipboard } from '@toeverything/utils';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useSettingFlags, type SettingFlags } from './use-setting-flags'; import { useSettingFlags, type SettingFlags } from './use-setting-flags';
import { import {
// useReadingMode,
clearWorkspace, clearWorkspace,
duplicatePage, duplicatePage,
exportHtml, exportHtml,
@@ -16,6 +17,7 @@ import {
interface BaseSettingItem { interface BaseSettingItem {
flag?: keyof SettingFlags; flag?: keyof SettingFlags;
key: string;
} }
interface SwitchItem extends BaseSettingItem { interface SwitchItem extends BaseSettingItem {
@@ -64,6 +66,7 @@ export const useSettings = (): SettingItem[] => {
const { workspaceId, pageId } = useWorkspaceAndPageId(); const { workspaceId, pageId } = useWorkspaceAndPageId();
const navigate = useNavigate(); const navigate = useNavigate();
const settingFlags = useSettingFlags(); const settingFlags = useSettingFlags();
const { t } = useTranslation();
// const { toggleReadingMode, readingMode } = useReadingMode(); // const { toggleReadingMode, readingMode } = useReadingMode();
const settings: SettingItem[] = [ const settings: SettingItem[] = [
@@ -80,7 +83,8 @@ export const useSettings = (): SettingItem[] => {
// }, // },
{ {
type: 'button', type: 'button',
name: 'Duplicate Page', name: t('Duplicate Page'),
key: 'Duplicate Page',
onClick: async () => { onClick: async () => {
const newPageInfo = await duplicatePage({ const newPageInfo = await duplicatePage({
workspaceId, workspaceId,
@@ -91,18 +95,29 @@ export const useSettings = (): SettingItem[] => {
}, },
{ {
type: 'button', type: 'button',
name: 'Copy Page Link', name: t('Copy Page Link'),
key: 'Copy Page Link',
onClick: () => { onClick: () => {
copyToClipboard(window.location.href); copyToClipboard(window.location.href);
message.success('Page link copied successfully'); message.success('Page link copied successfully');
}, },
}, },
{
type: 'button',
name: t('Language'),
key: 'Language',
onClick: () => {
// Do noting
},
},
{ {
type: 'separator', type: 'separator',
key: 'separator1',
}, },
{ {
type: 'button', type: 'button',
name: 'Export As Markdown', name: t('Export As Markdown'),
key: 'Export As Markdown',
onClick: async () => { onClick: async () => {
const title = await getPageTitle({ workspaceId, pageId }); const title = await getPageTitle({ workspaceId, pageId });
exportMarkdown({ workspaceId, rootBlockId: pageId, title }); exportMarkdown({ workspaceId, rootBlockId: pageId, title });
@@ -111,7 +126,8 @@ export const useSettings = (): SettingItem[] => {
}, },
{ {
type: 'button', type: 'button',
name: 'Export As HTML', name: t('Export As HTML'),
key: 'Export As HTML',
onClick: async () => { onClick: async () => {
const title = await getPageTitle({ workspaceId, pageId }); const title = await getPageTitle({ workspaceId, pageId });
exportHtml({ workspaceId, rootBlockId: pageId, title }); exportHtml({ workspaceId, rootBlockId: pageId, title });
@@ -120,28 +136,33 @@ export const useSettings = (): SettingItem[] => {
}, },
{ {
type: 'button', type: 'button',
name: 'Export As PDF (Unsupported)', name: t('Export As PDF (Unsupported)'),
key: 'Export As PDF (Unsupported)',
onClick: () => console.log('Export As PDF'), onClick: () => console.log('Export As PDF'),
flag: 'booleanExportPdf', flag: 'booleanExportPdf',
}, },
{ {
type: 'separator', type: 'separator',
key: 'separator2',
}, },
{ {
type: 'button', type: 'button',
name: 'Import Workspace', name: t('Import Workspace'),
key: 'Import Workspace',
onClick: () => importWorkspace(workspaceId), onClick: () => importWorkspace(workspaceId),
flag: 'booleanImportWorkspace', flag: 'booleanImportWorkspace',
}, },
{ {
type: 'button', type: 'button',
name: 'Export Workspace', name: t('Export Workspace'),
key: 'Export Workspace',
onClick: () => exportWorkspace(), onClick: () => exportWorkspace(),
flag: 'booleanExportWorkspace', flag: 'booleanExportWorkspace',
}, },
{ {
type: 'button', type: 'button',
name: 'Clear Workspace', name: t('Clear Workspace'),
key: 'Clear Workspace',
onClick: () => clearWorkspace(workspaceId), onClick: () => clearWorkspace(workspaceId),
flag: 'booleanClearWorkspace', flag: 'booleanClearWorkspace',
}, },

14
pnpm-lock.yaml generated
View File

@@ -196,10 +196,14 @@ importers:
specifiers: specifiers:
'@mui/icons-material': ^5.8.4 '@mui/icons-material': ^5.8.4
firebase: ^9.9.3 firebase: ^9.9.3
i18next: ^21.9.1
mini-css-extract-plugin: ^2.6.1 mini-css-extract-plugin: ^2.6.1
react-i18next: ^11.18.4
webpack: ^5.74.0 webpack: ^5.74.0
dependencies: dependencies:
'@mui/icons-material': 5.8.4 '@mui/icons-material': 5.8.4
i18next: 21.9.1
react-i18next: 11.18.4_i18next@21.9.1
devDependencies: devDependencies:
firebase: 9.9.3 firebase: 9.9.3
mini-css-extract-plugin: 2.6.1_webpack@5.74.0 mini-css-extract-plugin: 2.6.1_webpack@5.74.0
@@ -501,7 +505,9 @@ importers:
'@types/turndown': ^5.0.1 '@types/turndown': ^5.0.1
clsx: ^1.2.1 clsx: ^1.2.1
date-fns: ^2.29.2 date-fns: ^2.29.2
i18next: ^21.9.1
jotai: ^1.8.1 jotai: ^1.8.1
react-i18next: ^11.18.4
tinycolor2: ^1.4.2 tinycolor2: ^1.4.2
turndown: 7.1.1 turndown: 7.1.1
dependencies: dependencies:
@@ -513,7 +519,9 @@ importers:
'@mui/icons-material': 5.8.4 '@mui/icons-material': 5.8.4
clsx: 1.2.1 clsx: 1.2.1
date-fns: 2.29.2 date-fns: 2.29.2
i18next: 21.9.1
jotai: 1.8.1 jotai: 1.8.1
react-i18next: 11.18.4_i18next@21.9.1
tinycolor2: 1.4.2 tinycolor2: 1.4.2
turndown: 7.1.1 turndown: 7.1.1
devDependencies: devDependencies:
@@ -7383,10 +7391,8 @@ packages:
indent-string: 4.0.0 indent-string: 4.0.0
dev: true dev: true
/ajv-formats/2.1.1_ajv@8.11.0: /ajv-formats/2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta: peerDependenciesMeta:
ajv: ajv:
optional: true optional: true
@@ -16953,7 +16959,7 @@ packages:
dependencies: dependencies:
'@types/json-schema': 7.0.11 '@types/json-schema': 7.0.11
ajv: 8.11.0 ajv: 8.11.0
ajv-formats: 2.1.1_ajv@8.11.0 ajv-formats: 2.1.1
ajv-keywords: 5.1.0_ajv@8.11.0 ajv-keywords: 5.1.0_ajv@8.11.0
dev: true dev: true