diff --git a/build/plugins/i18n.js b/build/plugins/i18n.js new file mode 100644 index 0000000..1d56c79 --- /dev/null +++ b/build/plugins/i18n.js @@ -0,0 +1,29 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export default (locale) => { + const map = fs + .readFile( + path.resolve(import.meta.dirname, '../../locales/', `${locale}.json`), + { encoding: 'utf8' }, + ) + .then(JSON.parse); + return { + resolveId(id) { + if (id.startsWith('at/i18n/')) { + return id; + } + return null; + }, + async load(id) { + if (id.startsWith('at/i18n/')) { + const key = id.replace(/at\/i18n\/(.+)/, '$1'); + const value = (await map)[key]; + if (!value) { + throw new Error('i18n key not found'); + } + return `export default ${JSON.stringify(value)}`; + } + }, + }; +}; diff --git a/build/rollup.js b/build/rollup.js index 53f3bf3..fbeb435 100644 --- a/build/rollup.js +++ b/build/rollup.js @@ -1,5 +1,6 @@ import devServer from './plugins/dev-server/index.js'; import generateTemplate from './plugins/generate-template.js'; +import i18n from './plugins/i18n.js'; import { readJson, ensureValue } from './utils.js'; import alias from '@rollup/plugin-alias'; import commonjs from '@rollup/plugin-commonjs'; @@ -13,7 +14,6 @@ import virtual from '@rollup/plugin-virtual'; import { dataToEsm } from '@rollup/pluginutils'; import autoprefixer from 'autoprefixer'; import cssnano from 'cssnano'; -import fs from 'node:fs/promises'; import { resolve } from 'node:path'; import postcss from 'rollup-plugin-postcss'; import { swc } from 'rollup-plugin-swc3'; @@ -30,14 +30,10 @@ export async function rollupOptions(config) { return { input: 'entry', plugins: [ + i18n(config.locale), virtual({ 'at/options': dataToEsm(templates[config.id]), 'at/locale': dataToEsm({ - map: JSON.parse( - await fs.readFile(`./src/locales/${config.locale}.json`, { - encoding: 'utf-8', - }), - ), locale: config.locale, }), entry: buildEntry(), diff --git a/src/locales/en.json b/locales/en.json similarity index 100% rename from src/locales/en.json rename to locales/en.json diff --git a/src/locales/zh.json b/locales/zh.json similarity index 100% rename from src/locales/zh.json rename to locales/zh.json diff --git a/src/components/card-shell.tsx b/src/components/card-shell.tsx index 9200818..657d70d 100644 --- a/src/components/card-shell.tsx +++ b/src/components/card-shell.tsx @@ -13,7 +13,10 @@ import { import { TimerBlock } from './timer'; import { useBack } from '@/hooks/use-back'; import { useField } from '@/hooks/use-field'; -import { t } from '@/utils/locale'; +import tAbout from 'at/i18n/about'; +import tAnswer from 'at/i18n/answer'; +import tBack from 'at/i18n/back'; +import tTemplateSetting from 'at/i18n/templateSetting'; import { locale } from 'at/locale'; import clsx from 'clsx'; import { useAtomValue } from 'jotai'; @@ -58,12 +61,12 @@ export const CardShell: FC = ({ - {t('templateSetting')} + {tTemplateSetting}
setShowSettings(false)} > - {t('back')} + {tBack}
} @@ -89,7 +92,7 @@ export const CardShell: FC = ({ className="cursor-pointer font-bold text-indigo-500" onClick={() => setShowSettings(true)} > - {t('templateSetting')} + {tTemplateSetting} } @@ -103,13 +106,13 @@ export const CardShell: FC = ({ {questionExtra}
{back && answer ? ( - + {answer} ) : null} {prefHideAbout ? null : ( - + )} diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 1367734..6d7add0 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -1,7 +1,18 @@ import { About } from './about'; import { Checkbox } from './checkbox'; -import { t } from '@/utils/locale'; import { atomWithLocalStorage } from '@/utils/storage'; +import tBiggerText from 'at/i18n/biggerText'; +import tBlurOptions from 'at/i18n/blurOptions'; +import tBlurOptionsDetail from 'at/i18n/blurOptionsDetail'; +import tHideAbout from 'at/i18n/hideAbout'; +import tHideQuestionType from 'at/i18n/hideQuestionType'; +import tHideQuestionTypeDetail from 'at/i18n/hideQuestionTypeDetail'; +import tHideTimer from 'at/i18n/hideTimer'; +import tNoScroll from 'at/i18n/noScroll'; +import tRandomOption from 'at/i18n/randomOption'; +import tRandomOptionDetail from 'at/i18n/randomOptionDetail'; +import tSelMenu from 'at/i18n/selMenu'; +import tSelMenuDetail from 'at/i18n/selMenuDetail'; import { id } from 'at/options'; import { useAtom } from 'jotai'; import { FC } from 'react'; @@ -40,28 +51,24 @@ const CommonOptions: FC = () => { return ( <> + - @@ -82,20 +89,20 @@ if (id === 'mcq') { return ( <> diff --git a/src/components/timer.tsx b/src/components/timer.tsx index 1f83e63..13c1a73 100644 --- a/src/components/timer.tsx +++ b/src/components/timer.tsx @@ -1,10 +1,20 @@ import { Block } from './block'; import { Input } from './input'; import { hideTimerAtom } from './settings'; -import { t } from '@/utils/locale'; import { atomWithLocalStorage } from '@/utils/storage'; import useCountDown from 'ahooks/es/useCountDown'; import useCreation from 'ahooks/es/useCreation'; +import tClose from 'at/i18n/close'; +import tDay from 'at/i18n/day'; +import tDefaultTimerTitle from 'at/i18n/defaultTimerTitle'; +import tHour from 'at/i18n/hour'; +import tMinute from 'at/i18n/minute'; +import tSecond from 'at/i18n/second'; +import tSetting from 'at/i18n/setting'; +import tTargetDate from 'at/i18n/targetDate'; +import tTimer from 'at/i18n/timer'; +import tTimerSetting from 'at/i18n/timerSetting'; +import tTimerTitle from 'at/i18n/timerTitle'; import { useAtom, useAtomValue } from 'jotai'; import { FC, useState } from 'react'; @@ -18,10 +28,10 @@ export const Timer: FC = () => { const { days, hours, minutes, seconds } = formattedRes; return [ - [t('day'), days], - [t('hour'), hours], - [t('minute'), minutes], - [t('second'), seconds], + [tDay, days], + [tHour, hours], + [tMinute, minutes], + [tSecond, seconds], ]; }, [formattedRes]); @@ -52,7 +62,7 @@ interface TimerProps { const defaultTimerProps = { targetDate: '2023-12-31', - title: t('defaultTimerTitle'), + title: tDefaultTimerTitle, }; export const timerAtom = atomWithLocalStorage( @@ -74,12 +84,12 @@ export const TimerBlock = () => { - {showSetting ? t('timerSetting') : t('timer')} + {showSetting ? tTimerSetting : tTimer}
setShowSetting((p) => !p)} > - {showSetting ? t('close') : t('setting')} + {showSetting ? tClose : tSetting}
} @@ -87,7 +97,7 @@ export const TimerBlock = () => { {showSetting ? ( <> setTimer((prevTimer) => ({ @@ -99,7 +109,7 @@ export const TimerBlock = () => { /> setTimer((prevTimer) => ({ diff --git a/src/entries/basic.tsx b/src/entries/basic.tsx index 431e645..ddb5f61 100644 --- a/src/entries/basic.tsx +++ b/src/entries/basic.tsx @@ -2,7 +2,7 @@ import { CardShell } from '../components/card-shell'; import { AnkiField } from '@/components/field'; import { FIELD_ID } from '@/utils/const'; import { isFieldEmpty } from '@/utils/field'; -import { t } from '@/utils/locale'; +import tQuestion from 'at/i18n/question'; import clsx from 'clsx'; export default () => { @@ -11,7 +11,7 @@ export default () => { return ( diff --git a/src/entries/mcq.tsx b/src/entries/mcq.tsx index 9b450ff..83c2d74 100644 --- a/src/entries/mcq.tsx +++ b/src/entries/mcq.tsx @@ -8,7 +8,6 @@ import { import { useBack } from '../hooks/use-back'; import { useCrossState } from '../hooks/use-cross-state'; import { useField } from '../hooks/use-field'; -import { t } from '../utils/locale'; import { randomOptionsAtom } from '@/components/settings'; import '@/styles/mcq.css'; import { flipToBack } from '@/utils/bridge'; @@ -18,6 +17,12 @@ import { useAutoAnimate } from '@formkit/auto-animate/preact'; import useCreation from 'ahooks/es/useCreation'; import useMemoizedFn from 'ahooks/es/useMemoizedFn'; import useSelections from 'ahooks/es/useSelections'; +import tCorrectAnswer from 'at/i18n/correctAnswer'; +import tMissedAnswer from 'at/i18n/missedAnswer'; +import tMultipleAnswer from 'at/i18n/multipleAnswer'; +import tQuestion from 'at/i18n/question'; +import tSingleAnswer from 'at/i18n/singleAnswer'; +import tWrongAnswer from 'at/i18n/wrongAnswer'; import { locale } from 'at/locale'; import { fields } from 'at/options'; import clsx from 'clsx'; @@ -25,6 +30,12 @@ import { useAtomValue } from 'jotai'; import { useEffect } from 'react'; import { doNothing, shuffle } from 'remeda'; +const ANSWER_TYPE_MAP = { + missedAnswer: tMissedAnswer, + correctAnswer: tCorrectAnswer, + wrongAnswer: tWrongAnswer, +}; + const fieldToAlpha = (field: string) => field.slice(field.length - 1); export default () => { @@ -111,9 +122,9 @@ export default () => { {isMultipleChoice ? t('multipleAnswer') : t('singleAnswer')} + <>{isMultipleChoice ? tMultipleAnswer : tSingleAnswer} ) } questionExtra={ @@ -143,11 +154,13 @@ export default () => { selectResult === 'correct', 'before:text-amber-500 after:bg-amber-500': selectResult === 'missed', - [`after:content-['${t( - `${ - selectResult as Exclude - }Answer`, - )}']`]: selectResult !== 'none', + [`after:content-['${ + ANSWER_TYPE_MAP[ + `${ + selectResult as Exclude + }Answer` + ] + }']`]: selectResult !== 'none', [clsx( `before:absolute before:content-['${fieldToAlpha( name, diff --git a/src/entries/tf.tsx b/src/entries/tf.tsx index 8c0aea3..93b32d8 100644 --- a/src/entries/tf.tsx +++ b/src/entries/tf.tsx @@ -5,9 +5,10 @@ import { useCrossState } from '@/hooks/use-cross-state'; import { FIELD_ID } from '@/utils/const'; import { extractTfItems } from '@/utils/extract-tf-items'; import { isFieldEmpty } from '@/utils/field'; -import { t } from '@/utils/locale'; import useCreation from 'ahooks/es/useCreation'; import useMemoizedFn from 'ahooks/es/useMemoizedFn'; +import tQuestion from 'at/i18n/question'; +import tYourWrongAnswer from 'at/i18n/yourWrongAnswer'; import clsx from 'clsx'; import { CheckCircle, XCircle, Triangle } from 'lucide-react'; import { useCallback } from 'react'; @@ -134,13 +135,13 @@ export default () => { return ( {items} {back ? (
- {t('yourWrongAnswer')} + {tYourWrongAnswer} map[key] || 'key'; diff --git a/tailwind.config.js b/tailwind.config.js index 43aec85..00452f1 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -import en from './src/locales/en.json'; -import zh from './src/locales/zh.json'; +import en from './locales/en.json'; +import zh from './locales/zh.json'; /** @type {import('tailwindcss').Config} */ module.exports = {