mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-19 02:51:47 +08:00
@@ -12,4 +12,5 @@ static
|
|||||||
web-static
|
web-static
|
||||||
public
|
public
|
||||||
packages/frontend/i18n/src/i18n-generated.ts
|
packages/frontend/i18n/src/i18n-generated.ts
|
||||||
|
packages/frontend/i18n/src/i18n-completenesses.json
|
||||||
packages/frontend/templates/*.gen.ts
|
packages/frontend/templates/*.gen.ts
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
- name: Run i18n codegen
|
- name: Run i18n codegen
|
||||||
run: yarn i18n-codegen gen
|
run: yarn workspace @affine/i18n build
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: yarn lint:eslint --max-warnings=0
|
run: yarn lint:eslint --max-warnings=0
|
||||||
- name: Run Prettier
|
- name: Run Prettier
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ Thumbs.db
|
|||||||
.vercel
|
.vercel
|
||||||
out/
|
out/
|
||||||
storybook-static
|
storybook-static
|
||||||
i18n-generated.ts
|
|
||||||
|
|
||||||
test-results
|
test-results
|
||||||
playwright-report
|
playwright-report
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public
|
|||||||
packages/backend/server/src/schema.gql
|
packages/backend/server/src/schema.gql
|
||||||
packages/backend/server/src/fundamentals/error/errors.gen.ts
|
packages/backend/server/src/fundamentals/error/errors.gen.ts
|
||||||
packages/frontend/i18n/src/i18n-generated.ts
|
packages/frontend/i18n/src/i18n-generated.ts
|
||||||
|
packages/frontend/i18n/src/i18n-completenesses.json
|
||||||
packages/frontend/graphql/src/graphql/index.ts
|
packages/frontend/graphql/src/graphql/index.ts
|
||||||
tests/affine-legacy/**/static
|
tests/affine-legacy/**/static
|
||||||
.yarnrc.yml
|
.yarnrc.yml
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Telemetry } from '@affine/core/components/telemetry';
|
|||||||
import { router } from '@affine/core/desktop/router';
|
import { router } from '@affine/core/desktop/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
||||||
|
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||||
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
||||||
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
configureSqliteWorkspaceEngineStorageProvider,
|
configureSqliteWorkspaceEngineStorageProvider,
|
||||||
} from '@affine/core/modules/workspace-engine';
|
} from '@affine/core/modules/workspace-engine';
|
||||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import {
|
import {
|
||||||
Framework,
|
Framework,
|
||||||
@@ -50,15 +50,6 @@ const future = {
|
|||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function loadLanguage() {
|
|
||||||
const i18n = createI18n();
|
|
||||||
document.documentElement.lang = i18n.language;
|
|
||||||
|
|
||||||
await setUpLanguage(i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
let languageLoadingPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
const framework = new Framework();
|
const framework = new Framework();
|
||||||
configureCommonModules(framework);
|
configureCommonModules(framework);
|
||||||
configureElectronStateStorageImpls(framework);
|
configureElectronStateStorageImpls(framework);
|
||||||
@@ -76,29 +67,27 @@ window.addEventListener('focus', () => {
|
|||||||
frameworkProvider.get(LifecycleService).applicationStart();
|
frameworkProvider.get(LifecycleService).applicationStart();
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (!languageLoadingPromise) {
|
|
||||||
languageLoadingPromise = loadLanguage().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
<CacheProvider value={cache}>
|
<CacheProvider value={cache}>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<I18nProvider>
|
||||||
<Telemetry />
|
<AffineContext store={getCurrentStore()}>
|
||||||
<CustomThemeModifier />
|
<Telemetry />
|
||||||
<GlobalLoading />
|
<CustomThemeModifier />
|
||||||
<RouterProvider
|
<GlobalLoading />
|
||||||
fallbackElement={<AppFallback />}
|
<RouterProvider
|
||||||
router={router}
|
fallbackElement={<AppFallback />}
|
||||||
future={future}
|
router={router}
|
||||||
/>
|
future={future}
|
||||||
{environment.isWindows && (
|
/>
|
||||||
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
|
{environment.isWindows && (
|
||||||
<WindowsAppControls />
|
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
|
||||||
</div>
|
<WindowsAppControls />
|
||||||
)}
|
</div>
|
||||||
</AffineContext>
|
)}
|
||||||
|
</AffineContext>
|
||||||
|
</I18nProvider>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</FrameworkRoot>
|
</FrameworkRoot>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import './setup';
|
import './setup';
|
||||||
|
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
|
||||||
function loadLanguage() {
|
|
||||||
const i18n = createI18n();
|
|
||||||
document.documentElement.lang = i18n.language;
|
|
||||||
|
|
||||||
setUpLanguage(i18n).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const handleMaximized = (maximized: boolean | undefined) => {
|
const handleMaximized = (maximized: boolean | undefined) => {
|
||||||
document.documentElement.dataset.maximized = String(maximized);
|
document.documentElement.dataset.maximized = String(maximized);
|
||||||
@@ -31,7 +23,6 @@ async function main() {
|
|||||||
events?.ui.onFullScreen(handleFullscreen);
|
events?.ui.onFullScreen(handleFullscreen);
|
||||||
events?.ui.onTabShellViewActiveChange(handleActive);
|
events?.ui.onTabShellViewActiveChange(handleActive);
|
||||||
|
|
||||||
loadLanguage();
|
|
||||||
mountApp();
|
mountApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Telemetry } from '@affine/core/components/telemetry';
|
|||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
|
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
||||||
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||||
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
|
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
configureBrowserWorkspaceFlavours,
|
configureBrowserWorkspaceFlavours,
|
||||||
configureIndexedDBWorkspaceEngineStorageProvider,
|
configureIndexedDBWorkspaceEngineStorageProvider,
|
||||||
} from '@affine/core/modules/workspace-engine';
|
} from '@affine/core/modules/workspace-engine';
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
|
||||||
import {
|
import {
|
||||||
Framework,
|
Framework,
|
||||||
FrameworkRoot,
|
FrameworkRoot,
|
||||||
@@ -25,15 +25,6 @@ const future = {
|
|||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function loadLanguage() {
|
|
||||||
const i18n = createI18n();
|
|
||||||
document.documentElement.lang = i18n.language;
|
|
||||||
|
|
||||||
await setUpLanguage(i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
let languageLoadingPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
const framework = new Framework();
|
const framework = new Framework();
|
||||||
configureCommonModules(framework);
|
configureCommonModules(framework);
|
||||||
configureBrowserWorkbenchModule(framework);
|
configureBrowserWorkbenchModule(framework);
|
||||||
@@ -51,21 +42,19 @@ window.addEventListener('focus', () => {
|
|||||||
frameworkProvider.get(LifecycleService).applicationStart();
|
frameworkProvider.get(LifecycleService).applicationStart();
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (!languageLoadingPromise) {
|
|
||||||
languageLoadingPromise = loadLanguage().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<I18nProvider>
|
||||||
<Telemetry />
|
<AffineContext store={getCurrentStore()}>
|
||||||
<RouterProvider
|
<Telemetry />
|
||||||
fallbackElement={<AppFallback />}
|
<RouterProvider
|
||||||
router={router}
|
fallbackElement={<AppFallback />}
|
||||||
future={future}
|
router={router}
|
||||||
/>
|
future={future}
|
||||||
</AffineContext>
|
/>
|
||||||
|
</AffineContext>
|
||||||
|
</I18nProvider>
|
||||||
</FrameworkRoot>
|
</FrameworkRoot>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AppFallback } from '@affine/core/components/affine/app-container';
|
|||||||
import { Telemetry } from '@affine/core/components/telemetry';
|
import { Telemetry } from '@affine/core/components/telemetry';
|
||||||
import { router } from '@affine/core/desktop/router';
|
import { router } from '@affine/core/desktop/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
|
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
||||||
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
||||||
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
configureIndexedDBWorkspaceEngineStorageProvider,
|
configureIndexedDBWorkspaceEngineStorageProvider,
|
||||||
} from '@affine/core/modules/workspace-engine';
|
} from '@affine/core/modules/workspace-engine';
|
||||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import {
|
import {
|
||||||
Framework,
|
Framework,
|
||||||
@@ -30,15 +30,6 @@ const future = {
|
|||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function loadLanguage() {
|
|
||||||
const i18n = createI18n();
|
|
||||||
document.documentElement.lang = i18n.language;
|
|
||||||
|
|
||||||
await setUpLanguage(i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
let languageLoadingPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
const framework = new Framework();
|
const framework = new Framework();
|
||||||
configureCommonModules(framework);
|
configureCommonModules(framework);
|
||||||
configureBrowserWorkbenchModule(framework);
|
configureBrowserWorkbenchModule(framework);
|
||||||
@@ -55,24 +46,22 @@ window.addEventListener('focus', () => {
|
|||||||
frameworkProvider.get(LifecycleService).applicationStart();
|
frameworkProvider.get(LifecycleService).applicationStart();
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (!languageLoadingPromise) {
|
|
||||||
languageLoadingPromise = loadLanguage().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
<CacheProvider value={cache}>
|
<CacheProvider value={cache}>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<I18nProvider>
|
||||||
<Telemetry />
|
<AffineContext store={getCurrentStore()}>
|
||||||
<CustomThemeModifier />
|
<Telemetry />
|
||||||
<GlobalLoading />
|
<CustomThemeModifier />
|
||||||
<RouterProvider
|
<GlobalLoading />
|
||||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
<RouterProvider
|
||||||
router={router}
|
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||||
future={future}
|
router={router}
|
||||||
/>
|
future={future}
|
||||||
</AffineContext>
|
/>
|
||||||
|
</AffineContext>
|
||||||
|
</I18nProvider>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</FrameworkRoot>
|
</FrameworkRoot>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { I18n } from '@affine/core/modules/i18n';
|
||||||
|
import type { useI18n } from '@affine/i18n';
|
||||||
|
import { track } from '@affine/track';
|
||||||
|
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
import { registerAffineCommand } from './registry';
|
||||||
|
|
||||||
|
export function registerAffineLanguageCommands({
|
||||||
|
i18n,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
i18n: I18n;
|
||||||
|
t: ReturnType<typeof useI18n>;
|
||||||
|
}) {
|
||||||
|
// Display Language
|
||||||
|
const disposables = i18n.languageList.map(language => {
|
||||||
|
return registerAffineCommand({
|
||||||
|
id: `affine:change-display-language-to-${language.name}`,
|
||||||
|
label: `${t['com.affine.cmdk.affine.display-language.to']()} ${
|
||||||
|
language.originalName
|
||||||
|
}`,
|
||||||
|
category: 'affine:settings',
|
||||||
|
icon: <SettingsIcon />,
|
||||||
|
preconditionStrategy: () =>
|
||||||
|
i18n.currentLanguage$.value.key !== language.key,
|
||||||
|
run() {
|
||||||
|
track.$.cmdk.settings.changeAppSetting({
|
||||||
|
key: 'language',
|
||||||
|
value: language.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n.changeLanguage(language.key);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposables.forEach(dispose => dispose());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import { appSettingAtom } from '@toeverything/infra';
|
|||||||
import type { createStore } from 'jotai';
|
import type { createStore } from 'jotai';
|
||||||
import type { useTheme } from 'next-themes';
|
import type { useTheme } from 'next-themes';
|
||||||
|
|
||||||
import type { useLanguageHelper } from '../components/hooks/affine/use-language-helper';
|
|
||||||
import type { EditorSettingService } from '../modules/editor-settting';
|
import type { EditorSettingService } from '../modules/editor-settting';
|
||||||
import { registerAffineCommand } from './registry';
|
import { registerAffineCommand } from './registry';
|
||||||
|
|
||||||
@@ -13,17 +12,14 @@ export function registerAffineSettingsCommands({
|
|||||||
t,
|
t,
|
||||||
store,
|
store,
|
||||||
theme,
|
theme,
|
||||||
languageHelper,
|
|
||||||
editorSettingService,
|
editorSettingService,
|
||||||
}: {
|
}: {
|
||||||
t: ReturnType<typeof useI18n>;
|
t: ReturnType<typeof useI18n>;
|
||||||
store: ReturnType<typeof createStore>;
|
store: ReturnType<typeof createStore>;
|
||||||
theme: ReturnType<typeof useTheme>;
|
theme: ReturnType<typeof useTheme>;
|
||||||
languageHelper: ReturnType<typeof useLanguageHelper>;
|
|
||||||
editorSettingService: EditorSettingService;
|
editorSettingService: EditorSettingService;
|
||||||
}) {
|
}) {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
const { onLanguageChange, languagesList, currentLanguage } = languageHelper;
|
|
||||||
const updateSettings = editorSettingService.editorSetting.set.bind(
|
const updateSettings = editorSettingService.editorSetting.set.bind(
|
||||||
editorSettingService.editorSetting
|
editorSettingService.editorSetting
|
||||||
);
|
);
|
||||||
@@ -148,29 +144,6 @@ export function registerAffineSettingsCommands({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Display Language
|
|
||||||
languagesList.forEach(language => {
|
|
||||||
unsubs.push(
|
|
||||||
registerAffineCommand({
|
|
||||||
id: `affine:change-display-language-to-${language.name}`,
|
|
||||||
label: `${t['com.affine.cmdk.affine.display-language.to']()} ${
|
|
||||||
language.originalName
|
|
||||||
}`,
|
|
||||||
category: 'affine:settings',
|
|
||||||
icon: <SettingsIcon />,
|
|
||||||
preconditionStrategy: () => currentLanguage?.tag !== language.tag,
|
|
||||||
run() {
|
|
||||||
track.$.cmdk.settings.changeAppSetting({
|
|
||||||
key: 'language',
|
|
||||||
value: language.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
onLanguageChange(language.tag);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Layout Style
|
// Layout Style
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './affine-creation';
|
export * from './affine-creation';
|
||||||
export * from './affine-help';
|
export * from './affine-help';
|
||||||
|
export * from './affine-i18n';
|
||||||
export * from './affine-layout';
|
export * from './affine-layout';
|
||||||
export * from './affine-navigation';
|
export * from './affine-navigation';
|
||||||
export * from './affine-settings';
|
export * from './affine-settings';
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||||
import { calcLocaleCompleteness } from '@affine/i18n';
|
import { type I18n, I18nService } from '@affine/core/modules/i18n';
|
||||||
import { DoneIcon } from '@blocksuite/icons/rc';
|
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useLanguageHelper } from '../../../components/hooks/affine/use-language-helper';
|
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
// Fixme: keyboard focus should be supported by Menu component
|
// Fixme: keyboard focus should be supported by Menu component
|
||||||
const LanguageMenuContent = memo(function LanguageMenuContent() {
|
const LanguageMenuContent = memo(function LanguageMenuContent({
|
||||||
const { currentLanguage, languagesList, onLanguageChange } =
|
current,
|
||||||
useLanguageHelper();
|
onChange,
|
||||||
|
i18n,
|
||||||
|
}: {
|
||||||
|
i18n: I18n;
|
||||||
|
current: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{languagesList.map(option => {
|
{i18n.languageList.map(lang => {
|
||||||
const selected = currentLanguage?.originalName === option.originalName;
|
const selected = current === lang.key;
|
||||||
const completeness = calcLocaleCompleteness(option.tag);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={option.name}
|
key={lang.name}
|
||||||
title={option.name}
|
title={lang.name}
|
||||||
lang={option.tag}
|
lang={lang.key}
|
||||||
onSelect={() => onLanguageChange(option.tag)}
|
onSelect={() => onChange(lang.key)}
|
||||||
suffix={(completeness * 100).toFixed(0) + '%'}
|
suffix={lang.completeness + '%'}
|
||||||
data-selected={selected}
|
data-selected={selected}
|
||||||
className={styles.menuItem}
|
className={styles.menuItem}
|
||||||
>
|
>
|
||||||
<div className={styles.languageLabelWrapper}>
|
<div className={styles.languageLabelWrapper}>
|
||||||
<div>{option.originalName}</div>
|
<div>{lang.originalName}</div>
|
||||||
{selected && <DoneIcon fontSize={'16px'} />}
|
{selected && <DoneIcon fontSize={'16px'} />}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -39,10 +43,20 @@ const LanguageMenuContent = memo(function LanguageMenuContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const LanguageMenu = () => {
|
export const LanguageMenu = () => {
|
||||||
const { currentLanguage } = useLanguageHelper();
|
const i18n = useService(I18nService).i18n;
|
||||||
|
const currentLanguage = useLiveData(i18n.currentLanguage$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={(<LanguageMenuContent />) as ReactElement}
|
items={
|
||||||
|
(
|
||||||
|
<LanguageMenuContent
|
||||||
|
current={currentLanguage.key}
|
||||||
|
onChange={i18n.changeLanguage}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
) as ReactElement
|
||||||
|
}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
className: styles.menu,
|
className: styles.menu,
|
||||||
align: 'end',
|
align: 'end',
|
||||||
@@ -53,7 +67,7 @@ export const LanguageMenu = () => {
|
|||||||
style={{ textTransform: 'capitalize', fontWeight: 600, width: '250px' }}
|
style={{ textTransform: 'capitalize', fontWeight: 600, width: '250px' }}
|
||||||
block={true}
|
block={true}
|
||||||
>
|
>
|
||||||
{currentLanguage?.originalName || ''}
|
{currentLanguage.originalName}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function AffinePageReference({
|
|||||||
}) {
|
}) {
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const journalHelper = useJournalInfoHelper();
|
const journalHelper = useJournalInfoHelper();
|
||||||
const t = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
let linkWithMode: DocMode | null = null;
|
let linkWithMode: DocMode | null = null;
|
||||||
let linkToNode = false;
|
let linkToNode = false;
|
||||||
@@ -59,9 +59,7 @@ export function AffinePageReference({
|
|||||||
const el = (
|
const el = (
|
||||||
<>
|
<>
|
||||||
<Icon className={styles.pageReferenceIcon} />
|
<Icon className={styles.pageReferenceIcon} />
|
||||||
<span className="affine-reference-title">
|
<span className="affine-reference-title">{i18n.t(title)}</span>
|
||||||
{typeof title === 'string' ? title : t[title.key]()}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ export function AffineSharedPageReference({
|
|||||||
}) {
|
}) {
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const journalHelper = useJournalInfoHelper();
|
const journalHelper = useJournalInfoHelper();
|
||||||
const t = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
let linkWithMode: DocMode | null = null;
|
let linkWithMode: DocMode | null = null;
|
||||||
let linkToNode = false;
|
let linkToNode = false;
|
||||||
@@ -155,9 +153,7 @@ export function AffineSharedPageReference({
|
|||||||
const el = (
|
const el = (
|
||||||
<>
|
<>
|
||||||
<Icon className={styles.pageReferenceIcon} />
|
<Icon className={styles.pageReferenceIcon} />
|
||||||
<span className="affine-reference-title">
|
<span className="affine-reference-title">{i18n.t(title)}</span>
|
||||||
{typeof title === 'string' ? title : t[title.key]()}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -23,7 +23,7 @@ import {
|
|||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { i18nTime, Trans, useI18n } from '@affine/i18n';
|
import { type I18nString, i18nTime, Trans, useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -43,17 +43,19 @@ import { BelieverCard } from '../plans/lifetime/believer-card';
|
|||||||
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
enum DescriptionI18NKey {
|
const DescriptionI18NKey = {
|
||||||
Basic = 'com.affine.payment.billing-setting.current-plan.description',
|
Basic: 'com.affine.payment.billing-setting.current-plan.description',
|
||||||
Monthly = 'com.affine.payment.billing-setting.current-plan.description.monthly',
|
Monthly:
|
||||||
Yearly = 'com.affine.payment.billing-setting.current-plan.description.yearly',
|
'com.affine.payment.billing-setting.current-plan.description.monthly',
|
||||||
Lifetime = 'com.affine.payment.billing-setting.current-plan.description.lifetime',
|
Yearly: 'com.affine.payment.billing-setting.current-plan.description.yearly',
|
||||||
}
|
Lifetime:
|
||||||
|
'com.affine.payment.billing-setting.current-plan.description.lifetime',
|
||||||
|
} as const satisfies { [key: string]: I18nString };
|
||||||
|
|
||||||
const getMessageKey = (
|
const getMessageKey = (
|
||||||
plan: SubscriptionPlan,
|
plan: SubscriptionPlan,
|
||||||
recurring: SubscriptionRecurring
|
recurring: SubscriptionRecurring
|
||||||
): DescriptionI18NKey => {
|
) => {
|
||||||
if (plan !== SubscriptionPlan.Pro) {
|
if (plan !== SubscriptionPlan.Pro) {
|
||||||
return DescriptionI18NKey.Basic;
|
return DescriptionI18NKey.Basic;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -370,10 +370,10 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.insert-links',
|
i18nKey: 'com.affine.cmdk.insert-links',
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
key: 'com.affine.cmdk.docs.placeholder',
|
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ export function createLinkedWidgetConfig(
|
|||||||
}).value;
|
}).value;
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
title: typeof title === 'string' ? title : I18n[title.key](),
|
title: I18n.t(title),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(({ title }) => isFuzzyMatch(title, query));
|
.filter(({ title }) => isFuzzyMatch(title, query));
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
|
||||||
import { LOCALES, useI18n } from '@affine/i18n';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
export function useLanguageHelper() {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const currentLanguage = useMemo(
|
|
||||||
() => LOCALES.find(item => item.tag === i18n.language),
|
|
||||||
[i18n.language]
|
|
||||||
);
|
|
||||||
const languagesList = useMemo(
|
|
||||||
() =>
|
|
||||||
LOCALES.map(item => ({
|
|
||||||
tag: item.tag,
|
|
||||||
originalName: item.originalName,
|
|
||||||
name: item.name,
|
|
||||||
})),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const onLanguageChange = useAsyncCallback(
|
|
||||||
async (event: string) => {
|
|
||||||
await i18n.changeLanguage(event);
|
|
||||||
},
|
|
||||||
[i18n]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentLanguage) {
|
|
||||||
document.documentElement.lang = currentLanguage.tag;
|
|
||||||
}
|
|
||||||
}, [currentLanguage]);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
currentLanguage,
|
|
||||||
languagesList,
|
|
||||||
onLanguageChange,
|
|
||||||
}),
|
|
||||||
[currentLanguage, languagesList, onLanguageChange]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||||
|
import { I18nService } from '@affine/core/modules/i18n';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
registerAffineCreationCommands,
|
registerAffineCreationCommands,
|
||||||
registerAffineHelpCommands,
|
registerAffineHelpCommands,
|
||||||
|
registerAffineLanguageCommands,
|
||||||
registerAffineLayoutCommands,
|
registerAffineLayoutCommands,
|
||||||
registerAffineNavigationCommands,
|
registerAffineNavigationCommands,
|
||||||
registerAffineSettingsCommands,
|
registerAffineSettingsCommands,
|
||||||
@@ -20,7 +22,6 @@ import { usePageHelper } from '../../components/blocksuite/block-suite-page-list
|
|||||||
import { CreateWorkspaceDialogService } from '../../modules/create-workspace';
|
import { CreateWorkspaceDialogService } from '../../modules/create-workspace';
|
||||||
import { EditorSettingService } from '../../modules/editor-settting';
|
import { EditorSettingService } from '../../modules/editor-settting';
|
||||||
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
|
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
|
||||||
import { useLanguageHelper } from './affine/use-language-helper';
|
|
||||||
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
||||||
import { useNavigateHelper } from './use-navigate-helper';
|
import { useNavigateHelper } from './use-navigate-helper';
|
||||||
|
|
||||||
@@ -65,7 +66,6 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const languageHelper = useLanguageHelper();
|
|
||||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const [editor] = useActiveBlocksuiteEditor();
|
const [editor] = useActiveBlocksuiteEditor();
|
||||||
@@ -73,6 +73,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const editorSettingService = useService(EditorSettingService);
|
const editorSettingService = useService(EditorSettingService);
|
||||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||||
const appSidebarService = useService(AppSidebarService);
|
const appSidebarService = useService(AppSidebarService);
|
||||||
|
const i18n = useService(I18nService).i18n;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
||||||
@@ -114,14 +115,25 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
store,
|
store,
|
||||||
t,
|
t,
|
||||||
theme,
|
theme,
|
||||||
languageHelper,
|
|
||||||
editorSettingService,
|
editorSettingService,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsub();
|
unsub();
|
||||||
};
|
};
|
||||||
}, [editorSettingService, languageHelper, store, t, theme]);
|
}, [editorSettingService, store, t, theme]);
|
||||||
|
|
||||||
|
// register AffineLanguageCommands
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = registerAffineLanguageCommands({
|
||||||
|
i18n,
|
||||||
|
t,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub();
|
||||||
|
};
|
||||||
|
}, [i18n, t]);
|
||||||
|
|
||||||
// register AffineLayoutCommands
|
// register AffineLayoutCommands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
Ref,
|
Ref,
|
||||||
VariableMap,
|
VariableMap,
|
||||||
} from '@affine/env/filter';
|
} from '@affine/env/filter';
|
||||||
import { createI18n, I18nextProvider } from '@affine/i18n';
|
import { getOrCreateI18n, I18nextProvider } from '@affine/i18n';
|
||||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
@@ -128,8 +128,8 @@ describe('eval filter', () => {
|
|||||||
|
|
||||||
describe('render filter', () => {
|
describe('render filter', () => {
|
||||||
test('boolean condition value change', async () => {
|
test('boolean condition value change', async () => {
|
||||||
const i18n = createI18n();
|
|
||||||
const is = filterMatcher.match(tBoolean.create());
|
const is = filterMatcher.match(tBoolean.create());
|
||||||
|
const i18n = getOrCreateI18n();
|
||||||
assertExists(is);
|
assertExists(is);
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
const [value, onChange] = useState(
|
const [value, onChange] = useState(
|
||||||
|
|||||||
@@ -278,10 +278,10 @@ function tagIdToTagOption(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PageTitle = ({ id }: { id: string }) => {
|
const PageTitle = ({ id }: { id: string }) => {
|
||||||
const t = useI18n();
|
const i18n = useI18n();
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const title = useLiveData(docDisplayMetaService.title$(id));
|
const title = useLiveData(docDisplayMetaService.title$(id));
|
||||||
return typeof title === 'string' ? title : t[title.key]();
|
return i18n.t(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UnifiedPageIcon = ({ id }: { id: string }) => {
|
const UnifiedPageIcon = ({ id }: { id: string }) => {
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ interface PageItemProps
|
|||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
}
|
}
|
||||||
const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
||||||
const t = useI18n();
|
const i18n = useI18n();
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const Icon = useLiveData(
|
const Icon = useLiveData(
|
||||||
docDisplayMetaService.icon$(docId, { compareDate: new Date() })
|
docDisplayMetaService.icon$(docId, { compareDate: new Date() })
|
||||||
);
|
);
|
||||||
const titleMeta = useLiveData(docDisplayMetaService.title$(docId));
|
const titleMeta = useLiveData(docDisplayMetaService.title$(docId));
|
||||||
const title = typeof titleMeta === 'string' ? titleMeta : t[titleMeta.key]();
|
const title = i18n.t(titleMeta);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkbenchLink
|
<WorkbenchLink
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ export interface SearchResLabelProps {
|
|||||||
export const SearchResLabel = ({ item }: SearchResLabelProps) => {
|
export const SearchResLabel = ({ item }: SearchResLabelProps) => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const text = !isI18nString(item.label)
|
return (
|
||||||
? i18n.t(item.label.title)
|
<HighlightText
|
||||||
: i18n.t(item.label);
|
text={i18n.t(isI18nString(item.label) ? item.label : item.label.title)}
|
||||||
return <HighlightText text={text} start="<b>" end="</b>" />;
|
start="<b>"
|
||||||
|
end="</b>"
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLanguageHelper } from '@affine/core/components/hooks/affine/use-language-helper';
|
import { I18nService } from '@affine/core/modules/i18n';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { SettingDropdownSelect } from '../dropdown-select';
|
import { SettingDropdownSelect } from '../dropdown-select';
|
||||||
@@ -7,24 +8,24 @@ import { RowLayout } from '../row.layout';
|
|||||||
|
|
||||||
export const LanguageSetting = () => {
|
export const LanguageSetting = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { currentLanguage, languagesList, onLanguageChange } =
|
const i18n = useService(I18nService).i18n;
|
||||||
useLanguageHelper();
|
const currentLanguage = useLiveData(i18n.currentLanguage$);
|
||||||
|
|
||||||
const languageOptions = useMemo(
|
const languageOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
languagesList.map(language => ({
|
i18n.languageList.map(language => ({
|
||||||
label: language.originalName,
|
label: language.originalName,
|
||||||
value: language.tag,
|
value: language.key,
|
||||||
})),
|
})),
|
||||||
[languagesList]
|
[i18n]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowLayout label={t['com.affine.mobile.setting.appearance.language']()}>
|
<RowLayout label={t['com.affine.mobile.setting.appearance.language']()}>
|
||||||
<SettingDropdownSelect
|
<SettingDropdownSelect
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
value={currentLanguage?.tag}
|
value={currentLanguage.key}
|
||||||
onChange={onLanguageChange}
|
onChange={i18n.changeLanguage}
|
||||||
menuOptions={{
|
menuOptions={{
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export class DocDisplayMetaService extends Service {
|
|||||||
if (options?.originalTitle) return options.originalTitle;
|
if (options?.originalTitle) return options.originalTitle;
|
||||||
|
|
||||||
// empty title
|
// empty title
|
||||||
if (!docTitle) return { key: 'Untitled' } as const;
|
if (!docTitle) return { i18nKey: 'Untitled' } as const;
|
||||||
|
|
||||||
// reference
|
// reference
|
||||||
if (options?.reference) return docTitle;
|
if (options?.reference) return docTitle;
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export const ExplorerDocNode = ({
|
|||||||
return (
|
return (
|
||||||
<ExplorerTreeNode
|
<ExplorerTreeNode
|
||||||
icon={Icon}
|
icon={Icon}
|
||||||
name={typeof docTitle === 'string' ? docTitle : t[docTitle.key]()}
|
name={t.t(docTitle)}
|
||||||
dndData={dndData}
|
dndData={dndData}
|
||||||
onDrop={handleDropOnDoc}
|
onDrop={handleDropOnDoc}
|
||||||
renameable
|
renameable
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { I18nextProvider } from '@affine/i18n';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { type PropsWithChildren, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { I18nService } from './services/i18n';
|
||||||
|
|
||||||
|
export function I18nProvider({ children }: PropsWithChildren) {
|
||||||
|
const i18n = useService(I18nService).i18n;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
i18n.init();
|
||||||
|
}, [i18n]);
|
||||||
|
|
||||||
|
return <I18nextProvider i18n={i18n.i18next}>{children}</I18nextProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { notify } from '@affine/component';
|
||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import {
|
||||||
|
getOrCreateI18n,
|
||||||
|
i18nCompletenesses,
|
||||||
|
type Language,
|
||||||
|
SUPPORTED_LANGUAGES,
|
||||||
|
} from '@affine/i18n';
|
||||||
|
import type { GlobalCache } from '@toeverything/infra';
|
||||||
|
import { effect, Entity, fromPromise, LiveData } from '@toeverything/infra';
|
||||||
|
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
|
export type LanguageInfo = {
|
||||||
|
key: Language;
|
||||||
|
name: string;
|
||||||
|
originalName: string;
|
||||||
|
completeness: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = new DebugLogger('i18n');
|
||||||
|
|
||||||
|
function mapLanguageInfo(language: Language = 'en'): LanguageInfo {
|
||||||
|
const languageInfo = SUPPORTED_LANGUAGES[language];
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: language,
|
||||||
|
name: languageInfo.name,
|
||||||
|
originalName: languageInfo.originalName,
|
||||||
|
completeness: i18nCompletenesses[language],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class I18n extends Entity {
|
||||||
|
private readonly i18n = getOrCreateI18n();
|
||||||
|
|
||||||
|
get i18next() {
|
||||||
|
return this.i18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly currentLanguageKey$ = LiveData.from(
|
||||||
|
this.cache.watch<Language>('i18n_lng'),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly currentLanguage$ = this.currentLanguageKey$
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map(mapLanguageInfo);
|
||||||
|
|
||||||
|
readonly languageList: Array<LanguageInfo> =
|
||||||
|
// @ts-expect-error same key indexing
|
||||||
|
Object.keys(SUPPORTED_LANGUAGES).map(mapLanguageInfo);
|
||||||
|
|
||||||
|
constructor(private readonly cache: GlobalCache) {
|
||||||
|
super();
|
||||||
|
this.i18n.on('languageChanged', (language: Language) => {
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
this.cache.set('i18n_lng', language);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.changeLanguage(this.currentLanguageKey$.value ?? 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLanguage = effect(
|
||||||
|
exhaustMap((language: string) =>
|
||||||
|
fromPromise(() => this.i18n.changeLanguage(language)).pipe(
|
||||||
|
catchError(error => {
|
||||||
|
notify({
|
||||||
|
theme: 'error',
|
||||||
|
title: 'Failed to change language',
|
||||||
|
message: 'Error occurs when loading language files',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.error('Failed to change language', error);
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
mergeMap(() => EMPTY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { type Framework, GlobalCache } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { I18nProvider } from './context';
|
||||||
|
import { I18n, type LanguageInfo } from './entities/i18n';
|
||||||
|
import { I18nService } from './services/i18n';
|
||||||
|
|
||||||
|
export function configureI18nModule(framework: Framework) {
|
||||||
|
framework.service(I18nService).entity(I18n, [GlobalCache]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { I18n, I18nProvider, I18nService, type LanguageInfo };
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { I18n } from '../entities/i18n';
|
||||||
|
|
||||||
|
export class I18nService extends Service {
|
||||||
|
public readonly i18n = this.framework.createEntity(I18n);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { configureEditorSettingModule } from './editor-settting';
|
|||||||
import { configureExplorerModule } from './explorer';
|
import { configureExplorerModule } from './explorer';
|
||||||
import { configureFavoriteModule } from './favorite';
|
import { configureFavoriteModule } from './favorite';
|
||||||
import { configureFindInPageModule } from './find-in-page';
|
import { configureFindInPageModule } from './find-in-page';
|
||||||
|
import { configureI18nModule } from './i18n';
|
||||||
import { configureImportTemplateModule } from './import-template';
|
import { configureImportTemplateModule } from './import-template';
|
||||||
import { configureNavigationModule } from './navigation';
|
import { configureNavigationModule } from './navigation';
|
||||||
import { configureOrganizeModule } from './organize';
|
import { configureOrganizeModule } from './organize';
|
||||||
@@ -30,6 +31,7 @@ import { configureThemeEditorModule } from './theme-editor';
|
|||||||
import { configureUserspaceModule } from './userspace';
|
import { configureUserspaceModule } from './userspace';
|
||||||
|
|
||||||
export function configureCommonModules(framework: Framework) {
|
export function configureCommonModules(framework: Framework) {
|
||||||
|
configureI18nModule(framework);
|
||||||
configureInfraModules(framework);
|
configureInfraModules(framework);
|
||||||
configureCollectionModule(framework);
|
configureCollectionModule(framework);
|
||||||
configureNavigationModule(framework);
|
configureNavigationModule(framework);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { highlighter } from '../utils/highlighter';
|
|||||||
const group = {
|
const group = {
|
||||||
id: 'collections',
|
id: 'collections',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.category.affine.collections',
|
i18nKey: 'com.affine.cmdk.affine.category.affine.collections',
|
||||||
},
|
},
|
||||||
score: 10,
|
score: 10,
|
||||||
} as QuickSearchGroup;
|
} as QuickSearchGroup;
|
||||||
@@ -60,7 +60,7 @@ export class CollectionsQuickSearchSession
|
|||||||
label: {
|
label: {
|
||||||
title: (highlighter(item.name, '<b>', '</b>', titleMatches ?? []) ??
|
title: (highlighter(item.name, '<b>', '</b>', titleMatches ?? []) ??
|
||||||
item.name) || {
|
item.name) || {
|
||||||
key: 'Untitled',
|
i18nKey: 'Untitled',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
|
|||||||
@@ -17,81 +17,81 @@ import { highlighter } from '../utils/highlighter';
|
|||||||
const categories = {
|
const categories = {
|
||||||
'affine:recent': {
|
'affine:recent': {
|
||||||
id: 'command:affine:recent',
|
id: 'command:affine:recent',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.recent' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.recent' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:navigation': {
|
'affine:navigation': {
|
||||||
id: 'command:affine:navigation',
|
id: 'command:affine:navigation',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.category.affine.navigation',
|
i18nKey: 'com.affine.cmdk.affine.category.affine.navigation',
|
||||||
},
|
},
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:creation': {
|
'affine:creation': {
|
||||||
id: 'command:affine:creation',
|
id: 'command:affine:creation',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.creation' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.creation' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:general': {
|
'affine:general': {
|
||||||
id: 'command:affine:general',
|
id: 'command:affine:general',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.general' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.general' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:layout': {
|
'affine:layout': {
|
||||||
id: 'command:affine:layout',
|
id: 'command:affine:layout',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.layout' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.layout' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:pages': {
|
'affine:pages': {
|
||||||
id: 'command:affine:pages',
|
id: 'command:affine:pages',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.pages' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.pages' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:edgeless': {
|
'affine:edgeless': {
|
||||||
id: 'command:affine:edgeless',
|
id: 'command:affine:edgeless',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.edgeless' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.edgeless' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:collections': {
|
'affine:collections': {
|
||||||
id: 'command:affine:collections',
|
id: 'command:affine:collections',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.category.affine.collections',
|
i18nKey: 'com.affine.cmdk.affine.category.affine.collections',
|
||||||
},
|
},
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:settings': {
|
'affine:settings': {
|
||||||
id: 'command:affine:settings',
|
id: 'command:affine:settings',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.settings' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.settings' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:updates': {
|
'affine:updates': {
|
||||||
id: 'command:affine:updates',
|
id: 'command:affine:updates',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.updates' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.updates' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:help': {
|
'affine:help': {
|
||||||
id: 'command:affine:help',
|
id: 'command:affine:help',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.affine.help' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.affine.help' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'editor:edgeless': {
|
'editor:edgeless': {
|
||||||
id: 'command:editor:edgeless',
|
id: 'command:editor:edgeless',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.editor.edgeless' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.editor.edgeless' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'editor:insert-object': {
|
'editor:insert-object': {
|
||||||
id: 'command:editor:insert-object',
|
id: 'command:editor:insert-object',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.editor.insert-object' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.editor.insert-object' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'editor:page': {
|
'editor:page': {
|
||||||
id: 'command:editor:page',
|
id: 'command:editor:page',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.editor.page' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.editor.page' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
'affine:results': {
|
'affine:results': {
|
||||||
id: 'command:affine:results',
|
id: 'command:affine:results',
|
||||||
label: { key: 'com.affine.cmdk.affine.category.results' },
|
label: { i18nKey: 'com.affine.cmdk.affine.category.results' },
|
||||||
score: 10,
|
score: 10,
|
||||||
},
|
},
|
||||||
} satisfies Required<{
|
} satisfies Required<{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { QuickSearchItem } from '../types/item';
|
|||||||
|
|
||||||
const group = {
|
const group = {
|
||||||
id: 'creation',
|
id: 'creation',
|
||||||
label: { key: 'com.affine.quicksearch.group.creation' },
|
label: { i18nKey: 'com.affine.quicksearch.group.creation' },
|
||||||
score: 0,
|
score: 0,
|
||||||
} as QuickSearchGroup;
|
} as QuickSearchGroup;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export class CreationQuickSearchSession
|
|||||||
id: 'creation:create-page',
|
id: 'creation:create-page',
|
||||||
source: 'creation',
|
source: 'creation',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.create-new-page-as',
|
i18nKey: 'com.affine.cmdk.affine.create-new-page-as',
|
||||||
options: { keyWord: query },
|
options: { keyWord: query },
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
@@ -41,7 +41,7 @@ export class CreationQuickSearchSession
|
|||||||
id: 'creation:create-edgeless',
|
id: 'creation:create-edgeless',
|
||||||
source: 'creation',
|
source: 'creation',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.create-new-edgeless-as',
|
i18nKey: 'com.affine.cmdk.affine.create-new-edgeless-as',
|
||||||
options: { keyWord: query },
|
options: { keyWord: query },
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class DocsQuickSearchSession
|
|||||||
group: {
|
group: {
|
||||||
id: 'docs',
|
id: 'docs',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.quicksearch.group.searchfor',
|
i18nKey: 'com.affine.quicksearch.group.searchfor',
|
||||||
options: { query: truncate(query) },
|
options: { query: truncate(query) },
|
||||||
},
|
},
|
||||||
score: 5,
|
score: 5,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class ExternalLinksQuickSearchSession
|
|||||||
source: 'external-link',
|
source: 'external-link',
|
||||||
icon: LinkIcon,
|
icon: LinkIcon,
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.insert-link',
|
i18nKey: 'com.affine.cmdk.affine.insert-link',
|
||||||
},
|
},
|
||||||
payload: { url: query },
|
payload: { url: query },
|
||||||
} as QuickSearchItem<'external-link', ExternalLinkPayload>,
|
} as QuickSearchItem<'external-link', ExternalLinkPayload>,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class LinksQuickSearchSession
|
|||||||
group: {
|
group: {
|
||||||
id: 'docs',
|
id: 'docs',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.quicksearch.group.searchfor',
|
i18nKey: 'com.affine.quicksearch.group.searchfor',
|
||||||
options: { query: truncate(query) },
|
options: { query: truncate(query) },
|
||||||
},
|
},
|
||||||
score: 5,
|
score: 5,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { QuickSearchItem } from '../types/item';
|
|||||||
const group = {
|
const group = {
|
||||||
id: 'recent-docs',
|
id: 'recent-docs',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.category.affine.recent',
|
i18nKey: 'com.affine.cmdk.affine.category.affine.recent',
|
||||||
},
|
},
|
||||||
score: 15,
|
score: 15,
|
||||||
} as QuickSearchGroup;
|
} as QuickSearchGroup;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { QuickSearchTagIcon } from '../views/tag-icon';
|
|||||||
const group: QuickSearchGroup = {
|
const group: QuickSearchGroup = {
|
||||||
id: 'tags',
|
id: 'tags',
|
||||||
label: {
|
label: {
|
||||||
key: 'com.affine.cmdk.affine.category.affine.tags',
|
i18nKey: 'com.affine.cmdk.affine.category.affine.tags',
|
||||||
},
|
},
|
||||||
score: 10,
|
score: 10,
|
||||||
};
|
};
|
||||||
@@ -72,7 +72,7 @@ export class TagsQuickSearchSession
|
|||||||
titleMatches ?? []
|
titleMatches ?? []
|
||||||
) ??
|
) ??
|
||||||
item.title) || {
|
item.title) || {
|
||||||
key: 'Untitled',
|
i18nKey: 'Untitled',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export class CMDKQuickSearchService extends Service {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
placeholder: {
|
placeholder: {
|
||||||
key: 'com.affine.cmdk.docs.placeholder',
|
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -232,12 +232,13 @@ export const CMDKGroup = ({
|
|||||||
style={{ overflowAnchor: 'none' }}
|
style={{ overflowAnchor: 'none' }}
|
||||||
>
|
>
|
||||||
{items.map(item => {
|
{items.map(item => {
|
||||||
const title = !isI18nString(item.label)
|
const [title, subTitle] = isI18nString(item.label)
|
||||||
? i18n.t(item.label.title)
|
? [i18n.t(item.label), null]
|
||||||
: i18n.t(item.label);
|
: [
|
||||||
const subTitle = !isI18nString(item.label)
|
i18n.t(item.label.title),
|
||||||
? item.label.subTitle && i18n.t(item.label.subTitle)
|
item.label.subTitle ? i18n.t(item.label.subTitle) : null,
|
||||||
: null;
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
lib
|
lib
|
||||||
|
i18n-generated.ts
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"input": "./packages/frontend/i18n/src/resources/en.json",
|
"input": "./src/resources/en.json",
|
||||||
"output": "./packages/frontend/i18n/src/i18n-generated",
|
"output": "./src/i18n-generated",
|
||||||
"parser": {
|
"parser": {
|
||||||
"type": "i18next",
|
"type": "i18next",
|
||||||
"contextSeparator": "$",
|
"contextSeparator": "$",
|
||||||
@@ -1,16 +1,59 @@
|
|||||||
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join, parse } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import { runCli } from '@magic-works/i18n-codegen';
|
import { runCli } from '@magic-works/i18n-codegen';
|
||||||
|
|
||||||
|
const isDev = process.argv.includes('--dev');
|
||||||
|
|
||||||
|
const pkgRoot = fileURLToPath(new URL('./', import.meta.url));
|
||||||
|
|
||||||
|
function calcCompletenesses() {
|
||||||
|
const resourcesDir = join(pkgRoot, 'src', 'resources');
|
||||||
|
|
||||||
|
const langs = readdirSync(resourcesDir)
|
||||||
|
.filter(file => file.endsWith('.json'))
|
||||||
|
.reduce((langs, file) => {
|
||||||
|
const filePath = `${resourcesDir}/${file}`;
|
||||||
|
const fileContent = JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||||
|
langs[parse(file).name] = fileContent;
|
||||||
|
return langs;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const base = Object.keys(langs.en).length;
|
||||||
|
|
||||||
|
const completenesses = {};
|
||||||
|
|
||||||
|
for (const key in langs) {
|
||||||
|
const [langPart, variantPart] = key.split('-');
|
||||||
|
|
||||||
|
const completeness = Object.keys(
|
||||||
|
variantPart ? { ...langs[langPart], ...langs[key] } : langs[key]
|
||||||
|
).length;
|
||||||
|
|
||||||
|
completenesses[key] = Math.min(
|
||||||
|
Math.ceil(/* avoid 0% */ (completeness / base) * 100),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(pkgRoot, 'src', 'i18n-completenesses.json'),
|
||||||
|
JSON.stringify(completenesses, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
runCli(
|
runCli(
|
||||||
{
|
{
|
||||||
config: fileURLToPath(
|
config: fileURLToPath(new URL('./.i18n-codegen.json', import.meta.url)),
|
||||||
new URL('../../../.i18n-codegen.json', import.meta.url)
|
watch: isDev,
|
||||||
),
|
|
||||||
watch: false,
|
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
if (!isDev) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
calcCompletenesses();
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { runCli } from '@magic-works/i18n-codegen';
|
|
||||||
|
|
||||||
runCli(
|
|
||||||
{
|
|
||||||
config: fileURLToPath(
|
|
||||||
new URL('../../../.i18n-codegen.json', import.meta.url)
|
|
||||||
),
|
|
||||||
watch: true,
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.mjs",
|
"build": "node build.mjs",
|
||||||
"dev": "node dev.mjs"
|
"dev": "node build.mjs --dev"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"url": "git+https://github.com/toeverything/AFFiNE.git"
|
"url": "git+https://github.com/toeverything/AFFiNE.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@affine/debug": "workspace:*",
|
||||||
"@magic-works/i18n-codegen": "^0.6.0",
|
"@magic-works/i18n-codegen": "^0.6.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"i18next": "^23.11.1",
|
"i18next": "^23.11.1",
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"ar": 87,
|
||||||
|
"ca": 6,
|
||||||
|
"da": 7,
|
||||||
|
"de": 33,
|
||||||
|
"en": 100,
|
||||||
|
"es-AR": 14,
|
||||||
|
"es-CL": 16,
|
||||||
|
"es": 14,
|
||||||
|
"fr": 78,
|
||||||
|
"hi": 2,
|
||||||
|
"it": 1,
|
||||||
|
"ja": 28,
|
||||||
|
"ko": 92,
|
||||||
|
"pl": 0,
|
||||||
|
"pt-BR": 100,
|
||||||
|
"ru": 85,
|
||||||
|
"sv-SE": 5,
|
||||||
|
"ur": 3,
|
||||||
|
"zh-Hans": 99,
|
||||||
|
"zh-Hant": 21
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { getI18n, useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import type { useAFFiNEI18N } from './i18n-generated';
|
|
||||||
|
|
||||||
export type I18nFuncs = ReturnType<typeof useAFFiNEI18N>;
|
|
||||||
|
|
||||||
export type I18nInfos = {
|
|
||||||
[K in keyof I18nFuncs]: I18nFuncs[K] extends (...a: infer Opt) => any
|
|
||||||
? Opt[0]
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type I18nKeys = keyof I18nInfos;
|
|
||||||
|
|
||||||
export type I18nString =
|
|
||||||
| {
|
|
||||||
[K in I18nKeys]: {
|
|
||||||
key: K;
|
|
||||||
} & (I18nInfos[K] extends undefined
|
|
||||||
? unknown
|
|
||||||
: { options: I18nInfos[K] });
|
|
||||||
}[I18nKeys]
|
|
||||||
| string;
|
|
||||||
|
|
||||||
export const isI18nString = (value: any): value is I18nString => {
|
|
||||||
return (
|
|
||||||
typeof value === 'string' || (typeof value === 'object' && 'key' in value)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function createI18nWrapper(
|
|
||||||
getI18nFn: () => ReturnType<typeof getI18n>,
|
|
||||||
getI18nT: () => ReturnType<typeof getI18n>['t']
|
|
||||||
) {
|
|
||||||
const I18nMethod = {
|
|
||||||
t(i18nStr: I18nString) {
|
|
||||||
const i18n = getI18nFn();
|
|
||||||
if (typeof i18nStr === 'object') {
|
|
||||||
return i18n.t(i18nStr.key, 'options' in i18nStr ? i18nStr.options : {});
|
|
||||||
}
|
|
||||||
return i18nStr;
|
|
||||||
},
|
|
||||||
get language() {
|
|
||||||
const i18n = getI18nFn();
|
|
||||||
return i18n.language;
|
|
||||||
},
|
|
||||||
changeLanguage(lng?: string | undefined) {
|
|
||||||
const i18n = getI18nFn();
|
|
||||||
return i18n.changeLanguage(lng);
|
|
||||||
},
|
|
||||||
get on() {
|
|
||||||
const i18n = getI18nFn();
|
|
||||||
return i18n.on.bind(i18n);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Proxy(I18nMethod, {
|
|
||||||
get(self, key) {
|
|
||||||
const i18n = getI18nFn();
|
|
||||||
if (typeof key === 'string' && i18n.exists(key)) {
|
|
||||||
return getI18nT().bind(null, key as string);
|
|
||||||
} else {
|
|
||||||
return (self as any)[key as string] as any;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as I18nFuncs & typeof I18nMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useI18n = () => {
|
|
||||||
const { i18n, t } = useTranslation();
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
createI18nWrapper(
|
|
||||||
() => i18n,
|
|
||||||
() => t
|
|
||||||
),
|
|
||||||
[i18n, t]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* I18n['com.affine.xxx']({ arg1: 'hello' }) -> '中文 hello'
|
|
||||||
*/
|
|
||||||
export const I18n = createI18nWrapper(getI18n, () => getI18n().t);
|
|
||||||
export type I18n = typeof I18n;
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import type { BackendModule, i18n } from 'i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { useAFFiNEI18N } from './i18n-generated';
|
||||||
|
import type { Language } from './resources';
|
||||||
|
import { SUPPORTED_LANGUAGES } from './resources';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('i18n');
|
||||||
|
|
||||||
|
const defaultLng: Language = 'en';
|
||||||
|
|
||||||
|
let _instance: i18n | null = null;
|
||||||
|
export const getOrCreateI18n = (): i18n => {
|
||||||
|
if (!_instance) {
|
||||||
|
_instance = i18next.createInstance();
|
||||||
|
_instance
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use({
|
||||||
|
type: 'backend',
|
||||||
|
init: () => {},
|
||||||
|
read: (lng: Language, _ns: string, callback) => {
|
||||||
|
const resource = SUPPORTED_LANGUAGES[lng].resource;
|
||||||
|
if (typeof resource === 'function') {
|
||||||
|
resource()
|
||||||
|
.then(data => {
|
||||||
|
logger.info(`Loaded i18n ${lng} resource`);
|
||||||
|
callback(null, data.default);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error(`Failed to load i18n ${lng} resource`, err);
|
||||||
|
callback(null, null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback(null, resource);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as BackendModule)
|
||||||
|
.init({
|
||||||
|
lng: defaultLng,
|
||||||
|
fallbackLng: code => {
|
||||||
|
// always fallback to english
|
||||||
|
const fallbacks: string[] = [defaultLng];
|
||||||
|
const langPart = code.split('-')[0];
|
||||||
|
|
||||||
|
// fallback xx-YY to xx, e.g. es-AR to es
|
||||||
|
// fallback zh-Hant to zh-Hans
|
||||||
|
if (langPart === 'cn') {
|
||||||
|
fallbacks.push('zh-Hans');
|
||||||
|
} else if (
|
||||||
|
langPart !== code &&
|
||||||
|
SUPPORTED_LANGUAGES[code as Language]
|
||||||
|
) {
|
||||||
|
fallbacks.unshift(langPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbacks;
|
||||||
|
},
|
||||||
|
supportedLngs: Object.keys(SUPPORTED_LANGUAGES),
|
||||||
|
debug: false,
|
||||||
|
partialBundledLanguages: true,
|
||||||
|
resources: {
|
||||||
|
[defaultLng]: {
|
||||||
|
translation: SUPPORTED_LANGUAGES[defaultLng].resource,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info('i18n initialized');
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
// NOTE(@forehalo):
|
||||||
|
// DO NOT ENABLE THIS
|
||||||
|
// This could bring typecheck for <Trans /> component,
|
||||||
|
// but it will make typecheck of whole codebase so laggy!
|
||||||
|
// check [./react.ts]
|
||||||
|
// resources: {
|
||||||
|
// translation: LanguageResource;
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type I18nFuncs = ReturnType<typeof useAFFiNEI18N>;
|
||||||
|
type KnownI18nKey = keyof I18nFuncs;
|
||||||
|
|
||||||
|
export type I18nString =
|
||||||
|
| KnownI18nKey
|
||||||
|
| string
|
||||||
|
| { i18nKey: string; options?: Record<string, any> };
|
||||||
|
|
||||||
|
export function isI18nString(value: unknown): value is I18nString {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return 'i18nKey' in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createI18nWrapper(getI18nFn: () => i18n) {
|
||||||
|
const I18nMethod = {
|
||||||
|
t(key: I18nString, options?: Record<string, any>) {
|
||||||
|
if (typeof key === 'object' && 'i18nKey' in key) {
|
||||||
|
options = key.options;
|
||||||
|
key = key.i18nKey as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = getI18nFn();
|
||||||
|
if (i18n.exists(key)) {
|
||||||
|
return i18n.t(key, options);
|
||||||
|
} else {
|
||||||
|
// unknown translate key 'xxx.xxx' returns itself
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get language() {
|
||||||
|
const i18n = getI18nFn();
|
||||||
|
return i18n.language;
|
||||||
|
},
|
||||||
|
changeLanguage(lng?: string | undefined) {
|
||||||
|
const i18n = getI18nFn();
|
||||||
|
return i18n.changeLanguage(lng);
|
||||||
|
},
|
||||||
|
get on() {
|
||||||
|
const i18n = getI18nFn();
|
||||||
|
return i18n.on.bind(i18n);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy(I18nMethod, {
|
||||||
|
get(self, key: string) {
|
||||||
|
if (key in self) {
|
||||||
|
// @ts-expect-error allow
|
||||||
|
return self[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return I18nMethod.t.bind(null, key);
|
||||||
|
},
|
||||||
|
}) as typeof I18nMethod &
|
||||||
|
ReturnType<typeof useAFFiNEI18N> & { [unknownKey: string]: () => string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I18n['com.affine.xxx']({ arg1: 'hello' }) -> '中文 hello'
|
||||||
|
*/
|
||||||
|
export const I18n = createI18nWrapper(getOrCreateI18n);
|
||||||
|
export type I18n = typeof I18n;
|
||||||
@@ -1,122 +1,7 @@
|
|||||||
import type { i18n, Resource } from 'i18next';
|
export * from './i18next';
|
||||||
import i18next from 'i18next';
|
export * from './react';
|
||||||
import type { I18nextProviderProps } from 'react-i18next';
|
export * from './resources';
|
||||||
import { I18nextProvider, initReactI18next, Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import { LOCALES } from './resources';
|
|
||||||
import type en_US from './resources/en.json';
|
|
||||||
|
|
||||||
export * from './i18n';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
import completenesses from './i18n-completenesses.json';
|
||||||
|
|
||||||
declare module 'i18next' {
|
export const i18nCompletenesses = completenesses;
|
||||||
// Refs: https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
|
|
||||||
interface CustomTypeOptions {
|
|
||||||
returnNull: false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const localStorage = {
|
|
||||||
// getItem() {
|
|
||||||
// return undefined;
|
|
||||||
// },
|
|
||||||
// setItem() {},
|
|
||||||
// };
|
|
||||||
// See https://react.i18next.com/latest/typescript
|
|
||||||
declare module 'react-i18next' {
|
|
||||||
interface CustomTypeOptions {
|
|
||||||
// custom namespace type if you changed it
|
|
||||||
// defaultNS: 'ns1';
|
|
||||||
// custom resources type
|
|
||||||
allowObjectInHTMLChildren: true;
|
|
||||||
resources: {
|
|
||||||
en: typeof en_US;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'i18n_lng';
|
|
||||||
|
|
||||||
export { I18nextProvider, LOCALES, Trans };
|
|
||||||
|
|
||||||
const resources = LOCALES.reduce<Resource>((acc, { tag, res }) => {
|
|
||||||
return Object.assign(acc, { [tag]: { translation: res } });
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const fallbackLng = 'en';
|
|
||||||
const standardizeLocale = (language: string) => {
|
|
||||||
if (language === 'zh-CN' || language === 'zh' || language === 'zh-Hans') {
|
|
||||||
language = 'zh-Hans';
|
|
||||||
} else if (language.slice(0, 2).toLowerCase() === 'zh') {
|
|
||||||
language = 'zh-Hant';
|
|
||||||
}
|
|
||||||
if (LOCALES.some(locale => locale.tag === language)) return language;
|
|
||||||
if (
|
|
||||||
LOCALES.some(locale => locale.tag === language.slice(0, 2).toLowerCase())
|
|
||||||
) {
|
|
||||||
return language.slice(0, 2).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackLng;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createI18n = (): I18nextProviderProps['i18n'] => {
|
|
||||||
const i18n: I18nextProviderProps['i18n'] = i18next.createInstance();
|
|
||||||
i18n
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
lng: 'en',
|
|
||||||
fallbackLng,
|
|
||||||
debug: false,
|
|
||||||
resources,
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
console.info('i18n init success');
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.error('i18n init failed');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (globalThis.localStorage) {
|
|
||||||
i18n.on('languageChanged', lng => {
|
|
||||||
localStorage.setItem(STORAGE_KEY, lng);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return i18n;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setUpLanguage(i: i18n) {
|
|
||||||
let language;
|
|
||||||
const localStorageLanguage = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (localStorageLanguage) {
|
|
||||||
language = standardizeLocale(localStorageLanguage);
|
|
||||||
} else {
|
|
||||||
language = standardizeLocale(navigator.language);
|
|
||||||
}
|
|
||||||
return i.changeLanguage(language);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedCompleteness: Record<string, number> = {};
|
|
||||||
export const calcLocaleCompleteness = (
|
|
||||||
locale: (typeof LOCALES)[number]['tag']
|
|
||||||
) => {
|
|
||||||
if (cachedCompleteness[locale]) {
|
|
||||||
return cachedCompleteness[locale];
|
|
||||||
}
|
|
||||||
const base = LOCALES.find(item => item.base);
|
|
||||||
if (!base) {
|
|
||||||
throw new Error('Base language not found');
|
|
||||||
}
|
|
||||||
const target = LOCALES.find(item => item.tag === locale);
|
|
||||||
if (!target) {
|
|
||||||
throw new Error('Locale not found');
|
|
||||||
}
|
|
||||||
const baseKeyCount = Object.keys(base.res).length;
|
|
||||||
const translatedKeyCount = Object.keys(target.res).length;
|
|
||||||
const completeness = translatedKeyCount / baseKeyCount;
|
|
||||||
cachedCompleteness[target.tag] = completeness;
|
|
||||||
return completeness;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { createI18nWrapper } from './i18next';
|
||||||
|
|
||||||
|
export const useI18n = () => {
|
||||||
|
const { i18n } = useTranslation('translation');
|
||||||
|
|
||||||
|
return useMemo(() => createI18nWrapper(() => i18n), [i18n]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { I18nextProvider, Trans } from 'react-i18next';
|
||||||
@@ -1,174 +1,153 @@
|
|||||||
import ar from './ar.json';
|
|
||||||
import ca from './ca.json';
|
|
||||||
import da from './da.json';
|
|
||||||
import de from './de.json';
|
|
||||||
import en from './en.json';
|
import en from './en.json';
|
||||||
import es from './es.json';
|
|
||||||
import es_AR from './es-AR.json';
|
|
||||||
import es_CL from './es-CL.json';
|
|
||||||
import fr from './fr.json';
|
|
||||||
import hi from './hi.json';
|
|
||||||
import it from './it.json';
|
|
||||||
import ja from './ja.json';
|
|
||||||
import ko from './ko.json';
|
|
||||||
import pt_BR from './pt-BR.json';
|
|
||||||
import ru from './ru.json';
|
|
||||||
import sv_SE from './sv-SE.json';
|
|
||||||
import ur from './ur.json';
|
|
||||||
import zh_Hans from './zh-Hans.json';
|
|
||||||
import zh_Hant from './zh-Hant.json';
|
|
||||||
|
|
||||||
export const LOCALES = [
|
export type Language =
|
||||||
{
|
| 'en'
|
||||||
name: 'Korean (South Korea)',
|
| 'zh-Hans'
|
||||||
tag: 'ko',
|
| 'zh-Hant'
|
||||||
originalName: '한국어(대한민국)',
|
| 'fr'
|
||||||
flagEmoji: '🇰🇷',
|
| 'es'
|
||||||
base: false,
|
| 'es-AR'
|
||||||
res: ko,
|
| 'es-CL'
|
||||||
},
|
| 'de'
|
||||||
{
|
| 'ru'
|
||||||
name: 'Portuguese (Brazil)',
|
| 'ja'
|
||||||
tag: 'pt-BR',
|
| 'it'
|
||||||
originalName: 'português (Brasil)',
|
| 'ca'
|
||||||
flagEmoji: '🇧🇷',
|
| 'da'
|
||||||
base: false,
|
| 'hi'
|
||||||
res: pt_BR,
|
| 'sv-SE'
|
||||||
},
|
| 'ur'
|
||||||
|
| 'ar'
|
||||||
|
| 'ko'
|
||||||
|
| 'pt-BR';
|
||||||
|
|
||||||
|
export type LanguageResource = typeof en;
|
||||||
|
export const SUPPORTED_LANGUAGES: Record<
|
||||||
|
Language,
|
||||||
{
|
{
|
||||||
|
name: string;
|
||||||
|
originalName: string;
|
||||||
|
flagEmoji: string;
|
||||||
|
resource:
|
||||||
|
| LanguageResource
|
||||||
|
| (() => Promise<{ default: Partial<LanguageResource> }>);
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
en: {
|
||||||
name: 'English',
|
name: 'English',
|
||||||
tag: 'en',
|
|
||||||
originalName: 'English',
|
originalName: 'English',
|
||||||
flagEmoji: '🇬🇧',
|
flagEmoji: '🇬🇧',
|
||||||
base: true,
|
resource: en,
|
||||||
res: en,
|
|
||||||
},
|
},
|
||||||
{
|
ko: {
|
||||||
name: 'Traditional Chinese',
|
name: 'Korean (South Korea)',
|
||||||
tag: 'zh-Hant',
|
originalName: '한국어(대한민국)',
|
||||||
originalName: '繁體中文',
|
flagEmoji: '🇰🇷',
|
||||||
flagEmoji: '🇭🇰',
|
resource: () => /* webpackChunkName "i18n-ko" */ import('./ko.json'),
|
||||||
base: false,
|
|
||||||
res: zh_Hant,
|
|
||||||
},
|
},
|
||||||
{
|
'pt-BR': {
|
||||||
|
name: 'Portuguese (Brazil)',
|
||||||
|
originalName: 'português (Brasil)',
|
||||||
|
flagEmoji: '🇧🇷',
|
||||||
|
resource: () => /* webpackChunkName "i18n-pt_BR" */ import('./pt-BR.json'),
|
||||||
|
},
|
||||||
|
'zh-Hans': {
|
||||||
name: 'Simplified Chinese',
|
name: 'Simplified Chinese',
|
||||||
tag: 'zh-Hans',
|
|
||||||
originalName: '简体中文',
|
originalName: '简体中文',
|
||||||
flagEmoji: '🇨🇳',
|
flagEmoji: '🇨🇳',
|
||||||
base: false,
|
resource: () =>
|
||||||
res: zh_Hans,
|
/* webpackChunkName "i18n-zh_Hans" */ import('./zh-Hans.json'),
|
||||||
},
|
},
|
||||||
{
|
'zh-Hant': {
|
||||||
|
name: 'Traditional Chinese',
|
||||||
|
originalName: '繁體中文',
|
||||||
|
flagEmoji: '🇭🇰',
|
||||||
|
resource: () =>
|
||||||
|
/* webpackChunkName "i18n-zh_Hant" */ import('./zh-Hant.json'),
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
name: 'French',
|
name: 'French',
|
||||||
tag: 'fr',
|
|
||||||
originalName: 'français',
|
originalName: 'français',
|
||||||
flagEmoji: '🇫🇷',
|
flagEmoji: '🇫🇷',
|
||||||
base: false,
|
resource: () => /* webpackChunkName "i18n-fr" */ import('./fr.json'),
|
||||||
res: fr,
|
|
||||||
},
|
},
|
||||||
{
|
es: {
|
||||||
name: 'Spanish',
|
name: 'Spanish',
|
||||||
tag: 'es',
|
|
||||||
originalName: 'español',
|
originalName: 'español',
|
||||||
flagEmoji: '🇪🇸',
|
flagEmoji: '🇪🇸',
|
||||||
base: false,
|
resource: () => /* webpackChunkName "i18n-es" */ import('./es.json'),
|
||||||
res: es,
|
|
||||||
},
|
},
|
||||||
{
|
'es-AR': {
|
||||||
name: 'German',
|
|
||||||
tag: 'de',
|
|
||||||
originalName: 'Deutsch',
|
|
||||||
flagEmoji: '🇩🇪',
|
|
||||||
base: false,
|
|
||||||
res: de,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Russian',
|
|
||||||
tag: 'ru',
|
|
||||||
originalName: 'русский',
|
|
||||||
flagEmoji: '🇷🇺',
|
|
||||||
base: false,
|
|
||||||
res: ru,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Japanese',
|
|
||||||
tag: 'ja',
|
|
||||||
originalName: '日本語',
|
|
||||||
flagEmoji: '🇯🇵',
|
|
||||||
base: false,
|
|
||||||
res: ja,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Italian',
|
|
||||||
tag: 'it',
|
|
||||||
originalName: 'italiano',
|
|
||||||
flagEmoji: '🇮🇹',
|
|
||||||
base: false,
|
|
||||||
res: it,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Catalan',
|
|
||||||
tag: 'ca',
|
|
||||||
originalName: 'català',
|
|
||||||
flagEmoji: '🇦🇩',
|
|
||||||
base: false,
|
|
||||||
res: ca,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Danish',
|
|
||||||
tag: 'da',
|
|
||||||
originalName: 'dansk',
|
|
||||||
flagEmoji: '🇩🇰',
|
|
||||||
base: false,
|
|
||||||
res: da,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Spanish (Chile)',
|
|
||||||
tag: 'es-CL',
|
|
||||||
originalName: 'español (Chile)',
|
|
||||||
flagEmoji: '🇨🇱',
|
|
||||||
base: false,
|
|
||||||
res: es_CL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Hindi',
|
|
||||||
tag: 'hi',
|
|
||||||
originalName: 'हिन्दी',
|
|
||||||
flagEmoji: '🇮🇳',
|
|
||||||
base: false,
|
|
||||||
res: hi,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Swedish (Sweden)',
|
|
||||||
tag: 'sv-SE',
|
|
||||||
originalName: 'svenska (Sverige)',
|
|
||||||
flagEmoji: '🇸🇪',
|
|
||||||
base: false,
|
|
||||||
res: sv_SE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Spanish (Argentina)',
|
name: 'Spanish (Argentina)',
|
||||||
tag: 'es-AR',
|
|
||||||
originalName: 'español (Argentina)',
|
originalName: 'español (Argentina)',
|
||||||
flagEmoji: '🇦🇷',
|
flagEmoji: '🇦🇷',
|
||||||
base: false,
|
resource: () => /* webpackChunkName "i18n-es_AR" */ import('./es-AR.json'),
|
||||||
res: es_AR,
|
|
||||||
},
|
},
|
||||||
{
|
'es-CL': {
|
||||||
|
name: 'Spanish (Chile)',
|
||||||
|
originalName: 'español (Chile)',
|
||||||
|
flagEmoji: '🇨🇱',
|
||||||
|
resource: () => /* webpackChunkName "i18n-es_CL" */ import('./es-CL.json'),
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
name: 'German',
|
||||||
|
originalName: 'Deutsch',
|
||||||
|
flagEmoji: '🇩🇪',
|
||||||
|
resource: () => /* webpackChunkName "i18n-de" */ import('./de.json'),
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
name: 'Russian',
|
||||||
|
originalName: 'русский',
|
||||||
|
flagEmoji: '🇷🇺',
|
||||||
|
resource: () => /* webpackChunkName "i18n-ru" */ import('./ru.json'),
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
name: 'Japanese',
|
||||||
|
originalName: '日本語',
|
||||||
|
flagEmoji: '🇯🇵',
|
||||||
|
resource: () => /* webpackChunkName "i18n-ja" */ import('./ja.json'),
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
name: 'Italian',
|
||||||
|
originalName: 'italiano',
|
||||||
|
flagEmoji: '🇮🇹',
|
||||||
|
resource: () => /* webpackChunkName "i18n-it" */ import('./it.json'),
|
||||||
|
},
|
||||||
|
ca: {
|
||||||
|
name: 'Catalan',
|
||||||
|
originalName: 'català',
|
||||||
|
flagEmoji: '🇦🇩',
|
||||||
|
resource: () => /* webpackChunkName "i18n-ca" */ import('./ca.json'),
|
||||||
|
},
|
||||||
|
da: {
|
||||||
|
name: 'Danish',
|
||||||
|
originalName: 'dansk',
|
||||||
|
flagEmoji: '🇩🇰',
|
||||||
|
resource: () => /* webpackChunkName "i18n-da" */ import('./da.json'),
|
||||||
|
},
|
||||||
|
hi: {
|
||||||
|
name: 'Hindi',
|
||||||
|
originalName: 'हिन्दी',
|
||||||
|
flagEmoji: '🇮🇳',
|
||||||
|
resource: () => /* webpackChunkName "i18n-hi" */ import('./hi.json'),
|
||||||
|
},
|
||||||
|
'sv-SE': {
|
||||||
|
name: 'Swedish (Sweden)',
|
||||||
|
originalName: 'svenska (Sverige)',
|
||||||
|
flagEmoji: '🇸🇪',
|
||||||
|
resource: () => /* webpackChunkName "i18n-sv_SE" */ import('./sv-SE.json'),
|
||||||
|
},
|
||||||
|
|
||||||
|
ur: {
|
||||||
name: 'Urdu',
|
name: 'Urdu',
|
||||||
tag: 'ur',
|
|
||||||
originalName: 'اردو',
|
originalName: 'اردو',
|
||||||
flagEmoji: '🇵🇰',
|
flagEmoji: '🇵🇰',
|
||||||
base: false,
|
resource: () => /* webpackChunkName "i18n-ur" */ import('./ur.json'),
|
||||||
res: ur,
|
|
||||||
},
|
},
|
||||||
{
|
ar: {
|
||||||
name: 'Arabic',
|
name: 'Arabic',
|
||||||
tag: 'ar',
|
|
||||||
originalName: 'العربية',
|
originalName: 'العربية',
|
||||||
flagEmoji: '🇸🇦',
|
flagEmoji: '🇸🇦',
|
||||||
base: false,
|
resource: () => /* webpackChunkName "i18n-ar" */ import('./ar.json'),
|
||||||
res: ar,
|
|
||||||
},
|
},
|
||||||
] as const;
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { createI18n, I18n } from '../../';
|
import { getOrCreateI18n, I18n } from '../../';
|
||||||
import { i18nTime } from '../time';
|
import { i18nTime } from '../time';
|
||||||
|
|
||||||
// Intl api is not available in github action, skip the test
|
// Intl api is not available in github action, skip the test
|
||||||
describe('humanTime', () => {
|
describe('humanTime', () => {
|
||||||
test('absolute', async () => {
|
test('absolute', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(i18nTime('2024-10-10 13:30:28')).toBe('Oct 10, 2024, 1:30:28 PM');
|
expect(i18nTime('2024-10-10 13:30:28')).toBe('Oct 10, 2024, 1:30:28 PM');
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-10 13:30:28', {
|
i18nTime('2024-10-10 13:30:28', {
|
||||||
@@ -48,7 +48,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('relative', async () => {
|
test('relative', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-10 13:30:28.005', {
|
i18nTime('2024-10-10 13:30:28.005', {
|
||||||
now: '2024-10-10 13:30:30',
|
now: '2024-10-10 13:30:30',
|
||||||
@@ -148,7 +148,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('relative - accuracy', async () => {
|
test('relative - accuracy', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-10 13:30:28.005', {
|
i18nTime('2024-10-10 13:30:28.005', {
|
||||||
now: '2024-10-10 13:30:30',
|
now: '2024-10-10 13:30:30',
|
||||||
@@ -224,7 +224,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('relative - disable yesterdayAndTomorrow', async () => {
|
test('relative - disable yesterdayAndTomorrow', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-9 13:30:30', {
|
i18nTime('2024-10-9 13:30:30', {
|
||||||
now: '2024-10-10 13:30:30',
|
now: '2024-10-10 13:30:30',
|
||||||
@@ -244,7 +244,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('relative - weekday', async () => {
|
test('relative - weekday', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-9 13:30:30', {
|
i18nTime('2024-10-9 13:30:30', {
|
||||||
now: '2024-10-10 13:30:30',
|
now: '2024-10-10 13:30:30',
|
||||||
@@ -302,7 +302,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('mix relative and absolute', async () => {
|
test('mix relative and absolute', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-9 14:30:30', {
|
i18nTime('2024-10-9 14:30:30', {
|
||||||
now: '2024-10-10 13:30:30',
|
now: '2024-10-10 13:30:30',
|
||||||
@@ -348,9 +348,9 @@ describe('humanTime', () => {
|
|||||||
).toBe('Oct 8, 2024');
|
).toBe('Oct 8, 2024');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chinese', () => {
|
test('chinese', async () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
I18n.changeLanguage('zh-Hans');
|
await I18n.changeLanguage('zh-Hans');
|
||||||
expect(i18nTime('2024-10-10 13:30:28.005')).toBe('2024年10月10日 13:30:28');
|
expect(i18nTime('2024-10-10 13:30:28.005')).toBe('2024年10月10日 13:30:28');
|
||||||
expect(
|
expect(
|
||||||
i18nTime('2024-10-10 13:30:28.005', {
|
i18nTime('2024-10-10 13:30:28.005', {
|
||||||
@@ -398,7 +398,7 @@ describe('humanTime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('invalid time', () => {
|
test('invalid time', () => {
|
||||||
createI18n();
|
getOrCreateI18n();
|
||||||
expect(i18nTime('foobar')).toBe('');
|
expect(i18nTime('foobar')).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { I18n } from '../i18n';
|
import { I18n } from '../i18next';
|
||||||
|
|
||||||
export type TimeUnit =
|
export type TimeUnit =
|
||||||
| 'second'
|
| 'second'
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.resources.json"
|
"path": "./tsconfig.resources.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../common/debug/tsconfig.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
"outDir": "lib/resources",
|
"outDir": "lib/resources",
|
||||||
"noEmit": false
|
"noEmit": false
|
||||||
},
|
},
|
||||||
"include": ["./src/resources/**/*.ts", "./src/resources/**/*.json"],
|
"include": ["./src/resources/**/*.ts", "./src/**/*.json"],
|
||||||
"exclude": ["dist", "lib"]
|
"exclude": ["dist", "lib"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ test('change language using keyboard', async ({ page }) => {
|
|||||||
await page.keyboard.press('ArrowDown', {
|
await page.keyboard.press('ArrowDown', {
|
||||||
delay: 50,
|
delay: 50,
|
||||||
});
|
});
|
||||||
|
// incase the current language is the top one
|
||||||
|
await page.keyboard.press('ArrowDown', {
|
||||||
|
delay: 50,
|
||||||
|
});
|
||||||
await page.keyboard.press('Enter', {
|
await page.keyboard.press('Enter', {
|
||||||
delay: 50,
|
delay: 50,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
|
|
||||||
import { getCwdFromDistribution } from '../config/cwd.cjs';
|
import { getCwdFromDistribution } from '../config/cwd.cjs';
|
||||||
import type { BuildFlags } from '../config/index.js';
|
import type { BuildFlags } from '../config/index.js';
|
||||||
import { buildI18N } from '../util/i18n.js';
|
|
||||||
import { createWebpackConfig } from '../webpack/webpack.config.js';
|
import { createWebpackConfig } from '../webpack/webpack.config.js';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
@@ -48,7 +49,9 @@ const flags = {
|
|||||||
static: false,
|
static: false,
|
||||||
} satisfies BuildFlags;
|
} satisfies BuildFlags;
|
||||||
|
|
||||||
buildI18N();
|
spawn('yarn', ['workspace', '@affine/i18n', 'build'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
webpack(createWebpackConfig(cwd!, flags), (err, stats) => {
|
webpack(createWebpackConfig(cwd!, flags), (err, stats) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ import WebpackDevServer from 'webpack-dev-server';
|
|||||||
|
|
||||||
import { getCwdFromDistribution, projectRoot } from '../config/cwd.cjs';
|
import { getCwdFromDistribution, projectRoot } from '../config/cwd.cjs';
|
||||||
import type { BuildFlags } from '../config/index.js';
|
import type { BuildFlags } from '../config/index.js';
|
||||||
import { watchI18N } from '../util/i18n.js';
|
|
||||||
import { createWebpackConfig } from '../webpack/webpack.config.js';
|
import { createWebpackConfig } from '../webpack/webpack.config.js';
|
||||||
|
|
||||||
const flags: BuildFlags = {
|
const flags: BuildFlags = {
|
||||||
@@ -119,7 +119,9 @@ if (flags.distribution === 'desktop') {
|
|||||||
console.info(flags);
|
console.info(flags);
|
||||||
|
|
||||||
if (!flags.static) {
|
if (!flags.static) {
|
||||||
watchI18N();
|
spawn('yarn', ['workspace', '@affine/i18n', 'dev'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { resolve } from 'node:path';
|
|
||||||
|
|
||||||
import { runCli } from '@magic-works/i18n-codegen';
|
|
||||||
|
|
||||||
import { projectRoot } from '../config/cwd.cjs';
|
|
||||||
|
|
||||||
const configPath = resolve(projectRoot, '.i18n-codegen.json');
|
|
||||||
|
|
||||||
export const watchI18N = () => {
|
|
||||||
runCli(
|
|
||||||
{
|
|
||||||
config: configPath,
|
|
||||||
watch: true,
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildI18N = () => {
|
|
||||||
runCli(
|
|
||||||
{
|
|
||||||
config: configPath,
|
|
||||||
watch: false,
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -552,6 +552,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/i18n@workspace:packages/frontend/i18n"
|
resolution: "@affine/i18n@workspace:packages/frontend/i18n"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@affine/debug": "workspace:*"
|
||||||
"@magic-works/i18n-codegen": "npm:^0.6.0"
|
"@magic-works/i18n-codegen": "npm:^0.6.0"
|
||||||
dayjs: "npm:^1.11.11"
|
dayjs: "npm:^1.11.11"
|
||||||
glob: "npm:^11.0.0"
|
glob: "npm:^11.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user