mirror of
https://github.com/core-hn/pseudobsidian-ization.git
synced 2026-07-22 06:57:06 +00:00
feat: i18n ru/es, export HTML noScribe word-level, fix auto-convert + export paths
- i18n: add Russian (ru) and Spanish (es) locales (~240 keys each) - feat: markdownToNoScribeHtml — reconstruct pseudonymized noScribe HTML with word-level ts_ anchors via proportional index mapping on words.json; add export-as-html command, panel button, notice.htmlExported - fix: autoConvert now skips files created in exports/mappings folders (prevented re-import of freshly written VTT/SRT/HTML exports) - fix: pseudonymizeActiveFile respects exportMirrorClasses — intermediate .pseudonymized.md is now written into the correct class subfolder - fix: extract getClassSub() helper shared by resolveExportPath and pseudonymizeActiveFile - fix: "Nouvelle classe" button in corpus panel now opens CorpusModal instead of window.prompt (blocked in Electron) - refactor: remove next-to-source export destination option Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a3204f1f76
commit
928ff8d42b
11 changed files with 1906 additions and 91 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import en from './locales/en.json';
|
||||
import es from './locales/es.json';
|
||||
import fr from './locales/fr.json';
|
||||
import ru from './locales/ru.json';
|
||||
|
||||
type Strings = Record<string, string>;
|
||||
|
||||
const LOCALES: Record<string, Strings> = { en, fr };
|
||||
const LOCALES: Record<string, Strings> = { en, es, fr, ru };
|
||||
const FALLBACK = 'en';
|
||||
|
||||
let _locale = FALLBACK;
|
||||
|
|
@ -36,5 +38,7 @@ export function t(key: string, ...args: (string | number)[]): string {
|
|||
/** Langues disponibles avec leur nom natif. */
|
||||
export const AVAILABLE_LANGUAGES: Record<string, string> = {
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
ru: 'Русский',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -201,7 +201,6 @@
|
|||
"panel.corpus.deleteClassConfirm": "Delete class \"{0}\"? Files inside will be moved to root.",
|
||||
"panel.corpus.exportSettings": "Final export destination",
|
||||
"panel.corpus.exportDest.vault": "In the vault",
|
||||
"panel.corpus.exportDest.nextToSource": "Next to source file",
|
||||
"panel.corpus.exportDest.external": "External folder",
|
||||
"panel.corpus.exportFolder": "Export folder (vault)",
|
||||
"panel.corpus.exportExternalPath": "Absolute path",
|
||||
|
|
@ -220,8 +219,12 @@
|
|||
"panel.exports.exportSrt.desc": "Rebuilds the pseudonymized SRT file from the Markdown.",
|
||||
"panel.exports.exportCha": "Re-export as CHAT (.cha)",
|
||||
"panel.exports.exportCha.desc": "Rebuilds the pseudonymized CHAT file from the Markdown.",
|
||||
"panel.exports.exportHtml": "Re-export as HTML",
|
||||
"panel.exports.exportHtml.desc": "Rebuilds the pseudonymized noScribe HTML file from the Markdown and word timestamps (.words.json).",
|
||||
"notice.htmlExported": "HTML exported: {0}",
|
||||
"command.exportAsSrt": "Export as SRT",
|
||||
"command.exportAsCha": "Export as CHAT",
|
||||
"command.exportAsHtml": "Export as HTML",
|
||||
|
||||
"panel.ner.scanBtn": "Identify candidates",
|
||||
"panel.ner.hint": "Settings for the automatic named entity detection scanner.",
|
||||
|
|
|
|||
378
src/i18n/locales/es.json
Normal file
378
src/i18n/locales/es.json
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
{
|
||||
"corpus.modal.title": "Organizar corpus",
|
||||
"corpus.modal.hint": "Cada clase crea carpetas espejo para las transcripciones, las tablas de correspondencia y los exportes.",
|
||||
"corpus.modal.noClasses": "No hay clases definidas. Añada una para organizar sus documentos por carpeta.",
|
||||
"corpus.modal.addClass": "Añadir una clase",
|
||||
"corpus.modal.classNamePlaceholder": "Nombre de la clase (p. ej. Sesión_A, Informante_1…)",
|
||||
"corpus.modal.classNameEmpty": "El nombre de la clase no puede estar vacío.",
|
||||
"corpus.modal.classExists": "Ya existe una clase con este nombre.",
|
||||
"corpus.modal.deleteConfirm": "¿Eliminar la clase «{0}»? Las carpetas serán retiradas del vault (los archivos dentro irán a la papelera).",
|
||||
"corpus.modal.files": "{0} archivo(s)",
|
||||
"corpus.modal.close": "Cerrar",
|
||||
"corpus.select.title": "¿En qué clase añadir?",
|
||||
"corpus.select.hint": "Elija la clase (carpeta) para esta transcripción.",
|
||||
"corpus.select.none": "Sin clase (raíz)",
|
||||
"corpus.select.confirm": "Añadir",
|
||||
"quickModal.title": "Pseudonimizar",
|
||||
"quickModal.source": "Expresión seleccionada",
|
||||
"quickModal.sourceDesc": "Término a reemplazar — no modificable",
|
||||
"quickModal.replaceBy": "Reemplazar por",
|
||||
"quickModal.replacementPlaceholder": "Pseudónimo o categoría analítica",
|
||||
"quickModal.scope": "Ámbito del reemplazo",
|
||||
"quickModal.scopeFile": "Todas las ocurrencias en este archivo",
|
||||
"quickModal.scopeOccurrence": "Solo esta ocurrencia",
|
||||
"quickModal.submit": "Pseudonimizar",
|
||||
"notice.appliedOccurrence": "✓ «{0}» → «{1}» (esta ocurrencia)",
|
||||
"notice.appliedFile": "✓ «{0}» → «{1}» ({2} ocurrencia{3})",
|
||||
"onboarding.dict.importOk": "{0} diccionario importado",
|
||||
"onboarding.dict.importOkMany": "{0} diccionarios importados",
|
||||
"onboarding.dict.importErr": "{0} archivo rechazado",
|
||||
"onboarding.dict.importErrMany": "{0} archivos rechazados",
|
||||
"onboarding.dict.importError": "Error al importar {0} — verifique el formato .dict.json",
|
||||
"notice.noActiveFile": "No hay archivo activo.",
|
||||
"notice.formatUnsupported": "Formato no compatible: .{0}",
|
||||
"notice.noRules": "No hay reglas para este archivo.\nCree reglas con Ctrl+P → «Crear una regla».",
|
||||
"notice.noOccurrences": "No se encontraron ocurrencias para las reglas activas.",
|
||||
"notice.noNerEntities": "El NER no detectó ninguna entidad.",
|
||||
"notice.nerEntitiesFound": "✓ {0} {1} detectada(s) — resaltada(s) en azul.\nClic derecho para crear una regla.",
|
||||
"notice.nerEntitiesFound.entity": "entidad",
|
||||
"notice.nerEntitiesFound.entities": "entidades",
|
||||
"notice.nerNotEnabled": "La detección NER de transformers.js no está activada.\nActívela en Configuración → Pseudonymizer Tool.",
|
||||
"notice.noDictDetection": "No hay diccionario de detección cargado.\nInstale uno desde el panel Diccionarios.",
|
||||
"notice.noDictEntities": "No se encontraron entidades en los diccionarios de detección.",
|
||||
"notice.noDictReplacements": "No hay reemplazos disponibles para las entidades encontradas.",
|
||||
"notice.ruleDeleted": "✓ Regla eliminada — «{0}» restaurado en el archivo activo",
|
||||
"notice.ruleCreated": "✓ Regla creada: «{0}» → «{1}»",
|
||||
"notice.rulesCreated": "✓ {0} {1} creada(s)",
|
||||
"notice.rulesCreated.rule": "regla",
|
||||
"notice.rulesCreated.rules": "reglas",
|
||||
"notice.occurrencesPseudonymized": "✓ {0} {1} pseudonimizada(s) en «{2}»",
|
||||
"notice.occurrencesPseudonymized.occurrence": "ocurrencia",
|
||||
"notice.occurrencesPseudonymized.occurrences": "ocurrencias",
|
||||
"notice.converted": "✓ {0} → {1}",
|
||||
"notice.conversionError": "Error de conversión de {0}: {1}",
|
||||
"notice.fileExists": "El archivo ya existe en el vault: {0}",
|
||||
"notice.conversionSkipped": "⚠ {0}.md ya existe — conversión omitida para {1}",
|
||||
"notice.exportDone": "✓ {0} regla(s) aplicada(s)\n→ {1}",
|
||||
"notice.mappingExported": "✓ Tabla de correspondencia exportada → {0}",
|
||||
"notice.noMapping": "No se encontró tabla de correspondencia para {0}",
|
||||
"notice.invalidFormat": "Formato no válido: {0}",
|
||||
"notice.dictImported": "✓ {0} {1} importado(s)",
|
||||
"notice.dictImported.one": "diccionario",
|
||||
"notice.dictImported.many": "diccionarios",
|
||||
"notice.dictRejected": "{0} {1} rechazado(s)",
|
||||
"notice.dictRejected.one": "archivo",
|
||||
"notice.dictRejected.many": "archivos",
|
||||
"notice.nerModelLoaded": "✓ Modelo NER cargado",
|
||||
"notice.nerModelLoading": "Cargando modelo NER (primer uso — ~66 MB)…",
|
||||
"notice.ruleNotFound": "Regla no encontrada en las tablas de correspondencia.",
|
||||
"notice.noCheckedDicts": "Ningún diccionario marcado.",
|
||||
"notice.audioImported": "Audio importado: {0}",
|
||||
"occurrencesCtx.hint": "{0} ocurrencia{1} — seleccione las que desea reemplazar.",
|
||||
"occurrencesCtx.hint.s": "s",
|
||||
"occurrencesCtx.hint.empty": "",
|
||||
"occurrencesCtx.validateAll": "Validar todas",
|
||||
"occurrencesCtx.ignoreAll": "Ignorar todas",
|
||||
"occurrencesCtx.cancel": "Cancelar",
|
||||
"occurrencesCtx.confirm": "Confirmar selección",
|
||||
"occurrencesCtx.btn.validate": "Validar",
|
||||
"occurrencesCtx.btn.ignore": "Ignorar",
|
||||
"occurrencesCtx.btn.fp": "Falso positivo",
|
||||
"occurrencesCtx.status.ignored": "Conservado tal cual",
|
||||
"occurrencesCtx.status.fp": "Falso positivo — excluido",
|
||||
"notice.notNoScribeFormat": "Este archivo no es una transcripción de noScribe (se espera pseudobs-format: vtt o html).",
|
||||
"notice.wordsJsonMissing": "Archivo de marcas de tiempo no encontrado: {0}.words.json",
|
||||
"notice.vttMismatch": "Aviso: el número de cues del Markdown y del words.json difiere — el VTT exportado puede estar incompleto.",
|
||||
"notice.vttExported": "VTT exportado: {0}",
|
||||
|
||||
"command.organizeCorpus": "Organizar corpus",
|
||||
"command.addTranscription": "Añadir una transcripción",
|
||||
"command.pseudonymizeFile": "Pseudonimizar el archivo actual",
|
||||
"command.createRule": "Crear una regla de reemplazo",
|
||||
"command.scanFile": "Escanear el archivo actual",
|
||||
"command.scanNer": "Escanear el archivo con detección NER",
|
||||
"command.scanDictionaries": "Escanear el archivo con diccionarios",
|
||||
"command.pseudonymizeSelection": "Pseudonimizar la selección",
|
||||
"command.exportAsVtt": "Exportar como VTT",
|
||||
"command.exportAsSrt": "Exportar como SRT",
|
||||
"command.exportAsCha": "Exportar como CHAT",
|
||||
"command.exportAsHtml": "Exportar como HTML",
|
||||
"command.openPanel": "Pseudonimización: abrir panel",
|
||||
|
||||
"contextMenu.declareException": "Declarar «{0}» como excepción aquí",
|
||||
"contextMenu.cancelPseudonymization": "Cancelar pseudonimización de «{0}»",
|
||||
"contextMenu.editRule": "Editar regla para «{0}»",
|
||||
"contextMenu.pseudonymize": "Pseudonimizar «{0}»",
|
||||
"contextMenu.redact": "Redactar «{0}»",
|
||||
"contextMenu.coulmont": "Pseudonimizar con el prof. Baptiste Coulmont",
|
||||
"contextMenu.createRule": "Crear una regla de reemplazo…",
|
||||
"redaction.checkbox": "Redactar (🀫)",
|
||||
"redaction.checkboxDesc": "Reemplazar con símbolos 🀫 (uno por sílaba) — para contenido identificador no esencial",
|
||||
|
||||
"ruleModal.delete": "Eliminar regla",
|
||||
"ruleModal.scopeWarnTitle": "Ámbito longitudinal",
|
||||
"ruleModal.scopeOkTitle": "Ámbito de archivo",
|
||||
"ruleModal.scopeOk": "Buena práctica. Este pseudónimo solo se aplicará a esta transcripción — cada archivo permanece independiente.",
|
||||
"ruleModal.title": "Crear una regla de reemplazo",
|
||||
"ruleModal.source": "Fuente",
|
||||
"ruleModal.sourceDesc": "Término original a reemplazar",
|
||||
"ruleModal.replacement": "Reemplazo",
|
||||
"ruleModal.replacementDesc": "Pseudónimo o categoría analítica",
|
||||
"ruleModal.replacementDescClass": "El índice exacto se calculará al crearlo según las reglas existentes en el ámbito.",
|
||||
"ruleModal.category": "Categoría",
|
||||
"ruleModal.scope": "Ámbito",
|
||||
"ruleModal.scopeFile": "Solo este archivo",
|
||||
"ruleModal.scopeFolder": "Esta carpeta",
|
||||
"ruleModal.scopeVault": "Todo el vault",
|
||||
"ruleModal.priority": "Prioridad",
|
||||
"ruleModal.priorityDesc": "Entero libre, como un z-index CSS — por defecto 0, mayor = aplicado primero",
|
||||
"ruleModal.submit": "Crear regla",
|
||||
"ruleModal.errorMissing": "La fuente y el reemplazo son obligatorios.",
|
||||
"ruleModal.errorNoFile": "No hay archivo activo.",
|
||||
"ruleModal.coulomontLabel": "Sugerencias de nombres equivalentes — elija:",
|
||||
"ruleModal.dictLabel": "Diccionario: «{0}» → clase {1}",
|
||||
"ruleModal.dictUseClass": "Usar «{0}» (ámbito: {1})",
|
||||
|
||||
"category.first_name": "Nombre",
|
||||
"category.last_name": "Apellido",
|
||||
"category.full_name": "Nombre completo",
|
||||
"category.place": "Lugar",
|
||||
"category.institution": "Institución",
|
||||
"category.date": "Fecha",
|
||||
"category.age": "Edad",
|
||||
"category.profession": "Profesión",
|
||||
"category.custom": "Otro",
|
||||
|
||||
"scope.file": "Archivo",
|
||||
"scope.folder": "Carpeta",
|
||||
"scope.vault": "Vault",
|
||||
|
||||
"status.validated": "Activo",
|
||||
"status.ignored": "Ignorado",
|
||||
"status.partial": "Parcial",
|
||||
"status.suggested": "Sugerido",
|
||||
"status.conflict": "Conflicto",
|
||||
"status.disabled": "Inactivo",
|
||||
"status.needs_review": "Revisión",
|
||||
|
||||
"panel.tab.mappings": "Correspondencias",
|
||||
"panel.tab.dictionaries": "Diccionarios",
|
||||
"panel.tab.exports": "Exportes",
|
||||
"panel.tab.ner": "NER",
|
||||
"panel.tab.corpus": "Corpus",
|
||||
|
||||
"panel.mappings.filterActive": "Solo archivo activo",
|
||||
"panel.mappings.warnBroadName": "⚠ Ampliar el ámbito de un nombre o apellido más allá del archivo solo debe hacerse como elección analítica deliberada — por ejemplo, para seguir de forma coherente al mismo participante en un estudio longitudinal. Incluso cuando el mismo individuo aparece en varios archivos, la pseudonimización a nivel de archivo suele ser preferible: mantiene cada transcripción independiente y reduce el riesgo de identificación cruzada.",
|
||||
"panel.mappings.addRule": "Añadir una regla",
|
||||
"panel.mappings.scanFile": "Escanear archivo",
|
||||
"panel.mappings.scanning": "Escaneando…",
|
||||
"panel.mappings.noFile": "No hay archivo activo.",
|
||||
"panel.mappings.noRules": "No hay reglas para {0}.",
|
||||
"panel.mappings.noRulesHint": "No hay reglas activas para este archivo. Cree reglas desde el menú contextual o el comando «Crear una regla».",
|
||||
"panel.mappings.col.source": "Fuente",
|
||||
"panel.mappings.col.replacement": "Reemplazo",
|
||||
"panel.mappings.col.category": "Categoría",
|
||||
"panel.mappings.col.scope": "Ámbito",
|
||||
"panel.mappings.col.priority": "P.",
|
||||
"panel.mappings.col.status": "Estado",
|
||||
"panel.mappings.exceptions": "Excepciones",
|
||||
"panel.mappings.exceptions.hint": "Ocurrencias explícitamente ignoradas para esta regla.",
|
||||
"panel.mappings.exceptions.delete": "Eliminar esta excepción",
|
||||
|
||||
"panel.dict.noneInstalled": "No hay diccionarios instalados. Instale uno desde el asistente (Configuración → Reconfigurar) o importe un archivo local.",
|
||||
"panel.dict.checkbox": "Incluir en el escaneo agrupado",
|
||||
"panel.dict.scanAll": "Escanear diccionarios marcados",
|
||||
"panel.dict.importLocal": "Importar un archivo local (.dict.json)",
|
||||
"panel.dict.scanWith": "Escanear el archivo activo con «{0}»",
|
||||
"panel.dict.remove": "Eliminar este diccionario",
|
||||
|
||||
"panel.corpus.noFolder": "Carpeta de transcripciones no encontrada. Configúrela en los ajustes.",
|
||||
"panel.corpus.empty": "No se encontraron transcripciones en esta carpeta.",
|
||||
"panel.corpus.rules": "{0} regla(s)",
|
||||
"panel.corpus.hasPseudo": "Versión pseudonimizada creada",
|
||||
"panel.corpus.noPseudo": "Aún no pseudonimizado",
|
||||
"panel.corpus.hasFinal": "Exporte final {0} presente",
|
||||
"panel.corpus.noFinal": "Sin exporte final",
|
||||
"panel.corpus.addClass": "Nueva clase",
|
||||
"panel.corpus.noClass": "Sin clase",
|
||||
"panel.corpus.moveTo": "Mover a…",
|
||||
"panel.corpus.moveToRoot": "Sin clase (raíz)",
|
||||
"panel.corpus.deleteClass": "Eliminar clase",
|
||||
"panel.corpus.deleteClassConfirm": "¿Eliminar la clase «{0}»? Los archivos dentro se moverán a la raíz.",
|
||||
"panel.corpus.exportSettings": "Destino del exporte final",
|
||||
"panel.corpus.exportDest.vault": "En el vault",
|
||||
"panel.corpus.exportDest.external": "Carpeta externa",
|
||||
"panel.corpus.exportFolder": "Carpeta de exporte (vault)",
|
||||
"panel.corpus.exportExternalPath": "Ruta absoluta",
|
||||
"panel.corpus.exportExternalPathPlaceholder": "/Volumes/Archives/Exports",
|
||||
"panel.corpus.exportMirrorClasses": "Replicar estructura de clases",
|
||||
|
||||
"panel.filenameWarning.msg": "El nombre de archivo «{0}» contiene un término pseudonimizable.",
|
||||
"panel.filenameWarning.edit": "Renombrar…",
|
||||
"panel.filenameWarning.editTitle": "Introduzca un nombre manualmente",
|
||||
"panel.filenameWarning.editPrompt": "Nuevo nombre para «{0}»",
|
||||
"panel.filenameWarning.wandTitle": "Renombrar a «{0}»",
|
||||
|
||||
"panel.exports.pseudonymize": "Crear versión pseudonimizada",
|
||||
"panel.exports.exportMapping": "Exportar tabla de correspondencia",
|
||||
"panel.exports.exportVtt": "Reexportar como VTT",
|
||||
"panel.exports.exportVtt.desc": "Reconstruye un archivo WebVTT pseudonimizado desde el Markdown y las marcas de tiempo (.words.json).",
|
||||
"panel.exports.exportSrt": "Reexportar como SRT",
|
||||
"panel.exports.exportSrt.desc": "Reconstruye el archivo SRT pseudonimizado desde el Markdown.",
|
||||
"panel.exports.exportCha": "Reexportar como CHAT (.cha)",
|
||||
"panel.exports.exportCha.desc": "Reconstruye el archivo CHAT pseudonimizado desde el Markdown.",
|
||||
"panel.exports.exportHtml": "Reexportar como HTML",
|
||||
"panel.exports.exportHtml.desc": "Reconstruye el archivo HTML noScribe pseudonimizado desde el Markdown y las marcas de tiempo (.words.json).",
|
||||
"notice.htmlExported": "HTML exportado: {0}",
|
||||
|
||||
"panel.ner.scanBtn": "Identificar candidatos",
|
||||
"panel.ner.hint": "Ajustes del escáner de detección automática de entidades nombradas.",
|
||||
"panel.ner.threshold": "Umbral de confianza",
|
||||
"panel.ner.thresholdDesc": "Las entidades con una puntuación inferior a este valor se ignoran. Mayor = menos falsos positivos pero menos detecciones.",
|
||||
"panel.ner.functionWords": "Palabras funcionales excluidas",
|
||||
"panel.ner.functionWordsDesc": "Una palabra por línea. Estos tokens nunca se conservarán como entidades nombradas, aunque el modelo los detecte.",
|
||||
"panel.ner.save": "Guardar",
|
||||
"panel.ner.saved": "Guardado",
|
||||
"panel.ner.reset": "Restablecer valores predeterminados",
|
||||
|
||||
"dictScanModal.title": "Revisión del escaneo — diccionarios",
|
||||
"dictScanModal.summary": "{0} {1} detectada(s) · {2}",
|
||||
"dictScanModal.summary.entity": "entidad",
|
||||
"dictScanModal.summary.entities": "entidades",
|
||||
"dictScanModal.hint": "Desmarque los falsos positivos. El prefijo es editable; el índice se calcula automáticamente.",
|
||||
"dictScanModal.apply": "Crear {0} {1}",
|
||||
"dictScanModal.apply.rule": "regla",
|
||||
"dictScanModal.apply.rules": "reglas",
|
||||
"dictScanModal.noRules": "No hay reglas que aplicar",
|
||||
"dictScanModal.cancel": "Cancelar",
|
||||
|
||||
"mappingScanModal.title": "Revisión del escaneo — correspondencias",
|
||||
"mappingScanModal.summary": "{0} {1} con ocurrencias en «{2}»",
|
||||
"mappingScanModal.summary.rule": "regla activa",
|
||||
"mappingScanModal.summary.rules": "reglas activas",
|
||||
"mappingScanModal.hint": "Desmarque las reglas que no desea aplicar. Los reemplazos se escribirán en el archivo fuente.",
|
||||
"mappingScanModal.col.source": "",
|
||||
"mappingScanModal.col.replacement": "Reemplazo",
|
||||
"mappingScanModal.col.occurrences": "Ocur.",
|
||||
"mappingScanModal.apply": "Pseudonimizar ({0} {1}, {2} {3})",
|
||||
"mappingScanModal.apply.rule": "regla",
|
||||
"mappingScanModal.apply.rules": "reglas",
|
||||
"mappingScanModal.apply.occurrence": "ocurrencia",
|
||||
"mappingScanModal.apply.occurrences": "ocurrencias",
|
||||
"mappingScanModal.noRules": "No hay reglas que aplicar",
|
||||
"mappingScanModal.cancel": "Cancelar",
|
||||
"mappingScanModal.saveExceptions": "Guardar excepciones",
|
||||
"mappingScanModal.exceptionsSaved": "Excepciones guardadas en la tabla de correspondencia.",
|
||||
"mappingScanModal.countTooltip": "Ver y seleccionar candidatos",
|
||||
|
||||
"onboarding.step.storage": "Almacenamiento",
|
||||
"onboarding.step.language": "Idioma",
|
||||
"onboarding.step.welcome": "Bienvenida",
|
||||
"onboarding.step.ner": "Detección NER",
|
||||
"onboarding.step.dictionaries": "Diccionarios",
|
||||
"onboarding.step.summary": "Resumen",
|
||||
"onboarding.welcome.title": "Pseudonymizer Tool",
|
||||
"onboarding.welcome.desc": "Este plugin le ayuda a corregir y pseudonimizar transcripciones de entrevistas y corpus interaccionales (convenciones Jefferson, ICOR, formatos .srt, .cha).",
|
||||
"onboarding.welcome.steps": "El asistente le guiará en unos pocos pasos rápidos:",
|
||||
"onboarding.welcome.step1": "Elija el idioma de la interfaz.",
|
||||
"onboarding.welcome.step2": "Elija un motor de detección automática de entidades nombradas (opcional).",
|
||||
"onboarding.welcome.step3": "Instale diccionarios de candidatos de reemplazo si los necesita.",
|
||||
"onboarding.welcome.hint": "Puede reconfigurar estas opciones en cualquier momento desde los ajustes del plugin.",
|
||||
"onboarding.storage.title": "Almacenamiento",
|
||||
"onboarding.storage.vaultPerCorpus": "Un vault por corpus",
|
||||
"onboarding.storage.vaultPerCorpusDesc": "Recomendamos usar un vault de Obsidian dedicado por corpus. Esto garantiza que los archivos de correspondencia, los diccionarios y los exportes permanezcan junto a las transcripciones, facilitando compartir o archivar un corpus.",
|
||||
"onboarding.storage.transcriptionsFolder": "Transcripciones",
|
||||
"onboarding.storage.mappingFolder": "Tablas de correspondencia",
|
||||
"onboarding.storage.dictionariesFolder": "Diccionarios",
|
||||
"onboarding.storage.exportsFolder": "Exportes",
|
||||
"onboarding.storage.hint": "Estas rutas son relativas a la raíz del vault. Puede modificarlas más tarde en Configuración.",
|
||||
"onboarding.language.title": "Idioma de la interfaz",
|
||||
"onboarding.language.hint": "El idioma de la interfaz no afecta al análisis de las transcripciones. Las convenciones (Jefferson, ICOR) son independientes del idioma.",
|
||||
"onboarding.ner.desc": "Un modelo NER (reconocimiento de entidades nombradas) puede detectar automáticamente nombres propios, apellidos, lugares e instituciones en las transcripciones — sin una lista exhaustiva, ejecutándose localmente en Obsidian.",
|
||||
"onboarding.ner.tfjsTitle": "transformers.js — modelo ONNX integrado",
|
||||
"onboarding.ner.tfjsBadge": "Python no requerido",
|
||||
"onboarding.ner.tfjsDesc": "Modelo NER multilingüe ejecutado localmente en el vault. Los archivos WASM (~19 MB) se descargan una vez. El modelo (~66 MB) se descarga en el primer escaneo.",
|
||||
"onboarding.ner.wasmReady": "Archivos WASM ya instalados",
|
||||
"onboarding.ner.installBtn": "Instalar y usar transformers.js",
|
||||
"onboarding.ner.selectedBtn": "Seleccionado",
|
||||
"onboarding.ner.noneActive": "Desactivado (solo reglas manuales)",
|
||||
"onboarding.ner.noneSkip": "Omitir — trabajaré manualmente",
|
||||
|
||||
"onboarding.dict.title": "Diccionarios de candidatos de reemplazo",
|
||||
"onboarding.dict.desc": "Los diccionarios proporcionan candidatos de reemplazo (ciudades, nombres…) y alimentan la detección automática. Están alojados en un repositorio dedicado y se descargan al vault — ningún dato de transcripción sale de Obsidian.",
|
||||
"onboarding.dict.catalogueLoading": "Cargando catálogo…",
|
||||
"onboarding.dict.catalogueError": "No se puede acceder al catálogo en línea. Compruebe su conexión o importe un archivo local a continuación.",
|
||||
"onboarding.dict.recommended": "Recomendado",
|
||||
"onboarding.dict.col.dict": "Diccionario",
|
||||
"onboarding.dict.col.lang": "Idioma",
|
||||
"onboarding.dict.col.roles": "Roles",
|
||||
"onboarding.dict.col.size": "Tamaño",
|
||||
"onboarding.dict.role.detection": "detección",
|
||||
"onboarding.dict.role.replacement": "reemplazo",
|
||||
"onboarding.dict.role.classes": "clases",
|
||||
"onboarding.dict.install": "Instalar",
|
||||
"onboarding.dict.reinstall": "Reinstalar",
|
||||
"onboarding.dict.installed": "Instalado",
|
||||
"onboarding.dict.manualHint": "¿Ya tiene un archivo .dict.json? Impórtelo manualmente:",
|
||||
"onboarding.dict.importBtn": "Importar un archivo local (.dict.json)",
|
||||
"onboarding.dict.listTitle": "Diccionarios instalados en este vault ({0}):",
|
||||
"onboarding.dict.remove": "Eliminar este diccionario",
|
||||
|
||||
"onboarding.summary.ner.none": "Desactivado — solo reglas manuales",
|
||||
"onboarding.summary.ner.tfjs": "transformers.js — modelo ONNX (descarga en el primer escaneo)",
|
||||
|
||||
"onboarding.nav.cancel": "Cancelar",
|
||||
"onboarding.nav.back": "Atrás",
|
||||
"onboarding.nav.skip": "Omitir este paso",
|
||||
"onboarding.nav.next": "Siguiente",
|
||||
"onboarding.nav.start": "Comenzar",
|
||||
"onboarding.nav.finish": "Empezar a trabajar",
|
||||
"onboarding.summary.title": "Configuración completa",
|
||||
"onboarding.summary.intro": "Esto es lo que se ha configurado:",
|
||||
"onboarding.summary.ner": "Detección NER",
|
||||
"onboarding.summary.dicts": "Diccionarios",
|
||||
"onboarding.summary.dicts.count": "{0} archivo(s) en el vault",
|
||||
"onboarding.summary.hint": "Estos ajustes pueden cambiarse en cualquier momento desde la configuración del plugin: Reconfigurar.",
|
||||
|
||||
"settings.heading.general": "General",
|
||||
"settings.heading.textDetection": "Detección de texto",
|
||||
"settings.heading.pseudonymization": "Pseudonimización",
|
||||
"settings.heading.ner": "Detección automática (NER)",
|
||||
"settings.heading.storage": "Almacenamiento",
|
||||
"settings.heading.security": "Seguridad",
|
||||
"settings.language": "Idioma de la interfaz",
|
||||
"settings.languageDesc": "No afecta al análisis de transcripciones. Las convenciones (Jefferson, ICOR) son independientes del idioma.",
|
||||
"settings.vaultPerCorpus": "Un vault por corpus",
|
||||
"settings.vaultPerCorpusDesc": "Recomendamos usar un vault de Obsidian dedicado por corpus. Esto garantiza que los archivos de correspondencia, los diccionarios y los exportes permanezcan junto a las transcripciones.",
|
||||
"settings.transcriptionsFolder": "Transcripciones importadas",
|
||||
"settings.transcriptionsFolderDesc": "Carpeta de destino para las transcripciones importadas.",
|
||||
"settings.mappingFolder": "Tablas de correspondencia",
|
||||
"settings.mappingFolderDesc": "Ruta relativa en el vault.",
|
||||
"settings.dictionariesFolder": "Diccionarios",
|
||||
"settings.dictionariesFolderDesc": "Ruta relativa en el vault.",
|
||||
"settings.exportsFolder": "Exportes",
|
||||
"settings.accentSensitive": "Sensible a los acentos",
|
||||
"settings.preserveCase": "Preservar mayúsculas",
|
||||
"settings.preserveCaseDesc": "Adaptar las mayúsculas del reemplazo para que coincidan con la fuente (p. ej. JUAN → PEDRO, juan → pedro, Juan → Pedro).",
|
||||
"settings.preserveAnalyticNotation": "Preservar notación analítica",
|
||||
"settings.preserveAnalyticNotationDesc": "No reemplazar nunca los símbolos de convención analítica de tipo Jefferson o ICOR.",
|
||||
"settings.useMarkerInExport": "Añadir marcador alrededor de los pseudónimos en el exporte",
|
||||
"settings.useMarkerInExportDesc": "Permite identificar visualmente los términos pseudonimizados en el archivo exportado.",
|
||||
"settings.markerOpen": "Marcador de apertura",
|
||||
"settings.markerOpenDesc": "Ejemplo: {{ ⟦ [ «",
|
||||
"settings.markerClose": "Marcador de cierre",
|
||||
"settings.markerCloseDesc": "Ejemplo: }} ⟧ ] »",
|
||||
"settings.nerBackend": "Motor de detección",
|
||||
"settings.nerBackendDesc": "Backend usado para la detección automática de entidades nombradas identificadoras.",
|
||||
"settings.nerBackend.none": "Desactivado — solo reglas manuales",
|
||||
"settings.nerBackend.tfjs": "transformers.js (modelo ONNX integrado)",
|
||||
"settings.reconfigure": "Asistente de configuración",
|
||||
"settings.reconfigureDesc2": "Relanzar el asistente para reconfigurar la detección NER y los diccionarios.",
|
||||
"settings.reconfigureBtn2": "Reconfigurar…",
|
||||
"settings.warnIfSyncedFolder": "Avisar si la carpeta está sincronizada",
|
||||
"settings.warnIfSyncedFolderDesc": "Alertar si las tablas de correspondencia están en una carpeta Git, iCloud o Synology Drive.",
|
||||
"settings.caseSensitive": "Sensible a mayúsculas",
|
||||
"settings.caseSensitiveDesc": "Si está activado, «Juan» y «juan» se tratan como distintos.",
|
||||
"settings.wholeWordOnly": "Solo palabras completas",
|
||||
"settings.wholeWordOnlyDesc": "Si está activado, solo se reemplazan las ocurrencias de palabras completas."
|
||||
}
|
||||
|
|
@ -201,7 +201,6 @@
|
|||
"panel.corpus.deleteClassConfirm": "Supprimer la classe « {0} » ? Les fichiers à l'intérieur seront déplacés à la racine.",
|
||||
"panel.corpus.exportSettings": "Destination des exports finaux",
|
||||
"panel.corpus.exportDest.vault": "Dans le vault",
|
||||
"panel.corpus.exportDest.nextToSource": "À côté du fichier source",
|
||||
"panel.corpus.exportDest.external": "Dossier externe",
|
||||
"panel.corpus.exportFolder": "Dossier d'export (vault)",
|
||||
"panel.corpus.exportExternalPath": "Chemin absolu",
|
||||
|
|
@ -220,8 +219,12 @@
|
|||
"panel.exports.exportSrt.desc": "Reconstruit le fichier SRT pseudonymisé à partir du Markdown.",
|
||||
"panel.exports.exportCha": "Re-exporter en CHAT (.cha)",
|
||||
"panel.exports.exportCha.desc": "Reconstruit le fichier CHAT pseudonymisé à partir du Markdown.",
|
||||
"panel.exports.exportHtml": "Re-exporter en HTML",
|
||||
"panel.exports.exportHtml.desc": "Reconstruit le fichier HTML noScribe pseudonymisé à partir du Markdown et des timestamps (.words.json).",
|
||||
"notice.htmlExported": "HTML exporté : {0}",
|
||||
"command.exportAsSrt": "Exporter en SRT",
|
||||
"command.exportAsCha": "Exporter en CHAT",
|
||||
"command.exportAsHtml": "Exporter en HTML",
|
||||
|
||||
"panel.ner.scanBtn": "Identifier des candidats",
|
||||
"panel.ner.hint": "Paramètres du scanner de détection automatique des entités nommées.",
|
||||
|
|
|
|||
378
src/i18n/locales/ru.json
Normal file
378
src/i18n/locales/ru.json
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
{
|
||||
"corpus.modal.title": "Организовать корпус",
|
||||
"corpus.modal.hint": "Каждый класс создаёт зеркальные папки для транскрипций, таблиц соответствия и экспортов.",
|
||||
"corpus.modal.noClasses": "Классы не определены. Добавьте один, чтобы организовать документы по папкам.",
|
||||
"corpus.modal.addClass": "Добавить класс",
|
||||
"corpus.modal.classNamePlaceholder": "Название класса (например, Сессия_А, Информант_1…)",
|
||||
"corpus.modal.classNameEmpty": "Название класса не может быть пустым.",
|
||||
"corpus.modal.classExists": "Класс с таким названием уже существует.",
|
||||
"corpus.modal.deleteConfirm": "Удалить класс «{0}»? Папки будут удалены из хранилища (файлы внутри будут перемещены в корзину).",
|
||||
"corpus.modal.files": "{0} файл(ов)",
|
||||
"corpus.modal.close": "Закрыть",
|
||||
"corpus.select.title": "В какой класс добавить?",
|
||||
"corpus.select.hint": "Выберите класс (папку) для этой транскрипции.",
|
||||
"corpus.select.none": "Без класса (корень)",
|
||||
"corpus.select.confirm": "Добавить",
|
||||
"quickModal.title": "Псевдонимизировать",
|
||||
"quickModal.source": "Выбранное выражение",
|
||||
"quickModal.sourceDesc": "Заменяемый термин — нельзя изменить",
|
||||
"quickModal.replaceBy": "Заменить на",
|
||||
"quickModal.replacementPlaceholder": "Псевдоним или аналитическая категория",
|
||||
"quickModal.scope": "Область замены",
|
||||
"quickModal.scopeFile": "Все вхождения в файле",
|
||||
"quickModal.scopeOccurrence": "Только это вхождение",
|
||||
"quickModal.submit": "Псевдонимизировать",
|
||||
"notice.appliedOccurrence": "✓ «{0}» → «{1}» (это вхождение)",
|
||||
"notice.appliedFile": "✓ «{0}» → «{1}» ({2} вхождений)",
|
||||
"onboarding.dict.importOk": "{0} словарь импортирован",
|
||||
"onboarding.dict.importOkMany": "{0} словарей импортировано",
|
||||
"onboarding.dict.importErr": "{0} файл отклонён",
|
||||
"onboarding.dict.importErrMany": "{0} файлов отклонено",
|
||||
"onboarding.dict.importError": "Ошибка импорта {0} — проверьте формат .dict.json",
|
||||
"notice.noActiveFile": "Нет активного файла.",
|
||||
"notice.formatUnsupported": "Неподдерживаемый формат: .{0}",
|
||||
"notice.noRules": "Нет правил для этого файла.\nСоздайте правила через Ctrl+P → «Создать правило».",
|
||||
"notice.noOccurrences": "Вхождений для активных правил не найдено.",
|
||||
"notice.noNerEntities": "NER не обнаружил сущностей.",
|
||||
"notice.nerEntitiesFound": "✓ Обнаружено {0} {1} — выделено синим.\nПравой кнопкой мыши — создать правило.",
|
||||
"notice.nerEntitiesFound.entity": "сущность",
|
||||
"notice.nerEntitiesFound.entities": "сущностей",
|
||||
"notice.nerNotEnabled": "Детектирование через transformers.js не включено.\nВключите в Настройках → Pseudonymizer Tool.",
|
||||
"notice.noDictDetection": "Словарь обнаружения не загружен.\nУстановите словарь в панели «Словари».",
|
||||
"notice.noDictEntities": "Сущностей в словарях обнаружения не найдено.",
|
||||
"notice.noDictReplacements": "Нет доступных замен для найденных сущностей.",
|
||||
"notice.ruleDeleted": "✓ Правило удалено — «{0}» восстановлено в активном файле",
|
||||
"notice.ruleCreated": "✓ Правило создано: «{0}» → «{1}»",
|
||||
"notice.rulesCreated": "✓ {0} {1} создано",
|
||||
"notice.rulesCreated.rule": "правило",
|
||||
"notice.rulesCreated.rules": "правил",
|
||||
"notice.occurrencesPseudonymized": "✓ {0} {1} псевдонимизировано в «{2}»",
|
||||
"notice.occurrencesPseudonymized.occurrence": "вхождение",
|
||||
"notice.occurrencesPseudonymized.occurrences": "вхождений",
|
||||
"notice.converted": "✓ {0} → {1}",
|
||||
"notice.conversionError": "Ошибка конвертации {0}: {1}",
|
||||
"notice.fileExists": "Файл уже существует в хранилище: {0}",
|
||||
"notice.conversionSkipped": "⚠ {0}.md уже существует — конвертация пропущена для {1}",
|
||||
"notice.exportDone": "✓ {0} правил(о) применено\n→ {1}",
|
||||
"notice.mappingExported": "✓ Таблица соответствия экспортирована → {0}",
|
||||
"notice.noMapping": "Таблица соответствия для {0} не найдена",
|
||||
"notice.invalidFormat": "Неверный формат: {0}",
|
||||
"notice.dictImported": "✓ {0} {1} импортировано",
|
||||
"notice.dictImported.one": "словарь",
|
||||
"notice.dictImported.many": "словарей",
|
||||
"notice.dictRejected": "{0} {1} отклонено",
|
||||
"notice.dictRejected.one": "файл",
|
||||
"notice.dictRejected.many": "файлов",
|
||||
"notice.nerModelLoaded": "✓ Модель NER загружена",
|
||||
"notice.nerModelLoading": "Загрузка модели NER (первый запуск — ~66 МБ)…",
|
||||
"notice.ruleNotFound": "Правило не найдено в таблицах соответствия.",
|
||||
"notice.noCheckedDicts": "Ни один словарь не выбран.",
|
||||
"notice.audioImported": "Аудио импортировано: {0}",
|
||||
"occurrencesCtx.hint": "{0} вхождени{1} — выберите нужные для замены.",
|
||||
"occurrencesCtx.hint.s": "й",
|
||||
"occurrencesCtx.hint.empty": "е",
|
||||
"occurrencesCtx.validateAll": "Выбрать все",
|
||||
"occurrencesCtx.ignoreAll": "Пропустить все",
|
||||
"occurrencesCtx.cancel": "Отмена",
|
||||
"occurrencesCtx.confirm": "Подтвердить выбор",
|
||||
"occurrencesCtx.btn.validate": "Выбрать",
|
||||
"occurrencesCtx.btn.ignore": "Пропустить",
|
||||
"occurrencesCtx.btn.fp": "Ложное срабатывание",
|
||||
"occurrencesCtx.status.ignored": "Оставлено как есть",
|
||||
"occurrencesCtx.status.fp": "Ложное срабатывание — исключено",
|
||||
"notice.notNoScribeFormat": "Этот файл не является транскрипцией noScribe (ожидается pseudobs-format: vtt или html).",
|
||||
"notice.wordsJsonMissing": "Файл временных меток не найден: {0}.words.json",
|
||||
"notice.vttMismatch": "Предупреждение: количество реплик в Markdown и words.json различается — экспортированный VTT может быть неполным.",
|
||||
"notice.vttExported": "VTT экспортирован: {0}",
|
||||
|
||||
"command.organizeCorpus": "Организовать корпус",
|
||||
"command.addTranscription": "Добавить транскрипцию",
|
||||
"command.pseudonymizeFile": "Псевдонимизировать текущий файл",
|
||||
"command.createRule": "Создать правило замены",
|
||||
"command.scanFile": "Сканировать текущий файл",
|
||||
"command.scanNer": "Сканировать файл с NER-детектированием",
|
||||
"command.scanDictionaries": "Сканировать файл по словарям",
|
||||
"command.pseudonymizeSelection": "Псевдонимизировать выделение",
|
||||
"command.exportAsVtt": "Экспортировать как VTT",
|
||||
"command.exportAsSrt": "Экспортировать как SRT",
|
||||
"command.exportAsCha": "Экспортировать как CHAT",
|
||||
"command.exportAsHtml": "Экспортировать как HTML",
|
||||
"command.openPanel": "Псевдонимизация: открыть панель",
|
||||
|
||||
"contextMenu.declareException": "Объявить «{0}» исключением здесь",
|
||||
"contextMenu.cancelPseudonymization": "Отменить псевдонимизацию «{0}»",
|
||||
"contextMenu.editRule": "Изменить правило для «{0}»",
|
||||
"contextMenu.pseudonymize": "Псевдонимизировать «{0}»",
|
||||
"contextMenu.redact": "Скрыть «{0}»",
|
||||
"contextMenu.coulmont": "Псевдонимизировать с проф. Батистом Кульмоном",
|
||||
"contextMenu.createRule": "Создать правило замены…",
|
||||
"redaction.checkbox": "Скрыть (🀫)",
|
||||
"redaction.checkboxDesc": "Заменить символами 🀫 (по одному на слог) — для несущественного идентифицирующего содержания",
|
||||
|
||||
"ruleModal.delete": "Удалить правило",
|
||||
"ruleModal.scopeWarnTitle": "Лонгитюдная область",
|
||||
"ruleModal.scopeOkTitle": "Область файла",
|
||||
"ruleModal.scopeOk": "Хорошая практика. Этот псевдоним будет применяться только к этой транскрипции — каждый файл остаётся независимым.",
|
||||
"ruleModal.title": "Создать правило замены",
|
||||
"ruleModal.source": "Источник",
|
||||
"ruleModal.sourceDesc": "Исходный термин для замены",
|
||||
"ruleModal.replacement": "Замена",
|
||||
"ruleModal.replacementDesc": "Псевдоним или аналитическая категория",
|
||||
"ruleModal.replacementDescClass": "Точный индекс будет рассчитан при создании на основе существующих правил в области.",
|
||||
"ruleModal.category": "Категория",
|
||||
"ruleModal.scope": "Область",
|
||||
"ruleModal.scopeFile": "Только этот файл",
|
||||
"ruleModal.scopeFolder": "Эта папка",
|
||||
"ruleModal.scopeVault": "Всё хранилище",
|
||||
"ruleModal.priority": "Приоритет",
|
||||
"ruleModal.priorityDesc": "Целое число, как CSS z-index — по умолчанию 0, чем выше — тем раньше применяется",
|
||||
"ruleModal.submit": "Создать правило",
|
||||
"ruleModal.errorMissing": "Источник и замена обязательны.",
|
||||
"ruleModal.errorNoFile": "Нет активного файла.",
|
||||
"ruleModal.coulomontLabel": "Предложения эквивалентных имён — выберите:",
|
||||
"ruleModal.dictLabel": "Словарь: «{0}» → класс {1}",
|
||||
"ruleModal.dictUseClass": "Использовать «{0}» (область: {1})",
|
||||
|
||||
"category.first_name": "Имя",
|
||||
"category.last_name": "Фамилия",
|
||||
"category.full_name": "Полное имя",
|
||||
"category.place": "Место",
|
||||
"category.institution": "Учреждение",
|
||||
"category.date": "Дата",
|
||||
"category.age": "Возраст",
|
||||
"category.profession": "Профессия",
|
||||
"category.custom": "Другое",
|
||||
|
||||
"scope.file": "Файл",
|
||||
"scope.folder": "Папка",
|
||||
"scope.vault": "Хранилище",
|
||||
|
||||
"status.validated": "Активно",
|
||||
"status.ignored": "Игнорируется",
|
||||
"status.partial": "Частично",
|
||||
"status.suggested": "Предложено",
|
||||
"status.conflict": "Конфликт",
|
||||
"status.disabled": "Неактивно",
|
||||
"status.needs_review": "На проверке",
|
||||
|
||||
"panel.tab.mappings": "Соответствия",
|
||||
"panel.tab.dictionaries": "Словари",
|
||||
"panel.tab.exports": "Экспорты",
|
||||
"panel.tab.ner": "NER",
|
||||
"panel.tab.corpus": "Корпус",
|
||||
|
||||
"panel.mappings.filterActive": "Только активный файл",
|
||||
"panel.mappings.warnBroadName": "⚠ Распространение имени или фамилии за пределы файла следует делать только как обдуманный аналитический выбор — например, для последовательного отслеживания одного участника в лонгитюдном исследовании. Даже если один и тот же человек фигурирует в нескольких файлах, псевдонимизация на уровне файла предпочтительнее: она сохраняет независимость каждой транскрипции и снижает риск идентификации через перекрёстный анализ.",
|
||||
"panel.mappings.addRule": "Добавить правило",
|
||||
"panel.mappings.scanFile": "Сканировать файл",
|
||||
"panel.mappings.scanning": "Сканирование…",
|
||||
"panel.mappings.noFile": "Нет активного файла.",
|
||||
"panel.mappings.noRules": "Нет правил для {0}.",
|
||||
"panel.mappings.noRulesHint": "Нет активных правил для этого файла. Создайте правила через контекстное меню или команду «Создать правило».",
|
||||
"panel.mappings.col.source": "Источник",
|
||||
"panel.mappings.col.replacement": "Замена",
|
||||
"panel.mappings.col.category": "Категория",
|
||||
"panel.mappings.col.scope": "Область",
|
||||
"panel.mappings.col.priority": "П.",
|
||||
"panel.mappings.col.status": "Статус",
|
||||
"panel.mappings.exceptions": "Исключения",
|
||||
"panel.mappings.exceptions.hint": "Вхождения, явно проигнорированные для этого правила.",
|
||||
"panel.mappings.exceptions.delete": "Удалить это исключение",
|
||||
|
||||
"panel.dict.noneInstalled": "Словари не установлены. Установите через мастер (Настройки → Перенастроить) или импортируйте локальный файл.",
|
||||
"panel.dict.checkbox": "Включить в групповое сканирование",
|
||||
"panel.dict.scanAll": "Сканировать отмеченные словари",
|
||||
"panel.dict.importLocal": "Импортировать локальный файл (.dict.json)",
|
||||
"panel.dict.scanWith": "Сканировать активный файл по «{0}»",
|
||||
"panel.dict.remove": "Удалить этот словарь",
|
||||
|
||||
"panel.corpus.noFolder": "Папка транскрипций не найдена. Настройте её в параметрах.",
|
||||
"panel.corpus.empty": "В этой папке транскрипций не найдено.",
|
||||
"panel.corpus.rules": "{0} правил(о)",
|
||||
"panel.corpus.hasPseudo": "Псевдонимизированная версия создана",
|
||||
"panel.corpus.noPseudo": "Ещё не псевдонимизировано",
|
||||
"panel.corpus.hasFinal": "Финальный экспорт {0} присутствует",
|
||||
"panel.corpus.noFinal": "Нет финального экспорта",
|
||||
"panel.corpus.addClass": "Новый класс",
|
||||
"panel.corpus.noClass": "Без класса",
|
||||
"panel.corpus.moveTo": "Переместить в…",
|
||||
"panel.corpus.moveToRoot": "Без класса (корень)",
|
||||
"panel.corpus.deleteClass": "Удалить класс",
|
||||
"panel.corpus.deleteClassConfirm": "Удалить класс «{0}»? Файлы внутри будут перемещены в корень.",
|
||||
"panel.corpus.exportSettings": "Назначение финального экспорта",
|
||||
"panel.corpus.exportDest.vault": "В хранилище",
|
||||
"panel.corpus.exportDest.external": "Внешняя папка",
|
||||
"panel.corpus.exportFolder": "Папка экспорта (хранилище)",
|
||||
"panel.corpus.exportExternalPath": "Абсолютный путь",
|
||||
"panel.corpus.exportExternalPathPlaceholder": "/Volumes/Archives/Exports",
|
||||
"panel.corpus.exportMirrorClasses": "Зеркалировать структуру классов",
|
||||
|
||||
"panel.filenameWarning.msg": "Имя файла «{0}» содержит псевдонимизируемый термин.",
|
||||
"panel.filenameWarning.edit": "Переименовать…",
|
||||
"panel.filenameWarning.editTitle": "Введите имя вручную",
|
||||
"panel.filenameWarning.editPrompt": "Новое имя для «{0}»",
|
||||
"panel.filenameWarning.wandTitle": "Переименовать в «{0}»",
|
||||
|
||||
"panel.exports.pseudonymize": "Создать псевдонимизированную версию",
|
||||
"panel.exports.exportMapping": "Экспортировать таблицу соответствия",
|
||||
"panel.exports.exportVtt": "Повторно экспортировать как VTT",
|
||||
"panel.exports.exportVtt.desc": "Восстанавливает псевдонимизированный WebVTT из Markdown и временных меток (.words.json).",
|
||||
"panel.exports.exportSrt": "Повторно экспортировать как SRT",
|
||||
"panel.exports.exportSrt.desc": "Восстанавливает псевдонимизированный SRT из Markdown.",
|
||||
"panel.exports.exportCha": "Повторно экспортировать как CHAT (.cha)",
|
||||
"panel.exports.exportCha.desc": "Восстанавливает псевдонимизированный CHAT из Markdown.",
|
||||
"panel.exports.exportHtml": "Повторно экспортировать как HTML",
|
||||
"panel.exports.exportHtml.desc": "Восстанавливает псевдонимизированный HTML noScribe из Markdown и временных меток (.words.json).",
|
||||
"notice.htmlExported": "HTML экспортирован: {0}",
|
||||
|
||||
"panel.ner.scanBtn": "Определить кандидатов",
|
||||
"panel.ner.hint": "Настройки автоматического сканера распознавания именованных сущностей.",
|
||||
"panel.ner.threshold": "Порог уверенности",
|
||||
"panel.ner.thresholdDesc": "Сущности с оценкой ниже этого значения игнорируются. Выше = меньше ложных срабатываний, но меньше обнаружений.",
|
||||
"panel.ner.functionWords": "Исключённые служебные слова",
|
||||
"panel.ner.functionWordsDesc": "По одному слову на строку. Эти токены никогда не будут сохранены как именованные сущности, даже если обнаружены моделью.",
|
||||
"panel.ner.save": "Сохранить",
|
||||
"panel.ner.saved": "Сохранено",
|
||||
"panel.ner.reset": "Сбросить до значений по умолчанию",
|
||||
|
||||
"dictScanModal.title": "Проверка сканирования по словарю",
|
||||
"dictScanModal.summary": "{0} {1} обнаружено · {2}",
|
||||
"dictScanModal.summary.entity": "сущность",
|
||||
"dictScanModal.summary.entities": "сущностей",
|
||||
"dictScanModal.hint": "Снимите галочки с ложных срабатываний. Префикс редактируется; индекс рассчитывается автоматически.",
|
||||
"dictScanModal.apply": "Создать {0} {1}",
|
||||
"dictScanModal.apply.rule": "правило",
|
||||
"dictScanModal.apply.rules": "правил",
|
||||
"dictScanModal.noRules": "Нет правил для применения",
|
||||
"dictScanModal.cancel": "Отмена",
|
||||
|
||||
"mappingScanModal.title": "Проверка сканирования соответствий",
|
||||
"mappingScanModal.summary": "{0} {1} с вхождениями в «{2}»",
|
||||
"mappingScanModal.summary.rule": "активное правило",
|
||||
"mappingScanModal.summary.rules": "активных правил",
|
||||
"mappingScanModal.hint": "Снимите галочки с правил, которые не нужно применять. Замены будут записаны в исходный файл.",
|
||||
"mappingScanModal.col.source": "",
|
||||
"mappingScanModal.col.replacement": "Замена",
|
||||
"mappingScanModal.col.occurrences": "Вхожд.",
|
||||
"mappingScanModal.apply": "Псевдонимизировать ({0} {1}, {2} {3})",
|
||||
"mappingScanModal.apply.rule": "правило",
|
||||
"mappingScanModal.apply.rules": "правил",
|
||||
"mappingScanModal.apply.occurrence": "вхождение",
|
||||
"mappingScanModal.apply.occurrences": "вхождений",
|
||||
"mappingScanModal.noRules": "Нет правил для применения",
|
||||
"mappingScanModal.cancel": "Отмена",
|
||||
"mappingScanModal.saveExceptions": "Сохранить исключения",
|
||||
"mappingScanModal.exceptionsSaved": "Исключения сохранены в таблицу соответствия.",
|
||||
"mappingScanModal.countTooltip": "Просмотр и выбор кандидатов",
|
||||
|
||||
"onboarding.step.storage": "Хранилище",
|
||||
"onboarding.step.language": "Язык",
|
||||
"onboarding.step.welcome": "Добро пожаловать",
|
||||
"onboarding.step.ner": "Детектирование NER",
|
||||
"onboarding.step.dictionaries": "Словари",
|
||||
"onboarding.step.summary": "Итоги",
|
||||
"onboarding.welcome.title": "Pseudonymizer Tool",
|
||||
"onboarding.welcome.desc": "Этот плагин помогает корректировать и псевдонимизировать транскрипции интервью и интеракциональных корпусов (конвенции Jefferson, ICOR, форматы .srt, .cha).",
|
||||
"onboarding.welcome.steps": "Мастер проведёт вас через несколько быстрых шагов:",
|
||||
"onboarding.welcome.step1": "Выберите язык интерфейса.",
|
||||
"onboarding.welcome.step2": "Выберите движок автоматического распознавания именованных сущностей (необязательно).",
|
||||
"onboarding.welcome.step3": "Установите словари кандидатов для замены, если необходимо.",
|
||||
"onboarding.welcome.hint": "Эти параметры можно изменить в любое время в настройках плагина.",
|
||||
"onboarding.storage.title": "Хранилище",
|
||||
"onboarding.storage.vaultPerCorpus": "Одно хранилище на корпус",
|
||||
"onboarding.storage.vaultPerCorpusDesc": "Рекомендуем использовать отдельное хранилище Obsidian для каждого корпуса. Это позволяет хранить таблицы соответствия, словари и экспорты вместе с транскрипциями, упрощая обмен и архивирование корпуса.",
|
||||
"onboarding.storage.transcriptionsFolder": "Папка транскрипций",
|
||||
"onboarding.storage.mappingFolder": "Таблицы соответствия",
|
||||
"onboarding.storage.dictionariesFolder": "Словари",
|
||||
"onboarding.storage.exportsFolder": "Экспорты",
|
||||
"onboarding.storage.hint": "Эти пути указаны относительно корня хранилища. Их можно изменить позже в Настройках.",
|
||||
"onboarding.language.title": "Язык интерфейса",
|
||||
"onboarding.language.hint": "Язык интерфейса не влияет на анализ транскрипций. Конвенции (Jefferson, ICOR) не зависят от языка.",
|
||||
"onboarding.ner.desc": "Модель NER (распознавание именованных сущностей) может автоматически обнаруживать имена, фамилии, места и учреждения в транскрипциях — без исчерпывающего списка, локально в Obsidian.",
|
||||
"onboarding.ner.tfjsTitle": "transformers.js — встроенная модель ONNX",
|
||||
"onboarding.ner.tfjsBadge": "Python не требуется",
|
||||
"onboarding.ner.tfjsDesc": "Многоязычная модель NER, работающая локально в хранилище. WASM-файлы (~19 МБ) загружаются один раз. Модель (~66 МБ) загружается при первом сканировании.",
|
||||
"onboarding.ner.wasmReady": "WASM-файлы уже установлены",
|
||||
"onboarding.ner.installBtn": "Установить и использовать transformers.js",
|
||||
"onboarding.ner.selectedBtn": "Выбрано",
|
||||
"onboarding.ner.noneActive": "Отключено (только ручные правила)",
|
||||
"onboarding.ner.noneSkip": "Пропустить — буду работать вручную",
|
||||
|
||||
"onboarding.dict.title": "Словари кандидатов для замены",
|
||||
"onboarding.dict.desc": "Словари предоставляют кандидатов для замены (города, имена…) и поддерживают автоматическое обнаружение. Они размещены в специальном репозитории и загружаются в хранилище — данные транскрипций не покидают Obsidian.",
|
||||
"onboarding.dict.catalogueLoading": "Загрузка каталога…",
|
||||
"onboarding.dict.catalogueError": "Не удалось подключиться к онлайн-каталогу. Проверьте соединение или импортируйте локальный файл ниже.",
|
||||
"onboarding.dict.recommended": "Рекомендуется",
|
||||
"onboarding.dict.col.dict": "Словарь",
|
||||
"onboarding.dict.col.lang": "Язык",
|
||||
"onboarding.dict.col.roles": "Роли",
|
||||
"onboarding.dict.col.size": "Размер",
|
||||
"onboarding.dict.role.detection": "обнаружение",
|
||||
"onboarding.dict.role.replacement": "замена",
|
||||
"onboarding.dict.role.classes": "классы",
|
||||
"onboarding.dict.install": "Установить",
|
||||
"onboarding.dict.reinstall": "Переустановить",
|
||||
"onboarding.dict.installed": "Установлено",
|
||||
"onboarding.dict.manualHint": "Уже есть файл .dict.json? Импортируйте вручную:",
|
||||
"onboarding.dict.importBtn": "Импортировать локальный файл (.dict.json)",
|
||||
"onboarding.dict.listTitle": "Словари, установленные в этом хранилище ({0}):",
|
||||
"onboarding.dict.remove": "Удалить этот словарь",
|
||||
|
||||
"onboarding.summary.ner.none": "Отключено — только ручные правила",
|
||||
"onboarding.summary.ner.tfjs": "transformers.js — модель ONNX (загрузка при первом сканировании)",
|
||||
|
||||
"onboarding.nav.cancel": "Отмена",
|
||||
"onboarding.nav.back": "Назад",
|
||||
"onboarding.nav.skip": "Пропустить этот шаг",
|
||||
"onboarding.nav.next": "Далее",
|
||||
"onboarding.nav.start": "Начать",
|
||||
"onboarding.nav.finish": "Приступить к работе",
|
||||
"onboarding.summary.title": "Настройка завершена",
|
||||
"onboarding.summary.intro": "Вот что было настроено:",
|
||||
"onboarding.summary.ner": "Детектирование NER",
|
||||
"onboarding.summary.dicts": "Словари",
|
||||
"onboarding.summary.dicts.count": "{0} файл(ов) в хранилище",
|
||||
"onboarding.summary.hint": "Эти настройки можно изменить в любое время в параметрах плагина: Перенастроить.",
|
||||
|
||||
"settings.heading.general": "Общие",
|
||||
"settings.heading.textDetection": "Обнаружение текста",
|
||||
"settings.heading.pseudonymization": "Псевдонимизация",
|
||||
"settings.heading.ner": "Автоматическое обнаружение (NER)",
|
||||
"settings.heading.storage": "Хранилище",
|
||||
"settings.heading.security": "Безопасность",
|
||||
"settings.language": "Язык интерфейса",
|
||||
"settings.languageDesc": "Не влияет на анализ транскрипций. Конвенции (Jefferson, ICOR) не зависят от языка.",
|
||||
"settings.vaultPerCorpus": "Одно хранилище на корпус",
|
||||
"settings.vaultPerCorpusDesc": "Рекомендуем использовать отдельное хранилище Obsidian для каждого корпуса. Это гарантирует, что таблицы соответствия, словари и экспорты хранятся вместе с транскрипциями.",
|
||||
"settings.transcriptionsFolder": "Импортированные транскрипции",
|
||||
"settings.transcriptionsFolderDesc": "Папка назначения для импортированных транскрипций.",
|
||||
"settings.mappingFolder": "Таблицы соответствия",
|
||||
"settings.mappingFolderDesc": "Относительный путь в хранилище.",
|
||||
"settings.dictionariesFolder": "Словари",
|
||||
"settings.dictionariesFolderDesc": "Относительный путь в хранилище.",
|
||||
"settings.exportsFolder": "Экспорты",
|
||||
"settings.accentSensitive": "Учитывать акценты",
|
||||
"settings.preserveCase": "Сохранять регистр",
|
||||
"settings.preserveCaseDesc": "Адаптировать регистр замены под исходный (например, ИВАН → ПЁТР, иван → пётр, Иван → Пётр).",
|
||||
"settings.preserveAnalyticNotation": "Сохранять аналитическую нотацию",
|
||||
"settings.preserveAnalyticNotationDesc": "Никогда не заменять символы аналитических конвенций Jefferson или ICOR.",
|
||||
"settings.useMarkerInExport": "Добавлять маркер вокруг псевдонимов в экспорте",
|
||||
"settings.useMarkerInExportDesc": "Позволяет визуально идентифицировать псевдонимизированные термины в экспортированном файле.",
|
||||
"settings.markerOpen": "Открывающий маркер",
|
||||
"settings.markerOpenDesc": "Пример: {{ ⟦ [ «",
|
||||
"settings.markerClose": "Закрывающий маркер",
|
||||
"settings.markerCloseDesc": "Пример: }} ⟧ ] »",
|
||||
"settings.nerBackend": "Движок обнаружения",
|
||||
"settings.nerBackendDesc": "Бэкенд для автоматического обнаружения идентифицирующих именованных сущностей.",
|
||||
"settings.nerBackend.none": "Отключено — только ручные правила",
|
||||
"settings.nerBackend.tfjs": "transformers.js (встроенная модель ONNX)",
|
||||
"settings.reconfigure": "Мастер настройки",
|
||||
"settings.reconfigureDesc2": "Перезапустить мастер для перенастройки NER-детектирования и словарей.",
|
||||
"settings.reconfigureBtn2": "Перенастроить…",
|
||||
"settings.warnIfSyncedFolder": "Предупреждать о синхронизированной папке",
|
||||
"settings.warnIfSyncedFolderDesc": "Предупреждать, если таблицы соответствия находятся в папке Git, iCloud или Synology Drive.",
|
||||
"settings.caseSensitive": "Учитывать регистр",
|
||||
"settings.caseSensitiveDesc": "Если включено, «Иван» и «иван» считаются разными.",
|
||||
"settings.wholeWordOnly": "Только целые слова",
|
||||
"settings.wholeWordOnlyDesc": "Если включено, заменяются только вхождения полных слов."
|
||||
}
|
||||
117
src/main.ts
117
src/main.ts
|
|
@ -22,7 +22,7 @@ import { ChatParser } from './parsers/ChatParser';
|
|||
import { VttParser } from './parsers/VttParser';
|
||||
import { NoScribeHtmlParser } from './parsers/NoScribeHtmlParser';
|
||||
import { NoScribeVttParser } from './parsers/NoScribeVttParser';
|
||||
import { srtToMarkdown, chatToMarkdown, vttToMarkdown, noScribeHtmlToMarkdown, extractWordData, markdownToVtt, markdownToSrt, markdownToCha, type VttCueData } from './parsers/TranscriptConverter';
|
||||
import { srtToMarkdown, chatToMarkdown, vttToMarkdown, noScribeHtmlToMarkdown, extractWordData, markdownToVtt, markdownToSrt, markdownToCha, markdownToNoScribeHtml, type VttCueData } from './parsers/TranscriptConverter';
|
||||
import { MappingStore } from './mappings/MappingStore';
|
||||
import { ScopeResolver } from './mappings/ScopeResolver';
|
||||
import { PseudonymizationEngine } from './pseudonymizer/PseudonymizationEngine';
|
||||
|
|
@ -183,6 +183,12 @@ export default class PseudObsPlugin extends Plugin {
|
|||
callback: () => void this.exportCurrentFileAsFormat('cha'),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'export-as-html',
|
||||
name: t('command.exportAsHtml'),
|
||||
callback: () => void this.exportCurrentFileAsHtml(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'pseudonymize-selection',
|
||||
name: t('command.pseudonymizeSelection'),
|
||||
|
|
@ -389,6 +395,26 @@ export default class PseudObsPlugin extends Plugin {
|
|||
* Calcule le chemin de l'export final selon les paramètres de destination.
|
||||
* Retourne { vaultPath } si dans le vault, { externalPath } si hors vault.
|
||||
*/
|
||||
/** Retourne le sous-dossier de classe (vide si pas de mirroring ou pas de classe). */
|
||||
private getClassSub(file: TFile): string {
|
||||
const s = this.settings;
|
||||
if (!s.exportMirrorClasses) return '';
|
||||
const transcRoot = s.transcriptionsFolder;
|
||||
const fileFolder = file.parent?.path ?? '';
|
||||
const rawBase = file.basename.replace(/\.pseudonymized$/, '');
|
||||
if (fileFolder.startsWith(transcRoot)) {
|
||||
return fileFolder.slice(transcRoot.length).replace(/^\//, '');
|
||||
}
|
||||
const mappingFile = this.findInMappings(`${rawBase}.mapping.json`);
|
||||
if (mappingFile) {
|
||||
const mFolder = mappingFile.parent?.path ?? '';
|
||||
if (mFolder.startsWith(s.mappingFolder)) {
|
||||
return mFolder.slice(s.mappingFolder.length).replace(/^\//, '');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
resolveExportPath(
|
||||
file: TFile,
|
||||
ext: string,
|
||||
|
|
@ -396,41 +422,13 @@ export default class PseudObsPlugin extends Plugin {
|
|||
const s = this.settings;
|
||||
const rawBase = file.basename.replace(/\.pseudonymized$/, '');
|
||||
const outputName = `${rawBase}.pseudonymized.${ext}`;
|
||||
const fileFolder = file.parent?.path ?? '';
|
||||
|
||||
// Sous-dossier de classe pour le mirroring.
|
||||
// Si le fichier est dans transcriptionsFolder, on le lit directement.
|
||||
// Sinon (fichier pseudonymisé dans exports/), on remonte via l'emplacement
|
||||
// du .mapping.json qui reflète fidèlement la structure de classes.
|
||||
let classSub = '';
|
||||
if (s.exportMirrorClasses) {
|
||||
const transcRoot = s.transcriptionsFolder;
|
||||
const fileFolder = file.parent?.path ?? '';
|
||||
if (fileFolder.startsWith(transcRoot)) {
|
||||
classSub = fileFolder.slice(transcRoot.length).replace(/^\//, '');
|
||||
} else {
|
||||
// Retrouver la classe depuis l'emplacement du mapping
|
||||
const mappingFile = this.findInMappings(`${rawBase}.mapping.json`);
|
||||
if (mappingFile) {
|
||||
const mFolder = mappingFile.parent?.path ?? '';
|
||||
const mRoot = s.mappingFolder;
|
||||
if (mFolder.startsWith(mRoot)) {
|
||||
classSub = mFolder.slice(mRoot.length).replace(/^\//, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const classSub = this.getClassSub(file);
|
||||
const join = (base: string) =>
|
||||
classSub ? `${base}/${classSub}/${outputName}` : `${base}/${outputName}`;
|
||||
|
||||
if (s.exportDestinationType === 'next-to-source') {
|
||||
return { vaultPath: `${fileFolder}/${outputName}`, externalPath: null };
|
||||
}
|
||||
if (s.exportDestinationType === 'external' && s.exportExternalPath) {
|
||||
return { vaultPath: null, externalPath: join(s.exportExternalPath) };
|
||||
}
|
||||
// 'vault' (défaut)
|
||||
return { vaultPath: join(s.exportFinalFolder || s.exportsFolder), externalPath: null };
|
||||
}
|
||||
|
||||
|
|
@ -879,6 +877,12 @@ export default class PseudObsPlugin extends Plugin {
|
|||
// --- Conversion automatique ---
|
||||
|
||||
private async autoConvert(file: TFile): Promise<void> {
|
||||
// Ne pas convertir les fichiers produits par le plugin lui-même
|
||||
const s = this.settings;
|
||||
const fileFolder = file.parent?.path ?? '';
|
||||
const outputFolders = [s.exportsFolder, s.mappingFolder, s.exportFinalFolder].filter(Boolean);
|
||||
if (outputFolders.some((f) => fileFolder === f || fileFolder.startsWith(f + '/'))) return;
|
||||
|
||||
try {
|
||||
const raw = await this.app.vault.read(file);
|
||||
const ext = file.extension.toLowerCase();
|
||||
|
|
@ -1141,8 +1145,12 @@ export default class PseudObsPlugin extends Plugin {
|
|||
pseudonymized = engine.pseudonymize(content, rules, marker);
|
||||
}
|
||||
|
||||
await this.ensureFolder(this.settings.exportsFolder);
|
||||
const outputPath = `${this.settings.exportsFolder}/${file.basename}.pseudonymized.${ext}`;
|
||||
const classSub = this.getClassSub(file);
|
||||
const exportBase = classSub
|
||||
? `${this.settings.exportsFolder}/${classSub}`
|
||||
: this.settings.exportsFolder;
|
||||
await this.ensureFolder(exportBase);
|
||||
const outputPath = `${exportBase}/${file.basename}.pseudonymized.${ext}`;
|
||||
const existing = this.app.vault.getAbstractFileByPath(outputPath);
|
||||
if (existing instanceof TFile) {
|
||||
await this.app.vault.modify(existing, pseudonymized);
|
||||
|
|
@ -1345,6 +1353,45 @@ export default class PseudObsPlugin extends Plugin {
|
|||
await this.writeExport(file, 'vtt', vtt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-exporte le Markdown noScribe actif au format HTML noScribe pseudonymisé.
|
||||
* Lit le .words.json correspondant pour les timestamps précis.
|
||||
*/
|
||||
async exportCurrentFileAsHtml(): Promise<void> {
|
||||
const file = this.getActiveOrLastFile();
|
||||
if (!file || file.extension !== 'md') {
|
||||
new Notice(t('notice.noActiveFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
const formatMatch = /^pseudobs-format:\s*(\w+)/m.exec(content);
|
||||
if (formatMatch?.[1] !== 'html') {
|
||||
new Notice(t('notice.notNoScribeFormat'));
|
||||
return;
|
||||
}
|
||||
|
||||
const rawBasename = file.basename.replace(/\.pseudonymized$/, '');
|
||||
const wordsJson = await this.findWordsJson(rawBasename);
|
||||
if (!wordsJson) {
|
||||
new Notice(t('notice.wordsJsonMissing', rawBasename));
|
||||
return;
|
||||
}
|
||||
|
||||
const audioMatch = /^pseudobs-audio:\s*"([^"]+)"/m.exec(content);
|
||||
const audioSource = audioMatch?.[1];
|
||||
|
||||
const wordData = JSON.parse(wordsJson) as VttCueData[];
|
||||
const { html, mismatch } = markdownToNoScribeHtml(content, wordData, audioSource);
|
||||
|
||||
if (mismatch) {
|
||||
new Notice(t('notice.vttMismatch'));
|
||||
}
|
||||
|
||||
await this.writeExport(file, 'html', html, 'notice.htmlExported');
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-exporte le Markdown pseudonymisé courant au format d'origine (srt ou cha).
|
||||
*/
|
||||
|
|
@ -1374,7 +1421,7 @@ export default class PseudObsPlugin extends Plugin {
|
|||
* Écrit le contenu d'un export final selon les paramètres de destination.
|
||||
* Gère vault, next-to-source et external.
|
||||
*/
|
||||
private async writeExport(file: TFile, ext: string, content: string): Promise<void> {
|
||||
private async writeExport(file: TFile, ext: string, content: string, noticeKey = 'notice.vttExported'): Promise<void> {
|
||||
const dest = this.resolveExportPath(file, ext);
|
||||
|
||||
if (dest.externalPath) {
|
||||
|
|
@ -1386,7 +1433,7 @@ export default class PseudObsPlugin extends Plugin {
|
|||
const nodePath = require('path') as typeof import('path');
|
||||
await nodeFs.promises.mkdir(nodePath.dirname(dest.externalPath), { recursive: true });
|
||||
await nodeFs.promises.writeFile(dest.externalPath, content, 'utf-8');
|
||||
new Notice(t('notice.vttExported', dest.externalPath));
|
||||
new Notice(t(noticeKey, dest.externalPath));
|
||||
} catch (e) {
|
||||
new Notice(`Export error: ${(e as Error).message}`);
|
||||
}
|
||||
|
|
@ -1404,7 +1451,7 @@ export default class PseudObsPlugin extends Plugin {
|
|||
} else {
|
||||
await this.app.vault.create(outputPath, content);
|
||||
}
|
||||
new Notice(t('notice.vttExported', outputPath));
|
||||
new Notice(t(noticeKey, outputPath));
|
||||
}
|
||||
|
||||
/** Cherche <basename>.words.json dans le dossier mappings et ses sous-dossiers. */
|
||||
|
|
|
|||
|
|
@ -286,6 +286,134 @@ export function noScribeHtmlToMarkdown(doc: VttDocument, sourceName: string, aud
|
|||
return vttDocToMarkdown(doc, sourceName, 'html', audioFilename);
|
||||
}
|
||||
|
||||
// ---- Re-export HTML noScribe ------------------------------------------------
|
||||
|
||||
/** Convertit HH:MM:SS.mmm en millisecondes. */
|
||||
function tsToMs(ts: string): number {
|
||||
const [h, m, s] = ts.split(':');
|
||||
return Math.round((parseInt(h, 10) * 3600 + parseInt(m, 10) * 60 + parseFloat(s)) * 1000);
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/** Conserve uniquement les caractères word ASCII pour les noms d'ancre ts_. */
|
||||
function sanitizeSpeakerId(speaker: string): string {
|
||||
return speaker.replace(/\W/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruit un fichier HTML noScribe pseudonymisé depuis :
|
||||
* - `mdContent` : le Markdown noScribe (pseudobs-format: html)
|
||||
* - `wordData` : le contenu de <basename>.words.json (timestamps précis)
|
||||
* - `audioSource` : chemin vers le fichier audio (optionnel, pour la meta tag)
|
||||
*
|
||||
* Chaque réplique devient un <p> avec deux ancres ts_ : le timestamp d'affichage
|
||||
* et le texte. Les timestamps word-level ne sont pas reconstitués (le texte
|
||||
* pseudonymisé ne s'aligne plus mot à mot sur l'original).
|
||||
*/
|
||||
export function markdownToNoScribeHtml(
|
||||
mdContent: string,
|
||||
wordData: VttCueData[],
|
||||
audioSource?: string,
|
||||
): { html: string; mismatch: boolean } {
|
||||
const bodyMatch = /^---\n[\s\S]*?\n---\n+([\s\S]*)$/.exec(mdContent);
|
||||
const body = bodyMatch ? bodyMatch[1] : mdContent;
|
||||
|
||||
const cueLines = body.split('\n').filter((l) => MD_CUE_RE.test(l.trim()));
|
||||
const mismatch = cueLines.length !== wordData.length;
|
||||
const count = Math.min(cueLines.length, wordData.length);
|
||||
|
||||
const P_STYLE = ' margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;';
|
||||
const paragraphs: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const m = MD_CUE_RE.exec(cueLines[i].trim())!;
|
||||
const speaker = m[1]?.trim() || wordData[i].speaker || '';
|
||||
const text = m[2]?.trim() ?? '';
|
||||
if (!text) continue;
|
||||
|
||||
const cueData = wordData[i];
|
||||
const cueStartMs = tsToMs(cueData.startTime);
|
||||
const cueEndMs = tsToMs(cueData.endTime);
|
||||
const speakerId = speaker ? sanitizeSpeakerId(speaker) : '';
|
||||
const cueAnchor = `ts_${cueStartMs}_${cueEndMs}_${speakerId}`;
|
||||
const displayTs = `[${cueData.startTime.slice(0, 8)}]`;
|
||||
|
||||
// Ancres word-level : mapper les tokens pseudonymisés sur les timestamps d'origine
|
||||
// par index proportionnel (floor). Un pseudonyme hérite du timestamp du nom original.
|
||||
const origWords = cueData.words.filter(w => w.time !== '');
|
||||
const pseudoTokens = text.split(/\s+/).filter(Boolean);
|
||||
|
||||
let contentHtml: string;
|
||||
|
||||
if (origWords.length === 0 || pseudoTokens.length === 0) {
|
||||
// Pas de timestamps word-level : ancre unique pour toute la réplique
|
||||
const prefix = speaker ? `${escapeHtml(speaker)} : ` : '';
|
||||
contentHtml = `<a name="${cueAnchor}">${prefix}${escapeHtml(text)}</a>`;
|
||||
} else {
|
||||
const wordStartMs = origWords.map(w => tsToMs(w.time));
|
||||
const wordEndMs = origWords.map((_, j) =>
|
||||
j + 1 < origWords.length ? wordStartMs[j + 1] : cueEndMs
|
||||
);
|
||||
|
||||
// Mapping floor : token pseudo j → orig word floor(j * M / N)
|
||||
const mapToOrig = (j: number): number =>
|
||||
Math.min(Math.floor(j * origWords.length / pseudoTokens.length), origWords.length - 1);
|
||||
|
||||
// Grouper les tokens consécutifs qui partagent le même index orig
|
||||
interface Group { origIdx: number; tokens: string[] }
|
||||
const groups: Group[] = [];
|
||||
for (let j = 0; j < pseudoTokens.length; j++) {
|
||||
const origIdx = mapToOrig(j);
|
||||
if (groups.length === 0 || groups[groups.length - 1].origIdx !== origIdx) {
|
||||
groups.push({ origIdx, tokens: [] });
|
||||
}
|
||||
groups[groups.length - 1].tokens.push(pseudoTokens[j]);
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
for (let g = 0; g < groups.length; g++) {
|
||||
const { origIdx, tokens } = groups[g];
|
||||
const anchorName = `ts_${wordStartMs[origIdx]}_${wordEndMs[origIdx]}_${speakerId}`;
|
||||
let groupText = tokens.join(' ');
|
||||
// Espace séparateur entre ancres (sauf la dernière)
|
||||
if (g < groups.length - 1) groupText += ' ';
|
||||
// Préfixe locuteur sur le premier groupe
|
||||
const content = (g === 0 && speaker)
|
||||
? `${escapeHtml(speaker)} : ${escapeHtml(groupText)}`
|
||||
: escapeHtml(groupText);
|
||||
parts.push(`<a name="${anchorName}">${content}</a>`);
|
||||
}
|
||||
contentHtml = parts.join('');
|
||||
}
|
||||
|
||||
paragraphs.push(
|
||||
`<p style="${P_STYLE}">` +
|
||||
`<a name="${cueAnchor}" href="${cueAnchor}"><span style=" color:#78909c;">${displayTs}</span></a>` +
|
||||
contentHtml +
|
||||
`</p>`
|
||||
);
|
||||
}
|
||||
|
||||
const audioMeta = audioSource
|
||||
? `<meta name="audio_source" content="${escapeHtml(audioSource)}" />\n`
|
||||
: '';
|
||||
|
||||
const html = [
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">',
|
||||
`<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" />\n${audioMeta}<style type="text/css">`,
|
||||
'p, li { white-space: pre-wrap; }',
|
||||
`</style></head><body style=" font-family:'Arial'; font-size:12pt; font-weight:400; font-style:normal;">`,
|
||||
...paragraphs,
|
||||
'</body></html>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
return { html, mismatch };
|
||||
}
|
||||
|
||||
function lineGroup(line: ChatLine): 'structural' | 'turn' | null {
|
||||
if (line.type === 'meta' || line.type === 'dependent') return 'structural';
|
||||
if (line.type === 'turn' || line.type === 'continuation') return 'turn';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { OnboardingModal } from './ui/OnboardingModal';
|
|||
import { t, setLocale, AVAILABLE_LANGUAGES } from './i18n';
|
||||
|
||||
export type NerBackend = 'none' | 'spacy' | 'transformers-js';
|
||||
export type ExportDestinationType = 'vault' | 'next-to-source' | 'external';
|
||||
export type ExportDestinationType = 'vault' | 'external';
|
||||
|
||||
export interface PseudObsSettings {
|
||||
transcriptionsFolder: string;
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ export function getCorpusClasses(app: App, transcriptionsFolder: string): string
|
|||
*/
|
||||
export class CorpusModal extends Modal {
|
||||
private plugin: PseudObsPlugin;
|
||||
private onCloseCb?: () => void;
|
||||
|
||||
constructor(app: App, plugin: PseudObsPlugin) {
|
||||
constructor(app: App, plugin: PseudObsPlugin, onCloseCb?: () => void) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.onCloseCb = onCloseCb;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
|
|
@ -145,6 +147,7 @@ export class CorpusModal extends Modal {
|
|||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
this.onCloseCb?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { t } from '../i18n';
|
|||
import type { DictionaryFile } from '../types';
|
||||
import type PseudObsPlugin from '../main';
|
||||
import { scanOccurrences } from '../scanner/OccurrenceScanner';
|
||||
import { getCorpusClasses } from './CorpusModal';
|
||||
import { getCorpusClasses, CorpusModal } from './CorpusModal';
|
||||
import { FolderSuggest } from './FolderSuggest';
|
||||
import { EditRuleModal } from './EditRuleModal';
|
||||
import { RuleModal } from './RuleModal';
|
||||
|
|
@ -211,9 +211,8 @@ export class PseudonymizationView extends ItemView {
|
|||
// Sélecteur de type
|
||||
const typeRow = exportSection.createDiv('pseudobs-corpus-export-type-row');
|
||||
for (const [val, labelKey] of [
|
||||
['vault', 'panel.corpus.exportDest.vault'],
|
||||
['next-to-source', 'panel.corpus.exportDest.nextToSource'],
|
||||
['external', 'panel.corpus.exportDest.external'],
|
||||
['vault', 'panel.corpus.exportDest.vault'],
|
||||
['external', 'panel.corpus.exportDest.external'],
|
||||
] as [string, string][]) {
|
||||
const lbl = typeRow.createEl('label', { cls: 'pseudobs-corpus-export-type-label' });
|
||||
const radio = lbl.createEl('input');
|
||||
|
|
@ -270,12 +269,8 @@ export class PseudonymizationView extends ItemView {
|
|||
text: `+ ${t('panel.corpus.addClass')}`,
|
||||
cls: 'pseudobs-corpus-add-class-btn',
|
||||
});
|
||||
addClassBtn.addEventListener('click', async () => {
|
||||
const name = this.promptText(t('corpus.modal.classNamePlaceholder'));
|
||||
if (!name) return;
|
||||
await this.plugin.ensureFolder(`${transcRoot}/${name}`);
|
||||
await this.plugin.ensureFolder(`${s.mappingFolder}/${name}`);
|
||||
void this.renderTab('corpus');
|
||||
addClassBtn.addEventListener('click', () => {
|
||||
new CorpusModal(this.app, this.plugin, () => void this.renderTab('corpus')).open();
|
||||
});
|
||||
|
||||
// ---- Liste des fichiers par classe ----------------------------
|
||||
|
|
@ -723,6 +718,16 @@ export class PseudonymizationView extends ItemView {
|
|||
void this.plugin.exportCurrentFileAsVtt();
|
||||
})
|
||||
);
|
||||
if (format === 'html') {
|
||||
new Setting(el)
|
||||
.setName(t('panel.exports.exportHtml'))
|
||||
.setDesc(t('panel.exports.exportHtml.desc'))
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText(t('command.exportAsHtml')).onClick(() => {
|
||||
void this.plugin.exportCurrentFileAsHtml();
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (format === 'srt') {
|
||||
new Setting(el)
|
||||
.setName(t('panel.exports.exportSrt'))
|
||||
|
|
|
|||
Loading…
Reference in a new issue