mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-29 11:44:21 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into feature/task_list
This commit is contained in:
		
						commit
						0e5b8af3a4
					
				
							
								
								
									
										831
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										831
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @ -49,7 +49,7 @@ | ||||
|     "build:webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts", | ||||
|     "build:prepare-dist": "npm run build:webpack && rimraf ./dist && tsc && tsx ./bin/copy-dist.ts", | ||||
| 
 | ||||
|     "test": "cross-env TRILIUM_DATA_DIR=./integration-tests/db vitest", | ||||
|     "test": "cross-env TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest", | ||||
|     "test:coverage": "cross-env TRILIUM_DATA_DIR=./integration-tests/db vitest --coverage", | ||||
|     "test:playwright": "playwright test", | ||||
| 
 | ||||
| @ -167,14 +167,14 @@ | ||||
|     "yauzl": "3.2.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@electron-forge/cli": "7.6.1", | ||||
|     "@electron-forge/maker-deb": "7.6.1", | ||||
|     "@electron-forge/maker-dmg": "7.6.1", | ||||
|     "@electron-forge/maker-flatpak": "7.6.1", | ||||
|     "@electron-forge/maker-rpm": "7.6.1", | ||||
|     "@electron-forge/maker-squirrel": "7.6.1", | ||||
|     "@electron-forge/maker-zip": "7.6.1", | ||||
|     "@electron-forge/plugin-auto-unpack-natives": "7.6.1", | ||||
|     "@electron-forge/cli": "7.7.0", | ||||
|     "@electron-forge/maker-deb": "7.7.0", | ||||
|     "@electron-forge/maker-dmg": "7.7.0", | ||||
|     "@electron-forge/maker-flatpak": "7.7.0", | ||||
|     "@electron-forge/maker-rpm": "7.7.0", | ||||
|     "@electron-forge/maker-squirrel": "7.7.0", | ||||
|     "@electron-forge/maker-zip": "7.7.0", | ||||
|     "@electron-forge/plugin-auto-unpack-natives": "7.7.0", | ||||
|     "@electron/rebuild": "3.7.1", | ||||
|     "@playwright/test": "1.50.1", | ||||
|     "@types/archiver": "6.0.3", | ||||
| @ -212,7 +212,7 @@ | ||||
|     "@types/ws": "8.5.14", | ||||
|     "@types/xml2js": "0.4.14", | ||||
|     "@types/yargs": "17.0.33", | ||||
|     "@vitest/coverage-v8": "3.0.5", | ||||
|     "@vitest/coverage-v8": "3.0.6", | ||||
|     "cross-env": "7.0.3", | ||||
|     "electron": "34.2.0", | ||||
|     "esm": "3.2.25", | ||||
| @ -224,10 +224,10 @@ | ||||
|     "rimraf": "6.0.1", | ||||
|     "swagger-jsdoc": "6.2.8", | ||||
|     "tslib": "2.8.1", | ||||
|     "tsx": "4.19.2", | ||||
|     "tsx": "4.19.3", | ||||
|     "typedoc": "0.27.7", | ||||
|     "typescript": "5.7.3", | ||||
|     "vitest": "3.0.5", | ||||
|     "vitest": "3.0.6", | ||||
|     "webpack": "5.98.0", | ||||
|     "webpack-cli": "6.0.1", | ||||
|     "webpack-dev-middleware": "7.4.2" | ||||
|  | ||||
| @ -222,7 +222,7 @@ export default class GeoMapTypeWidget extends TypeWidget { | ||||
| 
 | ||||
|         const [ lat, lng ] = latLng.split(",", 2).map((el) => parseFloat(el)); | ||||
|         const L = this.L; | ||||
|         const icon = this.#buildIcon(note.getIcon(), note.title); | ||||
|         const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title); | ||||
| 
 | ||||
|         const marker = L.marker(L.latLng(lat, lng), { | ||||
|             icon, | ||||
| @ -257,12 +257,12 @@ export default class GeoMapTypeWidget extends TypeWidget { | ||||
|         this.currentMarkerData[note.noteId] = marker; | ||||
|     } | ||||
| 
 | ||||
|     #buildIcon(bxIconClass: string, title: string) { | ||||
|     #buildIcon(bxIconClass: string, colorClass: string, title: string) { | ||||
|         return this.L.divIcon({ | ||||
|             html: `\ | ||||
|                 <img class="icon" src="${asset_path}/node_modules/leaflet/dist/images/marker-icon.png" /> | ||||
|                 <img class="icon-shadow" src="${asset_path}/node_modules/leaflet/dist/images/marker-shadow.png" /> | ||||
|                 <span class="bx ${bxIconClass}"></span> | ||||
|                 <span class="bx ${bxIconClass} ${colorClass}"></span> | ||||
|                 <span class="title-label">${title}</span>`,
 | ||||
|             iconSize: [ 25, 41 ], | ||||
|             iconAnchor: [ 12, 41 ] | ||||
| @ -361,7 +361,7 @@ export default class GeoMapTypeWidget extends TypeWidget { | ||||
|         // If any of note has its location attribute changed.
 | ||||
|         // TODO: Should probably filter by parent here as well.
 | ||||
|         const attributeRows = loadResults.getAttributeRows(); | ||||
|         if (attributeRows.find((at) => at.name === LOCATION_ATTRIBUTE)) { | ||||
|         if (attributeRows.find((at) => [ LOCATION_ATTRIBUTE, "color" ].includes(at.name ?? ""))) { | ||||
|             this.#reloadMarkers(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -2,6 +2,7 @@ import OptionsWidget from "./options_widget.js"; | ||||
| import toastService from "../../../services/toast.js"; | ||||
| import { t } from "../../../services/i18n.js"; | ||||
| import type { OptionDefinitions, OptionMap } from "../../../../../services/options_interface.js"; | ||||
| import optionsService from "../../../services/options.js"; | ||||
| 
 | ||||
| type TimeSelectorConstructor = { | ||||
|     widgetId: string; | ||||
| @ -9,6 +10,7 @@ type TimeSelectorConstructor = { | ||||
|     optionValueId: keyof OptionDefinitions; | ||||
|     optionTimeScaleId: keyof OptionDefinitions; | ||||
|     includedTimeScales?: Set<TimeSelectorScale>; | ||||
|     minimumSeconds?: number; | ||||
| }; | ||||
| 
 | ||||
| type TimeSelectorScale = "seconds" | "minutes" | "hours" | "days"; | ||||
| @ -43,6 +45,7 @@ export default class TimeSelector extends OptionsWidget { | ||||
|     private optionValueId: keyof OptionDefinitions; | ||||
|     private optionTimeScaleId: keyof OptionDefinitions; | ||||
|     private includedTimeScales: Set<TimeSelectorScale>; | ||||
|     private minimumSeconds: number; | ||||
| 
 | ||||
|     constructor(options: TimeSelectorConstructor) { | ||||
|         super(); | ||||
| @ -50,7 +53,8 @@ export default class TimeSelector extends OptionsWidget { | ||||
|         this.widgetLabelId = options.widgetLabelId; | ||||
|         this.optionValueId = options.optionValueId; | ||||
|         this.optionTimeScaleId = options.optionTimeScaleId; | ||||
|         this.includedTimeScales = !options.includedTimeScales ? new Set(["seconds", "minutes", "hours", "days"]) : options.includedTimeScales; | ||||
|         this.includedTimeScales = options.includedTimeScales || new Set(["seconds", "minutes", "hours", "days"]); | ||||
|         this.minimumSeconds = options.minimumSeconds || 0 | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
| @ -71,7 +75,8 @@ export default class TimeSelector extends OptionsWidget { | ||||
| 
 | ||||
|             if (!this.handleTimeValidation() || typeof timeScale !== "string" || !time) return; | ||||
| 
 | ||||
|             this.internalTimeInSeconds = this.convertTime(time, timeScale).toOption(); | ||||
|             this.setInternalTimeInSeconds(this.convertTime(time, timeScale).toOption()); | ||||
| 
 | ||||
|             this.updateOption(this.optionValueId, this.internalTimeInSeconds); | ||||
|         }); | ||||
| 
 | ||||
| @ -89,13 +94,17 @@ export default class TimeSelector extends OptionsWidget { | ||||
|     } | ||||
| 
 | ||||
|     async optionsLoaded(options: OptionMap) { | ||||
|         this.internalTimeInSeconds = options[this.optionValueId]; | ||||
|         const displayedTime = this.convertTime(options[this.optionValueId], options[this.optionTimeScaleId]).toDisplay(); | ||||
|         const optionValue = optionsService.getInt(this.optionValueId) || 0; | ||||
|         const optionTimeScale = optionsService.getInt(this.optionTimeScaleId) || 1; | ||||
| 
 | ||||
|         this.setInternalTimeInSeconds(optionValue); | ||||
| 
 | ||||
|         const displayedTime = this.convertTime(optionValue, optionTimeScale).toDisplay(); | ||||
|         this.$timeValueInput.val(displayedTime); | ||||
|         this.$timeScaleSelect.val(options[this.optionTimeScaleId]); | ||||
|         this.$timeScaleSelect.val(optionTimeScale); | ||||
|     } | ||||
| 
 | ||||
|     convertTime(time: string | number, timeScale: string | number) { | ||||
|     private convertTime(time: string | number, timeScale: string | number) { | ||||
|         const value = typeof time === "number" ? time : parseInt(time); | ||||
|         if (Number.isNaN(value)) { | ||||
|             throw new Error(`Time needs to be a valid integer, but received: ${time}`); | ||||
| @ -112,11 +121,20 @@ export default class TimeSelector extends OptionsWidget { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     handleTimeValidation() { | ||||
|     private handleTimeValidation() { | ||||
|         if (this.$timeValueInput.is(":invalid")) { | ||||
|             toastService.showError(t("time_selector.invalid_input")); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private setInternalTimeInSeconds(time: number) { | ||||
|         if (time < this.minimumSeconds) { | ||||
|             toastService.showError(t("time_selector.minimum_input", {minimumSeconds: this.minimumSeconds})); | ||||
|             return this.internalTimeInSeconds = this.minimumSeconds; | ||||
|         } | ||||
|         return this.internalTimeInSeconds = time; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1658,6 +1658,7 @@ | ||||
|     "days": "Days" | ||||
|   }, | ||||
|   "time_selector": { | ||||
|     "invalid_input": "The entered time value is not a valid number." | ||||
|     "invalid_input": "The entered time value is not a valid number.", | ||||
|     "minimum_input": "The entered time value needs to be at least {{minimumSeconds}} seconds." | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -88,7 +88,7 @@ function getType(options: TaskData, mime: string) { | ||||
|     const mimeLc = mime?.toLowerCase(); | ||||
| 
 | ||||
|     switch (true) { | ||||
|         case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown"].includes(mimeLc): | ||||
|         case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown", "text/mdx"].includes(mimeLc): | ||||
|             return "text"; | ||||
| 
 | ||||
|         case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc): | ||||
|  | ||||
							
								
								
									
										21
									
								
								src/services/import/samples/Text Note.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/services/import/samples/Text Note.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| Page 1 | ||||
| 
 | ||||
| Heading 1 | ||||
| --------- | ||||
| 
 | ||||
| Heading 2 | ||||
| --------- | ||||
| 
 | ||||
| ### Heading 3 | ||||
| 
 | ||||
| ``` | ||||
| class Foo { | ||||
|     hoistedNoteChangedEvent({ ntxId }) { | ||||
|         if (this.isNoteContext(ntxId)) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Page 2 | ||||
							
								
								
									
										
											BIN
										
									
								
								src/services/import/samples/mdx.zip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/services/import/samples/mdx.zip
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										49
									
								
								src/services/import/single.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/services/import/single.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| import { describe, expect, it } from "vitest"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import { fileURLToPath } from "url"; | ||||
| import { dirname } from "path"; | ||||
| import becca from "../../becca/becca.js"; | ||||
| import BNote from "../../becca/entities/bnote.js"; | ||||
| import TaskContext from "../task_context.js"; | ||||
| import cls from "../cls.js"; | ||||
| import sql_init from "../sql_init.js"; | ||||
| import { initializeTranslations } from "../i18n.js"; | ||||
| import single from "./single.js"; | ||||
| const scriptDir = dirname(fileURLToPath(import.meta.url)); | ||||
| 
 | ||||
| describe("processNoteContent", () => { | ||||
|     it("treats single MDX as Markdown", async () => { | ||||
|         const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", "Text Note.mdx")); | ||||
|         const taskContext = TaskContext.getInstance("import-mdx", "import", { | ||||
|             textImportedAsText: true | ||||
|         }); | ||||
| 
 | ||||
|         await new Promise<void>((resolve, reject) => { | ||||
|             cls.init(async () => { | ||||
|                 initializeTranslations(); | ||||
|                 sql_init.initializeDb(); | ||||
|                 await sql_init.dbReady; | ||||
| 
 | ||||
|                 const rootNote = becca.getNote("root"); | ||||
|                 if (!rootNote) { | ||||
|                     reject("Missing root note."); | ||||
|                 } | ||||
| 
 | ||||
|                 const importedNote = single.importSingleFile(taskContext, { | ||||
|                     originalname: "Text Note.mdx", | ||||
|                     mimetype: "text/mdx", | ||||
|                     buffer: mdxSample | ||||
|                 }, rootNote as BNote); | ||||
|                 try { | ||||
|                     expect(importedNote.mime).toBe("text/html"); | ||||
|                     expect(importedNote.type).toBe("text"); | ||||
|                     expect(importedNote.title).toBe("Text Note"); | ||||
|                 } catch (e) { | ||||
|                     reject(e); | ||||
|                 } | ||||
|                 resolve(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }) | ||||
| @ -19,7 +19,7 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot | ||||
|     if (taskContext?.data?.textImportedAsText) { | ||||
|         if (mime === "text/html") { | ||||
|             return importHtml(taskContext, file, parentNote); | ||||
|         } else if (["text/markdown", "text/x-markdown"].includes(mime)) { | ||||
|         } else if (["text/markdown", "text/x-markdown", "text/mdx"].includes(mime)) { | ||||
|             return importMarkdown(taskContext, file, parentNote); | ||||
|         } else if (mime === "text/plain") { | ||||
|             return importPlainText(taskContext, file, parentNote); | ||||
|  | ||||
							
								
								
									
										46
									
								
								src/services/import/zip.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/services/import/zip.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import { describe, expect, it } from "vitest"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import { fileURLToPath } from "url"; | ||||
| import { dirname } from "path"; | ||||
| import zip from "./zip.js"; | ||||
| import becca from "../../becca/becca.js"; | ||||
| import BNote from "../../becca/entities/bnote.js"; | ||||
| import TaskContext from "../task_context.js"; | ||||
| import cls from "../cls.js"; | ||||
| import sql_init from "../sql_init.js"; | ||||
| import { initializeTranslations } from "../i18n.js"; | ||||
| const scriptDir = dirname(fileURLToPath(import.meta.url)); | ||||
| 
 | ||||
| describe("processNoteContent", () => { | ||||
|     it("treats single MDX as Markdown in ZIP as text note", async () => { | ||||
|         const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", "mdx.zip")); | ||||
|         const taskContext = TaskContext.getInstance("import-mdx", "import", { | ||||
|             textImportedAsText: true | ||||
|         }); | ||||
| 
 | ||||
|         await new Promise<void>((resolve, reject) => { | ||||
|             cls.init(async () => { | ||||
|                 initializeTranslations(); | ||||
|                 sql_init.initializeDb(); | ||||
|                 await sql_init.dbReady; | ||||
| 
 | ||||
|                 const rootNote = becca.getNote("root"); | ||||
|                 if (!rootNote) { | ||||
|                     expect(rootNote).toBeTruthy(); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const importedNote = await zip.importZip(taskContext, mdxSample, rootNote as BNote); | ||||
|                 try { | ||||
|                     expect(importedNote.mime).toBe("text/mdx"); | ||||
|                     expect(importedNote.type).toBe("text"); | ||||
|                     expect(importedNote.title).toBe("Text Note"); | ||||
|                 } catch (e) { | ||||
|                     reject(e); | ||||
|                 } | ||||
|                 resolve(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }) | ||||
| @ -386,7 +386,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo | ||||
|     } | ||||
| 
 | ||||
|     function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) { | ||||
|         if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown"].includes(mime))) && typeof content === "string") { | ||||
|         if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown", "text/mdx"].includes(mime))) && typeof content === "string") { | ||||
|             content = markdownService.renderToHtml(content, noteTitle); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { crash } from "./utils.js"; | ||||
| import resourceDir from "./resource_dir.js"; | ||||
| import appInfo from "./app_info.js"; | ||||
| import cls from "./cls.js"; | ||||
| import { t } from "i18next"; | ||||
| 
 | ||||
| interface MigrationInfo { | ||||
|     dbVersion: number; | ||||
| @ -18,9 +19,7 @@ async function migrate() { | ||||
|     const currentDbVersion = getDbVersion(); | ||||
| 
 | ||||
|     if (currentDbVersion < 214) { | ||||
|         log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version."); | ||||
| 
 | ||||
|         await crash(); | ||||
|         await crash(t("migration.old_version")); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
| @ -83,10 +82,7 @@ async function migrate() { | ||||
| 
 | ||||
|                 log.info(`Migration to version ${mig.dbVersion} has been successful.`); | ||||
|             } catch (e: any) { | ||||
|                 log.error(`error during migration to version ${mig.dbVersion}: ${e.stack}`); | ||||
|                 log.error("migration failed, crashing hard"); // this is not very user-friendly :-/
 | ||||
| 
 | ||||
|                 crash(); | ||||
|                 crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack })); | ||||
|                 break; // crash() is sometimes async
 | ||||
|             } | ||||
|         } | ||||
| @ -136,11 +132,7 @@ async function migrateIfNecessary() { | ||||
|     const currentDbVersion = getDbVersion(); | ||||
| 
 | ||||
|     if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") { | ||||
|         log.error( | ||||
|             `Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.` | ||||
|         ); | ||||
| 
 | ||||
|         await crash(); | ||||
|         await crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion })); | ||||
|     } | ||||
| 
 | ||||
|     if (!isDbUpToDate()) { | ||||
|  | ||||
| @ -10,6 +10,8 @@ import path from "path"; | ||||
| import { fileURLToPath } from "url"; | ||||
| import { dirname, join } from "path"; | ||||
| import type NoteMeta from "./meta/note_meta.js"; | ||||
| import log from "./log.js"; | ||||
| import { t } from "i18next"; | ||||
| 
 | ||||
| const randtoken = generator({ source: "crypto" }); | ||||
| 
 | ||||
| @ -105,10 +107,13 @@ export function escapeRegExp(str: string) { | ||||
|     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | ||||
| } | ||||
| 
 | ||||
| export async function crash() { | ||||
| export async function crash(message: string) { | ||||
|     if (isElectron) { | ||||
|         (await import("electron")).app.exit(1); | ||||
|         const electron = await import("electron"); | ||||
|         electron.dialog.showErrorBox(t("modals.error_title"), message); | ||||
|         electron.app.exit(1); | ||||
|     } else { | ||||
|         log.error(message); | ||||
|         process.exit(1); | ||||
|     } | ||||
| } | ||||
| @ -168,6 +173,7 @@ export function removeTextFileExtension(filePath: string) { | ||||
| 
 | ||||
|     switch (extension) { | ||||
|         case ".md": | ||||
|         case ".mdx": | ||||
|         case ".markdown": | ||||
|         case ".html": | ||||
|         case ".htm": | ||||
|  | ||||
| @ -272,5 +272,13 @@ | ||||
|     "today": "Open today's journal note", | ||||
|     "new-note": "New note", | ||||
|     "show-windows": "Show windows" | ||||
|   }, | ||||
|   "migration": { | ||||
|     "old_version": "Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.", | ||||
|     "error_message": "Error during migration to version {{version}}: {{stack}}", | ||||
|     "wrong_db_version": "Current DB version {{version}} is newer than the current DB version {{targetVersion}}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue." | ||||
|   }, | ||||
|   "modals": { | ||||
|     "error_title": "Error" | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -272,5 +272,13 @@ | ||||
|     "today": "Mergi la notița de astăzi", | ||||
|     "tooltip": "TriliumNext Notes", | ||||
|     "show-windows": "Afișează ferestrele" | ||||
|   }, | ||||
|   "migration": { | ||||
|     "error_message": "Eroare la migrarea către versiunea {{version}}: {{stack}}", | ||||
|     "old_version": "Nu se poate migra la ultima versiune direct de la versiunea dvs. Actualizați mai întâi la versiunea v0.60.4 și ulterior la această versiune.", | ||||
|     "wrong_db_version": "Versiunea actuală a bazei de date ({{version}}) este mai nouă decât versiunea de bază de date suportată de aplicație ({{targetVersion}}), ceea ce înseamnă că a fost creată de către o versiune mai nouă de Trilium. Actualizați aplicația la ultima versiune pentru a putea continua." | ||||
|   }, | ||||
|   "modals": { | ||||
|     "error_title": "Eroare" | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran