Files
RecipesCam/docker/frontend/src/i18n/en.ts
T
3dtours 936414a3b6 feat(library): give a folder its missing thumbnails, and the wall its stars
Four asks, one screen: a folder can be given the tiles it is short of, by hand
or on the way in; the rescan row says RESCAN...; and the wall of thumbnails
scores a frame without the trip to the stage.

- Library.tsx + i18n: "RESCAN FROM THE TOP" is "RESCAN..." in both languages.
  The reading is unchanged — a row's roll is still read from the top, its
  subfolders included.
- engine/library.ts + Library.tsx: the tile of one frame is one function now
  (`makeThumb`: the store, the backup behind it, then the file's own preview
  for a RAW or a HEIC and a decode for anything else). The wall's lazy path
  calls it instead of keeping its own copy of the same branches.
- engine/library.ts: `missingThumbs` asks which of a folder's rows no tile was
  ever drawn for, in one transaction, and `makeThumbs` makes those — a few
  frames at a time, and giving up when the caller has moved on.
- Library.tsx: the folder menu carries MAKE THUMBS, which reads the row's
  folder and what lies under it; and opening a folder asks for its own frames'
  tiles by itself, silently, in the background — the branch too when the
  screen is drawing the branch. A frame that lands after the ask is asked for
  again, which is why the frame count is a dependency of the effect.
- Library.tsx: a wall card carries the same five stars the stage does, under
  the picture, and rates the frame on the click. The click stops at the star,
  so scoring a frame is not picking it.

Verified in Chrome at 1440x900 against OPFS standing in for the disk, three
frames with one of them in a subfolder: opening the folder leaves two tiles
(the node's own frames) and MAKE THUMBS takes the roll to three, the frame in
the subfolder included; clearing the store and reloading brings the two back
on the open; the menu reads RESCAN... and MAKE THUMBS; and the wall's fourth
star lights four, writes star 4 to the catalogue row, and takes it back on the
second click. The same checks pass on the built bundle at :8090.
2026-10-09 20:04:24 +07:00

461 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Dict } from './vi';
export const en: Dict = {
'lang.name': 'English',
'nav.demo': 'TRY DEMO',
'nav.login': 'LOG IN',
'nav.signup': 'SIGN UP',
'nav.logout': 'Log out',
'nav.account': 'Account',
'nav.theme': 'Theme',
'nav.language': 'Language',
// The phone's way back from a tab's strip to the rail that opened it.
'nav.tabs': 'TABS',
'nav.back': 'Back to home',
'nav.guest': 'Guest',
// The install offer (see vi.ts).
'install.title': 'Install RecipesCam',
'install.body': 'Add it to your home screen to open it faster — smoother, offline, full screen like an app.',
'install.yes': 'INSTALL',
'install.no': 'NO THANKS',
'install.ios': 'Tap the Share button in the toolbar, then choose “Add to Home Screen”.',
'act.undo': 'UNDO',
'act.redo': 'REDO',
'act.reset': 'RESET',
'act.export': 'EXPORT',
'act.exporting': 'Exporting…',
'act.upscaling': 'Upscaling the photo {done}/{total}…',
'act.cancel': 'Cancel',
'drop.title': 'Drag & drop a photo here',
'drop.hint': 'or click to pick one · paste from the clipboard with Ctrl+V',
'drop.replace': 'Open photo',
'drop.clear': 'Clear',
'drop.peeking': 'SHOWING THE ORIGINAL',
'drop.peekHint': 'Hold right-click or the \\ key to compare with the original',
'drop.compare': 'Compare',
'drop.keep': 'Save original',
'drop.keepHint': 'Save the untouched photo to the device photo library. A browser that cannot share files downloads it instead.',
'drop.demoLimit': 'Demo build: exports are capped at {px}px and carry a watermark.',
// The live viewfinder (see vi.ts for what the status line is for).
'cam.open': 'Open camera',
'cam.shot': 'SHOOT',
'cam.flip': 'Switch front/back camera',
'cam.starting': 'Opening the camera…',
'cam.status': '{w}×{h} · {fps} fps',
'cam.denied': 'The browser was not allowed the camera. Grant it, then open again.',
'cam.unsupported': 'This address cannot use a camera — it needs HTTPS or localhost.',
'cam.fail': 'The camera did not open.',
'cam.shotFail': 'The shot failed — try again.',
'hist.title': 'Histogram',
'hist.drag': 'Drag to move',
'hist.hide': 'Hide the histogram',
'tab.presets': 'PRESETS',
'tab.favorited': 'FAVORITED',
'tab.photos': 'MY PHOTOS',
'tab.light': 'LIGHT',
'tab.tools': 'TOOLS',
'tab.hsl': 'HSL',
'hsl.hint': 'PRESS PICK, THEN SAMPLE A COLOUR ON THE PHOTO',
'tab.frame': 'FRAME',
'tab.save': 'SAVE RECENT',
'tab.create': 'CREATE RECIPES',
'create.title': 'CREATE RECIPES',
'create.save': 'SAVE RECIPE',
'create.export': 'EXPORT .RECIPE FILE',
'create.guestHint':
'Guest: the recipe stays in RECIPES until you close the page. Log in to save it to your account and export it.',
'create.userHint':
'Saved to your account. Export the .recipe file to share it — encrypted exactly like the app’s.',
'sec.filmSims': 'PHOTO STYLE',
'sec.recipes': 'RECIPES',
'sec.saved': 'SAVED',
'sec.savedEmpty': 'No saved recipes yet. Log in and hit SAVE to sync them.',
'sec.frame': 'FRAME',
'sec.crop': 'CROP',
'sec.rotate': 'ROTATE',
'sec.watermark': 'WATERMARK',
'sec.gps': 'GPS',
'wb.temp': 'COLOR TEMP',
'frame.none': 'NO FRAME',
'frame.polaroid': 'RETRO INSTANT',
'frame.classic': 'CLASSIC BORDER',
'frame.cinematic': 'CINEMATIC',
'frame.wall': 'WALL FRAME PORTRAIT',
'frame.wallLand': 'WALL FRAME LANDSCAPE',
'frame.oldFilm': 'OLD FILM LANDSCAPE',
'frame.oldFilmPort': 'OLD FILM PORTRAIT',
'crop.none': 'NO CROP',
'crop.1:1': '1:1',
'crop.2:3': '2:3',
'crop.3:2': '3:2',
'crop.3:4': '3:4',
'crop.4:3': '4:3',
'crop.16:9': '16:9',
'crop.free': 'FREE',
'crop.apply': 'APPLY',
'crop.cancel': 'CANCEL',
'rot.0': '0°',
'rot.90': '90°',
'rot.180': '180°',
'rot.270': '270°',
'wm.text': 'TEXT',
'wm.color': 'COLOR',
'wm.size': 'SIZE',
'wm.gps': 'GPS STAMP',
'wm.place': 'PLACE NAME',
'wm.placePh': 'e.g. HANOI',
'wm.manual': 'ENTER COORDINATES',
'wm.lat': 'Latitude',
'wm.lng': 'Longitude',
'wm.hasExif': 'GPS read from the photo.',
'wm.noExif': 'This photo has no GPS — type it in, or use where you are now.',
'wm.useLocation': 'USE MY LOCATION',
'wm.locating': 'GETTING LOCATION…',
'wm.hasDevice': "GPS taken from the device's current position.",
'wm.hasManual': 'GPS typed in by you.',
'wm.locFail': 'Could not get a location — type the coordinates in.',
'auth.loginTitle': 'Log in',
'auth.signupTitle': 'Create an account',
'auth.email': 'Email',
'auth.password': 'Password',
'auth.passwordHint': 'At least 8 characters',
'auth.noAccount': 'No account yet? Sign up',
'auth.haveAccount': 'Already have an account? Log in',
'auth.continueDemo': 'Keep using the demo',
'auth.busy': 'Working…',
'auth.loggedInAs': 'Signed in as {email}',
'auth.verifyTitle': 'Verify your email',
'auth.verifyBody': 'We sent a six-digit code to {email}. Type it below — or open the link in the mail — to unlock the PRO features.',
'auth.verifyProHint': 'Until then the account stays on the basic tier: recipes, saved photos (12 max), and an export inside the long edge of the photo being edited. PRO features (HSL, TOOLS, RAW, exports past it) need the account to be activated.',
'auth.verifySent': 'A new code has been sent.',
'auth.codePlaceholder': 'The six digits from the mail',
'auth.verifyBtn': 'VERIFY',
'auth.codeBad': 'That code is wrong or has expired (15 minutes).',
'auth.codeLocked': 'Too many wrong tries — ask for a new code by email.',
'auth.signupVerifyHint': 'After signing up we email you a six-digit code — type it in to unlock the PRO features.',
// The trial notice under the header: which features are being tried out, on
// whom, and until when.
'notice.trial': 'Some features are on trial for registered users until 1 December; after that they open only in the Android app.',
// The PRO gate. A signed-in account the operator has not activated has the
// basic tier, so the studio needs one place that says what that means (the
// bar), one reason per locked tab (the chips), and one answer for a click on
// either of them.
'pro.hslChip': 'HSL IS A PRO FEATURE',
'pro.fixChip': 'FIX IS A PRO FEATURE',
'pro.gradientChip': 'GRADIENT MASK IS A PRO FEATURE',
'pro.basicBar': 'Account {email} is on the basic tier — PRO features need an account the operator has activated.',
'pro.locked': 'PRO feature — this account has not been activated. Ask the operator to tick "Activated Pro".',
'pro.resend': 'RESEND VERIFICATION EMAIL',
'pro.resent': 'SENT',
'save.title': 'Save recipe',
'save.name': 'Recipe name',
'save.recent': 'SAVE RECENT TO RECIPES',
'save.recentHint': 'Files the look on the stage — sim, WB, light, FX and frame — under a name, in this account alone.',
'save.recentEmpty': 'No recipes of your own yet.',
'save.delete': 'Delete this recipe',
'name.photoTitle': 'NAME THIS PHOTO',
'name.photoLabel': 'What the folder keeps it as, and the title on its landing frame.',
'name.recipeTitle': 'SAVE TO RECIPES',
'name.recipeLabel': 'A name for this look, in this account alone.',
'name.required': 'Give it a name first.',
'name.duplicate': 'You already have a recipe with that name.',
'name.save': 'SAVE',
'name.cancel': 'CANCEL',
'save.saved': 'Saved "{name}"',
'save.needLogin': 'Log in to save a recipe.',
'save.photo': 'SAVE PHOTO {n}/{max}',
'save.photoNeedLogin': 'Log in to save a photo.',
'save.photoSaved': 'Photo saved {n}/{max}.',
'save.photoFull': 'That is all {max} — remove one in My photos.',
'save.photoNoMax': 'SAVE PHOTO {n}',
'save.photoNoSource': 'Load a photo first.',
'save.photoFailed': 'Could not save the photo: {msg}',
'myp.empty': 'NO SAVED PHOTOS YET.',
'myp.history': 'HISTORY',
'myp.historyEmpty': 'NO EARLIER SAVES YET.',
'myp.consent': 'Allow this photo on the landing page',
'myp.remove': 'REMOVE PHOTO',
'del.title': 'REMOVE THIS PHOTO',
'del.warning': 'Removing this photo also removes the {n} looks its history kept before the last saves — they cannot be recovered. Download the photo first if you want to keep it.',
'del.download': 'DOWNLOAD FIRST',
'del.confirm': 'DELETE PHOTO',
'del.cancel': 'CANCEL',
// CLEAR's confirmation: keep the work one of two ways, or let it go.
'clear.title': 'Clear this photo?',
'clear.hint':
'The photo leaves the workspace. Save it to MY PHOTOS to carry on editing later, export it as an image file, or clear it without keeping anything.',
'clear.save': 'SAVE PHOTO',
'clear.export': 'EXPORT IMAGE',
'clear.discard': 'CLEAR WITHOUT SAVING',
'clear.cancel': 'CANCEL',
// Export size. A target over the photo's own longest edge is upscaled on the
// device by the model (engine/superRes), so the file carries the pixels asked for.
'export.title': 'Export photo',
'export.hint':
"Longest edge of the photo on the stage: {px}px. Anything bigger is upscaled on this device.",
'export.same': 'EXPORT UNCHANGED',
'export.2k': 'EXPORT 2K (2048px)',
'export.4k': 'EXPORT 4K (4096px)',
'export.customLabel': 'Longest edge (px)',
'export.custom': 'EXPORT',
'export.cap': 'This account can export up to {px}px.',
'export.share': 'Share to',
'export.shareHint':
"On a phone the network's own app opens with the picture already in its composer. On a desktop the picture goes to the clipboard — paste it (Ctrl+V) into the composer that opened; where pasting is refused, the file downloads for you to drop in.",
'photo.uploading': 'UPLOADING…',
'photos.title': 'My photos',
'photos.hint': 'Each account keeps up to {max} photos. What the studio saves lives here and shows up on the community reel.',
'photos.count': '{n}/{max} photos',
'photos.countNoMax': '{n} photos',
'photos.hintNoMax':
'Admin accounts keep no photo limit. What the studio saves lives here and shows up on the community reel.',
'photos.add': 'ADD A PHOTO',
'photos.empty': 'No photos yet. Open the studio and hit SAVE PHOTO, or add one right here.',
'photos.full': 'That is all {max}. Remove one before saving again.',
'photos.delete': 'DELETE',
'photos.deleted': 'Photo removed.',
'photos.failed': 'That did not work: {msg}',
'photos.noLabels': 'No strip labels yet',
'lib.title': 'LIBRARY',
'lib.locked': 'The library is for accounts. Log in to browse your photo folders.',
'lib.hint':
'Pick a photo folder on this machine. Nothing is uploaded — the page only remembers where the folder is and keeps one thumbnail per frame.',
'lib.add': 'ADD FOLDER',
'lib.stop': 'STOP SCAN',
'lib.rename': 'RENAME FOLDER',
'lib.renameAsk': 'Name shown for this folder (the folder on the disk is not renamed):',
'lib.update': 'UPDATE...',
'lib.rescan': 'RESCAN...',
'lib.makeThumbs': 'MAKE THUMBS',
'lib.thumbsMade': 'Made {n} thumbnails for {folder}.',
'lib.resume': 'KEEP READING {folder}',
'lib.resumeHint': 'The last reading of this folder was cut off. Press to read it on from where it stopped.',
'lib.remove': 'REMOVE',
'lib.reconnect': 'GRANT ACCESS AGAIN',
'lib.nodeHint': 'Right-click to rename, update, rescan, make the missing thumbnails, collapse or remove the folder.',
'lib.collapseAll': 'COLLAPSE ALL',
'lib.collapse': 'COLLAPSE',
'lib.colHint': 'Right-click the empty part of this column to add a folder.',
'lib.colWidth': 'Drag to set the width of the folder column (arrow keys when focused)',
'lib.viewPreview': 'SINGLE',
'lib.viewGrid': 'ALL THUMBS',
'lib.subsShow': 'SHOW SUBFOLDERS',
'lib.subsHide': 'THIS FOLDER ONLY',
'lib.subsHint': 'Frames in subfolders: the whole branch under the open folder, or only what that folder holds itself.',
'lib.open': 'Open in the studio',
'lib.openExplorer': 'Open in window explorer',
'lib.openLocation': 'Open at location',
'lib.openStudio': 'Open in studio',
'lib.mergePanorama': 'Panorama Merge…',
'lib.mergeHdr': 'HDR Merge (Anti-ghosting)…',
'lib.copiedPath': 'Copied location to clipboard: {path}',
'lib.edited': 'edited',
'lib.size': '{mb} MB',
'lib.count': '{n} photos',
// What the thumbnail view is read through. The score a frame is given is the
// reader's own, so the words say so: "stars and up" is a floor, not a target.
'lib.sort': 'SORT',
'lib.sortNewest': 'Newest first',
'lib.sortOldest': 'Oldest first',
'lib.sortStars': 'Most stars',
'lib.sortDateTaken': 'Date taken',
'lib.sortDateEdited': 'Date edited',
'lib.sortFileType': 'File type',
'lib.sortName': 'File name',
'lib.sortAsc': 'Ascending',
'lib.sortDesc': 'Descending',
'lib.rating': 'RATING',
'lib.ratingAll': 'Any rating',
'lib.ratingAtLeast': '{n}★ and up',
'lib.starHint': 'Give this frame {n} stars',
'lib.rotateLeft': 'Turn this frame left',
'lib.rotateRight': 'Turn this frame right',
'lib.year': 'YEAR',
'lib.yearAll': 'Any year',
'lib.hourFrom': 'Shot from',
'lib.hourFromAny': 'Any hour',
'lib.hourTo': 'Shot until',
'lib.hourToAny': 'Any hour',
'lib.scanning': 'Scanning {done}/{total} — {added} new…',
'lib.scanned': 'Scanned {folder}: {added}/{total} new frames.',
'lib.empty': 'Nothing in this folder yet.',
'lib.noFolders': 'No folder yet. Hit ADD FOLDER and pick a photo folder on this machine.',
'lib.blocked':
'Another tab of this app is holding the library open from an older build. Close that tab, or reload it, and this one picks the library up.',
'lib.missing': 'Could not open {name} — the file has been moved or deleted.',
'lib.failed': 'That folder could not be read.',
'lib.unsupported':
'This browser will not let a page read a folder from the disk (Chrome, Edge, Opera and Brave will). Photos still edit fine — drag and drop them into the studio.',
// The catalogue's own copy: the browser's storage is the one place a cleared
// profile takes the reading away from, and these two are what puts it back.
'lib.backup': 'BACK UP',
'lib.restore': 'RESTORE',
'lib.backupNone': 'No backup folder yet — press to pick one and write the catalogue into it.',
'lib.backupAt': 'Backup: {folder} · {n} frames · {time}',
'lib.backupBlocked': 'The browser has not let this page write to {folder}. Press BACK UP and allow it.',
'lib.backupPick': 'Pick the folder the catalogue is written into and read back from — the folder name is the backup name.',
'lib.backupRun': 'Backing up {done}/{total}…',
'lib.backupDone': 'Backed up {n} frames, {w} tiles written.',
'lib.backupFailed': 'The backup folder could not be written to ({why}).',
'lib.restoreConfirm': 'Restore {n} frames from {folder}, written {time}? Frames the library already holds are left as they are.',
'lib.restoreRun': 'Restoring {done}/{total}…',
'lib.restoreDone': 'Restored {n} frames. Add the folder again so every frame gets its file back.',
'lib.restoreNothing': 'There is no catalogue of its own in {folder} — pick the folder a backup was written into.',
'lib.restoreFailed': 'The backup folder could not be read.',
// The repair (Library.tsx, admin only): every roll read back to the disk, so
// the catalogue and the disk say the same thing again.
'lib.repair': 'REPAIR DATA',
'lib.repairConfirm': 'Read all {n} folders from the top so the library matches the disk? Frames that are gone will be dropped from the catalogue.',
'lib.repairRun': 'Repairing the library ({n} folders)…',
'lib.repairDone': 'Library repaired: {n} folders read back.',
'nav.admin': 'Admin',
'nav.photos': 'My photos',
'nav.profile': 'Profile',
'nav.library': 'LIBRARY',
'nav.studio': 'Studio',
'adm.title': 'Strip moderation',
'adm.subtitle': 'Photos contributed by users. Delete one to pull it off the landing page.',
'adm.upload': 'ADD A PHOTO',
'adm.uploads': 'Uploaded photos',
'adm.sections': 'Landing sections',
'adm.albumAll': 'All photos',
'adm.filterName': 'Filter by user name',
'adm.sort': 'Sort',
'adm.sortNewest': 'Newest first',
'adm.sortRecipe': 'Recipe A→Z',
'adm.sortRating': 'Highest rated',
'adm.rating': 'Rating',
'adm.ratingAll': 'Any rating',
'adm.ratingAtLeast': '{n}★ and up',
'adm.recipe': 'Recipe',
'adm.bigEmpty': 'Pick a frame to view.',
'adm.pickStrip': 'Film strip',
'adm.pickTester': 'Live preset tester',
'adm.pickCreator': 'Custom recipe creator',
'adm.pickQr': 'QR card',
'adm.noPreset': 'no look to hand out',
'adm.rowEmpty': 'Nothing here yet.',
'adm.slotHint': 'Tick the sections a photo should appear in — a photo can sit in several at once, and each section draws one of its own at random per visit. Unticking every box takes the photo off the landing page without deleting it — its owner keeps it in MY PHOTOS. The QR section is the one that hands the look out: its card shows a code that downloads the photo’s own .recipe file for the phone app to scan in.',
'adm.signIn': 'Sign in with an admin account',
'adm.notAdmin': 'This account has no admin rights.',
'adm.empty': 'No contributions yet.',
'adm.total': '{n} photos',
'adm.owner': 'Uploaded by',
'adm.size': 'Size',
'adm.uploaded': 'Uploaded',
'adm.delete': 'DELETE',
'adm.clearAll': 'DELETE ALL',
'adm.clearConfirm': 'Delete all {n} contributions? This cannot be undone.',
'adm.removed': 'Deleted {n} photos.',
'adm.back': 'Back to home',
'adm.settings': 'SETTINGS',
'adm.tabPictures': 'Pictures',
'adm.tabUsers': 'User account',
'adm.tabProfile': 'Profile',
'adm.close': 'Close',
'adm.userPicture': 'Picture',
'adm.userName': 'Name',
'adm.userEmail': 'Email',
'adm.userRole': 'Role',
'adm.userPro': 'Activated Pro',
'adm.userPhotos': 'Photos',
'adm.userJoined': 'Joined',
'adm.roleAdmin': 'Admin',
'adm.roleMember': 'Member',
'adm.viewPhotos': 'VIEW PHOTOS',
'adm.noUsers': 'No accounts yet.',
'adm.profileHint': 'Picture, email and password of the signed-in account. Changing the email or password needs the current password.',
'adm.changeAvatar': 'CHANGE PICTURE',
'adm.currentPassword': 'Current password',
'adm.newEmail': 'New email',
'adm.newPassword': 'New password',
'adm.save': 'SAVE',
'adm.saved': 'Profile saved.',
'adm.userActions': 'Actions',
'adm.userBlock': 'BLOCK',
'adm.userUnblock': 'UNBLOCK',
'adm.userRemove': 'REMOVE',
'adm.userRestore': 'RESTORE',
'adm.userDelete': 'DELETE',
'adm.userBlocked': 'Blocked',
'adm.userRemoved': 'Removed',
'adm.userDeleteConfirm': 'Permanently delete the account {email} with all of its photos and recipes? This cannot be undone.',
'adm.selectAll': 'SELECT ALL',
'adm.selectNone': 'SELECT NONE',
'adm.deleteSelected': 'DELETE SELECTED ({n})',
'adm.userDeleteSelectedConfirm': 'Permanently delete the {n} selected accounts with all of their photos and recipes? This cannot be undone.',
'adm.tabStats': 'STATS',
'adm.tabBackup': 'BACKUP',
'adm.backupHint': 'Download the whole system state — the accounts/recipes database and both media folders — as one .tar.gz. That same file is the data package which moves this install to another machine: restore with it.',
'adm.backupSave': 'DOWNLOAD BACKUP',
'adm.restorePick': 'RESTORE FROM FILE…',
'adm.restoreHint': 'A restore replaces all current data with the file’s, then the API restarts itself. The data being replaced is archived aside in the data folder first.',
'adm.restoreConfirm': 'Replace all current data with {file}? This cannot be undone. A safety archive is still kept in the data folder.',
'adm.restoreRunning': 'Restoring — the API is restarting…',
'adm.restoreDone': 'Restore finished.',
'adm.restoreSlow': 'The API never answered. Check the container log, then reload.',
'stats.days': '{n} DAYS',
'stats.hint': 'Traffic over the selected range.',
'stats.views': 'PAGE VIEWS',
'stats.clicks': 'FEATURE CLICKS',
'stats.visitors': 'VISITORS',
'stats.timeline': 'TRAFFIC PER DAY',
'stats.peak': 'peak',
'stats.pages': 'PAGES VIEWED',
'stats.targets': 'FEATURES CLICKED',
'stats.countries': 'COUNTRIES',
'stats.regions': 'REGIONS',
'stats.cities': 'CITIES',
'stats.browsers': 'BROWSERS',
'stats.systems': 'OPERATING SYSTEMS',
'stats.devices': 'DEVICES',
'stats.empty': 'No data yet.',
'profile.title': 'My profile',
'err.noWebgl':
'The browser refused a WebGL context — this build needs WebGL to process photos (the CPU path is thousands of times slower).',
'err.decode': 'Could not read this image. Try another JPG or PNG.',
'err.raw': 'Could not develop this RAW file. Try again, or use its JPG.',
'err.generic': 'Something went wrong: {msg}',
'err.offline': 'Could not reach the server.',
// The offline bar (src/ui/OfflineBar.tsx).
'offline.notice': 'No server — showing the copy saved on this machine.',
// A write the server never got. It is held, not lost; the sentence rides
// `err.generic`'s `{msg}`, so it reads as the second half of that one, and the
// count is the bar's own line for the queue (`offline.pending`).
'offline.queued': 'the server is out of reach, so this change is saved here and sent when it is back.',
'offline.pending': 'Not sent yet: {n}',
'meta.exported': 'Downloaded {name} ({kb}KB, 300 ppi)',
};
export default en;