mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
40
packages/frontend/core/src/commands/affine-i18n.tsx
Normal file
40
packages/frontend/core/src/commands/affine-i18n.tsx
Normal file
@@ -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 { useTheme } from 'next-themes';
|
||||
|
||||
import type { useLanguageHelper } from '../components/hooks/affine/use-language-helper';
|
||||
import type { EditorSettingService } from '../modules/editor-settting';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
@@ -13,17 +12,14 @@ export function registerAffineSettingsCommands({
|
||||
t,
|
||||
store,
|
||||
theme,
|
||||
languageHelper,
|
||||
editorSettingService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
theme: ReturnType<typeof useTheme>;
|
||||
languageHelper: ReturnType<typeof useLanguageHelper>;
|
||||
editorSettingService: EditorSettingService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const { onLanguageChange, languagesList, currentLanguage } = languageHelper;
|
||||
const updateSettings = editorSettingService.editorSetting.set.bind(
|
||||
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
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './affine-creation';
|
||||
export * from './affine-help';
|
||||
export * from './affine-i18n';
|
||||
export * from './affine-layout';
|
||||
export * from './affine-navigation';
|
||||
export * from './affine-settings';
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
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 { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useLanguageHelper } from '../../../components/hooks/affine/use-language-helper';
|
||||
import * as styles from './style.css';
|
||||
|
||||
// Fixme: keyboard focus should be supported by Menu component
|
||||
const LanguageMenuContent = memo(function LanguageMenuContent() {
|
||||
const { currentLanguage, languagesList, onLanguageChange } =
|
||||
useLanguageHelper();
|
||||
|
||||
const LanguageMenuContent = memo(function LanguageMenuContent({
|
||||
current,
|
||||
onChange,
|
||||
i18n,
|
||||
}: {
|
||||
i18n: I18n;
|
||||
current: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{languagesList.map(option => {
|
||||
const selected = currentLanguage?.originalName === option.originalName;
|
||||
const completeness = calcLocaleCompleteness(option.tag);
|
||||
{i18n.languageList.map(lang => {
|
||||
const selected = current === lang.key;
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
title={option.name}
|
||||
lang={option.tag}
|
||||
onSelect={() => onLanguageChange(option.tag)}
|
||||
suffix={(completeness * 100).toFixed(0) + '%'}
|
||||
key={lang.name}
|
||||
title={lang.name}
|
||||
lang={lang.key}
|
||||
onSelect={() => onChange(lang.key)}
|
||||
suffix={lang.completeness + '%'}
|
||||
data-selected={selected}
|
||||
className={styles.menuItem}
|
||||
>
|
||||
<div className={styles.languageLabelWrapper}>
|
||||
<div>{option.originalName}</div>
|
||||
<div>{lang.originalName}</div>
|
||||
{selected && <DoneIcon fontSize={'16px'} />}
|
||||
</div>
|
||||
</MenuItem>
|
||||
@@ -39,10 +43,20 @@ const LanguageMenuContent = memo(function LanguageMenuContent() {
|
||||
});
|
||||
|
||||
export const LanguageMenu = () => {
|
||||
const { currentLanguage } = useLanguageHelper();
|
||||
const i18n = useService(I18nService).i18n;
|
||||
const currentLanguage = useLiveData(i18n.currentLanguage$);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={(<LanguageMenuContent />) as ReactElement}
|
||||
items={
|
||||
(
|
||||
<LanguageMenuContent
|
||||
current={currentLanguage.key}
|
||||
onChange={i18n.changeLanguage}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) as ReactElement
|
||||
}
|
||||
contentOptions={{
|
||||
className: styles.menu,
|
||||
align: 'end',
|
||||
@@ -53,7 +67,7 @@ export const LanguageMenu = () => {
|
||||
style={{ textTransform: 'capitalize', fontWeight: 600, width: '250px' }}
|
||||
block={true}
|
||||
>
|
||||
{currentLanguage?.originalName || ''}
|
||||
{currentLanguage.originalName}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function AffinePageReference({
|
||||
}) {
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const journalHelper = useJournalInfoHelper();
|
||||
const t = useI18n();
|
||||
const i18n = useI18n();
|
||||
|
||||
let linkWithMode: DocMode | null = null;
|
||||
let linkToNode = false;
|
||||
@@ -59,9 +59,7 @@ export function AffinePageReference({
|
||||
const el = (
|
||||
<>
|
||||
<Icon className={styles.pageReferenceIcon} />
|
||||
<span className="affine-reference-title">
|
||||
{typeof title === 'string' ? title : t[title.key]()}
|
||||
</span>
|
||||
<span className="affine-reference-title">{i18n.t(title)}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -132,7 +130,7 @@ export function AffineSharedPageReference({
|
||||
}) {
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const journalHelper = useJournalInfoHelper();
|
||||
const t = useI18n();
|
||||
const i18n = useI18n();
|
||||
|
||||
let linkWithMode: DocMode | null = null;
|
||||
let linkToNode = false;
|
||||
@@ -155,9 +153,7 @@ export function AffineSharedPageReference({
|
||||
const el = (
|
||||
<>
|
||||
<Icon className={styles.pageReferenceIcon} />
|
||||
<span className="affine-reference-title">
|
||||
{typeof title === 'string' ? title : t[title.key]()}
|
||||
</span>
|
||||
<span className="affine-reference-title">{i18n.t(title)}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SubscriptionStatus,
|
||||
UserFriendlyError,
|
||||
} 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 { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -43,17 +43,19 @@ import { BelieverCard } from '../plans/lifetime/believer-card';
|
||||
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
||||
import * as styles from './style.css';
|
||||
|
||||
enum DescriptionI18NKey {
|
||||
Basic = 'com.affine.payment.billing-setting.current-plan.description',
|
||||
Monthly = 'com.affine.payment.billing-setting.current-plan.description.monthly',
|
||||
Yearly = 'com.affine.payment.billing-setting.current-plan.description.yearly',
|
||||
Lifetime = 'com.affine.payment.billing-setting.current-plan.description.lifetime',
|
||||
}
|
||||
const DescriptionI18NKey = {
|
||||
Basic: 'com.affine.payment.billing-setting.current-plan.description',
|
||||
Monthly:
|
||||
'com.affine.payment.billing-setting.current-plan.description.monthly',
|
||||
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 = (
|
||||
plan: SubscriptionPlan,
|
||||
recurring: SubscriptionRecurring
|
||||
): DescriptionI18NKey => {
|
||||
) => {
|
||||
if (plan !== SubscriptionPlan.Pro) {
|
||||
return DescriptionI18NKey.Basic;
|
||||
}
|
||||
|
||||
@@ -370,10 +370,10 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||
},
|
||||
{
|
||||
label: {
|
||||
key: 'com.affine.cmdk.insert-links',
|
||||
i18nKey: 'com.affine.cmdk.insert-links',
|
||||
},
|
||||
placeholder: {
|
||||
key: 'com.affine.cmdk.docs.placeholder',
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ export function createLinkedWidgetConfig(
|
||||
}).value;
|
||||
return {
|
||||
...meta,
|
||||
title: typeof title === 'string' ? title : I18n[title.key](),
|
||||
title: I18n.t(title),
|
||||
};
|
||||
})
|
||||
.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 { I18nService } from '@affine/core/modules/i18n';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
registerAffineCommand,
|
||||
registerAffineCreationCommands,
|
||||
registerAffineHelpCommands,
|
||||
registerAffineLanguageCommands,
|
||||
registerAffineLayoutCommands,
|
||||
registerAffineNavigationCommands,
|
||||
registerAffineSettingsCommands,
|
||||
@@ -20,7 +22,6 @@ import { usePageHelper } from '../../components/blocksuite/block-suite-page-list
|
||||
import { CreateWorkspaceDialogService } from '../../modules/create-workspace';
|
||||
import { EditorSettingService } from '../../modules/editor-settting';
|
||||
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
|
||||
import { useLanguageHelper } from './affine/use-language-helper';
|
||||
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
||||
import { useNavigateHelper } from './use-navigate-helper';
|
||||
|
||||
@@ -65,7 +66,6 @@ export function useRegisterWorkspaceCommands() {
|
||||
const t = useI18n();
|
||||
const theme = useTheme();
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const languageHelper = useLanguageHelper();
|
||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
@@ -73,6 +73,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const appSidebarService = useService(AppSidebarService);
|
||||
const i18n = useService(I18nService).i18n;
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
||||
@@ -114,14 +115,25 @@ export function useRegisterWorkspaceCommands() {
|
||||
store,
|
||||
t,
|
||||
theme,
|
||||
languageHelper,
|
||||
editorSettingService,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [editorSettingService, languageHelper, store, t, theme]);
|
||||
}, [editorSettingService, store, t, theme]);
|
||||
|
||||
// register AffineLanguageCommands
|
||||
useEffect(() => {
|
||||
const unsub = registerAffineLanguageCommands({
|
||||
i18n,
|
||||
t,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [i18n, t]);
|
||||
|
||||
// register AffineLayoutCommands
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
Ref,
|
||||
VariableMap,
|
||||
} from '@affine/env/filter';
|
||||
import { createI18n, I18nextProvider } from '@affine/i18n';
|
||||
import { getOrCreateI18n, I18nextProvider } from '@affine/i18n';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -128,8 +128,8 @@ describe('eval filter', () => {
|
||||
|
||||
describe('render filter', () => {
|
||||
test('boolean condition value change', async () => {
|
||||
const i18n = createI18n();
|
||||
const is = filterMatcher.match(tBoolean.create());
|
||||
const i18n = getOrCreateI18n();
|
||||
assertExists(is);
|
||||
const Wrapper = () => {
|
||||
const [value, onChange] = useState(
|
||||
|
||||
@@ -278,10 +278,10 @@ function tagIdToTagOption(
|
||||
}
|
||||
|
||||
const PageTitle = ({ id }: { id: string }) => {
|
||||
const t = useI18n();
|
||||
const i18n = useI18n();
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const title = useLiveData(docDisplayMetaService.title$(id));
|
||||
return typeof title === 'string' ? title : t[title.key]();
|
||||
return i18n.t(title);
|
||||
};
|
||||
|
||||
const UnifiedPageIcon = ({ id }: { id: string }) => {
|
||||
|
||||
@@ -43,13 +43,13 @@ interface PageItemProps
|
||||
right?: ReactNode;
|
||||
}
|
||||
const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
||||
const t = useI18n();
|
||||
const i18n = useI18n();
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const Icon = useLiveData(
|
||||
docDisplayMetaService.icon$(docId, { compareDate: new Date() })
|
||||
);
|
||||
const titleMeta = useLiveData(docDisplayMetaService.title$(docId));
|
||||
const title = typeof titleMeta === 'string' ? titleMeta : t[titleMeta.key]();
|
||||
const title = i18n.t(titleMeta);
|
||||
|
||||
return (
|
||||
<WorkbenchLink
|
||||
|
||||
@@ -8,8 +8,11 @@ export interface SearchResLabelProps {
|
||||
export const SearchResLabel = ({ item }: SearchResLabelProps) => {
|
||||
const i18n = useI18n();
|
||||
|
||||
const text = !isI18nString(item.label)
|
||||
? i18n.t(item.label.title)
|
||||
: i18n.t(item.label);
|
||||
return <HighlightText text={text} start="<b>" end="</b>" />;
|
||||
return (
|
||||
<HighlightText
|
||||
text={i18n.t(isI18nString(item.label) ? item.label : item.label.title)}
|
||||
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 { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SettingDropdownSelect } from '../dropdown-select';
|
||||
@@ -7,24 +8,24 @@ import { RowLayout } from '../row.layout';
|
||||
|
||||
export const LanguageSetting = () => {
|
||||
const t = useI18n();
|
||||
const { currentLanguage, languagesList, onLanguageChange } =
|
||||
useLanguageHelper();
|
||||
const i18n = useService(I18nService).i18n;
|
||||
const currentLanguage = useLiveData(i18n.currentLanguage$);
|
||||
|
||||
const languageOptions = useMemo(
|
||||
() =>
|
||||
languagesList.map(language => ({
|
||||
i18n.languageList.map(language => ({
|
||||
label: language.originalName,
|
||||
value: language.tag,
|
||||
value: language.key,
|
||||
})),
|
||||
[languagesList]
|
||||
[i18n]
|
||||
);
|
||||
|
||||
return (
|
||||
<RowLayout label={t['com.affine.mobile.setting.appearance.language']()}>
|
||||
<SettingDropdownSelect
|
||||
options={languageOptions}
|
||||
value={currentLanguage?.tag}
|
||||
onChange={onLanguageChange}
|
||||
value={currentLanguage.key}
|
||||
onChange={i18n.changeLanguage}
|
||||
menuOptions={{
|
||||
contentOptions: {
|
||||
style: {
|
||||
|
||||
@@ -159,7 +159,7 @@ export class DocDisplayMetaService extends Service {
|
||||
if (options?.originalTitle) return options.originalTitle;
|
||||
|
||||
// empty title
|
||||
if (!docTitle) return { key: 'Untitled' } as const;
|
||||
if (!docTitle) return { i18nKey: 'Untitled' } as const;
|
||||
|
||||
// reference
|
||||
if (options?.reference) return docTitle;
|
||||
|
||||
@@ -208,7 +208,7 @@ export const ExplorerDocNode = ({
|
||||
return (
|
||||
<ExplorerTreeNode
|
||||
icon={Icon}
|
||||
name={typeof docTitle === 'string' ? docTitle : t[docTitle.key]()}
|
||||
name={t.t(docTitle)}
|
||||
dndData={dndData}
|
||||
onDrop={handleDropOnDoc}
|
||||
renameable
|
||||
|
||||
15
packages/frontend/core/src/modules/i18n/context.tsx
Normal file
15
packages/frontend/core/src/modules/i18n/context.tsx
Normal file
@@ -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>;
|
||||
}
|
||||
83
packages/frontend/core/src/modules/i18n/entities/i18n.ts
Normal file
83
packages/frontend/core/src/modules/i18n/entities/i18n.ts
Normal file
@@ -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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
11
packages/frontend/core/src/modules/i18n/index.ts
Normal file
11
packages/frontend/core/src/modules/i18n/index.ts
Normal file
@@ -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 };
|
||||
7
packages/frontend/core/src/modules/i18n/services/i18n.ts
Normal file
7
packages/frontend/core/src/modules/i18n/services/i18n.ts
Normal file
@@ -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 { configureFavoriteModule } from './favorite';
|
||||
import { configureFindInPageModule } from './find-in-page';
|
||||
import { configureI18nModule } from './i18n';
|
||||
import { configureImportTemplateModule } from './import-template';
|
||||
import { configureNavigationModule } from './navigation';
|
||||
import { configureOrganizeModule } from './organize';
|
||||
@@ -30,6 +31,7 @@ import { configureThemeEditorModule } from './theme-editor';
|
||||
import { configureUserspaceModule } from './userspace';
|
||||
|
||||
export function configureCommonModules(framework: Framework) {
|
||||
configureI18nModule(framework);
|
||||
configureInfraModules(framework);
|
||||
configureCollectionModule(framework);
|
||||
configureNavigationModule(framework);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { highlighter } from '../utils/highlighter';
|
||||
const group = {
|
||||
id: 'collections',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.category.affine.collections',
|
||||
i18nKey: 'com.affine.cmdk.affine.category.affine.collections',
|
||||
},
|
||||
score: 10,
|
||||
} as QuickSearchGroup;
|
||||
@@ -60,7 +60,7 @@ export class CollectionsQuickSearchSession
|
||||
label: {
|
||||
title: (highlighter(item.name, '<b>', '</b>', titleMatches ?? []) ??
|
||||
item.name) || {
|
||||
key: 'Untitled',
|
||||
i18nKey: 'Untitled',
|
||||
},
|
||||
},
|
||||
group,
|
||||
|
||||
@@ -17,81 +17,81 @@ import { highlighter } from '../utils/highlighter';
|
||||
const categories = {
|
||||
'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,
|
||||
},
|
||||
'affine:navigation': {
|
||||
id: 'command:affine:navigation',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.category.affine.navigation',
|
||||
i18nKey: 'com.affine.cmdk.affine.category.affine.navigation',
|
||||
},
|
||||
score: 10,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'affine:collections': {
|
||||
id: 'command:affine:collections',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.category.affine.collections',
|
||||
i18nKey: 'com.affine.cmdk.affine.category.affine.collections',
|
||||
},
|
||||
score: 10,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
},
|
||||
'affine:results': {
|
||||
id: 'command:affine:results',
|
||||
label: { key: 'com.affine.cmdk.affine.category.results' },
|
||||
label: { i18nKey: 'com.affine.cmdk.affine.category.results' },
|
||||
score: 10,
|
||||
},
|
||||
} satisfies Required<{
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { QuickSearchItem } from '../types/item';
|
||||
|
||||
const group = {
|
||||
id: 'creation',
|
||||
label: { key: 'com.affine.quicksearch.group.creation' },
|
||||
label: { i18nKey: 'com.affine.quicksearch.group.creation' },
|
||||
score: 0,
|
||||
} as QuickSearchGroup;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class CreationQuickSearchSession
|
||||
id: 'creation:create-page',
|
||||
source: 'creation',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.create-new-page-as',
|
||||
i18nKey: 'com.affine.cmdk.affine.create-new-page-as',
|
||||
options: { keyWord: query },
|
||||
},
|
||||
group,
|
||||
@@ -41,7 +41,7 @@ export class CreationQuickSearchSession
|
||||
id: 'creation:create-edgeless',
|
||||
source: 'creation',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.create-new-edgeless-as',
|
||||
i18nKey: 'com.affine.cmdk.affine.create-new-edgeless-as',
|
||||
options: { keyWord: query },
|
||||
},
|
||||
group,
|
||||
|
||||
@@ -76,7 +76,7 @@ export class DocsQuickSearchSession
|
||||
group: {
|
||||
id: 'docs',
|
||||
label: {
|
||||
key: 'com.affine.quicksearch.group.searchfor',
|
||||
i18nKey: 'com.affine.quicksearch.group.searchfor',
|
||||
options: { query: truncate(query) },
|
||||
},
|
||||
score: 5,
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ExternalLinksQuickSearchSession
|
||||
source: 'external-link',
|
||||
icon: LinkIcon,
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.insert-link',
|
||||
i18nKey: 'com.affine.cmdk.affine.insert-link',
|
||||
},
|
||||
payload: { url: query },
|
||||
} as QuickSearchItem<'external-link', ExternalLinkPayload>,
|
||||
|
||||
@@ -63,7 +63,7 @@ export class LinksQuickSearchSession
|
||||
group: {
|
||||
id: 'docs',
|
||||
label: {
|
||||
key: 'com.affine.quicksearch.group.searchfor',
|
||||
i18nKey: 'com.affine.quicksearch.group.searchfor',
|
||||
options: { query: truncate(query) },
|
||||
},
|
||||
score: 5,
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { QuickSearchItem } from '../types/item';
|
||||
const group = {
|
||||
id: 'recent-docs',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.category.affine.recent',
|
||||
i18nKey: 'com.affine.cmdk.affine.category.affine.recent',
|
||||
},
|
||||
score: 15,
|
||||
} as QuickSearchGroup;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { QuickSearchTagIcon } from '../views/tag-icon';
|
||||
const group: QuickSearchGroup = {
|
||||
id: 'tags',
|
||||
label: {
|
||||
key: 'com.affine.cmdk.affine.category.affine.tags',
|
||||
i18nKey: 'com.affine.cmdk.affine.category.affine.tags',
|
||||
},
|
||||
score: 10,
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export class TagsQuickSearchSession
|
||||
titleMatches ?? []
|
||||
) ??
|
||||
item.title) || {
|
||||
key: 'Untitled',
|
||||
i18nKey: 'Untitled',
|
||||
},
|
||||
},
|
||||
group,
|
||||
|
||||
@@ -118,7 +118,7 @@ export class CMDKQuickSearchService extends Service {
|
||||
},
|
||||
{
|
||||
placeholder: {
|
||||
key: 'com.affine.cmdk.docs.placeholder',
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -232,12 +232,13 @@ export const CMDKGroup = ({
|
||||
style={{ overflowAnchor: 'none' }}
|
||||
>
|
||||
{items.map(item => {
|
||||
const title = !isI18nString(item.label)
|
||||
? i18n.t(item.label.title)
|
||||
: i18n.t(item.label);
|
||||
const subTitle = !isI18nString(item.label)
|
||||
? item.label.subTitle && i18n.t(item.label.subTitle)
|
||||
: null;
|
||||
const [title, subTitle] = isI18nString(item.label)
|
||||
? [i18n.t(item.label), null]
|
||||
: [
|
||||
i18n.t(item.label.title),
|
||||
item.label.subTitle ? i18n.t(item.label.subTitle) : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
key={item.id}
|
||||
|
||||
Reference in New Issue
Block a user