feat: per-file Convert in explorer menu + fix export success toast
Build and Deploy Verso / deploy (push) Successful in 9m50s
Build and Deploy Verso / deploy (push) Successful in 9m50s
- Add "Convert to Typst (.typ)" in the file tree context menu for .tex docs, and "Convert to LaTeX (.tex)" for .typ docs. Clicking runs pandoc on the file content and creates the converted file in the same folder. - New backend endpoint POST /project/:id/doc/:id/convert/:type that reads the doc from document-updater, runs pandoc directly, and creates the result via ProjectEntityUpdateHandler (file tree updates via socket). - Rewrite the export success toast for typst and latex conversions: no more link to /contact, replaced with a plain warning that errors are expected (pandoc does not support all constructs). - Add i18n keys: convert_to_typst, convert_to_latex, typst_export_feedback_message, latex_export_feedback_message (EN + FR) and all four to extracted-translations.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,15 @@ import { expressify } from '@overleaf/promise-utils'
|
|||||||
import { pipeline } from 'node:stream/promises'
|
import { pipeline } from 'node:stream/promises'
|
||||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
||||||
import { DocumentConversionError } from '../Errors/Errors.js'
|
import { DocumentConversionError } from '../Errors/Errors.js'
|
||||||
|
import ProjectLocator from '../Project/ProjectLocator.mjs'
|
||||||
|
import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.mjs'
|
||||||
|
import { execFile } from 'node:child_process'
|
||||||
|
import { promisify } from 'node:util'
|
||||||
|
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import nodePath from 'node:path'
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
const { z, zz, parseReq } = Validation
|
const { z, zz, parseReq } = Validation
|
||||||
|
|
||||||
@@ -166,9 +175,86 @@ async function downloadPreparedProjectExport(req, res) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOC_CONVERSION_CONFIGS = {
|
||||||
|
typst: { fromExt: 'tex', toExt: 'typ', pandocFrom: 'latex', pandocTo: 'typst' },
|
||||||
|
latex: { fromExt: 'typ', toExt: 'tex', pandocFrom: 'typst', pandocTo: 'latex' },
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertDocInProject(req, res) {
|
||||||
|
const { Project_id: projectId, Doc_id: docId, type } = req.params
|
||||||
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
|
|
||||||
|
const config = DOC_CONVERSION_CONFIGS[type]
|
||||||
|
if (!config) return res.status(400).json({ error: 'unsupported conversion type' })
|
||||||
|
|
||||||
|
const { lines } = await DocumentUpdaterHandler.promises.getDocument(
|
||||||
|
projectId,
|
||||||
|
docId,
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
const content = lines.join('\n')
|
||||||
|
|
||||||
|
const { element, folder } = await ProjectLocator.promises.findElement({
|
||||||
|
project_id: projectId,
|
||||||
|
element_id: docId,
|
||||||
|
type: 'doc',
|
||||||
|
})
|
||||||
|
const parentFolderId = folder._id
|
||||||
|
|
||||||
|
const baseName = element.name.endsWith('.' + config.fromExt)
|
||||||
|
? element.name.slice(0, -(config.fromExt.length + 1))
|
||||||
|
: element.name
|
||||||
|
const outputName = baseName + '.' + config.toExt
|
||||||
|
|
||||||
|
const tmpDir = await mkdtemp(nodePath.join(tmpdir(), 'verso-convert-'))
|
||||||
|
let convertedContent
|
||||||
|
try {
|
||||||
|
const inputPath = nodePath.join(tmpDir, `input.${config.fromExt}`)
|
||||||
|
const outputPath = nodePath.join(tmpDir, `output.${config.toExt}`)
|
||||||
|
await writeFile(inputPath, content, 'utf8')
|
||||||
|
try {
|
||||||
|
await execFileAsync(
|
||||||
|
'pandoc',
|
||||||
|
[
|
||||||
|
inputPath,
|
||||||
|
'--from', config.pandocFrom,
|
||||||
|
'--to', config.pandocTo,
|
||||||
|
'--output', outputPath,
|
||||||
|
'--standalone',
|
||||||
|
],
|
||||||
|
{ timeout: 30_000 }
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(422).json({ error: err.stderr || err.message })
|
||||||
|
}
|
||||||
|
convertedContent = await readFile(outputPath, 'utf8')
|
||||||
|
} finally {
|
||||||
|
await rm(tmpDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { doc } = await ProjectEntityUpdateHandler.promises.addDoc(
|
||||||
|
projectId,
|
||||||
|
parentFolderId,
|
||||||
|
outputName,
|
||||||
|
convertedContent.split('\n'),
|
||||||
|
userId,
|
||||||
|
'convert'
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectAuditLogHandler.addEntryInBackground(
|
||||||
|
projectId,
|
||||||
|
'doc-converted',
|
||||||
|
userId,
|
||||||
|
req.ip
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({ docId: doc._id, name: doc.name })
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
exportProjectConversion: expressify(exportProjectConversion),
|
exportProjectConversion: expressify(exportProjectConversion),
|
||||||
downloadPreparedProjectExport: expressify(downloadPreparedProjectExport),
|
downloadPreparedProjectExport: expressify(downloadPreparedProjectExport),
|
||||||
|
convertDocInProject: expressify(convertDocInProject),
|
||||||
|
|
||||||
downloadProject(req, res, next) {
|
downloadProject(req, res, next) {
|
||||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
|
|||||||
@@ -836,6 +836,12 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||||
ProjectDownloadsController.downloadPreparedProjectExport
|
ProjectDownloadsController.downloadPreparedProjectExport
|
||||||
)
|
)
|
||||||
|
webRouter.post(
|
||||||
|
'/project/:Project_id/doc/:Doc_id/convert/:type',
|
||||||
|
AuthenticationController.requireLogin(),
|
||||||
|
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
|
||||||
|
ProjectDownloadsController.convertDocInProject
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
webRouter.get(
|
webRouter.get(
|
||||||
|
|||||||
@@ -387,6 +387,8 @@
|
|||||||
"continue_using_free_features": "",
|
"continue_using_free_features": "",
|
||||||
"continue_with_free_plan": "",
|
"continue_with_free_plan": "",
|
||||||
"conversion_error_details": "",
|
"conversion_error_details": "",
|
||||||
|
"convert_to_latex": "",
|
||||||
|
"convert_to_typst": "",
|
||||||
"cookie_banner": "",
|
"cookie_banner": "",
|
||||||
"cookie_banner_info": "",
|
"cookie_banner_info": "",
|
||||||
"copied": "",
|
"copied": "",
|
||||||
@@ -1099,6 +1101,7 @@
|
|||||||
"last_verified": "",
|
"last_verified": "",
|
||||||
"latam_discount_modal_info": "",
|
"latam_discount_modal_info": "",
|
||||||
"latam_discount_modal_title": "",
|
"latam_discount_modal_title": "",
|
||||||
|
"latex_export_feedback_message": "",
|
||||||
"latex_places_figures_according_to_a_special_algorithm": "",
|
"latex_places_figures_according_to_a_special_algorithm": "",
|
||||||
"latex_places_tables_according_to_a_special_algorithm": "",
|
"latex_places_tables_according_to_a_special_algorithm": "",
|
||||||
"layout_options": "",
|
"layout_options": "",
|
||||||
@@ -2260,6 +2263,7 @@
|
|||||||
"turn_off_link_sharing": "",
|
"turn_off_link_sharing": "",
|
||||||
"turn_on": "",
|
"turn_on": "",
|
||||||
"turn_on_link_sharing": "",
|
"turn_on_link_sharing": "",
|
||||||
|
"typst_export_feedback_message": "",
|
||||||
"unarchive": "",
|
"unarchive": "",
|
||||||
"uncategorized": "",
|
"uncategorized": "",
|
||||||
"uncategorized_projects": "",
|
"uncategorized_projects": "",
|
||||||
|
|||||||
+43
@@ -8,6 +8,11 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from '@/shared/components/dropdown/dropdown-menu'
|
} from '@/shared/components/dropdown/dropdown-menu'
|
||||||
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
||||||
|
import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
|
||||||
|
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||||
|
import { findInTree } from '../../util/find-in-tree'
|
||||||
|
import useConvertDoc from '@/features/ide-react/hooks/use-convert-doc'
|
||||||
|
import getMeta from '@/utils/meta'
|
||||||
|
|
||||||
function FileTreeItemMenuItems() {
|
function FileTreeItemMenuItems() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -30,6 +35,25 @@ function FileTreeItemMenuItems() {
|
|||||||
const { project } = useProjectContext()
|
const { project } = useProjectContext()
|
||||||
const projectOwner = project?.owner?._id
|
const projectOwner = project?.owner?._id
|
||||||
|
|
||||||
|
const { fileTreeData } = useFileTreeData()
|
||||||
|
const { selectedEntityIds } = useFileTreeSelectable()
|
||||||
|
const selectedEntityId =
|
||||||
|
selectedEntityIds.size === 1 ? Array.from(selectedEntityIds)[0] : null
|
||||||
|
const selectedEntityType = selectedEntityId
|
||||||
|
? findInTree(fileTreeData, selectedEntityId)?.type
|
||||||
|
: null
|
||||||
|
const isDoc = selectedEntityType === 'doc'
|
||||||
|
const enablePandocConversions =
|
||||||
|
getMeta('ol-ExposedSettings')?.enablePandocConversions
|
||||||
|
|
||||||
|
const canConvertToTypst =
|
||||||
|
enablePandocConversions && canRename && isDoc && selectedFileName?.endsWith('.tex')
|
||||||
|
const canConvertToLatex =
|
||||||
|
enablePandocConversions && canRename && isDoc && selectedFileName?.endsWith('.typ')
|
||||||
|
|
||||||
|
const { convert: convertToTypst } = useConvertDoc('typst', selectedEntityId)
|
||||||
|
const { convert: convertToLatex } = useConvertDoc('latex', selectedEntityId)
|
||||||
|
|
||||||
const downloadWithAnalytics = useCallback(() => {
|
const downloadWithAnalytics = useCallback(() => {
|
||||||
// we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations
|
// we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations
|
||||||
if (selectedFileName?.endsWith('.bib')) {
|
if (selectedFileName?.endsWith('.bib')) {
|
||||||
@@ -75,6 +99,25 @@ function FileTreeItemMenuItems() {
|
|||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{(canConvertToTypst || canConvertToLatex) ? (
|
||||||
|
<>
|
||||||
|
<DropdownDivider />
|
||||||
|
{canConvertToTypst && (
|
||||||
|
<li role="none">
|
||||||
|
<DropdownItem onClick={convertToTypst}>
|
||||||
|
{t('convert_to_typst')}
|
||||||
|
</DropdownItem>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{canConvertToLatex && (
|
||||||
|
<li role="none">
|
||||||
|
<DropdownItem onClick={convertToLatex}>
|
||||||
|
{t('convert_to_latex')}
|
||||||
|
</DropdownItem>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{canDelete ? (
|
{canDelete ? (
|
||||||
<>
|
<>
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
|
|||||||
+6
-1
@@ -42,6 +42,7 @@ const ExportDocumentErrorToast = ({ data }: { data?: any }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ExportDocumentSuccessToast = ({ data }: { data?: any }) => {
|
const ExportDocumentSuccessToast = ({ data }: { data?: any }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const type = data?.type
|
const type = data?.type
|
||||||
if (type === 'docx') {
|
if (type === 'docx') {
|
||||||
return (
|
return (
|
||||||
@@ -85,6 +86,10 @@ const ExportDocumentSuccessToast = ({ data }: { data?: any }) => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (type === 'typst') {
|
||||||
|
return <span>{t('typst_export_feedback_message')}</span>
|
||||||
|
} else if (type === 'latex') {
|
||||||
|
return <span>{t('latex_export_feedback_message')}</span>
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Trans
|
<Trans
|
||||||
@@ -175,7 +180,7 @@ export const hidePreparingExportToast = (handle: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const showExportDocumentSuccess = (
|
export const showExportDocumentSuccess = (
|
||||||
type: 'docx' | 'markdown' | 'html'
|
type: 'docx' | 'markdown' | 'html' | 'typst' | 'latex'
|
||||||
) => {
|
) => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('ide:show-toast', {
|
new CustomEvent('ide:show-toast', {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { postJSON } from '@/infrastructure/fetch-json'
|
||||||
|
import { useProjectContext } from '@/shared/context/project-context'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import {
|
||||||
|
showExportDocumentError,
|
||||||
|
hideExportDocumentError,
|
||||||
|
} from '../components/toolbar/export-document-toasts'
|
||||||
|
|
||||||
|
export default function useConvertDoc(
|
||||||
|
type: 'typst' | 'latex',
|
||||||
|
docId: string | null
|
||||||
|
) {
|
||||||
|
const { projectId } = useProjectContext()
|
||||||
|
const [converting, setConverting] = useState(false)
|
||||||
|
|
||||||
|
const convert = useCallback(async () => {
|
||||||
|
if (!docId) return
|
||||||
|
setConverting(true)
|
||||||
|
hideExportDocumentError()
|
||||||
|
try {
|
||||||
|
await postJSON(`/project/${projectId}/doc/${docId}/convert/${type}`)
|
||||||
|
// File tree updates automatically via socket event
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err?.data?.error
|
||||||
|
showExportDocumentError(errorMessage)
|
||||||
|
} finally {
|
||||||
|
setConverting(false)
|
||||||
|
}
|
||||||
|
}, [projectId, docId, type])
|
||||||
|
|
||||||
|
return { convert, converting }
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ export default function useConvertProject(
|
|||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
const url = new URL(downloadUrl, window.location.origin)
|
const url = new URL(downloadUrl, window.location.origin)
|
||||||
location.assign(url.toString())
|
location.assign(url.toString())
|
||||||
showExportDocumentSuccess(type)
|
showExportDocumentSuccess(type as 'docx' | 'markdown' | 'html' | 'typst' | 'latex')
|
||||||
} else {
|
} else {
|
||||||
showExportDocumentError()
|
showExportDocumentError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -878,6 +878,8 @@
|
|||||||
"explore_plans": "Explore plans",
|
"explore_plans": "Explore plans",
|
||||||
"export_as_docx": "Export as Word document (.docx)",
|
"export_as_docx": "Export as Word document (.docx)",
|
||||||
"export_as_html": "Export as HTML (.html)",
|
"export_as_html": "Export as HTML (.html)",
|
||||||
|
"convert_to_latex": "Convert to LaTeX (.tex)",
|
||||||
|
"convert_to_typst": "Convert to Typst (.typ)",
|
||||||
"export_as_typst": "Export as Typst",
|
"export_as_typst": "Export as Typst",
|
||||||
"export_as_latex": "Export as LaTeX (.tex)",
|
"export_as_latex": "Export as LaTeX (.tex)",
|
||||||
"export_as_markdown": "Export as Markdown (.md)",
|
"export_as_markdown": "Export as Markdown (.md)",
|
||||||
@@ -1429,6 +1431,7 @@
|
|||||||
"latex_articles_page_title": "Articles - Papers, Presentations, Reports and more",
|
"latex_articles_page_title": "Articles - Papers, Presentations, Reports and more",
|
||||||
"latex_examples": "LaTeX examples",
|
"latex_examples": "LaTeX examples",
|
||||||
"latex_examples_page_title": "Examples - Equations, Formatting, TikZ, Packages and More",
|
"latex_examples_page_title": "Examples - Equations, Formatting, TikZ, Packages and More",
|
||||||
|
"latex_export_feedback_message": "This conversion may contain errors — pandoc does not support all Typst constructs.",
|
||||||
"latex_places_figures_according_to_a_special_algorithm": "LaTeX places figures according to a special algorithm. You can use something called ‘placement parameters’ to influence the positioning of the figure. <0>Find out how</>",
|
"latex_places_figures_according_to_a_special_algorithm": "LaTeX places figures according to a special algorithm. You can use something called ‘placement parameters’ to influence the positioning of the figure. <0>Find out how</>",
|
||||||
"latex_places_tables_according_to_a_special_algorithm": "LaTeX places tables according to a special algorithm. You can use “placement parameters” to influence the position of the table. <0>This article</0> explains how to do this.",
|
"latex_places_tables_according_to_a_special_algorithm": "LaTeX places tables according to a special algorithm. You can use “placement parameters” to influence the position of the table. <0>This article</0> explains how to do this.",
|
||||||
"latex_templates": "LaTeX Templates",
|
"latex_templates": "LaTeX Templates",
|
||||||
@@ -2895,6 +2898,7 @@
|
|||||||
"turn_on_link_sharing": "Turn on link sharing",
|
"turn_on_link_sharing": "Turn on link sharing",
|
||||||
"turn_on_password_visibility": "Turn on password visibility",
|
"turn_on_password_visibility": "Turn on password visibility",
|
||||||
"tutorials": "Tutorials",
|
"tutorials": "Tutorials",
|
||||||
|
"typst_export_feedback_message": "This conversion may contain errors — pandoc does not support all LaTeX constructs.",
|
||||||
"uk": "Ukrainian",
|
"uk": "Ukrainian",
|
||||||
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
||||||
"unarchive": "Restore",
|
"unarchive": "Restore",
|
||||||
|
|||||||
@@ -881,6 +881,8 @@
|
|||||||
"explore_plans": "Découvrir les offres",
|
"explore_plans": "Découvrir les offres",
|
||||||
"export_as_docx": "Exporter sous forme de document Word (.docx)",
|
"export_as_docx": "Exporter sous forme de document Word (.docx)",
|
||||||
"export_as_html": "Exporter au format HTML (.html)",
|
"export_as_html": "Exporter au format HTML (.html)",
|
||||||
|
"convert_to_latex": "Convertir en LaTeX (.tex)",
|
||||||
|
"convert_to_typst": "Convertir en Typst (.typ)",
|
||||||
"export_as_typst": "Exporter en Typst",
|
"export_as_typst": "Exporter en Typst",
|
||||||
"export_as_latex": "Exporter en LaTeX (.tex)",
|
"export_as_latex": "Exporter en LaTeX (.tex)",
|
||||||
"export_as_markdown": "Exporter en Markdown (.md)",
|
"export_as_markdown": "Exporter en Markdown (.md)",
|
||||||
@@ -1432,6 +1434,7 @@
|
|||||||
"latex_articles_page_title": "Articles - Documents, présentations, rapports et plus",
|
"latex_articles_page_title": "Articles - Documents, présentations, rapports et plus",
|
||||||
"latex_examples": "Exemples LaTeX",
|
"latex_examples": "Exemples LaTeX",
|
||||||
"latex_examples_page_title": "Exemples - Équations, formatage, TikZ, packages et plus",
|
"latex_examples_page_title": "Exemples - Équations, formatage, TikZ, packages et plus",
|
||||||
|
"latex_export_feedback_message": "Cette conversion peut contenir des erreurs — pandoc ne supporte pas toutes les constructions Typst.",
|
||||||
"latex_places_figures_according_to_a_special_algorithm": "LaTeX place les figures selon un algorithme spécial. Vous pouvez utiliser quelque chose appelé « paramètres de placement » pour influencer le positionnement de la figure. <0>Découvrez comment</>",
|
"latex_places_figures_according_to_a_special_algorithm": "LaTeX place les figures selon un algorithme spécial. Vous pouvez utiliser quelque chose appelé « paramètres de placement » pour influencer le positionnement de la figure. <0>Découvrez comment</>",
|
||||||
"latex_places_tables_according_to_a_special_algorithm": "LaTeX place les tableaux selon un algorithme spécial. Vous pouvez utiliser des « paramètres de placement » pour influencer la position du tableau. <0>This article</0> explique comment procéder.",
|
"latex_places_tables_according_to_a_special_algorithm": "LaTeX place les tableaux selon un algorithme spécial. Vous pouvez utiliser des « paramètres de placement » pour influencer la position du tableau. <0>This article</0> explique comment procéder.",
|
||||||
"latex_templates": "Modèles LaTeX",
|
"latex_templates": "Modèles LaTeX",
|
||||||
@@ -2900,6 +2903,7 @@
|
|||||||
"turn_on_link_sharing": "Activer le partage par lien",
|
"turn_on_link_sharing": "Activer le partage par lien",
|
||||||
"turn_on_password_visibility": "Activer la visibilité du mot de passe",
|
"turn_on_password_visibility": "Activer la visibilité du mot de passe",
|
||||||
"tutorials": "Tutoriels",
|
"tutorials": "Tutoriels",
|
||||||
|
"typst_export_feedback_message": "Cette conversion peut contenir des erreurs — pandoc ne supporte pas toutes les constructions LaTeX.",
|
||||||
"uk": "Ukrainien",
|
"uk": "Ukrainien",
|
||||||
"unable_to_extract_the_supplied_zip_file": "L’ouverture de ce contenu sur Overleaf a échoué car l’archive n’a pas pu être extraite. Veuillez vous assurer de la validité de cette archive. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
|
"unable_to_extract_the_supplied_zip_file": "L’ouverture de ce contenu sur Overleaf a échoué car l’archive n’a pas pu être extraite. Veuillez vous assurer de la validité de cette archive. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
|
||||||
"unarchive": "Restaurer",
|
"unarchive": "Restaurer",
|
||||||
|
|||||||
Reference in New Issue
Block a user