Migrate to lingui

Switch to "lingui" for better DX.
Tried to persist existing languages as much as possible.

Removed "vite-plugin-node-polyfills" because it's incompatible with "lingui"
This commit is contained in:
schroda
2026-01-10 19:45:02 +01:00
parent c1ac58f4d6
commit 65a9a905be
287 changed files with 120766 additions and 37748 deletions

View File

@@ -6,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import {
ExtensionAction,
ExtensionGroupState,
@@ -14,7 +16,6 @@ import {
InstalledStates,
} from '@/features/extension/Extensions.types.ts';
import { DefaultLanguage } from '@/base/utils/Languages.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const EXTENSION_ACTION_TO_STATE_MAP: { [action in ExtensionAction]: ExtensionState } = {
[ExtensionAction.UPDATE]: ExtensionState.UPDATING,
@@ -28,31 +29,33 @@ export const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]:
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
} as const;
export const INSTALLED_STATE_TO_TRANSLATION_KEY_MAP: { [installedState in InstalledStates]: TranslationKey } = {
[InstalledState.UNINSTALL]: 'extension.action.label.uninstall',
[InstalledState.INSTALL]: 'extension.action.label.install',
[InstalledState.UPDATE]: 'extension.action.label.update',
[InstalledState.OBSOLETE]: 'extension.state.label.obsolete',
[InstalledState.UPDATING]: 'extension.state.label.updating',
[InstalledState.UNINSTALLING]: 'extension.state.label.uninstalling',
[InstalledState.INSTALLING]: 'extension.state.label.installing',
export const INSTALLED_STATE_TO_TRANSLATION_MAP: { [installedState in InstalledStates]: MessageDescriptor } = {
[InstalledState.UNINSTALL]: msg`Uninstall`,
[InstalledState.INSTALL]: msg`Install`,
[InstalledState.UPDATE]: msg`Update`,
[InstalledState.OBSOLETE]: msg`Obsolete`,
[InstalledState.UPDATING]: msg`Updating`,
[InstalledState.UNINSTALLING]: msg`Uninstalling`,
[InstalledState.INSTALLING]: msg`Installing`,
} as const;
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP: {
[action in ExtensionAction]: TranslationKey;
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP: {
[action in ExtensionAction]: MessageDescriptor;
} = {
[ExtensionAction.UPDATE]: 'extension.label.update_failed',
[ExtensionAction.INSTALL]: 'extension.label.installation_failed',
[ExtensionAction.UNINSTALL]: 'extension.label.uninstallation_failed',
[ExtensionAction.UPDATE]: msg`{count, plural, one {Could not update the extension} other {Could not update the extensions}}`,
[ExtensionAction.INSTALL]: msg`{count, plural, one {Could not install the extension} other {Could not install the extensions}}`,
[ExtensionAction.UNINSTALL]: msg`{count, plural, one {Could not uninstall the extension} other {Could not uninstall the extensions}}`,
};
export const extensionLanguageToTranslationKey: { [state in ExtensionGroupState | DefaultLanguage]: TranslationKey } = {
[ExtensionGroupState.INSTALLED]: 'extension.state.label.installed',
[ExtensionGroupState.UPDATE_PENDING]: 'extension.state.label.update_pending',
[ExtensionGroupState.OBSOLETE]: 'extension.state.label.obsolete',
[DefaultLanguage.ALL]: 'extension.language.all',
[DefaultLanguage.OTHER]: 'extension.language.other',
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
[DefaultLanguage.PINNED]: 'global.label.pinned',
[DefaultLanguage.LAST_USED_SOURCE]: 'global.label.last_used',
export const extensionLanguageToTranslation: {
[state in ExtensionGroupState | DefaultLanguage]: MessageDescriptor;
} = {
[ExtensionGroupState.INSTALLED]: msg`Installed`,
[ExtensionGroupState.UPDATE_PENDING]: msg`Update pending`,
[ExtensionGroupState.OBSOLETE]: msg`Obsolete`,
[DefaultLanguage.ALL]: msg`All`,
[DefaultLanguage.OTHER]: msg`Other`,
[DefaultLanguage.LOCAL_SOURCE]: msg`Other`,
[DefaultLanguage.PINNED]: msg`Pinned`,
[DefaultLanguage.LAST_USED_SOURCE]: msg`Last used`,
};

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t } from 'i18next';
import {
ExtensionAction,
ExtensionGroupState,
@@ -23,14 +22,15 @@ import {
toComparableLanguages,
} from '@/base/utils/Languages.ts';
import {
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP,
extensionLanguageToTranslationKey,
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP,
extensionLanguageToTranslation,
} from '@/features/extension/Extensions.constants.ts';
import { enhancedCleanup } from '@/base/utils/Strings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
import { i18n } from '@/i18n';
export const getInstalledState = (
isInstalled: boolean,
@@ -65,7 +65,7 @@ export const isExtensionStateOrLanguage = (languageCode: string): boolean =>
export const translateExtensionLanguage = (languageCode: string): string =>
isExtensionStateOrLanguage(languageCode)
? t(extensionLanguageToTranslationKey[languageCode as ExtensionGroupState | DefaultLanguage])
? i18n._(extensionLanguageToTranslation[languageCode as ExtensionGroupState | DefaultLanguage])
: languageCodeToName(languageCode);
export function groupExtensionsByLanguage(extensions: TExtension[]): GroupedExtensionsResult {
@@ -165,7 +165,8 @@ export const updateExtension = async (
}
} catch (e) {
makeToast(
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action], { count: 1 }),
/* lingui-extract-ignore */
i18n.t({ ...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[action], values: { count: 1 } }),
'error',
getErrorMessage(e),
);

View File

@@ -8,8 +8,8 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import { INSTALLED_STATE_TO_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
import { useLingui } from '@lingui/react/macro';
import { INSTALLED_STATE_TO_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { useBackButton } from '@/base/hooks/useBackButton.ts';
@@ -17,7 +17,7 @@ import { ExtensionAction, InstalledState, TExtension } from '@/features/extensio
export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtension) => {
const handleBack = useBackButton();
const { t } = useTranslation();
const { t } = useLingui();
const installedState = getInstalledState(isInstalled, isObsolete, hasUpdate);
@@ -44,7 +44,7 @@ export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TE
}
}}
>
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
</Button>
</Box>
);

View File

@@ -7,25 +7,19 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { TExtension } from '@/features/extension/Extensions.types.ts';
import { ExtensionMetadata } from '@/features/extension/info/components/ExtensionMetadata.tsx';
import { languageCodeToName } from '@/base/utils/Languages.ts';
export const Meta = ({ versionName, lang, isNsfw }: TExtension) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<ExtensionMetadata title={t('global.label.version')} value={versionName} />
<ExtensionMetadata title={t('global.language.label.language')} value={languageCodeToName(lang)} />
{isNsfw && (
<ExtensionMetadata
title={t('extension.label.age_rating')}
value="18+"
valueProps={{ color: 'error' }}
/>
)}
<ExtensionMetadata title={t`Version`} value={versionName} />
<ExtensionMetadata title={t`Language`} value={languageCodeToName(lang)} />
{isNsfw && <ExtensionMetadata title={t`Age rating`} value="18+" valueProps={{ color: 'error' }} />}
</Stack>
);
};

View File

@@ -7,13 +7,13 @@
*/
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import Switch from '@mui/material/Switch';
import CardActionArea from '@mui/material/CardActionArea';
import { useLingui } from '@lingui/react/macro';
import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
@@ -29,11 +29,11 @@ import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/feat
export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => {
const { id, isConfigurable } = source;
const { t } = useTranslation();
const { t } = useLingui();
const { isEnabled } = useGetSourceMetadata(source);
const updateSetting = createUpdateSourceMetadata(source, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
return (
@@ -45,7 +45,7 @@ export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceCon
{translateExtensionLanguage(Sources.getLanguage(source))}
</Typography>
{isConfigurable && (
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton
component={Link}
to={AppRoutes.sources.childRoutes.configure.path(id)}

View File

@@ -8,9 +8,9 @@
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
@@ -26,10 +26,10 @@ import { ActionButton } from '@/features/extension/info/components/ActionButton.
import { SourceCard } from '@/features/extension/info/components/SourceCard.tsx';
export const ExtensionInfo = () => {
const { t } = useTranslation();
const { t } = useLingui();
const { pkgName } = useParams<{ pkgName: string }>();
useAppTitle(t('source.extension_info.title'));
useAppTitle(t`Extension info`);
const extensionResponse = requestManager.useGetExtension(pkgName);
const sourcesResponse = requestManager.useGetSourceList();
@@ -55,7 +55,7 @@ export const ExtensionInfo = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(extensionResponse.error)}
retry={() => {
if (extensionResponse.error) {