refactor(i18n): lazy load languages (#8456)

closes AF-1397
This commit is contained in:
forehalo
2024-10-10 09:03:06 +00:00
parent f833017e45
commit 9043e6607e
60 changed files with 731 additions and 668 deletions
@@ -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 }) => {