diff --git a/assets/cloud.svg b/assets/cloud.svg
new file mode 100644
index 00000000..2e06424f
--- /dev/null
+++ b/assets/cloud.svg
@@ -0,0 +1,3 @@
+
diff --git a/e2e/cloudSync.e2e.ts b/e2e/cloudSync.e2e.ts
new file mode 100644
index 00000000..dee04ec5
--- /dev/null
+++ b/e2e/cloudSync.e2e.ts
@@ -0,0 +1,150 @@
+import { test, expect, Page } from "@playwright/test";
+import * as fs from "fs";
+import * as Path from "path";
+import { LametaE2ERunner } from "./lametaE2ERunner";
+import { createNewProject, E2eProject } from "./various-e2e-helpers";
+
+// Exercises the cloud-sync UI (row status icon, " Status" card,
+// hydrate button) end to end using the e2e-controllable FakeCloudFileProvider
+// (see src/other/fakeCloudProvider.ts) instead of a real OneDrive/Dropbox/
+// iCloud account -- so this test runs the same way on any machine/OS.
+//
+// The fake provider reads its manifest (which file(s) are cloud-only, and
+// which folder is the fake sync root) from the JSON file named by
+// E2E_FAKE_CLOUD_PROVIDER, and it loads that manifest exactly once, on first
+// use -- which happens as soon as the app constructs its very first File
+// object (the project's own metadata file), well before we've created a
+// session or added a file through the UI. So the manifest has to name the
+// media file's FINAL on-disk path before the app is even launched.
+//
+// Project.addSession() names a brand-new project's first session folder
+// deterministically: sanitizeForArchive("New Session") under the default
+// ("ASCII") file-naming rules turns spaces into underscores, giving
+// "New_Session", and getUniqueFolder() will use that name as-is since
+// nothing else has claimed it yet in a fresh project. That lets us predict
+// the session folder -- and therefore the added media file's absolute path
+// -- before launch.
+let lameta: LametaE2ERunner;
+let page: Page;
+let project: E2eProject;
+
+const projectName = "CloudSyncE2E";
+const mediaFileName = "cloud-test-audio.mp3";
+
+let projectDirectory: string;
+let sessionDirectory: string;
+let mediaFilePath: string;
+let manifestPath: string;
+
+test.describe("Cloud sync UI (fake provider)", () => {
+ test.beforeAll(async ({}) => {
+ // process.env.E2ERoot is set once, for the whole Playwright run, by
+ // e2e/globalSetup.ts -- see various-e2e-helpers.ts for the same
+ // assumption (E2eProject.createNewProject computes projectDirectory the
+ // same way).
+ const e2eRoot = process.env.E2ERoot!;
+ projectDirectory = Path.join(e2eRoot, "lameta", projectName);
+ sessionDirectory = Path.join(projectDirectory, "Sessions", "New_Session");
+ mediaFilePath = Path.join(sessionDirectory, mediaFileName);
+
+ manifestPath = Path.join(e2eRoot, "fake-cloud-manifest.json");
+ fs.writeFileSync(
+ manifestPath,
+ JSON.stringify({
+ syncRoots: [{ path: projectDirectory, providerName: "FakeDrive" }],
+ cloudOnly: [mediaFilePath],
+ hydrateDelayMs: 500
+ })
+ );
+ // Must be set before launch(): LametaE2ERunner.launch() spreads
+ // process.env into the Electron process's env at spawn time.
+ process.env.E2E_FAKE_CLOUD_PROVIDER = manifestPath;
+
+ lameta = new LametaE2ERunner();
+ page = await lameta.launch();
+ await lameta.cancelRegistration();
+ project = await createNewProject(lameta, projectName);
+
+ // Fail fast with a clear message if the project directory didn't land
+ // where predicted (e.g. a duplicate-name retry appended a suffix)
+ // instead of a confusing failure later on.
+ expect(project.projectDirectory).toBe(projectDirectory);
+ });
+
+ test.afterAll(async ({}) => {
+ delete process.env.E2E_FAKE_CLOUD_PROVIDER;
+ await lameta.quit();
+ });
+
+ test("shows the fake provider's cloud status, lets the user fetch the file, and clears once hydrated", async ({}) => {
+ await project.goToSessions();
+ await project.addSession();
+
+ // (a) Add a real media file -- its bytes are real and readable the whole
+ // time; only the STATUS lameta reads is faked (see fakeCloudProvider.ts).
+ const sourceMp3 = Path.join(
+ process.cwd(),
+ "sample data",
+ "Edolo sample",
+ "Sessions",
+ "ETR009",
+ "ETR009_Careful.mp3"
+ );
+ if (!fs.existsSync(sourceMp3)) {
+ throw new Error(`Source sample mp3 not found at ${sourceMp3}`);
+ }
+ const stagedSource = Path.join(process.env.E2ERoot!, mediaFileName);
+ fs.copyFileSync(sourceMp3, stagedSource);
+
+ await lameta.mockShowOpenDialog([stagedSource]);
+ await page.getByRole("button", { name: "Add Files" }).click();
+
+ const fileCell = page.getByRole("gridcell", { name: mediaFileName });
+ await expect(fileCell).toBeVisible({ timeout: 10000 });
+
+ // Sanity check our predicted path actually matches where lameta put the
+ // file -- if this fails, the manifest named the wrong path and every
+ // assertion below would otherwise fail for a confusing reason. The row
+ // can appear before the background copy finishes (a known race -- see
+ // the comments in FileList-e2e-helpers.ts's addFile()), so poll instead
+ // of asserting once.
+ await expect
+ .poll(() => fs.existsSync(mediaFilePath), { timeout: 10000 })
+ .toBe(true);
+
+ // (b) The row shows the "online-only" cloud status icon (an svg with
+ // role="img" whose accessible name comes from its ; see
+ // CloudStatusIcon.tsx).
+ const fileRow = page.getByRole("row").filter({ has: fileCell });
+ await expect(fileRow.getByRole("img", { name: /Online-only/ })).toBeVisible(
+ { timeout: 10000 }
+ );
+
+ // (c) Selecting the file shows the "FakeDrive Status" card with a
+ // "Download this file to my computer" button instead of a preview
+ // (CloudFileFetchControl renders this whenever canPin is false and the
+ // file isCloudFileNotPresent; see CloudFilePanel.tsx).
+ await fileCell.click();
+ const statusHeading = page.getByRole("heading", {
+ name: "FakeDrive Status"
+ });
+ await expect(statusHeading).toBeVisible({ timeout: 10000 });
+ const downloadButton = page.getByRole("button", {
+ name: "Download this file to my computer"
+ });
+ await expect(downloadButton).toBeVisible();
+
+ // (d) Clicking it asks the fake provider to hydrate the file; after
+ // hydrateDelayMs (500ms) the fake reports "local" and, once the
+ // polling reads that (every 1.5s, see cloudFilePoller.ts), the card
+ // goes away because the file is no longer isCloudFileNotPresent.
+ await downloadButton.click();
+ await expect(statusHeading).not.toBeVisible({ timeout: 10000 });
+
+ // The row's "online-only" icon is gone too (a local file shows no cloud
+ // icon at all, per CloudStatusIcon.tsx's current design).
+ await expect(
+ fileRow.getByRole("img", { name: /Online-only/ })
+ ).not.toBeVisible({ timeout: 5000 });
+ });
+});
diff --git a/e2e/renameContention.e2e.ts b/e2e/renameContention.e2e.ts
new file mode 100644
index 00000000..fc78de75
--- /dev/null
+++ b/e2e/renameContention.e2e.ts
@@ -0,0 +1,172 @@
+import fs from "fs";
+import * as Path from "path";
+import { spawn, ChildProcess } from "child_process";
+import { Page, test, expect } from "@playwright/test";
+import { LametaE2ERunner } from "./lametaE2ERunner";
+import { createNewProject, E2eProject } from "./various-e2e-helpers";
+import { E2eFileList } from "./FileList-e2e-helpers";
+
+/*
+ Renaming a person/session renames its folder and the files inside — the
+ operation most at risk from other processes (antivirus, sync clients,
+ indexers) holding transient locks. These tests verify, through the real app,
+ the PatientFS contract:
+
+ - while another process holds a file in the folder WITHOUT delete-sharing
+ (which on Windows blocks renaming the folder), a rename attempt must fail
+ CLEANLY: everything keeps its old name, nothing half-renamed;
+ - once the lock is gone, the rename must succeed completely.
+
+ Windows-only: on POSIX an open handle doesn't block renames, so the
+ contention class doesn't exist there.
+*/
+
+let lameta: LametaE2ERunner;
+let page: Page;
+let project: E2eProject;
+let fileList: E2eFileList;
+
+test.describe("Rename under file-lock contention", () => {
+ test.skip(process.platform !== "win32", "Windows-only contention semantics");
+ // the failed rename alone spends ~10s in the PatientFS retry loop
+ test.setTimeout(180_000);
+
+ test.beforeEach(async () => {
+ lameta = new LametaE2ERunner();
+ page = await lameta.launch();
+ await lameta.cancelRegistration();
+ project = await createNewProject(
+ lameta,
+ "RenameContention" + Math.random().toString()
+ );
+ fileList = new E2eFileList(lameta, page, project.projectDirectory);
+ });
+ test.afterEach(async () => {
+ await lameta.quit();
+ });
+
+ test("rename fails cleanly while a file is locked, then succeeds after release", async () => {
+ await project.goToPeople();
+ await project.addPerson();
+ await setFullName("Paul Hewson");
+ await page.waitForTimeout(1000);
+
+ const personDir = Path.join(
+ project.projectDirectory,
+ "People",
+ "Paul_Hewson"
+ );
+ expect(fs.existsSync(Path.join(personDir, "Paul_Hewson.person"))).toBe(
+ true
+ );
+ const mediaPath = Path.join(personDir, "Paul_Hewson_foo.txt");
+ await fileList.addFile("Paul_Hewson_foo.txt", { page, path: mediaPath });
+ await fileList.selectFile("Paul_Hewson.person");
+
+ // another process opens the media file with no delete-sharing: renaming
+ // the file fails with EBUSY and renaming the folder fails with EPERM
+ const holder = await holdFileOpen(mediaPath, 90);
+ try {
+ const filesBefore = fs.readdirSync(personDir).sort();
+
+ // this triggers the rename attempt; the app spends a PatientFS retry
+ // budget (~10s) on the locked file, another on the folder itself, then
+ // gives up and rolls the file renames back
+ await setFullName("Bono", 60_000);
+
+ // first, prove the rename attempt really started: some file transiently
+ // carries the new name (the mid-flight window lasts ~20s, poll can't
+ // miss it). Without this gate the next poll could pass trivially before
+ // the app even began.
+ await expect
+ .poll(
+ () =>
+ fs.existsSync(personDir)
+ ? fs.readdirSync(personDir).some((f) => f.startsWith("Bono"))
+ : false,
+ {
+ message:
+ "the rename attempt never started (no file transiently got the new name)",
+ timeout: 30_000
+ }
+ )
+ .toBe(true);
+
+ // then poll until the folder is back to exactly its old contents
+ await expect
+ .poll(
+ () =>
+ fs.existsSync(personDir) ? fs.readdirSync(personDir).sort() : [],
+ {
+ message:
+ "after a failed rename, the folder must return to exactly its old contents (rollback)",
+ timeout: 60_000
+ }
+ )
+ .toEqual(filesBefore);
+ expect(
+ fs.existsSync(
+ Path.join(project.projectDirectory, "People", "Bono")
+ ),
+ "no folder with the new name may exist after a failed rename"
+ ).toBe(false);
+ } finally {
+ holder.kill();
+ }
+ // let the OS release the handle
+ await page.waitForTimeout(1000);
+
+ // now the same rename must go through completely
+ await setFullName("Ali G", 60_000);
+ await page.waitForTimeout(2000);
+
+ const newDir = Path.join(project.projectDirectory, "People", "Ali_G");
+ expect(fs.existsSync(newDir), "renamed folder must exist").toBe(true);
+ expect(fs.existsSync(personDir), "old folder must be gone").toBe(false);
+ expect(fs.existsSync(Path.join(newDir, "Ali_G.person"))).toBe(true);
+ expect(fs.existsSync(Path.join(newDir, "Ali_G_foo.txt"))).toBe(true);
+ expect(
+ fs
+ .readdirSync(newDir)
+ .filter((f) => f.startsWith("Paul_Hewson") || f.startsWith("Bono")),
+ "no files with stale names may remain"
+ ).toEqual([]);
+ });
+});
+
+// Open a file from a separate process with FileShare.ReadWrite (no Delete),
+// the way scanners/sync clients do. Resolves once the handle is held.
+function holdFileOpen(file: string, seconds: number): Promise {
+ const ps = `
+ $h = [System.IO.File]::Open('${file.replace(/'/g, "''")}',
+ [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read,
+ [System.IO.FileShare]::ReadWrite);
+ [Console]::Out.WriteLine('HELD');
+ Start-Sleep -Seconds ${seconds};
+ $h.Close();
+ `;
+ const child = spawn("powershell", ["-NoProfile", "-Command", ps], {
+ stdio: ["ignore", "pipe", "inherit"]
+ });
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(
+ () => reject(new Error("file holder never became ready")),
+ 15000
+ );
+ child.stdout!.on("data", (d) => {
+ if (d.toString().includes("HELD")) {
+ clearTimeout(timer);
+ resolve(child);
+ }
+ });
+ });
+}
+
+async function setFullName(name: string, timeout = 5000) {
+ const fullNameField = page.getByTestId("field-name-edit");
+ await fullNameField.waitFor({ state: "visible", timeout });
+ await expect(fullNameField).not.toBeEmpty({ timeout });
+ await fullNameField.click({ clickCount: 3, timeout });
+ await page.keyboard.type(name);
+ await fullNameField.press("Tab", { timeout });
+}
diff --git a/e2e/twoUserSync.e2e.ts b/e2e/twoUserSync.e2e.ts
new file mode 100644
index 00000000..250fc7b9
--- /dev/null
+++ b/e2e/twoUserSync.e2e.ts
@@ -0,0 +1,243 @@
+import fs from "fs";
+import * as Path from "path";
+import { Page, test, expect } from "@playwright/test";
+import { LametaE2ERunner } from "./lametaE2ERunner";
+import { createNewProject, E2eProject } from "./various-e2e-helpers";
+
+/*
+ EXPERIMENT (opt-in): what happens when two people share a lameta project via
+ a cloud-sync service (OneDrive/Dropbox/...) and both have lameta open?
+
+ From the point of view of a running lameta instance on machine B, everything
+ user A does arrives as EXTERNAL filesystem changes made by the sync client
+ while the project is open. We simulate exactly that: launch the real app,
+ then mutate/rename/delete project files behind its back and observe what the
+ app does next. No cloud account needed — the sync client's local behavior is
+ just file I/O.
+
+ These tests LOG findings (grep the output for TWOUSER-FINDING); assertions
+ pin only what we're confident of. Run with:
+
+ LAMETA_EXPERIMENT=1 npx playwright test twoUserSync
+*/
+
+const experimentEnabled = !!process.env.LAMETA_EXPERIMENT;
+
+let lameta: LametaE2ERunner;
+let page: Page;
+let project: E2eProject;
+
+function sessionDir(id: string) {
+ return Path.join(project.projectDirectory, "Sessions", id);
+}
+function sessionFile(id: string) {
+ return Path.join(sessionDir(id), `${id}.session`);
+}
+function logFinding(msg: string) {
+ console.log(`TWOUSER-FINDING: ${msg}`);
+}
+
+async function createSession(id: string, baselineNote: string) {
+ await project.goToSessions();
+ await project.addSession();
+ const idField = page.getByTestId("field-id-edit");
+ await idField.waitFor({ state: "visible" });
+ await idField.click();
+ await idField.fill(id);
+ await page.keyboard.press("Tab");
+ await typeIntoNotes(baselineNote);
+ // back to the main session form
+ await page.getByRole("tab", { name: "Session", exact: true }).first().click();
+ await page.waitForTimeout(300);
+}
+
+async function setLocation(text: string) {
+ const locationField = page.getByTestId("field-location-edit");
+ await locationField.waitFor({ state: "visible" });
+ await locationField.click();
+ await locationField.fill(text);
+ await page.keyboard.press("Tab");
+ await page.waitForTimeout(300);
+}
+
+async function selectSession(id: string) {
+ await page.getByRole("gridcell", { name: id }).first().click();
+ await page.waitForTimeout(300);
+}
+
+async function typeIntoNotes(text: string) {
+ await project.goToNotesOfThisSession();
+ const notes = page.getByTestId("field-notes-edit");
+ await notes.waitFor({ state: "visible" });
+ await notes.click();
+ await page.keyboard.type(text);
+ await page.keyboard.press("Tab");
+ await page.waitForTimeout(300);
+}
+
+test.describe("Two-user cloud-sync hazards (experiment)", () => {
+ test.skip(!experimentEnabled, "opt-in experiment: set LAMETA_EXPERIMENT=1");
+ test.setTimeout(180_000);
+
+ // Fresh app per test: a remotely-vanished session wounds the running app
+ // (the session-detail pane stops rendering until restart — itself one of the
+ // findings), so scenarios can't share an instance.
+ test.beforeEach(async () => {
+ lameta = new LametaE2ERunner();
+ page = await lameta.launch();
+ await lameta.cancelRegistration();
+ project = await createNewProject(lameta, `TwoUserSync_${Date.now()}`);
+ // a "parking" session we can select to force the previous one to save
+ await createSession("parking", "parking lot note");
+ });
+ test.afterEach(async () => {
+ await lameta.quit();
+ });
+
+ test("stale-overwrite: does user B's save clobber user A's synced-down edit?", async () => {
+ await createSession("stalecase", "original note text");
+ await selectSession("parking"); // forces save of stalecase
+ await expect
+ .poll(() => fs.existsSync(sessionFile("stalecase")), { timeout: 10_000 })
+ .toBe(true);
+ const savedXml = fs.readFileSync(sessionFile("stalecase"), "utf8");
+ expect(savedXml).toContain("original note text");
+
+ // user A edits the notes on their machine; the change syncs down to B's
+ // disk while B's lameta still holds the session in memory
+ fs.writeFileSync(
+ sessionFile("stalecase"),
+ savedXml.replace("original note text", "REMOTE EDIT FROM USER A")
+ );
+
+ // B looks at the session: does the UI show A's edit or the stale memory?
+ await selectSession("stalecase");
+ await project.goToNotesOfThisSession();
+ const shownNotes = await page.getByTestId("field-notes-edit").innerText();
+ logFinding(
+ `after remote edit synced down, B's UI shows notes: "${shownNotes.trim()}"`
+ );
+ await page.getByRole("tab", { name: "Session", exact: true }).first().click();
+
+ // B edits a DIFFERENT field (location) and it saves
+ await setLocation("location typed by user B");
+ await selectSession("parking"); // forces save of stalecase
+
+ await expect
+ .poll(
+ () => fs.readFileSync(sessionFile("stalecase"), "utf8"),
+ { timeout: 10_000 }
+ )
+ .toContain("location typed by user B");
+ const finalXml = fs.readFileSync(sessionFile("stalecase"), "utf8");
+ const remoteEditSurvived = finalXml.includes("REMOTE EDIT FROM USER A");
+
+ // With the freshness check in File.save(), B's stale write no longer
+ // clobbers A's synced-down edit: before writing its own version to the
+ // canonical name, lameta moves the externally-changed file aside to a
+ // sibling. Look for that set-aside copy and confirm A's edit is preserved
+ // in it (the canonical file now holds B's location edit).
+ const setAside = fs
+ .readdirSync(sessionDir("stalecase"))
+ .filter((f) => f.includes("changed on another computer"));
+ const setAsideHoldsRemoteEdit = setAside.some((f) =>
+ fs
+ .readFileSync(Path.join(sessionDir("stalecase"), f), "utf8")
+ .includes("REMOTE EDIT FROM USER A")
+ );
+ const nothingLost = remoteEditSurvived || setAsideHoldsRemoteEdit;
+ logFinding(
+ `after B saved another field: canonical file still holds A's edit? ${remoteEditSurvived}; set-aside sibling(s) preserving A's edit: [${setAside.join(
+ ", "
+ )}]`
+ );
+ logFinding(
+ nothingLost
+ ? `user A's remote edit was NOT lost (preserved ${
+ setAsideHoldsRemoteEdit ? "in a set-aside sibling" : "in place"
+ }); B's own edit is in the canonical file`
+ : "user A's remote edit was SILENTLY REVERTED by user B's save (stale in-memory state written over it, no set-aside copy)"
+ );
+ // B's own edit must have landed in the canonical file regardless.
+ expect(finalXml).toContain("location typed by user B");
+ });
+
+ test("remote delete: user A deleted the session; B edits it afterwards", async () => {
+ await createSession("delcase", "Delete Victim");
+ await selectSession("parking");
+ await expect
+ .poll(() => fs.existsSync(sessionFile("delcase")), { timeout: 10_000 })
+ .toBe(true);
+
+ // user A deletes the session; sync engine removes it on B's disk
+ fs.rmSync(sessionDir("delcase"), { recursive: true, force: true });
+
+ // B (list still shows it) selects it and tries to edit
+ await selectSession("delcase");
+ try {
+ await typeIntoNotes("edit after remote delete");
+ logFinding("after remote delete, B could still type into the session");
+ } catch (e) {
+ logFinding(
+ `after remote delete, B's UI broke trying to edit the session: ${String(e).split("\n")[0]}`
+ );
+ }
+ try {
+ await selectSession("parking");
+ } catch (e) {
+ logFinding(
+ `after remote delete, B could not even select another session: ${String(e).split("\n")[0]}`
+ );
+ }
+ await page.waitForTimeout(2000);
+
+ const recreated = fs.existsSync(sessionFile("delcase"));
+ logFinding(
+ recreated
+ ? "B's save RESURRECTED the deleted session (it will sync back to user A)"
+ : `B's save did not resurrect the deleted session; dir exists: ${fs.existsSync(sessionDir("delcase"))}`
+ );
+ });
+ test("remote rename: user A renamed the session; B edits it afterwards", async () => {
+ await createSession("renbefore", "Rename Victim");
+ await selectSession("parking");
+ await expect
+ .poll(() => fs.existsSync(sessionFile("renbefore")), { timeout: 10_000 })
+ .toBe(true);
+
+ // user A renames the session id on their machine → folder + files rename,
+ // and the sync engine applies that on B's disk:
+ fs.renameSync(
+ sessionFile("renbefore"),
+ Path.join(sessionDir("renbefore"), "renafter.session")
+ );
+ fs.renameSync(sessionDir("renbefore"), sessionDir("renafter"));
+ // (contents of renafter.session still say id "renbefore"; on A's machine
+ // it would say "renafter" — patch that too for realism)
+ const xml = fs.readFileSync(sessionFile("renafter"), "utf8");
+ fs.writeFileSync(sessionFile("renafter"), xml.replace(/renbefore/g, "renafter"));
+
+ // B (whose list still shows the old id) selects it and edits notes
+ await selectSession("renbefore");
+ await typeIntoNotes("edit after remote rename");
+ await selectSession("parking");
+ await page.waitForTimeout(2000);
+
+ const sessionsRoot = Path.join(project.projectDirectory, "Sessions");
+ const tree = fs
+ .readdirSync(sessionsRoot)
+ .map(
+ (d) =>
+ `${d}: [${fs.readdirSync(Path.join(sessionsRoot, d)).join(", ")}]`
+ )
+ .join(" | ");
+ logFinding(`after remote rename + local edit, Sessions tree: ${tree}`);
+ const oldRecreated = fs.existsSync(sessionDir("renbefore"));
+ logFinding(
+ oldRecreated
+ ? "B's save RECREATED the old-named folder → project now has BOTH old and new folders; when this syncs back, user A sees a duplicated session"
+ : "B's save did not recreate the old-named folder"
+ );
+ });
+
+});
diff --git a/e2e/vanishedSession.e2e.ts b/e2e/vanishedSession.e2e.ts
new file mode 100644
index 00000000..47be0dbd
--- /dev/null
+++ b/e2e/vanishedSession.e2e.ts
@@ -0,0 +1,102 @@
+import fs from "fs";
+import * as Path from "path";
+import { Page, test, expect } from "@playwright/test";
+import { LametaE2ERunner } from "./lametaE2ERunner";
+import { createNewProject, E2eProject } from "./various-e2e-helpers";
+
+/*
+ Regression: a session folder/metadata file that vanishes from disk EXTERNALLY
+ while the project is open (a collaborator renamed/deleted it on another machine
+ and OneDrive/Dropbox applied it locally; or someone deleted it in Explorer)
+ must not wound the running app. Concretely:
+
+ (symptom 1) a stale save must NOT spam a raw ENOENT error toast on every save
+ trigger (window blur, selection change). It should fail softly.
+ (symptom 2) the session-detail pane must keep rendering for OTHER sessions --
+ including a brand-new one made with the New Session button.
+
+ Unlike twoUserSync.e2e.ts (which relaunches the app per scenario and so cannot
+ observe the wound persisting), this test keeps ONE app instance so it pins the
+ "does the wound carry into the next session?" symptom.
+*/
+
+let lameta: LametaE2ERunner;
+let page: Page;
+let project: E2eProject;
+
+// Raw file.save() error toasts seen in the renderer console during the test.
+let rawSaveErrors: string[] = [];
+
+function sessionDir(id: string) {
+ return Path.join(project.projectDirectory, "Sessions", id);
+}
+
+async function createSession(id: string) {
+ await project.goToSessions();
+ await project.addSession();
+ const idField = page.getByTestId("field-id-edit");
+ await idField.waitFor({ state: "visible" });
+ await idField.click();
+ await idField.fill(id);
+ await page.keyboard.press("Tab");
+ await page.waitForTimeout(300);
+}
+
+async function selectSession(id: string) {
+ await page.getByRole("gridcell", { name: id }).first().click();
+ await page.waitForTimeout(300);
+}
+
+test.describe("vanished session must not wound the app", () => {
+ test.setTimeout(120_000);
+
+ test.beforeEach(async () => {
+ lameta = new LametaE2ERunner();
+ page = await lameta.launch();
+ rawSaveErrors = [];
+ // Watch for the raw, per-trigger "(file.save)" error that symptom 1 spammed.
+ page.on("console", (msg) => {
+ if (msg.type() === "error" && msg.text().includes("(file.save)")) {
+ rawSaveErrors.push(msg.text());
+ }
+ });
+ await lameta.cancelRegistration();
+ project = await createNewProject(lameta, `Vanished_${Date.now()}`);
+ });
+ test.afterEach(async () => {
+ await lameta.quit();
+ });
+
+ test("after a session vanishes and saves fail, a NEW session still renders and there is no raw error spam", async () => {
+ await createSession("keeper");
+ await createSession("victim");
+ // force victim to save so its folder/files exist on disk
+ await selectSession("keeper");
+ await expect
+ .poll(() => fs.existsSync(sessionDir("victim")), { timeout: 10_000 })
+ .toBe(true);
+
+ // The session vanishes from disk behind the app's back.
+ fs.rmSync(sessionDir("victim"), { recursive: true, force: true });
+
+ // The user (whose list still shows it) selects it -- this renders the
+ // detail pane against a File whose on-disk file is now gone -- then moves
+ // back and forth several times, each of which triggers a (failing) save of
+ // the stale victim. Before the fix, each of these popped a fresh raw toast.
+ await selectSession("victim");
+ await selectSession("keeper");
+ await selectSession("victim");
+ await selectSession("keeper");
+
+ // symptom 2: a brand-new session must still render its detail form.
+ await project.goToSessions();
+ await project.addSession();
+ const idField = page.getByTestId("field-id-edit");
+ await idField.waitFor({ state: "visible", timeout: 15_000 });
+ await expect(idField).toBeVisible();
+
+ // symptom 1: the stale saves must NOT have produced raw "(file.save)"
+ // error toasts (the fix warns softly, at most once, via a different path).
+ expect(rawSaveErrors).toHaveLength(0);
+ });
+});
diff --git a/electron-builder.json5 b/electron-builder.json5
index 02bc84d5..682f8a9d 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -56,6 +56,7 @@
"!node_modules/ffmpeg-ffprobe-static/bin/*/ffmpeg*"
],
extraFiles: [],
+ asarUnpack: ["**/node_modules/fswin/**"],
directories: {
buildResources: "resources",
output: "release"
diff --git a/locale/en/fields.po b/locale/en/fields.po
index 2771eeee..3cd386fc 100644
--- a/locale/en/fields.po
+++ b/locale/en/fields.po
@@ -206,6 +206,22 @@ msgctxt "lameta/project/location"
msgid "Location/Address"
msgstr ""
+msgctxt "lameta/project/metadataLanguages"
+msgid "Metadata Languages"
+msgstr ""
+
+msgctxt "lameta/project/metadataLanguages/description"
+msgid "Languages used in metadata fields throughout the project (e.g., descriptions, titles). These determine the language options available for multilingual text fields."
+msgstr ""
+
+msgctxt "lameta/project/multilingualConversionPending"
+msgid "Multilingual Conversion Pending"
+msgstr ""
+
+msgctxt "lameta/project/multilingualConversionPending/description"
+msgid "Internal flag: true if slash-syntax fields need conversion to tagged format"
+msgstr ""
+
msgctxt "lameta/project/region"
msgid "Region"
msgstr ""
diff --git a/locale/en/messages.po b/locale/en/messages.po
index 7ec0c00b..7c2dbbb5 100644
--- a/locale/en/messages.po
+++ b/locale/en/messages.po
@@ -40,6 +40,9 @@ msgstr ""
msgid "About lameta"
msgstr ""
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr ""
@@ -58,6 +61,15 @@ msgstr ""
msgid "All Sessions"
msgstr ""
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -77,6 +89,15 @@ msgstr ""
msgid "Audio"
msgstr ""
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -89,6 +110,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr ""
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr ""
@@ -116,6 +140,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -152,6 +179,9 @@ msgstr ""
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -176,6 +206,9 @@ msgstr ""
msgid "Custom Field"
msgstr ""
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr ""
@@ -209,6 +242,9 @@ msgstr ""
msgid "Description Documents"
msgstr ""
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr ""
@@ -221,6 +257,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr ""
@@ -230,9 +269,15 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr ""
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr ""
@@ -254,6 +299,9 @@ msgstr ""
msgid "Field"
msgstr ""
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr ""
@@ -266,6 +314,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr ""
@@ -299,6 +356,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr ""
@@ -344,6 +407,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr ""
@@ -395,6 +464,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr ""
@@ -404,18 +482,27 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr ""
msgid "New Session"
msgstr ""
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -431,6 +518,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr ""
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr ""
@@ -467,6 +560,12 @@ msgstr ""
msgid "Please Register"
msgstr ""
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -494,6 +593,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr ""
@@ -527,9 +629,18 @@ msgstr ""
msgid "Restart lameta and do the rename before playing the video again."
msgstr ""
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr ""
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr ""
@@ -539,6 +650,15 @@ msgstr ""
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -560,6 +680,12 @@ msgstr ""
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -587,6 +713,9 @@ msgstr ""
msgid "Smaller"
msgstr ""
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -599,9 +728,18 @@ msgstr ""
msgid "Start Screen"
msgstr ""
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr ""
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr ""
@@ -611,6 +749,9 @@ msgstr ""
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -650,9 +791,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -665,6 +812,9 @@ msgstr ""
msgid "Undo"
msgstr ""
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr ""
@@ -683,6 +833,15 @@ msgstr ""
msgid "View Release Notes"
msgstr ""
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -698,12 +857,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr ""
msgid "You cannot add folders."
msgstr ""
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr ""
@@ -737,6 +905,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr ""
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -746,6 +920,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr ""
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr ""
@@ -762,8 +939,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr ""
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/es/messages.po b/locale/es/messages.po
index 8bf3fdd2..b6f3f156 100644
--- a/locale/es/messages.po
+++ b/locale/es/messages.po
@@ -45,6 +45,9 @@ msgstr "Abandonar archivos que aún se estén copiando"
msgid "About lameta"
msgstr "Sobre Lameta"
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr "Agregar archivos"
@@ -63,6 +66,15 @@ msgstr ""
msgid "All Sessions"
msgstr "Todas las sesiones"
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -82,6 +94,15 @@ msgstr ""
msgid "Audio"
msgstr "Audio"
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -94,6 +115,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr "No se pueden añadir archivos de ese tipo"
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr "Cambiar el nombre a:"
@@ -121,6 +145,9 @@ msgstr "Borrar"
msgid "Close"
msgstr "Cerrar"
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -157,6 +184,9 @@ msgstr "Copiando..."
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr "Crear"
@@ -181,6 +211,9 @@ msgstr "Créditos"
msgid "Custom Field"
msgstr "Campo personalizado"
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr "Cortar"
@@ -214,6 +247,9 @@ msgstr "Eliminando..."
msgid "Description Documents"
msgstr "Documentos descriptivos"
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr "No salga del programa"
@@ -226,6 +262,9 @@ msgstr ""
msgid "Download"
msgstr "Descargar"
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr "Duplicar Persona"
@@ -235,9 +274,15 @@ msgstr "Duplicar Sesión"
msgid "Edit"
msgstr "Editar"
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr "Correo electrónico"
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr "Exportar"
@@ -259,6 +304,9 @@ msgstr ""
msgid "Field"
msgstr "Campo"
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr "Archivo"
@@ -271,6 +319,15 @@ msgstr "Archivo a importar:"
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr "Tamaño de fuente"
@@ -304,6 +361,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr "Ayuda"
@@ -349,6 +412,12 @@ msgstr "Importar {chosenCount} Sesiones"
msgid "Importing..."
msgstr "Importando..."
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr "Indica que este es el idioma principal del padre o de la madre."
@@ -400,6 +469,15 @@ msgstr "Carpeta de medios"
msgid "Media Folder Settings..."
msgstr "Configuración de la carpeta de medios..."
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr "Modificado"
@@ -409,18 +487,27 @@ msgstr "Más campos"
msgid "Name"
msgstr "Nombre"
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr "Nueva persona"
msgid "New Session"
msgstr "Nueva Sesión"
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr "Por lo general, cuando agregue un archivo a un proyecto, sesión o persona, este se copiará en la misma carpeta de su proyecto Lameta. Para algunos archivos, quizá prefiera dejar los archivos donde están y solo \"vincularlos\"."
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
"Tenga en cuenta que este es un enlace a un archivo en su carpeta de medios. Aquí, usted está renombrando el enlace, no el archivo original que se indica. Si más adelante crea una exportación que incluya este archivo, la exportación usará este nombre.\n"
@@ -438,6 +525,12 @@ msgstr "OPEX + Archivos"
msgid "One or more files are still being copied into your project."
msgstr "Uno o más archivos aún están siendo copiados a su proyecto."
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr "Abrir"
@@ -474,6 +567,12 @@ msgstr "Persona"
msgid "Please Register"
msgstr "Registrarse"
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr "Elimine caracteres que no estén permitidos por la configuración del archivado."
@@ -501,6 +600,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr "Rehacer"
@@ -534,9 +636,18 @@ msgstr "Reportar un problema"
msgid "Restart lameta and do the rename before playing the video again."
msgstr "Reiniciar Lameta y hacer el cambio de nombre antes de reproducir el video de nuevo."
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr "Rol"
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr "Guardar como"
@@ -546,6 +657,15 @@ msgstr "Guardando"
msgid "Saving before rename..."
msgstr "Guardar antes de renombrar..."
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -567,6 +687,12 @@ msgstr "Mostrar Todo"
msgid "Show IMDI previews"
msgstr "Mostrar IMDI"
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr "Modo PARADISEC"
@@ -594,6 +720,9 @@ msgstr "Tamaño"
msgid "Smaller"
msgstr "Más pequeño"
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr "Algunos sistemas operativos no permiten ese nombre."
@@ -606,9 +735,18 @@ msgstr "Lo sentimos, no se permiten barras"
msgid "Start Screen"
msgstr "Pantalla de Inicio"
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr "Estado"
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr "Texto"
@@ -618,6 +756,9 @@ msgstr "Ese nombre hará que el nombre del archivo esté vacío."
msgid "The IMDI files were validated."
msgstr "Los archivos IMDI han sido validados."
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr "El archivo no aparece en la ubicación esperada en la carpeta de medios. La carpeta de medios está configurada para {m} y el archivo debería estar en {e}"
@@ -657,9 +798,15 @@ msgstr "Este archivo XML se ajusta al esquema IMDI."
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr "Este es un enlace, pero Lameta no logra encontrar la carpeta de medios para este proyecto. Vea Archivo: Configuración de carpeta de medios."
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr "Esta configuración es exclusiva para la cuenta que se utiliza en este equipo. Un colega deberá configurar este valor a la ubicación correcta en su equipo."
@@ -672,6 +819,9 @@ msgstr "Tipo"
msgid "Undo"
msgstr "Deshacer"
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr "Actualización disponible"
@@ -690,6 +840,15 @@ msgstr "Vista"
msgid "View Release Notes"
msgstr "Ver Notas de esta Versión"
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -705,12 +864,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr "Está usando la versión de Lameta:"
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr "No se puede añadir archivos de ese tipo"
msgid "You cannot add folders."
msgstr "No se puede añadir carpetas."
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr "Archivo ZIP"
@@ -744,6 +912,12 @@ msgstr "Lameta no pudo encontrar una Persona que coincida con los siguientes col
msgid "lameta could not rename the directory."
msgstr "Lameta no pudo cambiar el nombre del directorio."
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr "Lameta esperaba encontrar un archivo .link en {e}"
@@ -753,6 +927,9 @@ msgstr "Lameta intentó renombrar la carpeta \"{oldFolderName}\" a \"{newFolderN
msgid "lameta was not able to delete the file."
msgstr "Lameta no pudo eliminar el archivo."
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr "Lameta no pudo enviar este archivo a la papelera de reciclaje"
@@ -769,8 +946,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr "Lameta abrirá esta carpeta en su disco duro externo y luego se cerrará. Deberá abrir estos archivos {projectType} en un editor de texto y debe decidir cuáles desea mantener y borrar el resto. El archivo que elija deberá llamarse {name}."
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr "{0} Sesiones Marcadas"
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr "{descriptionOfWhatWillBeDeleted} se enviará a la Papelera de reciclaje"
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/fa/messages.po b/locale/fa/messages.po
index 94c64ee5..f4c580c2 100644
--- a/locale/fa/messages.po
+++ b/locale/fa/messages.po
@@ -47,6 +47,9 @@ msgstr "از فایل هایی که در حال کپی شدن هستند، صر
msgid "About lameta"
msgstr "درباره لامتا"
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr "افزودن فایلها"
@@ -65,6 +68,15 @@ msgstr ""
msgid "All Sessions"
msgstr "همه جلسات"
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -84,6 +96,15 @@ msgstr ""
msgid "Audio"
msgstr "فایل صوتی"
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -96,6 +117,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr "نمی توان این نوع فایلها را اضافه کرد"
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr "تغییر نام به:"
@@ -123,6 +147,9 @@ msgstr ""
msgid "Close"
msgstr "بستن"
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -159,6 +186,9 @@ msgstr "در حال رونوشت گرفتن / کپی کردن فایلها"
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -183,6 +213,9 @@ msgstr "اعتبار"
msgid "Custom Field"
msgstr "فیلد سفارشی"
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr "برش"
@@ -216,6 +249,9 @@ msgstr ""
msgid "Description Documents"
msgstr "مدارک توصیف"
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr "خارج نشوید"
@@ -228,6 +264,9 @@ msgstr ""
msgid "Download"
msgstr "دانلود"
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr "شخص تکراری"
@@ -237,9 +276,15 @@ msgstr "نشست تکراری"
msgid "Edit"
msgstr "ویرایش"
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr "آدرس ایمیل"
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr "خروجی"
@@ -261,6 +306,9 @@ msgstr ""
msgid "Field"
msgstr "فیلد"
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr "فایل"
@@ -273,6 +321,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr "اندازه فونت"
@@ -306,6 +363,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr "راهنما"
@@ -351,6 +414,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr "نشان می دهد که این زبان اول پدر یا مادر است."
@@ -402,6 +471,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr "اصلاح شده"
@@ -411,18 +489,27 @@ msgstr "فیلدهای بیشتر"
msgid "Name"
msgstr "نام"
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr "فرد جدید"
msgid "New Session"
msgstr "نشست جدید"
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -438,6 +525,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr "یک یا چند پرونده در پروژه شما در حال کپی شدن هستند."
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr "بازکردن"
@@ -474,6 +567,12 @@ msgstr "فرد"
msgid "Please Register"
msgstr "لطفا ثبت نام کنید"
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -501,6 +600,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr "دوباره انجام دادن"
@@ -534,9 +636,18 @@ msgstr "گزارش یک مشکل"
msgid "Restart lameta and do the rename before playing the video again."
msgstr "لمتا را دوباره راه اندازی کنید، نام فایل را قبل از نمایش ویدئو تغییر دهید."
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr "نقش"
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr "ذخیره فایل به عنوان"
@@ -546,6 +657,15 @@ msgstr "در حال ذخیره فایل"
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -567,6 +687,12 @@ msgstr "نمایش همه"
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -594,6 +720,9 @@ msgstr "اندازه"
msgid "Smaller"
msgstr "کوچکتر"
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -606,9 +735,18 @@ msgstr ""
msgid "Start Screen"
msgstr "صفحه آغازین"
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr "وضعیت"
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr "متن"
@@ -618,6 +756,9 @@ msgstr "این نام منجر به یک پرونده خالی می شود."
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -657,9 +798,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -672,6 +819,9 @@ msgstr "تایپ کردن"
msgid "Undo"
msgstr "واگرد/ برگشت"
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr "بروزرسانی موجود است"
@@ -690,6 +840,15 @@ msgstr "دیدن"
msgid "View Release Notes"
msgstr "یادداشتهای انتشار را مشاهده کنید"
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -705,12 +864,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr "نمی توانید پرونده هایی از این نوع اضافه کنید"
msgid "You cannot add folders."
msgstr "نمی توانید فولدر اضافه کنید."
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr "آرشیو فایل زیپ"
@@ -744,6 +912,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr "لمتا نمیتواند فایل مورد نظر را تغییر نام بدهد."
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -753,6 +927,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr "لمتا نمیتواند فایل را حذف کند."
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr "لمتا قادر به حذف این فایل نیست."
@@ -769,8 +946,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr "نشستهای مشخص شده"
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/fr/messages.po b/locale/fr/messages.po
index a6d85653..8ea5adcd 100644
--- a/locale/fr/messages.po
+++ b/locale/fr/messages.po
@@ -45,6 +45,9 @@ msgstr "Abandonner les fichiers qui sont encore en cours de copie"
msgid "About lameta"
msgstr "A propos de Lameta"
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr "Ajouter des fichiers"
@@ -63,6 +66,15 @@ msgstr ""
msgid "All Sessions"
msgstr "Toutes les sessions"
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -82,6 +94,15 @@ msgstr ""
msgid "Audio"
msgstr "Audio"
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -94,6 +115,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr "Impossible d'ajouter des fichiers de ce type"
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr "Changer le nom à :"
@@ -121,6 +145,9 @@ msgstr "Effacer"
msgid "Close"
msgstr "Fermer"
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -157,6 +184,9 @@ msgstr "Copiage..."
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr "Créer"
@@ -181,6 +211,9 @@ msgstr "Reconnaissance"
msgid "Custom Field"
msgstr "Champs Personnalisées"
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr "Couper"
@@ -214,6 +247,9 @@ msgstr "Suppression en cours..."
msgid "Description Documents"
msgstr "Documents de description"
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr "Ne quittez pas"
@@ -226,6 +262,9 @@ msgstr ""
msgid "Download"
msgstr "Télécharger"
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr "Dupliquer la personne"
@@ -235,9 +274,15 @@ msgstr "Dupliquer la session"
msgid "Edit"
msgstr "Modifier"
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr "Adresse email"
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr "Exporter"
@@ -259,6 +304,9 @@ msgstr ""
msgid "Field"
msgstr "Champ"
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr "Fichier"
@@ -271,6 +319,15 @@ msgstr "Fichier à importer :"
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr "Taille de police"
@@ -304,6 +361,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr "Aide"
@@ -349,6 +412,12 @@ msgstr "Importer {chosenCount} Session(s)"
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr "Indiquez que c'est la langue principale du père ou de la mère."
@@ -400,6 +469,15 @@ msgstr "Dossier Multimédia"
msgid "Media Folder Settings..."
msgstr "Réglages Fichiers Multimédia..."
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr "Modifié"
@@ -409,18 +487,27 @@ msgstr "Champs supplémentaires"
msgid "Name"
msgstr "Nom"
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr "Nouvelle personne"
msgid "New Session"
msgstr "Nouvelle session"
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr "Normalement, lorsque vous ajoutez un fichier à un projet, une session ou une personne, ce fichier est copié dans le même dossier que votre projet lameta. Pour certains de vos fichiers, vous préférerez peut-être plutôt laisser les fichiers là où ils se trouvent et simplement les \"lier\" à eux."
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr "Notez qu'il s'agit d'un lien vers un fichier de votre répertoire Multimédia . Ici, vous êtes en train de renommer ce lien, pas le fichier d'origine vers lequel il pointe. Si vous créez ultérieurement une exportation qui inclut ce fichier, l'exportation utilisera ce nom."
@@ -436,6 +523,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr "Un ou plusieurs fichiers sont toujours en cours de copie dans votre projet."
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr "Ouvrir"
@@ -472,6 +565,12 @@ msgstr "Personne"
msgid "Please Register"
msgstr "Veuillez vous inscrire"
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr "Veuillez supprimer les caractères non autorisés par les paramètres d'archivage."
@@ -499,6 +598,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr "Refaire"
@@ -532,9 +634,18 @@ msgstr "Signaler un problème"
msgid "Restart lameta and do the rename before playing the video again."
msgstr "Redémarrez lameta et renommez avant de rejouer la vidéo."
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr "Rôle"
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr "Enregistrer sous"
@@ -544,6 +655,15 @@ msgstr "Sauvegarde"
msgid "Saving before rename..."
msgstr "Sauvegarder avant de renommer..."
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -565,6 +685,12 @@ msgstr "Tout afficher"
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -592,6 +718,9 @@ msgstr "Taille"
msgid "Smaller"
msgstr "Plus petit"
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr "Certains systèmes d'exploitation n'autoriseraient pas ce nom."
@@ -604,9 +733,18 @@ msgstr "Désolé, aucune barre oblique n'est autorisée"
msgid "Start Screen"
msgstr "Ecran de démarrage"
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr "État"
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr "Texte"
@@ -616,6 +754,9 @@ msgstr "Ce nom conduirait à un nom de fichier vide."
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr "Le fichier est absent de son emplacement prévu dans le dossier multimédia. Le dossier multimédia est défini sur {m} et ce fichier est censé être à {e}"
@@ -655,9 +796,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr "Ceci est un lien, mais Lameta ne sait pas où trouver le dossier multimédia pour ce projet. Voir Fichier:Paramètres du dossier multimédia."
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr "Ce paramètre est unique au compte que vous utilisez sur l’ordinateur. Un collègue sur un autre ordinateur devra définir cette valeur à l'emplacement correct sur son ordinateur."
@@ -670,6 +817,9 @@ msgstr "Type"
msgid "Undo"
msgstr "Annuler l'action"
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr "Une mise à jour est disponible"
@@ -688,6 +838,15 @@ msgstr "Affichage"
msgid "View Release Notes"
msgstr "Consulter les notes de versions"
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -706,12 +865,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr "Vous utilisez la version lameta:"
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr "Vous ne pouvez pas ajouter des fichiers de ce type"
msgid "You cannot add folders."
msgstr "Vous ne pouvez pas ajouter de dossiers."
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr "Archive ZIP"
@@ -745,6 +913,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr "lameta n'a pas pu renommer le répertoire."
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr "lameta s'attendait à trouver un .lien fichier sur {e}"
@@ -754,6 +928,9 @@ msgstr "lameta a essayé de renommer le dossier \"{oldFolderName}\" en \"{newFol
msgid "lameta was not able to delete the file."
msgstr "lameta n'a pas pu supprimer ce fichier."
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr "lameta n'a pas pu mettre ce fichier à la corbeille"
@@ -770,8 +947,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr "Lameta va maintenant ouvrir ce dossier sur votre disque dur ensuite va quitter. Vous devez ouvrir ces fichiers {projectType} dans un éditeur de texte et décider lequel vous voulez, et supprimer les autres. Celui que vous choisissez doit s'appeler {name}."
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr "{0} Sessions Cochées"
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/id/messages.po b/locale/id/messages.po
index e99003a3..df8bf647 100644
--- a/locale/id/messages.po
+++ b/locale/id/messages.po
@@ -45,6 +45,9 @@ msgstr "Abaikan file-file yang sedang disalin"
msgid "About lameta"
msgstr "Tentang lameta"
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr "Tambah File-File"
@@ -63,6 +66,15 @@ msgstr ""
msgid "All Sessions"
msgstr "Semua Sesi"
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -82,6 +94,15 @@ msgstr ""
msgid "Audio"
msgstr "Audio"
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -94,6 +115,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr "Tidak dapat tambah file jenis itu"
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr "Ubah Nama Menjadi:"
@@ -121,6 +145,9 @@ msgstr "Hapus"
msgid "Close"
msgstr "Tutup"
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -157,6 +184,9 @@ msgstr "Menyalin..."
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr "Buat"
@@ -181,6 +211,9 @@ msgstr "Kredit/Penghargaan"
msgid "Custom Field"
msgstr "Kotak yang disesuaikan"
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr "Potong"
@@ -214,6 +247,9 @@ msgstr "Hapus..."
msgid "Description Documents"
msgstr "Dokumen-Dokumen Keterangan"
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr "Jangan tutup"
@@ -226,6 +262,9 @@ msgstr ""
msgid "Download"
msgstr "Unduh"
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr "Duplikasi orang"
@@ -235,9 +274,15 @@ msgstr "Duplikasi Sesi"
msgid "Edit"
msgstr "Edit"
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr "Alamat Email"
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr "Ekspor"
@@ -259,6 +304,9 @@ msgstr ""
msgid "Field"
msgstr "Kotak"
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr "File"
@@ -271,6 +319,15 @@ msgstr "File untuk Impor:"
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr "Ukuran Font"
@@ -304,6 +361,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr "Bantuan"
@@ -349,6 +412,12 @@ msgstr "Impor {chosenCount} Sesi"
msgid "Importing..."
msgstr "Sedang mengimpor..."
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr "Menunjukkan bahwa ini adalah bahasa utama ayah atau ibu."
@@ -400,6 +469,15 @@ msgstr "Folder Media"
msgid "Media Folder Settings..."
msgstr "Pengaturan Folder Media..."
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr "Dimodifikasi"
@@ -409,18 +487,27 @@ msgstr "Kotak Lainnya"
msgid "Name"
msgstr "Nama"
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr "Orang Baru"
msgid "New Session"
msgstr "Sesi Baru"
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr "Biasanya, ketika Anda menambahkan file ke proyek, sesi, atau orang, file tersebut disalin ke dalam folder yang sama dengan proyek lameta Anda. Untuk beberapa file, mungkin Anda lebih suka tidak menyalin file tersebut dari lokasi asalnya dan hanya membuat 'tautan' ke file tersebut."
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr "Perhatikan bahwa ini adalah tautan ke file di direktori Media Files Anda. Di sini, Anda mengganti nama tautan ini, bukan file asli yang ditunjuknya. Jika nanti Anda membuat ekspor yang menyertakan file ini, ekspor tersebut akan menggunakan nama ini."
@@ -436,6 +523,12 @@ msgstr "OPEX + File-File"
msgid "One or more files are still being copied into your project."
msgstr "Satu atau beberapa file masih dalam proses penyalinan ke proyek Anda."
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr "Buka"
@@ -472,6 +565,12 @@ msgstr "Orang"
msgid "Please Register"
msgstr "Tolong Daftarkan"
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr "Silakan hapus karakter yang tidak diizinkan oleh Pengaturan Arsip."
@@ -499,6 +598,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr "Ulangi"
@@ -532,9 +634,18 @@ msgstr "Laporkan masalah"
msgid "Restart lameta and do the rename before playing the video again."
msgstr "Restart lameta dan lakukan penggantian nama sebelum memutar video lagi."
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr "Peran"
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr "Simpan Sebagai"
@@ -544,6 +655,15 @@ msgstr "Sedang menyimpan"
msgid "Saving before rename..."
msgstr "Menyimpan sebelum ubah nama..."
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -565,6 +685,12 @@ msgstr "Tampilkan Semua"
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -592,6 +718,9 @@ msgstr "Ukuran"
msgid "Smaller"
msgstr "Lebih kecil"
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr "Beberapa sistem operasi mungkin tidak mengizinkan nama tersebut."
@@ -604,9 +733,18 @@ msgstr "Maaf, tidak diperbolehkan menggunakan tanda garis miring (\"/\")"
msgid "Start Screen"
msgstr "Layar Awal"
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr "Status"
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr "Teks"
@@ -616,6 +754,9 @@ msgstr "Nama itu akan menghasilkan nama file kosong."
msgid "The IMDI files were validated."
msgstr "File IMDI telah divalidasi."
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr "File tersebut tidak ada di lokasi yang diharapkan di Folder Media. Folder Media diatur ke {m} dan fili ini seharusnya berada di {e}"
@@ -655,9 +796,15 @@ msgstr "."
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr "Ini adalah sebuah tautan, namun lameta tidak tahu di mana Folder Media untuk proyek ini. Lihat file: Pengaturan Folder Media."
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr "Pengaturan ini unik untuk akun yang Anda gunakan di komputer ini. Seorang rekan di komputer lain perlu mengatur nilai ini ke lokasi yang benar di komputer mereka."
@@ -670,6 +817,9 @@ msgstr "Tipe"
msgid "Undo"
msgstr "Batalkan"
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr "Pembaruan/Update tersedia"
@@ -688,6 +838,15 @@ msgstr "Lihat"
msgid "View Release Notes"
msgstr "Lihat Catatan Rilis"
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -703,12 +862,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr "Anda menjalankan versi lameta:"
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr "Tidak dapat tambah file jenis itu"
msgid "You cannot add folders."
msgstr "Anda tidak bisa menambahkan folder."
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr "Arsip ZIP"
@@ -742,6 +910,12 @@ msgstr "lameta tidak dapat menemukan Orang yang sesuai dengan kontributor beriku
msgid "lameta could not rename the directory."
msgstr "lameta tidak bisa mengubah nama direktori tersebut."
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr "lameta mengharapkan menemukan sebuah file .link di {e}"
@@ -751,6 +925,9 @@ msgstr "lameta mencoba mengubah nama folder \"{oldFolderName}\" menjadi \"{newFo
msgid "lameta was not able to delete the file."
msgstr "lameta tidak dapat menghapus file tersebut."
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr "lameta tidak dapat memindahkan file ini ke tempat sampah"
@@ -767,8 +944,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr "Sekarang, lameta akan membuka folder ini di hard disk Anda dan kemudian akan keluar. Anda sebaiknya membuka file-file {projectType} ini di editor teks dan memutuskan mana yang ingin Anda gunakan, kemudian hapus yang lainnya. File yang Anda pilih sebaiknya dinamai {name}."
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr "{0} Sesi yang Ditandai"
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr "{descriptionOfWhatWillBeDeleted} akan dipindahkan ke Tempat Sampah"
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/ps/messages.po b/locale/ps/messages.po
index 8419695a..d5f0e63c 100644
--- a/locale/ps/messages.po
+++ b/locale/ps/messages.po
@@ -40,6 +40,9 @@ msgstr ""
msgid "About lameta"
msgstr ""
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr ""
@@ -58,6 +61,15 @@ msgstr ""
msgid "All Sessions"
msgstr ""
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -77,6 +89,15 @@ msgstr ""
msgid "Audio"
msgstr ""
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -89,6 +110,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr ""
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr ""
@@ -116,6 +140,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -152,6 +179,9 @@ msgstr ""
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -176,6 +206,9 @@ msgstr ""
msgid "Custom Field"
msgstr ""
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr ""
@@ -209,6 +242,9 @@ msgstr ""
msgid "Description Documents"
msgstr ""
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr ""
@@ -221,6 +257,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr ""
@@ -230,9 +269,15 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr ""
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr ""
@@ -254,6 +299,9 @@ msgstr ""
msgid "Field"
msgstr ""
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr ""
@@ -266,6 +314,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr ""
@@ -299,6 +356,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr ""
@@ -344,6 +407,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr ""
@@ -395,6 +464,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr ""
@@ -404,18 +482,27 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr ""
msgid "New Session"
msgstr ""
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -431,6 +518,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr ""
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr ""
@@ -467,6 +560,12 @@ msgstr ""
msgid "Please Register"
msgstr ""
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -494,6 +593,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr ""
@@ -527,9 +629,18 @@ msgstr ""
msgid "Restart lameta and do the rename before playing the video again."
msgstr ""
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr ""
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr ""
@@ -539,6 +650,15 @@ msgstr ""
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -560,6 +680,12 @@ msgstr ""
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -587,6 +713,9 @@ msgstr ""
msgid "Smaller"
msgstr ""
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -599,9 +728,18 @@ msgstr ""
msgid "Start Screen"
msgstr ""
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr ""
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr ""
@@ -611,6 +749,9 @@ msgstr ""
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -650,9 +791,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -665,6 +812,9 @@ msgstr ""
msgid "Undo"
msgstr ""
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr ""
@@ -683,6 +833,15 @@ msgstr ""
msgid "View Release Notes"
msgstr ""
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -698,12 +857,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr ""
msgid "You cannot add folders."
msgstr ""
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr ""
@@ -737,6 +905,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr ""
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -746,6 +920,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr ""
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr ""
@@ -762,8 +939,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr ""
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/pt-BR/messages.po b/locale/pt-BR/messages.po
index 53301b14..6ef1e00d 100644
--- a/locale/pt-BR/messages.po
+++ b/locale/pt-BR/messages.po
@@ -40,6 +40,9 @@ msgstr ""
msgid "About lameta"
msgstr ""
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr ""
@@ -58,6 +61,15 @@ msgstr ""
msgid "All Sessions"
msgstr ""
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -77,6 +89,15 @@ msgstr ""
msgid "Audio"
msgstr ""
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -89,6 +110,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr ""
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr ""
@@ -116,6 +140,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -152,6 +179,9 @@ msgstr ""
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -176,6 +206,9 @@ msgstr ""
msgid "Custom Field"
msgstr ""
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr ""
@@ -209,6 +242,9 @@ msgstr ""
msgid "Description Documents"
msgstr ""
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr ""
@@ -221,6 +257,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr ""
@@ -230,9 +269,15 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr ""
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr ""
@@ -254,6 +299,9 @@ msgstr ""
msgid "Field"
msgstr ""
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr ""
@@ -266,6 +314,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr ""
@@ -299,6 +356,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr ""
@@ -344,6 +407,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr ""
@@ -395,6 +464,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr ""
@@ -404,18 +482,27 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr ""
msgid "New Session"
msgstr ""
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -431,6 +518,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr ""
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr ""
@@ -467,6 +560,12 @@ msgstr ""
msgid "Please Register"
msgstr ""
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -494,6 +593,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr ""
@@ -527,9 +629,18 @@ msgstr ""
msgid "Restart lameta and do the rename before playing the video again."
msgstr ""
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr ""
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr ""
@@ -539,6 +650,15 @@ msgstr ""
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -560,6 +680,12 @@ msgstr ""
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -587,6 +713,9 @@ msgstr ""
msgid "Smaller"
msgstr ""
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -599,9 +728,18 @@ msgstr ""
msgid "Start Screen"
msgstr ""
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr ""
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr ""
@@ -611,6 +749,9 @@ msgstr ""
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -650,9 +791,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -665,6 +812,9 @@ msgstr ""
msgid "Undo"
msgstr ""
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr ""
@@ -683,6 +833,15 @@ msgstr ""
msgid "View Release Notes"
msgstr ""
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -698,12 +857,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr ""
msgid "You cannot add folders."
msgstr ""
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr ""
@@ -737,6 +905,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr ""
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -746,6 +920,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr ""
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr ""
@@ -762,8 +939,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr ""
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/ru/messages.po b/locale/ru/messages.po
index ba8c56dd..e823b921 100644
--- a/locale/ru/messages.po
+++ b/locale/ru/messages.po
@@ -45,6 +45,9 @@ msgstr ""
msgid "About lameta"
msgstr ""
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr "Добавление файлов"
@@ -63,6 +66,15 @@ msgstr ""
msgid "All Sessions"
msgstr ""
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -82,6 +94,15 @@ msgstr ""
msgid "Audio"
msgstr "Аудио"
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -94,6 +115,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr ""
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr "Изменить имя на:"
@@ -121,6 +145,9 @@ msgstr ""
msgid "Close"
msgstr "Закрыть"
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -157,6 +184,9 @@ msgstr ""
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -181,6 +211,9 @@ msgstr ""
msgid "Custom Field"
msgstr ""
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr ""
@@ -214,6 +247,9 @@ msgstr ""
msgid "Description Documents"
msgstr "Документы описания"
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr ""
@@ -226,6 +262,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr ""
@@ -235,9 +274,15 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr ""
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr "Экспортировать"
@@ -259,6 +304,9 @@ msgstr ""
msgid "Field"
msgstr "Поле"
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr ""
@@ -271,6 +319,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr ""
@@ -304,6 +361,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr "Справка"
@@ -349,6 +412,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr ""
@@ -400,6 +469,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr ""
@@ -409,18 +487,27 @@ msgstr ""
msgid "Name"
msgstr "Имя"
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr "Новое лицо"
msgid "New Session"
msgstr "Новое событие"
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -436,6 +523,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr ""
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr "Открыть"
@@ -472,6 +565,12 @@ msgstr "Лицо"
msgid "Please Register"
msgstr ""
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -499,6 +598,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr ""
@@ -532,9 +634,18 @@ msgstr ""
msgid "Restart lameta and do the rename before playing the video again."
msgstr ""
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr "Роль"
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr ""
@@ -544,6 +655,15 @@ msgstr ""
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -565,6 +685,12 @@ msgstr ""
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -592,6 +718,9 @@ msgstr "Размер"
msgid "Smaller"
msgstr ""
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -604,9 +733,18 @@ msgstr ""
msgid "Start Screen"
msgstr ""
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr "Статус"
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr ""
@@ -616,6 +754,9 @@ msgstr ""
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -655,9 +796,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -670,6 +817,9 @@ msgstr "Тип"
msgid "Undo"
msgstr "Отменить"
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr ""
@@ -688,6 +838,15 @@ msgstr "Вид"
msgid "View Release Notes"
msgstr ""
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -703,12 +862,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr ""
msgid "You cannot add folders."
msgstr ""
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr ""
@@ -742,6 +910,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr ""
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -751,6 +925,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr ""
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr ""
@@ -767,8 +944,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr ""
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/locale/zh-cn/messages.po b/locale/zh-cn/messages.po
index 6bcb63fc..293c7731 100644
--- a/locale/zh-cn/messages.po
+++ b/locale/zh-cn/messages.po
@@ -43,6 +43,9 @@ msgstr ""
msgid "About lameta"
msgstr ""
+msgid "Access"
+msgstr ""
+
msgid "Add Files"
msgstr ""
@@ -61,6 +64,15 @@ msgstr ""
msgid "All Sessions"
msgstr ""
+msgid "All translations complete"
+msgstr ""
+
+msgid "Always"
+msgstr ""
+
+msgid "Always available on this device."
+msgstr ""
+
msgid "Archive"
msgstr ""
@@ -80,6 +92,15 @@ msgstr ""
msgid "Audio"
msgstr ""
+msgid "Automatically make cloud files available if smaller than"
+msgstr ""
+
+msgid "Available after this file is made available on this device"
+msgstr ""
+
+msgid "Available on this device. {providerName} may free up this space if the file goes unused."
+msgstr ""
+
msgid "CSV export complete"
msgstr ""
@@ -92,6 +113,9 @@ msgstr ""
msgid "Cannot add files of that type"
msgstr ""
+msgid "Cannot export to IMDI while multilingual text migration is pending."
+msgstr ""
+
msgid "Change Name To:"
msgstr ""
@@ -119,6 +143,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Cloud"
+msgstr ""
+
msgid "Collection"
msgstr ""
@@ -155,6 +182,9 @@ msgstr ""
msgid "Could not find this file. Restart lameta to bring it up to date with what is actually on your hard drive. {path}"
msgstr ""
+msgid "Could not read media information for this file."
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -179,6 +209,9 @@ msgstr ""
msgid "Custom Field"
msgstr ""
+msgid "Custom vocabulary item"
+msgstr ""
+
msgid "Cut"
msgstr ""
@@ -212,6 +245,9 @@ msgstr ""
msgid "Description Documents"
msgstr ""
+msgid "Dismiss"
+msgstr ""
+
msgid "Do not quit"
msgstr ""
@@ -224,6 +260,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download this file to my computer"
+msgstr ""
+
msgid "Duplicate Person"
msgstr ""
@@ -233,9 +272,15 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "ElectronDropZone: preload script did not set window.electronAPI.getPathForFile; drag-and-drop will not work."
+msgstr ""
+
msgid "Email Address"
msgstr ""
+msgid "Example:"
+msgstr ""
+
msgid "Export"
msgstr ""
@@ -257,6 +302,9 @@ msgstr ""
msgid "Field"
msgstr ""
+msgid "Fields with Unknown Languages"
+msgstr ""
+
msgid "File"
msgstr ""
@@ -269,6 +317,15 @@ msgstr ""
msgid "Find"
msgstr ""
+msgid "Finish Migration"
+msgstr ""
+
+msgid "Finish Migration to Multilingual Fields"
+msgstr ""
+
+msgid "Fix in Project:"
+msgstr ""
+
msgid "Font Size"
msgstr ""
@@ -302,6 +359,12 @@ msgstr ""
msgid "Generating RO-Crate metadata..."
msgstr ""
+msgid "Genres"
+msgstr ""
+
+msgid "Getting project information from {0}..."
+msgstr ""
+
msgid "Help"
msgstr ""
@@ -347,6 +410,12 @@ msgstr ""
msgid "Importing..."
msgstr ""
+msgid "In cloud only, and this computer appears to be offline."
+msgstr ""
+
+msgid "In cloud only, lameta cannot read it yet."
+msgstr ""
+
msgid "Indicates that this is the father or mother's primary language."
msgstr ""
@@ -398,6 +467,15 @@ msgstr ""
msgid "Media Folder Settings..."
msgstr ""
+msgid "Migrate"
+msgstr ""
+
+msgid "Migrating: {0}"
+msgstr ""
+
+msgid "Migration complete! {0} fields were migrated. You can hide language tags from View → \"Show language tags on multilingual fields\" if you don't need them anymore."
+msgstr ""
+
msgid "Modified"
msgstr ""
@@ -407,18 +485,27 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Never"
+msgstr ""
+
msgid "New Person"
msgstr ""
msgid "New Session"
msgstr ""
+msgid "No custom values found"
+msgstr ""
+
msgid "Normally, when you add a file to a project, session, or person, that file is copied into the same folder as you lameta project. For some of your files, you might prefer to instead leave the files where they are and just 'link' to them."
msgstr ""
msgid "Not important to the archive, but will be included"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this is a link to a file in your Media Files directory. Here, you are renaming this link, not the original file that it points to. If you later create an export that includes this file, the export will use this name."
msgstr ""
@@ -434,6 +521,12 @@ msgstr ""
msgid "One or more files are still being copied into your project."
msgstr ""
+msgid "Online-only ({providerName}), and this computer appears to be offline, so the file cannot be downloaded right now."
+msgstr ""
+
+msgid "Online-only ({providerName}). The content of this file is not on this computer."
+msgstr ""
+
msgid "Open"
msgstr ""
@@ -470,6 +563,12 @@ msgstr ""
msgid "Please Register"
msgstr ""
+msgid "Please add the metadata languages you've been using with these \"/\" fields (at least two) before migrating."
+msgstr ""
+
+msgid "Please complete the migration in the Project tab under Languages before exporting."
+msgstr ""
+
msgid "Please remove characters that not allowed by the Archive Settings."
msgstr ""
@@ -497,6 +596,9 @@ msgstr ""
msgid "RO-Crate export complete"
msgstr ""
+msgid "Ready to scan..."
+msgstr ""
+
msgid "Redo"
msgstr ""
@@ -530,9 +632,18 @@ msgstr ""
msgid "Restart lameta and do the rename before playing the video again."
msgstr ""
+msgid "Retry"
+msgstr ""
+
+msgid "Retrying…"
+msgstr ""
+
msgid "Role"
msgstr ""
+msgid "Roles"
+msgstr ""
+
msgid "Save As"
msgstr ""
@@ -542,6 +653,15 @@ msgstr ""
msgid "Saving before rename..."
msgstr ""
+msgid "Scan complete"
+msgstr ""
+
+msgid "Scan failed"
+msgstr ""
+
+msgid "Scanning..."
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -563,6 +683,12 @@ msgstr ""
msgid "Show IMDI previews"
msgstr ""
+msgid "Show Language Tags"
+msgstr ""
+
+msgid "Show Language Tags (locked during migration)"
+msgstr ""
+
msgid "Show PARADISEC previews"
msgstr ""
@@ -590,6 +716,9 @@ msgstr ""
msgid "Smaller"
msgstr ""
+msgid "Some fields have more language segments than there are Metadata Languages defined ({0} of {1} fields). If you proceed, these extra segments will be labeled as \"unknown\" languages."
+msgstr ""
+
msgid "Some operating systems would not allow that name."
msgstr ""
@@ -602,9 +731,18 @@ msgstr ""
msgid "Start Screen"
msgstr ""
+msgid "Starting scan..."
+msgstr ""
+
msgid "Status"
msgstr ""
+msgid "Stop waiting"
+msgstr ""
+
+msgid "Tell {providerName} that I want this file on my computer"
+msgstr ""
+
msgid "Text"
msgstr ""
@@ -614,6 +752,9 @@ msgstr ""
msgid "The IMDI files were validated."
msgstr ""
+msgid "The columns here are based on your project's metadata languages."
+msgstr ""
+
msgid "The file is missing from its expected location in the Media Folder. The Media Folder is set to {m} and this file is supposed to be at {e}"
msgstr ""
@@ -653,9 +794,15 @@ msgstr ""
msgid "This file does not comply with the file naming rules of the current archive."
msgstr ""
+msgid "This file is online-only ({providerName}). Select it to see how to make it available on this device."
+msgstr ""
+
msgid "This is a link, but lameta does not know where to find the Media Folder for this project. See File:Media Folder Settings."
msgstr ""
+msgid "This project appears to use \"/\" to store multiple languages within a single field. In this version of lameta, we have a better approach in which lameta knows which languages are used in a field. After you have selected the appropriate metadata languages above and arranged them in the correct order, all multilingual fields in Project, Sessions, and People should display with the correct language tags. Once you have confirmed that everything appears as expected, click the button below to apply these changes permanently."
+msgstr ""
+
msgid "This setting is unique to the account you are using on this computer. A colleague on another computer will need to set this value to the correct location on their computer."
msgstr ""
@@ -668,6 +815,9 @@ msgstr ""
msgid "Undo"
msgstr ""
+msgid "Unknown error while scanning vocabulary"
+msgstr ""
+
msgid "Update available"
msgstr ""
@@ -686,6 +836,15 @@ msgstr ""
msgid "View Release Notes"
msgstr ""
+msgid "Vocabulary Translations"
+msgstr ""
+
+msgid "Vocabulary scan failed"
+msgstr ""
+
+msgid "Waiting"
+msgstr ""
+
msgid ""
"We don't have specific tips for this error. If you cannot solve it by\n"
"restarting your computer, please report the problem to\n"
@@ -701,12 +860,21 @@ msgstr ""
msgid "You are running lameta version:"
msgstr ""
+msgid "You can either add more Metadata Languages above to match all segments, or proceed with the migration and manually fix the unknown language fields later."
+msgstr ""
+
msgid "You cannot add files of that type"
msgstr ""
msgid "You cannot add folders."
msgstr ""
+msgid "You need at least two metadata languages defined to use vocabulary translations. Please add metadata languages in the Languages tab."
+msgstr ""
+
+msgid "You requested this file less than a minute ago. lameta will show it when it becomes available."
+msgstr ""
+
msgid "ZIP Archive"
msgstr ""
@@ -740,6 +908,12 @@ msgstr ""
msgid "lameta could not rename the directory."
msgstr ""
+msgid "lameta couldn't download 1 file from {providerName}. It may be offline or busy right now."
+msgstr ""
+
+msgid "lameta couldn't download {count} files from {providerName}. It may be offline or busy right now."
+msgstr ""
+
msgid "lameta expected to find a .link file at {e}"
msgstr ""
@@ -749,6 +923,9 @@ msgstr ""
msgid "lameta was not able to delete the file."
msgstr ""
+msgid "lameta was not able to make this file available on this device."
+msgstr ""
+
msgid "lameta was not able to put this file in the trash"
msgstr ""
@@ -765,8 +942,32 @@ msgstr ""
msgid "lameta will now open this folder on your hard disk and then exit. You should open these {projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named {name}."
msgstr ""
+msgid "missing"
+msgstr ""
+
+msgid "recorded when the file was last on this device"
+msgstr ""
+
+msgid "the cloud"
+msgstr ""
+
msgid "{0} Marked Sessions"
msgstr ""
+msgid "{0} of {1} items processed"
+msgstr ""
+
msgid "{descriptionOfWhatWillBeDeleted} will be moved to the Trash"
msgstr ""
+
+msgid "{minutesSinceRequest, plural, one {# minute since you requested this file. lameta will show it when it becomes available.} other {# minutes since you requested this file. lameta will show it when it becomes available.}}"
+msgstr ""
+
+msgid "{providerName} Status"
+msgstr ""
+
+msgid "{providerName} is downloading this file to this computer."
+msgstr ""
+
+msgid "{totalMissing} items need translation"
+msgstr ""
diff --git a/package.json b/package.json
index 9de1f36a..529234c3 100644
--- a/package.json
+++ b/package.json
@@ -198,7 +198,6 @@
"fs-extra": "^11.2.0",
"fuse.js": "^7.0.0",
"glob": "^9.3.3",
- "graceful-fs": "^4.2.11",
"history": "^4.6.1",
"hotkeys-js": "^3.3.1",
"humanize-duration": "^3.15.3",
@@ -276,5 +275,8 @@
"vite-electron-plugin": "upgrade from 12 to 14 broke because they apparently don't support node in renderer anymore",
"electron-builder": "upgrade from 25 to 26 broke, package just hangs.",
"react-dropzone": "They broke electron in 14.3.0 https://github.com/react-dropzone/react-dropzone/issues/1411"
+ },
+ "optionalDependencies": {
+ "fswin": "^3.25.1108"
}
}
diff --git a/readme-cloud-testing.md b/readme-cloud-testing.md
new file mode 100644
index 00000000..c65f513c
--- /dev/null
+++ b/readme-cloud-testing.md
@@ -0,0 +1,256 @@
+# Manual test plan: cloud sync (macOS + Windows)
+
+How to gain confidence in lameta's cloud-sync support, with emphasis on boundary
+conditions that could lose data or crash the app at startup. Written for a human
+tester on macOS 12.3+ (FileProvider era); a Windows regression pass is at the end.
+
+Implementation background that shapes these tests:
+
+- lameta detects cloud placeholders by **dataless stat** (`size > 0, blocks == 0`)
+ but only for paths under a known **sync root**: `~/Library/CloudStorage/*`
+ (OneDrive, Dropbox, Google Drive, Box), home-folder symlinks into it
+ (`~/OneDrive`), iCloud Drive (`~/Library/Mobile Documents/com~apple~CloudDocs`),
+ and — when Desktop & Documents sync is on — `~/Desktop` and `~/Documents`.
+- "Download" on macOS is a **one-shot fetch** (there is no durable pin like
+ Windows' "Always keep on this device"); the provider may re-evict files later.
+- At project load, lameta reads every `.sprj` / `.session` / `.person` / `.meta`
+ file synchronously; evicted ones hydrate on demand during that read. A failed
+ hydration should trip a **circuit breaker**: one "couldn't reach {provider}"
+ banner with Retry — never a storm of error dialogs, never an aborted load.
+- Auto-fetch: selecting a cloud-only file smaller than the threshold
+ (**default 10 MB**, File menu) downloads it after a ~1.5 s dwell. Skipped while
+ offline.
+- Offline detection uses the OS "network up" signal (`navigator.onLine`).
+
+## Tester's toolbox
+
+| Action | How |
+|---|---|
+| Evict an iCloud file (make it cloud-only) | `brctl evict ` in Terminal, or Finder → right-click → Remove Download |
+| Evict a OneDrive/Dropbox/GDrive file | Finder → right-click → Remove Download, or `fileproviderctl evict ` |
+| Check whether a file is really dataless | `stat -f "blocks=%b size=%z" ` → dataless = `blocks=0` with `size>0` |
+| Force download outside lameta | open the file in Finder, or `brctl download ` (iCloud) |
+| Go offline | Wi-Fi off in menu bar (see also the "LAN but no internet" case below) |
+| Pause OneDrive without going offline | OneDrive menu-bar icon → Pause syncing |
+| Watch lameta's logs | run a dev build (`yarn dev`) and watch the terminal, or Console.app filtered to lameta |
+
+Use a **throwaway copy** of a real project for anything in the data-loss section.
+Keep a pristine zip of it outside any cloud folder so you can diff afterwards
+(`diff -r pristine/ tested/` ignoring media).
+
+---
+
+## 1. Smoke test, per provider
+
+Run this list once per provider you care about — at minimum **OneDrive** and
+**iCloud Drive** (they use different plumbing), ideally also Dropbox and Google
+Drive (untested by the developers so far):
+
+- [ ] Put a project in the provider's folder, let it sync, evict all media.
+- [ ] Open the project: evicted files show a **blue cloud icon** and grey name;
+ local files show no icon.
+- [ ] Select an evicted file > 10 MB: the "{Provider} Status" card appears with
+ the provider's real name in the title, and a green **Download this file to
+ my computer** button. It must NOT download by itself.
+- [ ] Click Download: card shows "Waiting" + spinner, row icon becomes sync
+ arrows; when the download finishes, the preview (player/image/text)
+ appears by itself — no reselect needed.
+- [ ] Select an evicted file < 10 MB and just wait ~2 s: it downloads by itself
+ (auto-fetch) and the preview appears.
+- [ ] "Stop waiting" during a large download returns the card to the Download
+ state (the provider may quietly finish the download anyway — that's fine).
+- [ ] "Show in Finder" opens the enclosing folder.
+- [ ] Open the same project through the other path spelling where applicable:
+ `~/OneDrive/...` (symlink) vs `~/Library/CloudStorage/OneDrive-.../...`.
+ Both must behave identically.
+- [ ] iCloud only: put the project in `~/Documents` (with Desktop & Documents
+ sync on) rather than in iCloud Drive proper; icons and card must still
+ appear and say "iCloud Drive".
+
+## 2. Startup / project-load boundaries (crash hunting)
+
+These are the highest-value crash tests. After each, the bar is: **lameta opens,
+shows what it can, and shows at most one banner** — no crash, no dialog storm,
+no half-written files.
+
+- [ ] **Everything evicted, online.** Evict the entire project folder including
+ the `.sprj` itself (Finder → Remove Download on the folder). Open lameta.
+ Expect: project loads with all metadata intact. lameta prefetches all
+ evicted metadata files in parallel (watch for the
+ `[cloudMetadataPrefetch] Prefetching N…` log line), so the load should
+ take roughly as long as the slowest single file, not minutes — but the UI
+ still freezes without feedback for that duration. Note how bad it feels;
+ a progress indicator may still be warranted.
+- [ ] **Everything evicted, offline.** Same starting point, Wi-Fi off, open
+ lameta. Expect: single "couldn't reach {provider}" banner with Retry; rows
+ show a cloud-unavailable look; NO crash; NO storm of provider error
+ dialogs. Turn Wi-Fi on, press Retry: everything loads.
+- [ ] **Metadata evicted, media local** and the reverse. Both should load fine.
+- [ ] **Wi-Fi dies mid-load.** Start opening a fully-evicted project online,
+ turn Wi-Fi off while sessions are still appearing. Expect: banner for the
+ remainder, already-loaded sessions fine, Retry finishes the job later.
+- [ ] **Provider app not running.** Quit OneDrive completely (menu bar icon →
+ Quit), then open a project inside the OneDrive folder with evicted files.
+ Expect: files may read as unavailable and stat calls can take a long time
+ (macOS gives "Operation timed out" after ~1 min per access when the
+ provider extension is dead) — lameta must survive it, however slowly, and
+ recover after OneDrive restarts. This is a known rough edge: note the hang
+ duration.
+- [ ] **Provider signed out / mid-setup.** Sign out of OneDrive (or test right
+ after first sign-in while it's still indexing) and open the project.
+ Same expectation as above.
+- [ ] **Zero-byte and truncated metadata.** Manually create a 0-byte
+ `x.session` file and a `y.session` containing half an XML document inside
+ a session folder (simulates an interrupted sync). Open the project.
+ Expect: one error message per broken file — not a crash. lameta now
+ refuses to save over an unparseable metadata file (unit-tested), so after
+ editing and quitting, verify the broken file's bytes on disk are
+ unchanged (recoverable by hand or from the provider's version history).
+- [ ] **Legacy iCloud placeholder.** If you can find/construct one, a session
+ folder where the real file is missing and only `.name.session.icloud`
+ exists. Expect: treated as cloud-only, not as "file missing".
+- [ ] **Project outside any cloud folder** (plain local). Everything behaves as
+ before this feature existed: no icons, no cards, no cloud code in the way.
+- [ ] **Reopen loop.** Open/close the app 5 times in a row against a cloud
+ project, alternating online/offline. Watch for crashes on the 2nd+ launch
+ (stale state, half-hydrated files).
+
+## 3. Losing the network at every awkward moment
+
+- [ ] **During a download.** Click Download on a ~100 MB file, kill Wi-Fi at
+ ~50%. Expect: "Waiting" persists (with the minutes counter hidden and the
+ offline wording shown); when Wi-Fi returns, the download completes and the
+ preview appears. "Stop waiting" must work while offline.
+- [ ] **Between selection and auto-fetch.** Select a small evicted file and cut
+ Wi-Fi within a second. Expect: no download starts, no hang; card shows
+ offline wording; Download button disabled (grey).
+- [ ] **Offline UI coherence.** While offline with a cloud-only file selected:
+ row icons show the crossed-out cloud, card says "…appears to be offline",
+ Download button is grey/disabled, "Show in Finder" still works. Going back
+ online re-enables the button *without* reselecting or restarting.
+- [ ] **Router up, internet down.** Unplug the router's WAN side (or block the
+ provider's traffic) while Wi-Fi stays associated. The OS still reports
+ "online", so lameta will allow a Download that cannot complete.
+ Expect: card sits in "Waiting" indefinitely with the minutes counter
+ climbing; "Stop waiting" recovers. This is a known blind spot — confirm
+ nothing worse than an honest endless wait happens.
+- [ ] **Captive-ish flakiness.** Toggle Wi-Fi off/on repeatedly (5×, a few
+ seconds apart) while a project with evicted files is open and a download
+ is running. Expect: no crash, status icons settle correctly afterwards.
+
+## 4. Data-loss risk scenarios (use a throwaway project!)
+
+The theme: lameta writes metadata files and renames files/folders; the sync
+engine moves data underneath it. Diff against your pristine copy after each.
+
+- [ ] **Edit metadata while offline.** Open project offline (metadata already
+ local), edit session titles/notes, quit, go online, let it sync. On a
+ second machine (or web view), confirm the edits arrived and no
+ "conflicted copy" files appeared.
+- [ ] **Two-machine conflict.** Edit the *same session* on two machines while
+ one is offline, then reconnect. The provider will create a conflict file
+ (`ETR009 (conflicted copy).session`, `-DESKTOP-XYZ` suffixes, etc.).
+ Reopen in lameta. Documented (unit-tested) behavior: the real session
+ loads normally and the conflicted copy appears as an ordinary attached
+ file in the file list — visible for a human to resolve, never merged or
+ silently chosen. Confirm that holds with a real provider-generated
+ conflict file, and that opening/deleting the conflict file from within
+ lameta works.
+- [ ] **Rename with evicted files.** Change a session's ID (which renames the
+ folder and every file in it) while that session's media is evicted.
+ Expect: rename succeeds, placeholders stay placeholders under the new
+ name, and after downloading, content matches the pristine copy. Repeat
+ while offline; repeat again mid-download of one of the files (this is the
+ scariest one — a rename racing an in-flight materialization).
+- [ ] **"Free Up Space" under a running app.** With lameta open and a session
+ selected, use Finder/OneDrive to evict that session's media (and then,
+ separately, its already-read `.session` file). Expect: icons flip back to
+ clouds on the next poll/selection; nothing crashes; editing and saving
+ the session still works (the save must rehydrate or rewrite, never write
+ a 0-byte file).
+- [ ] **Evict while playing.** Play a long audio file and evict it mid-playback.
+ Playback will die — acceptable — but lameta must not crash, and the file
+ must return to cloud-only state cleanly.
+- [ ] **Quit mid-download / mid-save.** Quit lameta (and once, force-quit) while
+ a large download is in "Waiting" and again immediately after editing
+ metadata. Relaunch. Expect: clean startup; the metadata file on disk is
+ whole (never truncated); the interrupted download either finished (the
+ provider owns it) or the file is still a valid placeholder.
+- [ ] **Add Files while offline.** Use "Add Files" to copy new media into a
+ cloud project while offline, quit, reconnect. Expect: files upload, the
+ project on a second machine sees them, nothing is lost.
+- [ ] **Delete vs evict confusion.** Delete a file from within lameta while it
+ is cloud-only (0 local bytes). Confirm the deletion propagates to the
+ cloud (check the provider's web UI + trash) — and that lameta's trash
+ behavior (`.trash` folder) works when the file had no local content.
+- [ ] **Move the whole project** (Finder-drag it between a cloud and a
+ non-cloud folder) while lameta has it open. Expect: bad things may happen
+ to the open session, but no data destroyed and next launch recovers
+ (worst case: "project not found" and a clean re-open).
+
+## 5. Boundary values & odd files
+
+- [ ] File exactly 0 bytes in the cloud (e.g. the sample project's empty
+ `.txt`): must read as **local** (a 0-byte file has 0 blocks — make sure it
+ isn't misread as cloud-only, which would block its preview forever).
+- [ ] File just under / just over the 10 MB auto-fetch threshold; then set the
+ threshold to "never" and "always" in the File menu and confirm both
+ extremes behave.
+- [ ] A genuinely **sparse** local file inside the cloud folder if you can make
+ one (`dd if=/dev/zero of=sparse bs=1 count=1 seek=100m` then evict/restore)
+ — and one *outside* any cloud folder (must show no cloud UI at all).
+- [ ] Filenames with spaces, `é`/diacritics (NFC/NFD!), emoji, very long names;
+ a session folder > 200 chars deep. Evict, reopen, download.
+- [ ] Case games: rename `Sound.WAV` → `sound.wav` on one machine while evicted
+ on the other (APFS and OneDrive are both case-insensitive-ish; watch for
+ duplicate/lost files).
+- [ ] A file that exists in the provider's web UI but was **deleted locally**
+ (sync lag): row should disappear or show missing — not crash.
+- [ ] Hundreds of files in one session, all evicted: file list stays responsive
+ (status is polled per file); select-all-ish operations don't trigger a
+ mass download.
+
+## 6. Provider misbehavior
+
+- [ ] Kill the provider's process (`killall OneDrive`) during a download.
+ Expect: eventual failure back to cloud-only or endless Waiting +
+ functional "Stop waiting" — no crash. ⚠️ The exact error signature macOS
+ returns here feeds lameta's "couldn't reach provider" detection and is
+ only provisionally implemented — please capture lameta's log output for
+ the developers when you do this.
+- [ ] OneDrive "Pause syncing" (2 h) with evicted metadata, then open the
+ project: does it hydrate anyway (OS-level requests sometimes bypass
+ pause), or produce the single banner? Either is acceptable; dialogs
+ storming or a crash is not.
+- [ ] Storage quota full on the provider (if testable): uploading new files
+ from a lameta project fails at the provider level; lameta shouldn't care
+ or corrupt anything.
+- [ ] iCloud "Optimize Mac Storage" ON with a nearly-full disk: let macOS evict
+ things on its own overnight, then reopen the project.
+- [ ] Uninstall/reinstall the provider between lameta runs (sync-root list is
+ cached per app-run; a re-launch must pick up the new state).
+
+## 7. Windows regression pass (the interface changed underneath it)
+
+Quick pass on Windows 10/11 + OneDrive to confirm nothing regressed:
+
+- [ ] Icons, status card, and the **checkbox** (not a button — Windows keeps
+ "Tell OneDrive that I want this file on my computer") still work; checkbox
+ pins durably (file survives "Free up space").
+- [ ] `.meta` sidecars still get pinned automatically when their media is read.
+- [ ] Offline: checkbox behavior unchanged, banner + Retry on failed metadata
+ reads unchanged.
+- [ ] Auto-fetch under threshold works; the new offline suppression also
+ applies (select small cloud file while offline → no fetch attempt).
+
+## 8. What to record when something goes wrong
+
+1. The provider, macOS version, and whether the path went through a symlink.
+2. `stat -f "blocks=%b size=%z flags=%f" ` for the file in question.
+3. lameta's log (dev terminal or Console.app) — especially any line starting
+ with `cloudFileStatus:` or mentioning the cloud read guard, and the exact
+ error `{ code, errno, syscall }` if shown.
+4. Whether the provider's own client showed an error dialog at the same time
+ (a design goal is that lameta *prevents* triggering those storms).
+5. For suspected data loss: the `diff -r` against your pristine copy, plus the
+ provider's web-UI version history for the affected file.
diff --git a/readme-images/onedrive-file-list-icons.png b/readme-images/onedrive-file-list-icons.png
new file mode 100644
index 00000000..57e88130
Binary files /dev/null and b/readme-images/onedrive-file-list-icons.png differ
diff --git a/readme-images/onedrive-status-card-offline.png b/readme-images/onedrive-status-card-offline.png
new file mode 100644
index 00000000..03c9bf35
Binary files /dev/null and b/readme-images/onedrive-status-card-offline.png differ
diff --git a/readme-images/onedrive-status-card-waiting.png b/readme-images/onedrive-status-card-waiting.png
new file mode 100644
index 00000000..7b358461
Binary files /dev/null and b/readme-images/onedrive-status-card-waiting.png differ
diff --git a/readme-images/onedrive-status-card.png b/readme-images/onedrive-status-card.png
new file mode 100644
index 00000000..5d4e5e12
Binary files /dev/null and b/readme-images/onedrive-status-card.png differ
diff --git a/readme-onedrive.md b/readme-onedrive.md
new file mode 100644
index 00000000..0f439cc6
--- /dev/null
+++ b/readme-onedrive.md
@@ -0,0 +1,99 @@
+# Using lameta with Cloud Sync services (OneDrive, Dropbox, Google Drive, iCloud, etc.)
+
+If your lameta project lives inside a folder managed by a cloud sync service,
+that service may keep some of your files "in the cloud" instead of on your
+computer. This saves disk space: a file that is *online-only* takes up almost
+no room, but its actual content (the recording, the video, the photo…) is not
+on your computer until the cloud service downloads it.
+
+lameta understands these cloud files and will never download a big file
+without being asked. This page explains what you will see and how to get a
+file onto your computer when you need it.
+
+The screenshots below happen to show OneDrive, but lameta works the same way
+with any of these services — the status box simply shows the name of whichever
+service is managing your folder.
+
+## What you will see in the file list
+
+Next to each file, lameta shows the same kind of icons that Windows File
+Explorer uses:
+
+- **Blue cloud** — the file is online-only. Its content is in the cloud, not
+on this computer. Its name is also shown dimmed.
+- **Spinning arrows** — you (or lameta) asked for this file, and the cloud
+service is downloading it.
+- **Grey crossed-out cloud** — the file is online-only *and* your computer is
+currently offline, so it cannot be downloaded right now.
+- **No icon** — the file is on your computer and ready to use.
+
+
+
+Your session and person information always works, even when files are
+online-only. lameta automatically keeps its own small record-keeping files on
+your computer, so you can always see and edit titles, dates, contributors,
+notes, and so on. Only the large files (audio, video, images, documents) wait
+in the cloud.
+
+## Getting a file onto your computer
+
+1. Click the file in the list. Instead of the usual preview, you will see a
+ box titled with your cloud service's name — for example **OneDrive Status**
+ or **Dropbox Status**.
+ 
+2. Tick **"Tell [your cloud service] that I want this file on my computer"**.
+3. The status changes to **Waiting**. The cloud service downloads the file in
+ the background — how long this takes depends on the file size and your
+ internet connection. lameta shows the file as soon as it arrives.
+ 
+
+Changed your mind? Untick the box to cancel the request. (This never removes
+anything from your computer or from the cloud.)
+
+## Downloading small files automatically
+
+Downloading every tiny file by hand would be tedious, so lameta can request
+small files for you. In the **View** menu, choose **"Automatically make cloud
+files available if smaller than"** and pick a size (for example 10 MB). From
+then on, when you click an online-only file smaller than that, lameta asks the
+cloud service for it automatically. Choose **Never** to turn this off, or
+**Always** if you want every file you click to be downloaded regardless of
+size.
+
+## When you are offline
+
+If your computer has no internet connection, online-only files show a
+crossed-out cloud, and the status box explains that the file cannot
+be downloaded right now. You can still tick the box — the cloud service will
+remember and download the file once you are connected again.
+
+
+
+## Freeing up disk space
+
+You can hand space back to the cloud at any time: in Windows File Explorer,
+right-click a file (or a whole session folder) and choose **Free up space**
+(the exact wording depends on your cloud service). The files become
+online-only again.
+
+One thing to know: the next time you open such a session in lameta, lameta
+immediately re-downloads its own small record-keeping files (they are tiny —
+usually just a few kilobytes) so it can show you the session's information.
+Your recordings and other large files stay in the cloud until you ask for
+them.
+
+## Good to know
+
+- This feature works with cloud sync services on **Windows** that use the
+built-in Windows Cloud Files system — including OneDrive, Dropbox, Google
+Drive, iCloud Drive, and Box. On other systems, or with services that don't
+use this system, lameta treats files normally.
+- lameta never deletes or "de-downloads" anything. Moving files back to the
+cloud is always something you do yourself, in Windows File Explorer.
+- Hover over any status icon to see a short explanation.
diff --git a/src/components/CloudFilePanel.tsx b/src/components/CloudFilePanel.tsx
new file mode 100644
index 00000000..3b2e1160
--- /dev/null
+++ b/src/components/CloudFilePanel.tsx
@@ -0,0 +1,444 @@
+import { css, keyframes } from "@emotion/react";
+/* removed emotion jsx declaration */
+
+import * as React from "react";
+import { observer } from "mobx-react";
+import { t, Trans, Plural } from "@lingui/macro";
+import { File } from "../model/file/File";
+import { NotifyError } from "./Notify";
+import {
+ getCloudFileProvider,
+ getCloudProviderNameForPath
+} from "../other/cloudFileStatus";
+import {
+ revealInFolder,
+ revealInFolderLabel
+} from "../other/crossPlatformUtilities";
+import { networkStatus } from "../other/networkStatus";
+import {
+ CloudDisplayStatus,
+ getCloudDisplayStatusOfFile
+} from "./CloudStatusIcon";
+
+// Palette from the "OneDrive Status" design (claude.ai/design):
+const cardGreen = "#6a9a3a";
+const titleColor = "#1a1d1a";
+const bodyColor = "#5a615a";
+const dividerColor = "#ececec";
+const checkboxBorder = "#b8bdb6";
+
+function useMinutesSinceHydrationRequest(file: File): number {
+ const [minutes, setMinutes] = React.useState(0);
+
+ React.useEffect(() => {
+ if (file.cloudStatus !== "hydrating" || file.hydratingSinceMs === undefined) {
+ setMinutes(0);
+ return;
+ }
+ const start = file.hydratingSinceMs;
+ const update = () => setMinutes(Math.floor((Date.now() - start) / 60000));
+ update();
+ const interval = window.setInterval(update, 30000);
+ return () => window.clearInterval(interval);
+ }, [file.cloudStatus, file.hydratingSinceMs]);
+
+ return minutes;
+}
+
+function getStatusLabel(status: CloudDisplayStatus): string {
+ switch (status) {
+ case "hydrating":
+ return t`Waiting`;
+ case "cloudOnlyOffline":
+ return t`In cloud only, and this computer appears to be offline.`;
+ default:
+ return t`In cloud only, lameta cannot read it yet.`;
+ }
+}
+
+const spin = keyframes`
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+`;
+
+// Stroke-style icons matching the design's outlined cloud, in the card green.
+const PanelStatusIcon: React.FunctionComponent<{
+ status: CloudDisplayStatus;
+}> = ({ status }) => {
+ const common = {
+ width: 22,
+ height: 22,
+ viewBox: "0 0 24 24",
+ fill: "none" as const,
+ css: css`
+ flex-shrink: 0;
+ color: ${cardGreen};
+ `
+ };
+ const cloudPath = (
+
+ );
+ switch (status) {
+ case "hydrating":
+ return (
+
+ );
+ case "cloudOnlyOffline":
+ return (
+
+ );
+ default:
+ return ;
+ }
+};
+
+// The cloud status card ("OneDrive Status", "Dropbox Status", ...): title
+// row, explanation, divider, and the
+// request checkbox. Shared by every tab that would need to read the file's
+// content (Audio/Video/Image/Text/PDF previews). Renders nothing once the
+// file is on this computer, ready to read.
+export const CloudFileFetchControl: React.FunctionComponent<{
+ file: File;
+}> = observer((props) => {
+ const { file } = props;
+ const minutesSinceRequest = useMinutesSinceHydrationRequest(file);
+
+ const capabilities = getCloudFileProvider().capabilities;
+ if (!capabilities.canFetch) {
+ return null;
+ }
+ if (!file.isCloudFileNotPresent) {
+ return null;
+ }
+
+ const hydrating = file.cloudStatus === "hydrating";
+ const displayStatus = getCloudDisplayStatusOfFile(file) ?? "cloudOnly";
+ // "OneDrive", "Dropbox", ... whichever sync engine owns this folder.
+ const providerName =
+ getCloudProviderNameForPath(file.getActualFilePath()) ?? t`Cloud`;
+
+ return (
+
+ {minutesSinceRequest < 1 ? (
+
+ You requested this file less than a minute ago. lameta will
+ show it when it becomes available.
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+});
+
+// Shown instead of the media/text/pdf preview when the file's content has not
+// been fetched from the cloud. See file.isCloudFileNotPresent.
+export const CloudFilePanel: React.FunctionComponent<{ file: File }> =
+ observer((props) => {
+ return (
+
+
+
+ );
+ });
diff --git a/src/components/CloudStatusIcon.spec.ts b/src/components/CloudStatusIcon.spec.ts
new file mode 100644
index 00000000..d0a15191
--- /dev/null
+++ b/src/components/CloudStatusIcon.spec.ts
@@ -0,0 +1,44 @@
+import { describe, it, expect } from "vitest";
+import { getCloudDisplayStatus } from "./CloudStatusIcon";
+
+describe("getCloudDisplayStatus", () => {
+ it("shows the blue cloud for a cloud-only file while online", () => {
+ expect(getCloudDisplayStatus("cloudOnly", true, true)).toBe("cloudOnly");
+ });
+
+ it("shows the offline cloud for a cloud-only file while offline", () => {
+ expect(getCloudDisplayStatus("cloudOnly", false, true)).toBe(
+ "cloudOnlyOffline"
+ );
+ });
+
+ it("shows sync arrows while hydrating online", () => {
+ expect(getCloudDisplayStatus("hydrating", true, true)).toBe("hydrating");
+ });
+
+ it("shows the offline cloud while hydrating offline, since the download cannot progress", () => {
+ expect(getCloudDisplayStatus("hydrating", false, true)).toBe(
+ "cloudOnlyOffline"
+ );
+ });
+
+ // Checkmarks for locally-available files are currently disabled (see the
+ // commented-out cases in getCloudDisplayStatus); if re-enabled, these
+ // should go back to expecting "local" / "localPinned" under a sync root.
+ it("shows nothing for hydrated files, even under a sync root", () => {
+ expect(getCloudDisplayStatus("local", true, true)).toBeUndefined();
+ expect(getCloudDisplayStatus("localPinned", true, true)).toBeUndefined();
+ expect(getCloudDisplayStatus("local", true, false)).toBeUndefined();
+ expect(getCloudDisplayStatus("localPinned", true, false)).toBeUndefined();
+ });
+
+ it("still shows cloud states outside a detected sync root (attributes prove they are cloud files)", () => {
+ expect(getCloudDisplayStatus("cloudOnly", true, false)).toBe("cloudOnly");
+ expect(getCloudDisplayStatus("hydrating", true, false)).toBe("hydrating");
+ });
+
+ it("shows nothing when the status is unknown", () => {
+ expect(getCloudDisplayStatus("unknown", true, true)).toBeUndefined();
+ expect(getCloudDisplayStatus("unknown", false, true)).toBeUndefined();
+ });
+});
diff --git a/src/components/CloudStatusIcon.tsx b/src/components/CloudStatusIcon.tsx
new file mode 100644
index 00000000..9247b458
--- /dev/null
+++ b/src/components/CloudStatusIcon.tsx
@@ -0,0 +1,184 @@
+import { css, keyframes } from "@emotion/react";
+/* removed emotion jsx declaration */
+
+import * as React from "react";
+import { observer } from "mobx-react";
+import { t } from "@lingui/macro";
+import { File } from "../model/file/File";
+import {
+ CloudFileStatus,
+ getCloudProviderNameForPath,
+ isUnderCloudSyncRoot
+} from "../other/cloudFileStatus";
+import { networkStatus } from "../other/networkStatus";
+
+// The sync-state a file should display, mirroring the icons OneDrive shows in
+// File Explorer. Unlike CloudFileStatus, this folds in connectivity (a
+// cloud-only file cannot arrive while offline) and whether the file is under
+// a sync root at all (a plain local file outside OneDrive shows no icon).
+export type CloudDisplayStatus =
+ | "cloudOnly" // blue cloud: online-only
+ | "cloudOnlyOffline" // grey crossed-out cloud: online-only and no network
+ | "hydrating" // blue sync arrows: downloading
+ | "local" // green outlined check: on this device, could be dehydrated
+ | "localPinned"; // solid green check: always keep on this device
+
+export function getCloudDisplayStatus(
+ cloudStatus: CloudFileStatus,
+ isOnline: boolean,
+ isUnderSyncRoot: boolean
+): CloudDisplayStatus | undefined {
+ switch (cloudStatus) {
+ case "cloudOnly":
+ return isOnline ? "cloudOnly" : "cloudOnlyOffline";
+ case "hydrating":
+ // The download cannot progress without a network; show why.
+ return isOnline ? "hydrating" : "cloudOnlyOffline";
+ // Per John (2026-07): don't show checkmarks for files that are simply
+ // here on this device -- but keep the ability around in case he changes
+ // his mind. Re-enabling this gives Explorer-style green checks (outlined
+ // for "local", solid for "always keep on this device").
+ // case "local":
+ // case "localPinned":
+ // // Attributes cannot distinguish a hydrated OneDrive file from a plain
+ // // local file, so only claim "available on this device" under a sync root.
+ // return isUnderSyncRoot ? cloudStatus : undefined;
+ default:
+ return undefined;
+ }
+}
+
+export function getCloudDisplayStatusOfFile(
+ file: File
+): CloudDisplayStatus | undefined {
+ return getCloudDisplayStatus(
+ file.cloudStatus,
+ networkStatus.isOnline,
+ isUnderCloudSyncRoot(file.getActualFilePath())
+ );
+}
+
+function getTooltip(
+ status: CloudDisplayStatus,
+ providerName: string
+): string {
+ switch (status) {
+ case "cloudOnly":
+ return t`Online-only (${providerName}). The content of this file is not on this computer.`;
+ case "cloudOnlyOffline":
+ return t`Online-only (${providerName}), and this computer appears to be offline, so the file cannot be downloaded right now.`;
+ case "hydrating":
+ return t`${providerName} is downloading this file to this computer.`;
+ case "local":
+ return t`Available on this device. ${providerName} may free up this space if the file goes unused.`;
+ case "localPinned":
+ return t`Always available on this device.`;
+ }
+}
+
+const oneDriveBlue = "#0078d4";
+const availableGreen = "#0f7b0f";
+const offlineGrey = "#605e5c";
+
+const spin = keyframes`
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+`;
+
+// Renders a given display status as an icon closely matching the ones
+// File Explorer shows for OneDrive files, so users instantly recognize them.
+// `color` overrides the per-status OneDrive colors (e.g. the cloud panel
+// draws the icon in the surrounding text color per the design).
+export const CloudStatusGlyph: React.FunctionComponent<{
+ status: CloudDisplayStatus;
+ size?: number;
+ color?: string;
+ providerName?: string; // "OneDrive", "Dropbox", ... for the tooltip
+}> = ({ status, size = 16, color, providerName }) => {
+ const common = {
+ width: size,
+ height: size,
+ viewBox: "0 0 24 24",
+ role: "img" as const
+ };
+ const title = {getTooltip(status, providerName ?? t`Cloud`)};
+ switch (status) {
+ case "cloudOnly":
+ return (
+
+ );
+ case "cloudOnlyOffline":
+ return (
+
+ );
+ case "hydrating":
+ return (
+
+ );
+ case "local":
+ return (
+
+ );
+ case "localPinned":
+ return (
+
+ );
+ }
+};
+
+// The shared entry point: shows the OneDrive-style status icon for a file,
+// or nothing when the file has no cloud story to tell. Used by the file
+// list's status column and by the panel that lets the user request a file.
+export const CloudStatusIcon: React.FunctionComponent<{
+ file: File;
+ size?: number;
+}> = observer((props) => {
+ const status = getCloudDisplayStatusOfFile(props.file);
+ if (!status) {
+ return null;
+ }
+ return (
+
+ );
+});
diff --git a/src/components/CloudUnavailableBanner.tsx b/src/components/CloudUnavailableBanner.tsx
new file mode 100644
index 00000000..51fce789
--- /dev/null
+++ b/src/components/CloudUnavailableBanner.tsx
@@ -0,0 +1,108 @@
+import { css } from "@emotion/react";
+import * as React from "react";
+import { observer } from "mobx-react";
+import { t, Trans } from "@lingui/macro";
+import { cloudReadGuard } from "../other/cloudReadGuard";
+import { Project } from "../model/Project/Project";
+
+// A single, persistent banner shown across the top of the project view when a
+// cloud sync provider (OneDrive, Dropbox, Nextcloud, ...) couldn't deliver one
+// or more files during load. It replaces what would otherwise be a storm of
+// per-file error toasts (and provider modal dialogs). See cloudReadGuard.
+export const CloudUnavailableBanner: React.FunctionComponent<{
+ project: Project;
+}> = observer((props) => {
+ const [dismissed, setDismissed] = React.useState(false);
+ const [retrying, setRetrying] = React.useState(false);
+
+ const failures = cloudReadGuard.failedReads;
+ const count = failures.length;
+
+ // Re-show the banner if a fresh batch of failures comes in after a dismissal.
+ React.useEffect(() => {
+ if (count > 0) setDismissed(false);
+ }, [count]);
+
+ if (count === 0 || dismissed) {
+ return null;
+ }
+
+ // If every failure is from the same provider, name it; otherwise stay generic.
+ const providerNames = Array.from(
+ new Set(failures.map((f) => f.providerName).filter(Boolean))
+ );
+ const providerName =
+ providerNames.length === 1 ? (providerNames[0] as string) : t`the cloud`;
+
+ const message =
+ count === 1
+ ? t`lameta couldn't download 1 file from ${providerName}. It may be offline or busy right now.`
+ : t`lameta couldn't download ${count} files from ${providerName}. It may be offline or busy right now.`;
+
+ const onRetry = () => {
+ setRetrying(true);
+ // Let the button repaint as "retrying" before the synchronous re-reads run.
+ window.setTimeout(() => {
+ try {
+ props.project.retryFailedCloudReads();
+ } finally {
+ setRetrying(false);
+ }
+ }, 0);
+ };
+
+ return (
+
+
+ ⚠️
+
+ {message}
+
+
+
+ );
+});
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
index aa95f479..53ce807b 100644
--- a/src/components/ErrorBoundary.tsx
+++ b/src/components/ErrorBoundary.tsx
@@ -8,6 +8,12 @@ import { sentryException } from "../other/errorHandling";
interface IProps {
children: ReactNode;
context?: string;
+ // When any value in this array changes, a boundary that is currently showing
+ // its fallback resets and re-renders its children. Pass something that
+ // identifies the current subject (e.g. the selected file's path) so that a
+ // render error caused by one item cannot wedge the pane for every other item
+ // until the app is restarted.
+ resetKeys?: unknown[];
}
interface IState {
@@ -39,7 +45,11 @@ const handleError = (error: Error, info: any) => {
export const ErrorBoundary = (props: IProps) => {
return (
// todo: there is props.context that we are not using yet
-
+
{props.children}
);
diff --git a/src/components/FileList.css b/src/components/FileList.css
index d054613d..e5e4bd01 100644
--- a/src/components/FileList.css
+++ b/src/components/FileList.css
@@ -74,3 +74,6 @@
.noMediaFolderConnection {
color: var(--project-color) !important;
}
+.cloudOnly {
+ color: rgba(0, 0, 0, 0.5) !important;
+}
diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx
index dcfe19e4..31dd9042 100644
--- a/src/components/FileList.tsx
+++ b/src/components/FileList.tsx
@@ -6,7 +6,10 @@ import { ElectronDropZone } from "./ElectronDropZone";
import { OpenDialogOptions, ipcRenderer } from "electron";
import * as remote from "@electron/remote";
import "./FileList.css";
-import { revealInFolder } from "../other/crossPlatformUtilities";
+import {
+ revealInFolder,
+ revealInFolderLabel
+} from "../other/crossPlatformUtilities";
import { ShowRenameDialog } from "./RenameFileDialog/RenameFileDialog";
import { i18n, translateFileType } from "../other/localization";
import { t, Trans } from "@lingui/macro";
@@ -30,7 +33,19 @@ import HighlightSearchTerm from "./HighlightSearchTerm";
import { lameta_orange } from "../containers/theme";
import SearchIcon from "@mui/icons-material/Search";
import { getTestEnvironment } from "../getTestEnvironment";
+import {
+ AutoFetchCloudFiles,
+ mbToBytes
+} from "../other/autoFetchCloudFiles";
+import {
+ CloudStatusIcon,
+ getCloudDisplayStatusOfFile
+} from "./CloudStatusIcon";
const electron = require("electron");
+
+// Throttle window-focus-triggered cloud status refreshes so that rapid
+// alt-tabbing doesn't hammer the filesystem for every file in the folder.
+const kWindowFocusRefreshThrottleMs = 5000;
export const _FileList: React.FunctionComponent<{
folder: Folder;
extraButtons?: object[];
@@ -65,6 +80,33 @@ export const _FileList: React.FunctionComponent<{
const { searchTerm } = React.useContext(SearchContext);
const highlight = (text: string) => ;
+ // Debounced/capped auto-fetch of small cloud-only files as the selection
+ // rests on them. One scheduler per mounted FileList (i.e. per folder).
+ const autoFetchSchedulerRef = React.useRef();
+ if (!autoFetchSchedulerRef.current) {
+ autoFetchSchedulerRef.current = new AutoFetchCloudFiles();
+ }
+ React.useEffect(() => {
+ return () => autoFetchSchedulerRef.current?.dispose();
+ }, []);
+
+ // Refresh cloud status of this folder's files when the window regains
+ // focus (e.g. after OneDrive finishes syncing in the background), but no
+ // more often than every few seconds.
+ const lastFocusRefreshRef = React.useRef(0);
+ React.useEffect(() => {
+ const onFocus = () => {
+ const now = Date.now();
+ if (now - lastFocusRefreshRef.current < kWindowFocusRefreshThrottleMs) {
+ return;
+ }
+ lastFocusRefreshRef.current = now;
+ props.folder.files.forEach((f) => f.updateCloudStatus());
+ };
+ window.addEventListener("focus", onFocus);
+ return () => window.removeEventListener("focus", onFocus);
+ }, [props.folder]);
+
const fileHasMetadataMatch = (file: any, trimmed: string): boolean => {
if (!trimmed || !file) return false;
let metadataMatch = false;
@@ -99,6 +141,18 @@ export const _FileList: React.FunctionComponent<{
return metadataMatch;
};
+ // MobX tracking: deliberately touch getLinkStatusIconPath and the cloud
+ // display status (and through them cloudStatus and networkStatus) for
+ // EVERY file, with no short-circuit -- if a later file's status changes
+ // (e.g. OneDrive finishes hydrating it), this render must re-run so its
+ // row icon and cloudOnly row class update.
+ const linkStatusIconPaths = props.folder.files.map((f) =>
+ getLinkStatusIconPath(f)
+ );
+ const cloudDisplayStatuses = props.folder.files.map((f) =>
+ getCloudDisplayStatusOfFile(f)
+ );
+
let columns: any[] = [
{
id: "icon",
@@ -124,12 +178,17 @@ export const _FileList: React.FunctionComponent<{
id: "linkStatus",
Header: "",
width: 30,
- show: props.folder.files.some((f) => getLinkStatusIconPath(f)),
- accessor: (d: any) => {
- const f: File = d;
- return getLinkStatusIconPath(f);
- },
- Cell: (p) => (p.value ? : null)
+ show:
+ linkStatusIconPaths.some((p) => !!p) ||
+ cloudDisplayStatuses.some((s) => !!s),
+ accessor: (d: any) => d,
+ Cell: (p) => {
+ const f: File = p.value;
+ const iconPath = getLinkStatusIconPath(f);
+ // Link/missing/naming problems take precedence; otherwise show the
+ // OneDrive sync-state icon (or nothing for non-cloud files).
+ return iconPath ? : ;
+ }
},
{
id: "type",
@@ -361,6 +420,11 @@ export const _FileList: React.FunctionComponent<{
}
props.folder.selectedFile = rowInfo.original;
setSelectedFile(rowInfo.original); // trigger re-render so that the following style: takes effect
+ file.updateCloudStatus();
+ autoFetchSchedulerRef.current?.onSelectionChanged(
+ file,
+ mbToBytes(userSettings.AutoFetchCloudFilesUnderMB)
+ );
}
},
className:
@@ -403,10 +467,7 @@ function showFileMenu(
let items = [
{
- label:
- process.platform === "darwin"
- ? t`Show in Finder`
- : t`Show in File Explorer`,
+ label: revealInFolderLabel(),
click: () => {
revealInFolder(file.getActualFilePath());
},
diff --git a/src/components/FolderList.tsx b/src/components/FolderList.tsx
index 52d5a1a2..0ec444d3 100644
--- a/src/components/FolderList.tsx
+++ b/src/components/FolderList.tsx
@@ -261,11 +261,23 @@ class FolderList extends React.Component {
//NB: "rowInfo.row" is a subset of things that are mentioned with an accessor. "original" is the original.
return {
onClick: (e: any, x: any) => {
- // Save current selection before changing
+ // Save current selection before changing. This must never abort
+ // the selection change: if saving the outgoing folder throws
+ // (e.g. its folder vanished from disk externally), we still want
+ // the user to be able to move to another folder. save() already
+ // fails softly, but guard here too so no future throw can wedge
+ // the UI on the old, now-unusable selection.
if (selectedFolderIndex > -1) {
- this.props.folders.items[
- selectedFolderIndex
- ].saveAllFilesInFolder();
+ try {
+ this.props.folders.items[
+ selectedFolderIndex
+ ].saveAllFilesInFolder();
+ } catch (err) {
+ console.error(
+ "Error saving the outgoing folder while switching selection; continuing anyway.",
+ err
+ );
+ }
}
// rowInfo.index refers to the index within the currently displayed (possibly filtered) data set.
// We need to store the index relative to the full, unfiltered items array, because other code
diff --git a/src/components/FolderPane.tsx b/src/components/FolderPane.tsx
index f0da9d2e..be607cc2 100644
--- a/src/components/FolderPane.tsx
+++ b/src/components/FolderPane.tsx
@@ -42,6 +42,7 @@ import { TiffViewer } from "./TiffViewer";
import AccessChooser from "./session/AccessChooser";
import { TextFieldEdit } from "./TextFieldEdit";
import { getSessionFormClass } from "./session/SessionFormVariant";
+import { CloudFilePanel } from "./CloudFilePanel";
export interface IProps {
folder: Folder;
@@ -115,7 +116,14 @@ export const FolderPane: React.FunctionComponent<
file={props.folder.selectedFile}
fileName={props.folder.selectedFile.getTextProperty("filename")}
/>
-
+
>
@@ -162,6 +170,10 @@ const FileTabs: React.FunctionComponent<
}
const notesField = file.properties.getTextField("notes");
const filename = file.getTextProperty("filename");
+ // Reading media/text/pdf content would hydrate (fully download) a
+ // cloud-only OneDrive placeholder file, which can be huge and slow. Show
+ // CloudFilePanel instead until the user explicitly fetches it.
+ const previewBlocked = file.isCloudFileNotPresent;
const notesPanel = directoryObject.properties.getValue("notes") ? (
@@ -527,9 +539,13 @@ const FileTabs: React.FunctionComponent<
{projectDocsTabs}
-
+ {previewBlocked ? (
+
+ ) : (
+
+ )}
{standardMetaPanels}
{projectDocsPanels}
@@ -546,17 +562,21 @@ const FileTabs: React.FunctionComponent<
{projectDocsTabs}
- {
- NotifyError("video error:" + e);
- }}
- />
+ {previewBlocked ? (
+
+ ) : (
+ {
+ NotifyError("video error:" + e);
+ }}
+ />
+ )}
{standardMetaPanels}
{projectDocsPanels}
@@ -574,7 +594,9 @@ const FileTabs: React.FunctionComponent<
{projectDocsTabs}
- {isTiff ? (
+ {previewBlocked ? (
+
+ ) : isTiff ? (
) : (
- {/* NB: not a url, just a file here path */}
-
+ {previewBlocked ? (
+
+ ) : (
+ /* NB: not a url, just a file here path */
+
+ )}
{standardMetaPanels}
{projectDocsPanels}
@@ -615,31 +641,35 @@ const FileTabs: React.FunctionComponent<
{projectDocsTabs}
-
+ {previewBlocked ? (
+
+ ) : (
+
+ )}
{standardMetaPanels}
{projectDocsPanels}
diff --git a/src/components/LoadingProjectDialog.tsx b/src/components/LoadingProjectDialog.tsx
index 5507f0e3..7c3ab312 100644
--- a/src/components/LoadingProjectDialog.tsx
+++ b/src/components/LoadingProjectDialog.tsx
@@ -4,6 +4,11 @@ import { t } from "@lingui/macro";
import { Dialog, DialogContent, LinearProgress } from "@mui/material";
import { DialogMiddle } from "./LametaDialog";
import { Project, ProjectHolder } from "../model/Project/Project";
+import {
+ getCloudFileProvider,
+ getCloudProviderNameForPath
+} from "../other/cloudFileStatus";
+import { collectMetadataFilePaths } from "../other/cloudMetadataPrefetch";
export interface LoadingProgress {
phase: "sessions" | "people";
@@ -16,20 +21,74 @@ export interface LoadingProgress {
// Projects with more than this many sessions will show a loading dialog
export const ASYNC_LOADING_THRESHOLD = 30;
-// Check if a project directory should use async loading with progress
-export const shouldUseAsyncLoading = (directory: string): boolean => {
+// Does this project have any cloud-evicted (cloud-only) metadata file that a
+// real provider could fetch? Such files hydrate one-at-a-time when read, at a
+// few seconds each, which would freeze the renderer -- so we route these
+// projects through the async loader + progress dialog just like large ones.
+export const hasCloudEvictedMetadata = (directory: string): boolean => {
+ try {
+ const provider = getCloudFileProvider();
+ // No provider that can deliver placeholders: nothing would be cloud-only.
+ if (!provider.capabilities.canFetch) {
+ return false;
+ }
+ for (const path of collectMetadataFilePaths(directory)) {
+ if (provider.getStatus(path) === "cloudOnly") {
+ return true; // short-circuit on the first hit
+ }
+ }
+ return false;
+ } catch {
+ return false;
+ }
+};
+
+// The decision about how to load a project, computed once per load so we don't
+// stat every metadata file twice (deciding async vs sync, and labeling the
+// dialog).
+export interface LoadPlan {
+ // Whether to use the async, yielding loader with a progress dialog.
+ useAsync: boolean;
+ // Set only when cloud-evicted metadata is what makes this load slow -- the
+ // name of the sync engine ("OneDrive", "Dropbox", ...) for the dialog.
+ cloudProviderName?: string;
+}
+
+// Compute the load plan for a directory in a single pass: async when the
+// project is large (> ASYNC_LOADING_THRESHOLD sessions) or has cloud-evicted
+// metadata that would otherwise block the renderer while it hydrates.
+export const getLoadPlan = (directory: string): LoadPlan => {
+ // Cloud eviction, when present, dominates the wait and also labels the
+ // dialog, so check it first.
+ if (hasCloudEvictedMetadata(directory)) {
+ let cloudProviderName: string;
+ try {
+ cloudProviderName =
+ getCloudProviderNameForPath(directory) ?? t`the cloud`;
+ } catch {
+ cloudProviderName = t`the cloud`;
+ }
+ return { useAsync: true, cloudProviderName };
+ }
const folderCounts = Project.countFoldersInDirectory(directory);
- return folderCounts.sessionCount > ASYNC_LOADING_THRESHOLD;
+ return { useAsync: folderCounts.sessionCount > ASYNC_LOADING_THRESHOLD };
};
-// Load a project either synchronously or asynchronously based on size.
-// For large projects (>10 sessions), loads async with progress callback.
+// Check if a project directory should use async loading with progress.
+export const shouldUseAsyncLoading = (directory: string): boolean =>
+ getLoadPlan(directory).useAsync;
+
+// Load a project either synchronously or asynchronously based on the plan.
+// Callers that already computed a plan (to drive dialog visibility/message)
+// should pass it so we don't recompute it; others get it computed here.
// Returns the loaded project.
export const loadProject = async (
directory: string,
- onProgress?: (progress: LoadingProgress) => void
+ onProgress?: (progress: LoadingProgress) => void,
+ plan?: LoadPlan
): Promise => {
- if (shouldUseAsyncLoading(directory)) {
+ const { useAsync } = plan ?? getLoadPlan(directory);
+ if (useAsync) {
return Project.fromDirectoryAsync(directory, onProgress);
} else {
return Project.fromDirectory(directory);
@@ -50,6 +109,9 @@ export const loadProjectIntoHolder = async (
export const LoadingProjectDialog: React.FunctionComponent<{
open: boolean;
progress: LoadingProgress;
+ // When the load was triggered by cloud-evicted files, the name of the sync
+ // engine ("OneDrive", "Dropbox", ...) so we can say where we're waiting on.
+ cloudProviderName?: string;
}> = (props) => {
if (!props.open) {
return null;
@@ -66,6 +128,12 @@ export const LoadingProjectDialog: React.FunctionComponent<{
const phaseLabel =
props.progress.phase === "sessions" ? t`Sessions` : t`People`;
+ // When cloud files are being fetched, the wait is dominated by the download,
+ // not by our per-item work, so lead with that instead of the phase.
+ const message = props.cloudProviderName
+ ? t`Getting project information from ${props.cloudProviderName}...`
+ : t`Loading ${phaseLabel}...`;
+
return (