feat: support i18n

closes #91
This commit is contained in:
Pionxzh
2023-04-15 03:41:04 +08:00
parent 0f91b5698b
commit 918ff41dc4
19 changed files with 367 additions and 38 deletions

View File

@@ -27,6 +27,7 @@
"eventemitter3": "^5.0.0",
"hast-util-to-html": "^8.0.4",
"html2canvas": "^1.4.1",
"i18next": "^22.4.14",
"jszip": "3.9.1",
"mdast": "^3.0.0",
"mdast-util-from-markdown": "^1.3.0",
@@ -36,6 +37,7 @@
"mdast-util-to-markdown": "^1.5.0",
"micromark-extension-gfm": "^2.0.1",
"preact": "^10.13.2",
"react-i18next": "^12.2.0",
"sanitize-filename": "^1.6.3",
"sentinel-js": "^0.0.5",
"urlcat": "^2.0.4",

View File

@@ -1,6 +1,7 @@
export const baseUrl = 'https://chat.openai.com'
export const LEGACY_KEY_FILENAME_FORMAT = 'exporter-format'
export const KEY_LANGUAGE = 'exporter:language'
export const KEY_FILENAME_FORMAT = 'exporter:filename_format'
export const KEY_TIMESTAMP_ENABLED = 'exporter:enable_timestamp'
export const KEY_TIMESTAMP_24H = 'exporter:timestamp_24h'

View File

@@ -1,6 +1,7 @@
import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import { KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, baseUrl } from '../constants'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice, getUserAvatar } from '../page'
import templateHtml from '../template.html?raw'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
@@ -13,7 +14,7 @@ import type { ExportMeta } from '../ui/SettingContext'
export async function exportToHtml(fileNameFormat: string, metaList: ExportMeta[]) {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}

View File

@@ -1,4 +1,5 @@
import html2canvas from 'html2canvas'
import i18n from '../i18n'
import { checkIfConversationStarted, getChatIdFromUrl } from '../page'
import { downloadUrl, getFileNameWithFormat } from '../utils/download'
import { Effect } from '../utils/effect'
@@ -11,7 +12,7 @@ function fnIgnoreElements(el: any) {
export async function exportToPng(fileNameFormat: string) {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}

View File

@@ -1,12 +1,13 @@
import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import type { ApiConversationWithId } from '../api'
export async function exportToJson(fileNameFormat: string) {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}

View File

@@ -1,6 +1,7 @@
import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import { baseUrl } from '../constants'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { fromMarkdown, toMarkdown } from '../utils/markdown'
@@ -11,7 +12,7 @@ import type { ExportMeta } from '../ui/SettingContext'
export async function exportToMarkdown(fileNameFormat: string, metaList: ExportMeta[]) {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}

View File

@@ -1,4 +1,5 @@
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { copyToClipboard } from '../utils/clipboard'
import { flatMap, fromMarkdown, toMarkdown } from '../utils/markdown'
@@ -7,7 +8,7 @@ import type { Emphasis, Strong } from 'mdast'
export async function exportToText() {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}
@@ -43,7 +44,7 @@ export async function exportToText() {
export async function exportToTextFromIndex(index: number) {
if (!checkIfConversationStarted()) {
alert('Please start a conversation first.')
alert(i18n.t('Please start a conversation first'))
return false
}

View File

@@ -0,0 +1,124 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import { KEY_LANGUAGE } from './constants'
import en_US from './locales/en.json'
import ja_JP from './locales/jp.json'
import zh_Hans from './locales/zh-Hans.json'
import zh_Hant from './locales/zh-Hant.json'
import { ScriptStorage } from './utils/storage'
declare module 'i18next' {
// Refs: https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
interface CustomTypeOptions {
returnNull: false
}
}
interface Locale {
name: string
code: string
aliases?: string[]
resource: Record<string, string>
}
const EN_US = {
name: 'English',
code: 'en-US',
resource: en_US,
}
const JA_JP = {
name: '日本語',
code: 'ja-JP',
resource: ja_JP,
}
const ZH_Hans = {
name: '简体中文',
code: 'zh-Hans',
resource: zh_Hans,
}
const ZH_Hant = {
name: '繁體中文',
code: 'zh-Hant',
resource: zh_Hant,
}
export const LOCALES: Locale[] = [
EN_US,
JA_JP,
ZH_Hans,
ZH_Hant,
]
const LanguageMapping: Record<string, string> = {
'en': EN_US.code,
'en-US': EN_US.code,
'ja': JA_JP.code,
'ja-JP': JA_JP.code,
'zh': ZH_Hans.code,
'zh-CN': ZH_Hans.code,
'zh-SG': ZH_Hans.code,
'zh-Hans': ZH_Hans.code,
'zh-HK': ZH_Hant.code,
'zh-MO': ZH_Hant.code,
'zh-TW': ZH_Hant.code,
'zh-Hant': ZH_Hant.code,
}
const resources = LOCALES.reduce<Record<string, { translation: Record<string, string> }>>((acc, cur) => {
acc[cur.code] = { translation: cur.resource }
return acc
}, {})
const standardizeLanguage = (language: string) => {
if (language in LanguageMapping) return LanguageMapping[language]
const shortLang = language.split('-')[0]
if (shortLang in LanguageMapping) return LanguageMapping[shortLang]
return language
}
const getNavigatorLanguage = () => {
const { language, languages } = navigator
if (language) return language
if (languages && languages.length) {
return languages[0]
}
return null
}
const getDefaultLanguage = () => {
const storedLanguage = ScriptStorage.get<string>(KEY_LANGUAGE)
if (storedLanguage) return standardizeLanguage(storedLanguage)
const browserLanguage = getNavigatorLanguage()
if (browserLanguage) return standardizeLanguage(browserLanguage)
return EN_US.code
}
i18n
.use(initReactI18next)
.init({
fallbackLng: EN_US.code,
lng: getDefaultLanguage(),
debug: process.env.NODE_ENV === 'development',
resources,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
})
i18n.on('languageChanged', (lng) => {
ScriptStorage.set(KEY_LANGUAGE, lng)
})
export default i18n

View File

@@ -0,0 +1,32 @@
{
"title": "ChatGPT Exporter",
"ExportHelper": "Export",
"Setting": "Setting",
"Language": "Language",
"Copy Text": "Copy Text",
"Copied!": "Copied!",
"Screenshot": "Screenshot",
"Markdown": "Markdown",
"HTML": "HTML",
"JSON": "JSON",
"Save": "Save",
"Delete": "Delete",
"Select All": "Select All",
"Export": "Export",
"Error": "Error",
"Loading": "Loading",
"Preview": "Preview",
"File Name": "File Name",
"Export All": "Export All",
"Exporter Settings": "Exporter Settings",
"Export Dialog Title": "Export Conversations",
"Available variables": "Available variables",
"Conversation Timestamp": "Conversation Timestamp",
"Conversation Timestamp Description": "Will show on the page and HTML files.",
"Use 24-hour format": "Use 24-hour format (eg. 23:59)",
"Export Metadata": "Export Metadata",
"Export Metadata Description": "Add metadata to exported Markdown and HTML files.",
"Conversation Delete Alert": "Are you sure you want to delete all selected conversations?",
"Conversation Deleted Message": "All selected conversations have been deleted. Please refresh the page to see the changes.",
"Please start a conversation first": "Please start a conversation first."
}

View File

@@ -0,0 +1,32 @@
{
"title": "ChatGPTエクスポーター",
"ExportHelper": "エクスポート",
"Setting": "設定",
"Language": "言語",
"Copy Text": "テキストをコピー",
"Copied!": "コピーしました!",
"Screenshot": "スクリーンショット",
"Markdown": "Markdown",
"HTML": "HTML",
"JSON": "JSON",
"Save": "保存",
"Delete": "削除",
"Select All": "すべて選択",
"Export": "エクスポート",
"Error": "エラー",
"Loading": "読み込み中",
"Preview": "プレビュー",
"File Name": "ファイル名",
"Export All": "すべてエクスポート",
"Exporter Settings": "エクスポーター設定",
"Export Dialog Title": "会話をエクスポート",
"Available variables": "使用可能な変数",
"Conversation Timestamp": "会話のタイムスタンプ",
"Conversation Timestamp Description": "ページとHTMLファイルに表示されます。",
"Use 24-hour format": "24時間形式を使用する (例: 23:59)",
"Export Metadata": "メタデータをエクスポート",
"Export Metadata Description": "エクスポートされたMarkdownおよびHTMLファイルにメタデータを追加します。",
"Conversation Delete Alert": "選択したすべての会話を削除してもよろしいですか?",
"Conversation Deleted Message": "選択したすべての会話が削除されました。変更を表示するには、ページを更新してください。",
"Please start a conversation first": "まず会話を開始してください。"
}

View File

@@ -0,0 +1,32 @@
{
"title": "ChatGPT Exporter",
"ExportHelper": "导出助手",
"Setting": "设置",
"Language": "语言",
"Copy Text": "复制文字",
"Copied!": "已复制!",
"Screenshot": "截屏",
"Markdown": "Markdown",
"HTML": "HTML",
"JSON": "JSON",
"Save": "保存",
"Delete": "删除",
"Select All": "全选",
"Export": "导出",
"Error": "错误",
"Loading": "加载中",
"Preview": "预览",
"File Name": "文件名",
"Export All": "批量导出",
"Exporter Settings": "导出设置",
"Export Dialog Title": "导出对话",
"Available variables": "可用变量",
"Conversation Timestamp": "对话时间戳",
"Conversation Timestamp Description": "会添加至页面以及 HTML 导出。",
"Use 24-hour format": "使用24小时制 (例如 23:59)",
"Export Metadata": "导出元数据",
"Export Metadata Description": "会添加至 Markdown 以及 HTML 导出。",
"Conversation Delete Alert": "确定要删除所有选取的对话?",
"Conversation Deleted Message": "所有所选的对话已删除。请刷新页面。",
"Please start a conversation first": "请先开始一次对话。"
}

View File

@@ -0,0 +1,32 @@
{
"title": "ChatGPT Exporter",
"ExportHelper": "Export",
"Setting": "設定",
"Language": "語言",
"Copy Text": "複製文字",
"Copied!": "已複製!",
"Screenshot": "截圖",
"Markdown": "Markdown",
"HTML": "HTML",
"JSON": "JSON",
"Save": "保存",
"Delete": "刪除",
"Select All": "全選",
"Export": "匯出",
"Error": "錯誤",
"Loading": "載入中",
"Preview": "預覽",
"File Name": "檔案名稱",
"Export All": "批量匯出",
"Exporter Settings": "設定",
"Export Dialog Title": "匯出對話",
"Available variables": "可用變數",
"Conversation Timestamp": "對話時間戳",
"Conversation Timestamp Description": "會添加至頁面以及 HTML 匯出。",
"Use 24-hour format": "使用24小時制 (例如 23:59)",
"Export Metadata": "匯出元資料",
"Export Metadata Description": "會添加至 Markdown 以及 HTML 匯出。",
"Conversation Delete Alert": "確定要刪除所有選取的對話?",
"Conversation Deleted Message": "所有選取的對話已刪除。請重新整理頁面。",
"Please start a conversation first": "請先開始一次對話。"
}

View File

@@ -8,6 +8,7 @@ import { Menu } from './ui/Menu'
import { SecondaryToolbar } from './ui/SecondaryToolbar'
import { onloadSafe } from './utils/utils'
import './i18n'
import './styles/missing-tailwind.css'
/**

View File

@@ -55,7 +55,6 @@
padding: 0 15px;
font-size: 15px;
line-height: 1;
font-weight: 500;
height: 35px;
}
.Button.green {

View File

@@ -1,5 +1,6 @@
import * as Dialog from '@radix-ui/react-dialog'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { useTranslation } from 'react-i18next'
import { deleteConversation, fetchAllConversations, fetchConversation } from '../api'
import { exportAllToHtml } from '../exporter/html'
import { exportAllToJson } from '../exporter/json'
@@ -34,11 +35,13 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
loading,
error,
}) => {
const { t } = useTranslation()
return (
<>
<div className="SelectToolbar">
<CheckBox
label="Select All"
label={t('Select All')}
disabled={disabled}
checked={selected.length === conversations.length}
onCheckedChange={(checked) => {
@@ -47,8 +50,8 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
/>
</div>
<ul className="SelectList">
{loading && <li className="SelectItem">Loading...</li>}
{error && <li className="SelectItem">Error: {error}</li>}
{loading && <li className="SelectItem">{t('Loading')}...</li>}
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
{conversations.map(c => (
<li className="SelectItem" key={c.id}>
<CheckBox
@@ -76,6 +79,7 @@ interface ExportDialogProps {
}
export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange, children }) => {
const { t } = useTranslation()
const { enableMeta, exportMetaList } = useSettingContext()
const metaList = useMemo(() => enableMeta ? exportMetaList : [], [enableMeta, exportMetaList])
@@ -129,10 +133,10 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
setProcessing(false)
setConversations(conversations.filter(c => !selected.some(s => s.id === c.id)))
setSelected([])
alert('All selected conversations have been deleted. Please refresh the page to see the changes.')
alert(t('Conversation Deleted Message'))
})
return () => off()
}, [deleteQueue, conversations, selected])
}, [deleteQueue, conversations, selected, t])
const exportAll = useCallback(() => {
if (disabled) return
@@ -152,7 +156,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
const deleteAll = useCallback(() => {
if (disabled) return
const result = confirm('Are you sure you want to delete all selected conversations?')
const result = confirm(t('Conversation Delete Alert'))
if (!result) return
deleteQueue.clear()
@@ -165,7 +169,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
})
deleteQueue.start()
}, [disabled, selected, deleteQueue])
}, [disabled, selected, deleteQueue, t])
useEffect(() => {
setLoading(true)
@@ -186,7 +190,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
<Dialog.Title className="DialogTitle">Export Conversations</Dialog.Title>
<Dialog.Title className="DialogTitle">{t('Export Dialog Title')}</Dialog.Title>
<ConversationSelect
conversations={conversations}
@@ -199,15 +203,15 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
<div className="flex mt-6" style={{ justifyContent: 'space-between' }}>
<select className="Select" disabled={processing} value={exportType} onChange={e => setExportType(e.currentTarget.value)}>
{exportAllOptions.map(({ label }) => (
<option key={label} value={label}>{label}</option>
<option key={t(label)} value={label}>{label}</option>
))}
</select>
<div className="flex flex-grow"></div>
<button className="Button red" disabled={disabled} onClick={deleteAll}>
Delete
{t('Delete')}
</button>
<button className="Button green ml-4" disabled={disabled} onClick={exportAll}>
Export
{t('Export')}
</button>
</div>
{processing && (

View File

@@ -1,5 +1,6 @@
import * as HoverCard from '@radix-ui/react-hover-card'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { useTranslation } from 'react-i18next'
import { exportToHtml } from '../exporter/html'
import { exportToPng } from '../exporter/image'
import { exportToJson } from '../exporter/json'
@@ -21,6 +22,7 @@ But History feature is disabled by OpenAI temporarily.
We all have to wait for them to bring it back.`
function MenuInner({ container }: { container: HTMLDivElement }) {
const { t } = useTranslation()
const disabled = getHistoryDisabled()
const [open, setOpen] = useState(false)
@@ -85,7 +87,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
<HoverCard.Trigger>
<MenuItem
className="mt-1"
text="Export"
text={t('ExportHelper')}
icon={IconArrowRightFromBracket}
onClick={() => {
setOpen(true)
@@ -116,37 +118,37 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
onOpenChange={setSettingOpen}
>
<div className="row-full">
<MenuItem text="Setting" icon={IconSetting} />
<MenuItem text={t('Setting')} icon={IconSetting} />
</div>
</SettingDialog>
<MenuItem
text="Copy Text"
successText="Copied!"
text={t('Copy Text')}
successText={t('Copied!')}
icon={() => <IconCopy className="w-4 h-4" />}
className="row-full"
onClick={onClickText}
/>
<MenuItem
text="Screenshot"
text={t('Screenshot')}
icon={IconCamera}
className="row-half"
onClick={onClickPng}
/>
<MenuItem
text="Markdown"
text={t('Markdown')}
icon={IconMarkdown}
className="row-half"
onClick={onClickMarkdown}
/>
<MenuItem
text="HTML"
text={t('HTML')}
icon={FileCode}
className="row-half"
onClick={onClickHtml}
/>
<MenuItem
text="JSON"
text={t('JSON')}
icon={IconJSON}
className="row-half"
onClick={onClickJSON}
@@ -158,7 +160,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
>
<div className="row-full">
<MenuItem
text="Export All"
text={t('Export All')}
icon={IconZip}
/>
</div>

View File

@@ -1,7 +1,9 @@
import * as Dialog from '@radix-ui/react-dialog'
import { useTranslation } from 'react-i18next'
import sanitize from 'sanitize-filename'
import { baseUrl } from '../constants'
import { useTitle } from '../hooks/useTitle'
import { LOCALES } from '../i18n'
import { getChatIdFromUrl } from '../page'
import { getFileNameWithFormat } from '../utils/download'
import { timestamp as _timestamp, dateStr } from '../utils/utils'
@@ -31,6 +33,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
enableMeta, setEnableMeta,
exportMetaList, setExportMetaList,
} = useSettingContext()
const { t, i18n } = useTranslation()
const _title = useTitle()
const date = dateStr()
const timestamp = _timestamp()
@@ -51,17 +54,35 @@ export const SettingDialog: FC<SettingDialogProps> = ({
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
<Dialog.Title className="DialogTitle">Exporter Setting</Dialog.Title>
<Dialog.Title className="DialogTitle">{t('Exporter Settings')}</Dialog.Title>
<dl className="space-y-6">
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
File Name
{`${t('Language')} 🌐`}
</dt>
<dd>
<select
className="Select mt-3"
value={i18n.language}
onChange={e => i18n.changeLanguage(e.currentTarget.value)}
>
{LOCALES.map(({ name, code }) => (
<option key={code} value={code}>{name}</option>
))}
</select>
</dd>
</div>
</div>
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
{t('File Name')}
</dt>
<dd>
<p className="text-sm text-gray-700 dark:text-gray-300">
Available variables:{' '}
{t('Available variables')}:{' '}
<Variable name="{title}" title={title} />
,{' '}
<Variable name="{date}" title={date} />
@@ -72,7 +93,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
</p>
<input className="Input mt-4" id="filename" value={format} onChange={e => setFormat(e.currentTarget.value)} />
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
Preview:{' '}
{t('Preview')}:{' '}
<span className="select-all" style={{ 'text-decoration': 'underline', 'text-underline-offset': 4 }}>{preview}</span>
</p>
</dd>
@@ -81,14 +102,14 @@ export const SettingDialog: FC<SettingDialogProps> = ({
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
Conversation Timestamp
{t('Conversation Timestamp')}
</dt>
<dd className="text-sm text-gray-700 dark:text-gray-300">
Will show on the page and HTML files.
{t('Conversation Timestamp Description')}
{enableTimestamp && (
<div className="mt-2">
<Toggle
label="Use 24-hour format (eg. 23:59)"
label={t('Use 24-hour format')}
checked={timeStamp24H}
onCheckedUpdate={setTimeStamp24H}
/>
@@ -103,15 +124,15 @@ export const SettingDialog: FC<SettingDialogProps> = ({
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
Export Metadata
{t('Export Metadata')}
</dt>
<dd className="text-sm text-gray-700 dark:text-gray-300">
Add metadata to exported Markdown and HTML files.
{t('Export Metadata Description')}
{enableMeta && (
<>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Available variables:{' '}
{t('Available variables')}:{' '}
<Variable name="{title}" title={title} />
,{' '}
<Variable name="{date}" title={date} />
@@ -174,7 +195,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
</dl>
<div className="flex mt-6" style={{ justifyContent: 'flex-end' }}>
<Dialog.Close asChild>
<button className="Button green">Save</button>
<button className="Button green font-bold">{t('Save')}</button>
</Dialog.Close>
</div>
<Dialog.Close asChild>

View File

@@ -8,6 +8,7 @@
},
"include": [
"src",
"src/locales/*.json",
"package.json",
"vite.config.ts"
]

41
pnpm-lock.yaml generated
View File

@@ -45,6 +45,7 @@ importers:
eventemitter3: ^5.0.0
hast-util-to-html: ^8.0.4
html2canvas: ^1.4.1
i18next: ^22.4.14
jszip: 3.9.1
mdast: ^3.0.0
mdast-util-from-markdown: ^1.3.0
@@ -54,6 +55,7 @@ importers:
mdast-util-to-markdown: ^1.5.0
micromark-extension-gfm: ^2.0.1
preact: ^10.13.2
react-i18next: ^12.2.0
sanitize-filename: ^1.6.3
sentinel-js: ^0.0.5
urlcat: ^2.0.4
@@ -67,6 +69,7 @@ importers:
eventemitter3: 5.0.0
hast-util-to-html: 8.0.4
html2canvas: 1.4.1
i18next: 22.4.14
jszip: 3.9.1
mdast: 3.0.0
mdast-util-from-markdown: 1.3.0
@@ -76,6 +79,7 @@ importers:
mdast-util-to-markdown: 1.5.0
micromark-extension-gfm: 2.0.1
preact: 10.13.2
react-i18next: 12.2.0_57ubdvajp6562okxygabugvlve
sanitize-filename: 1.6.3
sentinel-js: 0.0.5_4xqsksf5w62wic46kncafprj3e
urlcat: 2.0.4
@@ -3156,6 +3160,12 @@ packages:
lru-cache: 6.0.0
dev: true
/html-parse-stringify/3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/html-void-elements/2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
dev: false
@@ -3202,6 +3212,12 @@ packages:
hasBin: true
dev: true
/i18next/22.4.14:
resolution: {integrity: sha512-VtLPtbdwGn0+DAeE00YkiKKXadkwg+rBUV+0v8v0ikEjwdiJ0gmYChVE4GIa9HXymY6wKapkL93vGT7xpq6aTw==}
dependencies:
'@babel/runtime': 7.21.0
dev: false
/ignore/5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@@ -4707,6 +4723,26 @@ packages:
scheduler: 0.23.0
dev: false
/react-i18next/12.2.0_57ubdvajp6562okxygabugvlve:
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
peerDependencies:
i18next: '>= 19.0.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.21.0
html-parse-stringify: 3.0.1
i18next: 22.4.14
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
@@ -5728,6 +5764,11 @@ packages:
optionalDependencies:
fsevents: 2.3.2
/void-elements/3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
dev: false
/web-namespaces/2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
dev: false