From 9adaa766f84bdb6e4c1d2cb76bed6ac365a7348b Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 26 Oct 2026 23:24:59 +0100 Subject: [PATCH 1/4] refactor(webapp): move the i18n module to state/i18n/index.tsx Signed-off-by: InstaZDLL --- webapp/src/state/{i18n.tsx => i18n/index.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename webapp/src/state/{i18n.tsx => i18n/index.tsx} (100%) diff --git a/webapp/src/state/i18n.tsx b/webapp/src/state/i18n/index.tsx similarity index 100% rename from webapp/src/state/i18n.tsx rename to webapp/src/state/i18n/index.tsx From d5fe3df29ec3fda20b9d4fdf43f02298b2ef9fcc Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 26 Oct 2026 23:27:28 +0100 Subject: [PATCH 2/4] refactor(webapp): split the translations into one file per domain and locale Signed-off-by: InstaZDLL --- webapp/src/state/i18n/en/browse.ts | 92 + webapp/src/state/i18n/en/folders.ts | 137 ++ webapp/src/state/i18n/en/index.ts | 30 + webapp/src/state/i18n/en/library.ts | 175 ++ webapp/src/state/i18n/en/maintenance.ts | 102 + webapp/src/state/i18n/en/playing.ts | 51 + webapp/src/state/i18n/en/podcasts.ts | 44 + webapp/src/state/i18n/en/quality.ts | 120 ++ webapp/src/state/i18n/en/scrobbling.ts | 95 + webapp/src/state/i18n/en/settings.ts | 53 + webapp/src/state/i18n/en/shell.ts | 80 + webapp/src/state/i18n/en/smart.ts | 83 + webapp/src/state/i18n/en/stats.ts | 53 + webapp/src/state/i18n/en/track.ts | 130 ++ webapp/src/state/i18n/fr/browse.ts | 94 + webapp/src/state/i18n/fr/folders.ts | 142 ++ webapp/src/state/i18n/fr/index.ts | 31 + webapp/src/state/i18n/fr/library.ts | 179 ++ webapp/src/state/i18n/fr/maintenance.ts | 107 + webapp/src/state/i18n/fr/playing.ts | 55 + webapp/src/state/i18n/fr/podcasts.ts | 47 + webapp/src/state/i18n/fr/quality.ts | 124 ++ webapp/src/state/i18n/fr/scrobbling.ts | 106 + webapp/src/state/i18n/fr/settings.ts | 57 + webapp/src/state/i18n/fr/shell.ts | 83 + webapp/src/state/i18n/fr/smart.ts | 86 + webapp/src/state/i18n/fr/stats.ts | 56 + webapp/src/state/i18n/fr/track.ts | 137 ++ webapp/src/state/i18n/index.tsx | 2380 +---------------------- 29 files changed, 2552 insertions(+), 2377 deletions(-) create mode 100644 webapp/src/state/i18n/en/browse.ts create mode 100644 webapp/src/state/i18n/en/folders.ts create mode 100644 webapp/src/state/i18n/en/index.ts create mode 100644 webapp/src/state/i18n/en/library.ts create mode 100644 webapp/src/state/i18n/en/maintenance.ts create mode 100644 webapp/src/state/i18n/en/playing.ts create mode 100644 webapp/src/state/i18n/en/podcasts.ts create mode 100644 webapp/src/state/i18n/en/quality.ts create mode 100644 webapp/src/state/i18n/en/scrobbling.ts create mode 100644 webapp/src/state/i18n/en/settings.ts create mode 100644 webapp/src/state/i18n/en/shell.ts create mode 100644 webapp/src/state/i18n/en/smart.ts create mode 100644 webapp/src/state/i18n/en/stats.ts create mode 100644 webapp/src/state/i18n/en/track.ts create mode 100644 webapp/src/state/i18n/fr/browse.ts create mode 100644 webapp/src/state/i18n/fr/folders.ts create mode 100644 webapp/src/state/i18n/fr/index.ts create mode 100644 webapp/src/state/i18n/fr/library.ts create mode 100644 webapp/src/state/i18n/fr/maintenance.ts create mode 100644 webapp/src/state/i18n/fr/playing.ts create mode 100644 webapp/src/state/i18n/fr/podcasts.ts create mode 100644 webapp/src/state/i18n/fr/quality.ts create mode 100644 webapp/src/state/i18n/fr/scrobbling.ts create mode 100644 webapp/src/state/i18n/fr/settings.ts create mode 100644 webapp/src/state/i18n/fr/shell.ts create mode 100644 webapp/src/state/i18n/fr/smart.ts create mode 100644 webapp/src/state/i18n/fr/stats.ts create mode 100644 webapp/src/state/i18n/fr/track.ts diff --git a/webapp/src/state/i18n/en/browse.ts b/webapp/src/state/i18n/en/browse.ts new file mode 100644 index 00000000..090cb9a1 --- /dev/null +++ b/webapp/src/state/i18n/en/browse.ts @@ -0,0 +1,92 @@ +// Administration, search, player buttons, OAuth consent, themes, browsing. +export const browse = { + "admin.title": "Administration", + "admin.detail": "Libraries, scans and accounts", + "admin.libraries": "Libraries", + "admin.libraryName": "Library name", + "admin.libraryPath": "Absolute server folder path", + "admin.registering": "Registering…", + "admin.register": "Register and scan", + "admin.scan": "Scan now", + "admin.accounts": "Accounts", + "admin.webPassword": "Web password, 12 characters minimum", + "admin.creating": "Creating…", + "admin.create": "Create account", + "admin.enable": "Enable", + "admin.disable": "Disable", + "admin.currentAccount": "You cannot disable your current account", + "admin.libraryError": "The library path could not be registered.", + "admin.accountError": "The account could not be created.", + "admin.statusError": "The account status could not be changed.", + "admin.scanError": "The scan could not be started.", + "admin.scanQueued": "Scan {id} queued.", + "admin.initialScan": "Initial scan {id} queued.", + "search.detail": "Find a title, album or artist", + "search.placeholder": "Title, album or artist", + "search.query": "Search query", + "search.tracks": "Tracks", + "search.empty": "Nothing found.", + "player.error": "Playback could not start. Check the server and try again.", + "player.previous": "Previous track", + "player.play": "Play", + "player.pause": "Pause", + "player.next": "Next track", + "player.seek": "Seek", + "authorize.title": "Authorisation", + "authorize.invalid": + "This authorisation link is incomplete or asks to send you somewhere we do not trust.", + "authorize.client": "Authorise {client}", + "authorize.detail": + "It will be able to browse your libraries, play your music and manage your playlists, favourites and ratings, everything this account can do.", + "authorize.redirect": "Sending you back to {url}", + "authorize.error": + "The application sent an authorisation request we cannot honour.", + "authorize.busy": "Authorising…", + "authorize.approve": "Authorise", + "notFound.title": "This room is silent", + "notFound.detail": "The page you asked for does not exist on this server.", + "notFound.back": "Back to the library", + "appearance.themes.default": "Emerald light", + "appearance.themes.midnightLight": "Midnight light", + "appearance.themes.sunsetLight": "Sunset light", + "appearance.themes.lavenderLight": "Lavender light", + "appearance.themes.crimsonLight": "Crimson light", + "appearance.themes.oceanLight": "Ocean light", + "appearance.themes.defaultDark": "Emerald dark", + "appearance.themes.oled": "OLED black", + "appearance.themes.midnight": "Midnight", + "appearance.themes.sunset": "Sunset", + "appearance.themes.lavender": "Lavender", + "appearance.themes.crimson": "Crimson", + "appearance.themes.ocean": "Ocean", + "appearance.themes.neon": "Neon", + "browse.sort": "Sort", + "browse.sortAlphabeticalByName": "By title", + "browse.sortAlphabeticalByArtist": "By artist", + "browse.sortAlphabeticalByAlbum": "By album", + "browse.sortNewest": "Recently added", + "browse.sortRecent": "Recently played", + "browse.sortFrequent": "Most played", + "browse.sortStarred": "Favourites", + "browse.sortByYear": "By year", + "browse.filterAlbums": "Filter by title or artist", + "browse.filterArtists": "Filter by name", + "browse.filterPlaceholder": "Filter…", + "browse.matches": { + one: "{count} of {total} shown", + other: "{count} of {total} shown", + }, + "browse.noMatch": "Nothing matches that filter.", + "card.play": "Play", + "card.queue": "Add to queue", + "rating.label": "Rating", + "rating.stars": { one: "{count} star", other: "{count} stars" }, + "nav.genres": "Genres", + "nav.history": "Recently played", + "nav.random": "Random", + "nav.playing": "Now playing", + "nav.upload": "Upload", + "nav.folders": "Folders", + "nav.attention": "Needs attention", + "nav.quality": "Quality", +} as const; diff --git a/webapp/src/state/i18n/en/folders.ts b/webapp/src/state/i18n/en/folders.ts new file mode 100644 index 00000000..341e6d21 --- /dev/null +++ b/webapp/src/state/i18n/en/folders.ts @@ -0,0 +1,137 @@ +// Artist splits and images, folders, filing and uploads. +export const folders = { + "artistSplit.offer": "Split into {count} artists", + "artistSplit.confirm": "Confirm: split into {count}", + "artistSplit.done": "Split into {parts}.", + "artistSplit.failed": "This credit could not be split. Try again.", + "common.back": "Back", + "artistImage.changePortrait": "Change portrait", + "artistImage.changeBanner": "Change banner", + "artistImage.resetPortrait": "Automatic portrait", + "artistImage.resetBanner": "No banner", + "artistImage.portraitChanged": "Portrait changed.", + "artistImage.bannerChanged": "Banner changed.", + "artistImage.portraitReverted": "Back to the automatic portrait.", + "artistImage.bannerReverted": "Banner removed.", + "artistImage.tooLarge": "This image is larger than 10 MB.", + "artistImage.notAnImage": "Choose a JPEG, PNG or WebP image.", + "artistImage.failed": "That did not work. Try again.", + "artistImage.chosen": "Portrait chosen by hand", + "artistImage.searchDeezer": "Search Deezer", + "artistImage.menu": "Edit images", + "artistImage.menuShort": "Edit", + "artistImage.menuLabel": "Images of {name}", + "artistImage.deezerTitle": "Choose a portrait from Deezer", + "artistImage.deezerQuery": "Artist name", + "artistImage.deezerSearch": "Search", + "artistImage.deezerSearching": "Searching Deezer…", + "artistImage.deezerResults": "Deezer artists", + "artistImage.deezerNone": "Deezer knows no artist by that name.", + "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, + "artistImage.deezerUnavailable": + "Searching Deezer is not enabled on this server.", + "artistImage.deezerBusy": + "Your previous search is still running. Try again in a moment.", + "artistImage.deezerExpired": "These results have expired. Search again.", + "folders.count": { + one: "{count} folder created in this library", + other: "{count} folders created in this library", + }, + "folders.where": "Folder path", + "folders.subfolders": "Folders", + "folders.noLibrary": "No library is available to this account.", + "folders.none": "No folder has been created in this library yet.", + "folders.system": "Kept by WaveFlow", + "folders.systemNote": + "To sort gathers every track that is in no folder. WaveFlow keeps it itself: it is not counted among the folders above, and it cannot be renamed, moved or deleted.", + "filed.unfiled": "To sort", + "filed.automatic": "Automatic", + "filed.topLevel": "Top level", + "filed.here": "Here now", + "filed.destinations": "Folders to choose from", + "filed.loadError": "The folders could not be loaded.", + "filed.moveFailed": "The move failed. Try again.", + "filed.moveTrack": "Move “{title}” to a folder", + "filed.moveFolder": "Move “{name}”", + "filed.note": + "Folders that exist only in WaveFlow. Filing a track here moves no file.", + "filed.newFolder": "New folder", + "filed.name": "Folder name", + "filed.renameLabel": "New name", + "filed.create": "Create", + "filed.save": "Save", + "filed.cancel": "Cancel", + "filed.rename": "Rename", + "filed.move": "Move…", + "filed.delete": "Delete", + "filed.confirmDelete": + "Delete “{name}” and its sub-folders? Their tracks go back to To sort; no file is touched.", + "filed.created": "“{name}” created.", + "filed.renamed": "Renamed to “{name}”.", + "filed.deleted": { + one: "Folder deleted: {count} track is back in To sort.", + other: "Folder deleted: {count} tracks are back in To sort.", + }, + "filed.filed": { + one: "{count} track filed in {folder}.", + other: "{count} tracks filed in {folder}.", + }, + "filed.folderMoved": "“{name}” moved.", + "filed.nameTaken": "A folder of that name is already here.", + "filed.refused": + "That folder cannot go there: check its name, or the tree is too deep or full.", + "filed.failed": "That did not work. Try again.", + "filed.missing": "This folder no longer exists.", + "filed.empty": "Nothing is filed here yet.", + "filed.allFiled": "Every track is filed.", + "filed.uploadHere": "Upload here", + "filed.unfiledNote": + "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed, and it cannot be renamed or deleted. To file a track, open its menu and choose “Move to folder…”.", + "filed.unfiledNoteReader": + "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed.", + "trackMenu.move": "Move to folder…", + "upload.into": "Received files go into the folder {folder}.", + "upload.doneUnfiled": "Added to the library, in To sort", + "upload.folderMissing": + "The folder this upload was opened for cannot be found: received files will go to To sort.", + "upload.folderUnchecked": + "The folder this upload was opened for could not be checked, so no file is taken yet.", + "upload.checkAgain": "Check again", + "nav.group.library": "Library", + "nav.group.listen": "Listening", + "upload.title": "Upload to {library}", + "upload.detail": + "Each file is fingerprinted in your browser first, so a file the library already holds is never sent again, and an interrupted transfer picks up where it stopped.", + "upload.pick": "Choose audio files", + "upload.formats": "Formats the library can index: {extensions}", + "upload.noLibrary": "Choose a library first.", + "upload.notAllowed": + "Only an owner or a manager of this library can add files to it.", + "upload.closed": + "This library does not take files. Its operator decides that on the server.", + "upload.waiting": "Waiting", + "upload.hashing": "Fingerprinting…", + "upload.negotiating": "Asking the server…", + "upload.uploading": "Sending…", + "upload.committing": "Adding to the library…", + "upload.done": "Added to the library", + "upload.present": "Already in the library", + "upload.refused": "Refused: {reason}", + "upload.failed": "Failed: {reason}", + "upload.progress": "Progress: {name}", + "upload.reason.unsupportedFormat": + "not an audio format the library can index", + "upload.reason.tooLarge": "larger than the server accepts", + "upload.reason.quotaExceeded": "the library has no room left", + "upload.reason.libraryClosed": "the library does not take files", + "upload.reason.tooManySessions": + "too many transfers already open — try again shortly", + "upload.reason.refusedFile": + "what arrived did not match the file, or is not audio the library can read", + "upload.reason.unavailable": + "the server could not take it just now — try again", + "upload.reason.network": "the connection was lost", + "upload.reason.notAllowed": "you may no longer add files to this library", + "upload.reason.error": "something went wrong", + "upload.reason.hashing": "this browser could not fingerprint the file", +} as const; diff --git a/webapp/src/state/i18n/en/index.ts b/webapp/src/state/i18n/en/index.ts new file mode 100644 index 00000000..3cce8f15 --- /dev/null +++ b/webapp/src/state/i18n/en/index.ts @@ -0,0 +1,30 @@ +// The English strings, one file per domain, in their original order. +import { browse } from "./browse"; +import { folders } from "./folders"; +import { library } from "./library"; +import { maintenance } from "./maintenance"; +import { playing } from "./playing"; +import { podcasts } from "./podcasts"; +import { quality } from "./quality"; +import { scrobbling } from "./scrobbling"; +import { settings } from "./settings"; +import { shell } from "./shell"; +import { smart } from "./smart"; +import { stats } from "./stats"; +import { track } from "./track"; + +export const en = { + ...shell, + ...settings, + ...track, + ...library, + ...browse, + ...quality, + ...folders, + ...playing, + ...maintenance, + ...scrobbling, + ...stats, + ...smart, + ...podcasts, +}; diff --git a/webapp/src/state/i18n/en/library.ts b/webapp/src/state/i18n/en/library.ts new file mode 100644 index 00000000..71ff3857 --- /dev/null +++ b/webapp/src/state/i18n/en/library.ts @@ -0,0 +1,175 @@ +// Sign-in, the library pages, import, the queue, shares and radio. +export const library = { + "login.heroEyebrow": "Your music, on your terms", + "login.heroTitle": "After dark, the library comes alive.", + "login.heroDetail": "Private streaming, one catalogue, every screen.", + "login.server": "WaveFlow server", + "login.createAdmin": "Create the administrator", + "login.welcome": "Welcome back", + "login.setupDetail": + "This is a new server. Choose the first administrator account.", + "login.setupToken": "Setup token", + "login.setupTokenHint": + "The server wrote it to the file setup-token in its data directory, and its log names the path. If you set WAVEFLOW_SETUP_TOKEN, enter that value.", + "login.setupTokenError": + "This setup token is not the right one. Copy it again from the setup-token file.", + "login.username": "Username", + "login.password": "Password", + "login.setupError": + "Setup failed. Use a valid username and at least 12 password characters.", + "login.error": "Wrong username or password.", + "login.wait": "Please wait…", + "login.create": "Create and sign in", + "login.signIn": "Sign in", + "albums.detail": { + one: "{count} record in your library", + other: "{count} records in your library", + }, + "albums.gridTitle": "In your library", + "artists.detail": { + one: "{count} voice or collaborator", + other: "{count} voices and collaborators", + }, + "artists.libraryCount": { + one: "{count} album in your library", + other: "{count} albums in your library", + }, + "artists.mostPlayed": "Most played here", + "artists.section.album": "Albums", + "artists.section.ep": "EPs", + "artists.section.single": "Singles", + "artists.section.live": "Live", + "artists.section.compilation": "Compilations", + "artists.section.appearsOn": "Appears on", + "artists.about": "About", + "artists.readMore": "Read more", + "artists.similar": "Similar artists in your library", + "artists.enrichmentPending": "Looking this artist up…", + "artists.bioSource": "Biography from {provider}, {date}.", + "artists.similarSource": "Suggested by {provider}, {date}.", + "artists.portraitSource": "Portrait from {provider}, {date}.", + "artists.bannerSource": "Banner from {provider}, {date}.", + "favourites.detail": { + one: "{count} saved track", + other: "{count} saved tracks", + }, + "favourites.empty": + "Star a track from an album or search result and it will appear here.", + "favourites.add": "Add favourite", + "favourites.remove": "Remove favourite", + "playlists.detail": { + one: "{count} collection", + other: "{count} collections", + }, + "playlists.newName": "New playlist name", + "playlists.create": "Create playlist", + "playlists.play": "Play", + "playlists.addQueue": "Add queue", + "playlists.emptyOne": "This playlist is empty.", + "playlists.export": "Save as .m3u8", + "playlists.exportError": "That playlist could not be saved.", + "import.title": "Bring playlists from another player", + "import.detail": + "A .m3u or .m3u8 file. Nothing is created until you have seen what it would lose.", + "import.pick": "Choose a playlist file", + "import.name": "Playlist name", + "import.found": "{resolved} of {entries} entries are in your library.", + "import.encoding": + "The file was not UTF-8 and was read as {encoding}. Check the names below before importing.", + "import.missing": { + one: "{count} entry cannot be placed:", + other: "{count} entries cannot be placed:", + }, + "import.line": "Line {line}", + "import.noMatch": "no matching track", + "import.ambiguous": "matches several tracks", + "import.notAPath": "not a file in a library", + "import.duplicate": "already in this playlist", + "import.confirm": "Import", + "import.cancel": "Cancel", + "import.failed": "That file could not be read as a playlist.", + "playlists.empty": "Create a playlist to keep a set of tracks together.", + "playlists.createError": "The playlist could not be created.", + "playlists.queueError": "The queue could not be added to this playlist.", + "playlists.deleteError": "The playlist could not be deleted.", + "queue.detail": { + one: "{count} track synchronized with your account", + other: "{count} tracks synchronized with your account", + }, + "queue.playStart": "Play from start", + "queue.clear": "Clear queue", + "queue.remove": "Remove", + "queue.empty": "Play an album or playlist to build your synchronized queue.", + "shares.detail": "Public links for selected music", + "shares.description": "Description", + "shares.create": "Share current queue", + "shares.needQueue": "Add tracks to the queue before creating a link.", + "shares.once": "This link is shown only once:", + "shares.defaultName": "Music share", + "shares.empty": "No public links have been created.", + "shares.createError": "The share could not be created.", + "shares.tooLong": + "A share holds up to {limit} tracks, and the queue holds {count}. Shorten the queue to share it.", + "shares.deleteError": "The share could not be deleted.", + "shares.maxVisits": "Visit limit (optional)", + "shares.visits": { one: "{count} visit", other: "{count} visits" }, + "shares.visitsOf": "{count} of {max} visits", + "share.kicker": "Shared with you", + "share.defaultTitle": "Music shared with you", + "share.until": "Available until {date}", + "share.empty": "None of these tracks can be played any more.", + "share.player": "Player", + "share.unavailableTitle": "This link opens nothing", + "share.unavailableDetail": + "It may have expired, been withdrawn, or been opened as many times as it allows.", + "playlists.public": "Visible to every account", + "playlists.publicError": "That playlist could not be published.", + "playlists.others": "From other accounts", + "playlists.othersDetail": + "Public playlists of other accounts, with the tracks your libraries hold.", + "playlists.by": "By {owner}", + "playlists.moveUp": "Move {title} up", + "playlists.moveDown": "Move {title} down", + "playlists.removeEntry": "Remove {title} from this playlist", + "playlists.moveError": + "That entry could not be moved. The playlist may have changed elsewhere; it has been read again.", + "playlists.removeEntryError": "That entry could not be removed.", + "radio.detail": "Internet stations, kept with your account", + "radio.name": "Station name", + "radio.address": "Stream address", + "radio.homepage": "Station page (optional)", + "radio.image": "Station image (optional)", + "radio.imageHint": + "The server downloads it once, when you save, and keeps it with the covers: your browser never contacts the image’s host.", + "radio.imageMissing": + "The image could not be fetched; save the station to try again.", + "radio.add": "Add station", + "radio.save": "Save station", + "radio.edit": "Edit", + "radio.replacing": + "Saving replaces every field, so an empty page address clears the stored one.", + "radio.listen": "Listen", + "radio.stop": "Stop", + "radio.visit": "Station page", + "radio.empty": "No stations yet. Add one to keep it here.", + "radio.addressRefused": "An address must start with http:// or https://.", + "radio.mixedContent": + "This page is served over HTTPS, so your browser will refuse a clear-text stream.", + "radio.full": "This account already holds as many stations as it may.", + "radio.saveError": "The station could not be saved.", + "radio.deleteError": "The station could not be deleted.", + "radio.playbackError": + "That station could not be played. The server never connects to it, so this is between your browser and the station.", + "radio.stations": "Stations", + "radio.newTitle": "Add a station", + "radio.editTitle": "Edit “{name}”", + "radio.listenTo": "Listen to {name}", + "radio.stopNamed": "Stop {name}", + "radio.actions": "Actions for {name}", + "radio.confirmDelete": + "Delete the station “{name}”? Its address and image are forgotten.", + "credential.password": "Dedicated Subsonic password", + "credential.rotate": "Rotate credential", + "credential.copy": "Copy this API key now:", + "credential.error": "Credential rotation failed.", +} as const; diff --git a/webapp/src/state/i18n/en/maintenance.ts b/webapp/src/state/i18n/en/maintenance.ts new file mode 100644 index 00000000..5283f9bb --- /dev/null +++ b/webapp/src/state/i18n/en/maintenance.ts @@ -0,0 +1,102 @@ +// Scan progress, accounts and tokens, maintenance and missing files. +export const maintenance = { + "scan.connecting": "Waiting for the first reading…", + "scan.queued": "Queued", + "scan.running": "Scanning", + "scan.completed": "Finished", + "scan.failed": "Failed", + "scan.progress": "Scan progress", + "scan.counted": "{processed} of {total} files", + "scan.tally": + "{added} added · {updated} updated · {moved} moved · {skipped} unchanged · {errors} errors", + "admin.scanning": "Scan in progress", + "admin.nowPlaying": "Playing now", + "admin.nobodyListening": "Nobody is listening right now.", + "admin.tokens": "API tokens for {username}", + "admin.tokenName": "What is it for?", + "admin.tokenCreate": "Issue", + "admin.tokenOnce": "Copy it now. It is shown once and never again:", + "admin.tokenError": "The token could not be issued.", + "admin.tokenRevoke": "Revoke", + "admin.tokenUsed": "already used", + "admin.tokenUnused": "never used", + "admin.tokenNone": "No token on this account.", + "scan.lost": + "The progress stream could not be opened. The scan itself is unaffected.", + "admin.members": "Who may see this library", + "admin.memberRole": "Role", + "admin.memberRemove": "Remove", + "admin.memberOwner": "owner, and stays one", + "admin.memberAdd": "Give access to", + "admin.memberChoose": "Choose an account…", + "admin.memberError": "That membership could not be changed.", + "admin.manage": "Name, folder and deletion", + "admin.rename": "Name", + "admin.path": "Folder on the server", + "admin.followLinks": + "Follow symbolic links whose target stays inside the library", + "admin.watch": + "Watch the folder and scan what changes (off for a network share changed by other machines)", + "admin.save": "Save", + "admin.saved": "Saved.", + "admin.savedScan": "Saved. Scan {id} queued.", + "admin.saveError": + "That change could not be saved. A folder must exist on the server and must not be a symbolic link.", + "admin.saveBusy": + "The library is being scanned, or another library already uses that folder. Try again once the scan is over.", + "admin.delete": "Delete this library", + "admin.deleteNote": + "Removes the library from the catalogue for every member: its tracks, albums and artists, with their playlist entries, favourites and plays. No file is touched on disk. Type the library’s name to confirm.", + "admin.deleteConfirm": "The library’s name, to confirm", + "admin.deleteButton": "Delete for good", + "admin.deleted": "{name} was deleted: {tracks} tracks left the catalogue.", + "admin.deleteError": "The library could not be deleted.", + "scan.leftOut": "{excluded} left out · {links} links not followed", + "nav.missing": "Missing files", + "nav.maintenance": "Maintenance", + "maintenance.sections": "Maintenance sections", + "maintenance.none": "Nothing here is yours to maintain in this library.", + "missing.total": { + one: "{count} file the scans can no longer find", + other: "{count} files the scans can no longer find", + }, + "missing.nothing": "Every file is where the catalogue expects it.", + "missing.notYours": + "Only the owners and managers of this library, and administrators, see its missing files.", + "missing.note": + "These tracks stay in the catalogue, with their favourites, playlist entries and plays, until they are purged; a file that comes back first keeps its track. A purge can be taken back for a while, then the track, and everything that hangs off it for every member, goes for good.", + "missing.since": "missing since {date}", + "missing.cost": + "{playlists} in playlists · {favorites} favourites · {plays} plays", + "missing.select": "Select {title}", + "missing.purgeSelected": "Purge the selected ({count})", + "missing.purgeAll": "Purge them all", + "missing.confirm": "Confirm: purge {count}", + "missing.purged": { + one: "{count} track purged.", + other: "{count} tracks purged.", + }, + "missing.scanning": + "The library is being scanned. Try again once the scan is over.", + "missing.failed": "The purge did not work. Try again.", + "missing.more": "Show more", + "missing.purgedUntil": { + one: "{count} track purged. It can be restored until {date}.", + other: "{count} tracks purged. They can be restored until {date}.", + }, + "missing.purgedTitle": { + one: "Purged, still restorable ({count})", + other: "Purged, still restorable ({count})", + }, + "missing.purgedNote": + "Nothing of these is lost yet. Restored, a track goes back to the missing files with everything that hung off it; a file that comes back restores its track by itself.", + "missing.deletedAfter": "deleted for good after {date}", + "missing.restore": "Restore", + "missing.restoreOne": "Restore {title}", + "missing.restoreAll": "Restore them all", + "missing.restored": { + one: "{count} track restored.", + other: "{count} tracks restored.", + }, + "missing.restoreFailed": "The restoration did not work. Try again.", +} as const; diff --git a/webapp/src/state/i18n/en/playing.ts b/webapp/src/state/i18n/en/playing.ts new file mode 100644 index 00000000..f03fd67c --- /dev/null +++ b/webapp/src/state/i18n/en/playing.ts @@ -0,0 +1,51 @@ +// Genres, history, random, lyrics, now playing, bookmarks, the player bar. +export const playing = { + "browse.filterGenres": "Filter by genre", + "genres.detail": { one: "{count} genre", other: "{count} genres" }, + "genres.empty": "No genre is tagged in this library yet.", + "genres.emptyOne": "Nothing is tagged with this genre any more.", + "history.detail": { + one: "{count} track, most recent first", + other: "{count} tracks, most recent first", + }, + "history.empty": "Nothing has been played from this account yet.", + "random.detail": { + one: "{count} track drawn", + other: "{count} tracks drawn", + }, + "random.again": "Draw again", + "random.fromGenre": "Play this genre at random", + "random.empty": "There is nothing to draw from yet.", + "lyrics.label": "Lyrics", + "lyrics.none": "No lyrics travel with this file.", + "playing.idle": "Nothing is playing.", + "playing.empty": "Start a track and it will appear here, with its lyrics.", + "lyrics.seekHint": "This sheet carries timings — press a line to jump to it.", + "lyrics.seekTo": "Jump to {when}", + "lyrics.seekLine": "{line}, jump to {when}", + "playing.viewLabel": "How to look at this track", + "playing.viewDetail": "Detail", + "playing.viewLyrics": "Lyrics", + "playing.viewImmersive": "Immersive", + "playing.immersiveTitle": "Immersive view", + "playing.leaveImmersive": "Leave the immersive view", + "bookmarks.title": "Saved positions", + "bookmarks.detail": "One per track. Saving again moves it.", + "bookmarks.save": "Save this position", + "bookmarks.forget": "Forget", + "player.shuffle": "Shuffle", + "player.repeatOff": "Repeat: off", + "player.repeatAll": "Repeat: the whole queue", + "player.repeatOne": "Repeat: this track", + "player.volume": "Volume", + "player.mute": "Mute", + "player.unmute": "Unmute", + "player.openAlbum": "Open the album", + "player.live": "Live", + "player.liveRadio": "Live radio", + "player.stopLive": "Stop the station", + "player.liveError": "This station could not be played.", + "player.closeAside": "Stop and close", + "library.active": "Library", + "search.thisLibrary": "Searching {library}.", +} as const; diff --git a/webapp/src/state/i18n/en/podcasts.ts b/webapp/src/state/i18n/en/podcasts.ts new file mode 100644 index 00000000..3578a1f4 --- /dev/null +++ b/webapp/src/state/i18n/en/podcasts.ts @@ -0,0 +1,44 @@ +// Podcast subscriptions and episodes. +export const podcasts = { + "nav.podcasts": "Podcasts", + "podcasts.detail": "Shows this server follows, for every account", + "podcasts.off": + "Podcasts are switched off on this server. What was kept before stays readable; nothing new is fetched.", + "podcasts.url": "Feed address", + "podcasts.shared": + "Visible to every account: anyone on this server sees the show and can play the episodes it downloads. Only administrators see the feed address.", + "podcasts.subscribe": "Subscribe", + "podcasts.refresh": "Refresh every feed", + "podcasts.refreshing": "The feeds are being fetched again.", + "podcasts.reload": "Reload", + "podcasts.empty": "No podcasts yet.", + "podcasts.emptyDetail": + "An administrator subscribes to a show’s feed here. The server downloads its episodes, and every account on this server can listen to them.", + "podcasts.addFirst": "Add your first podcast", + "podcasts.channels": "Shows", + "podcasts.showing": "Showing the episodes of {title}.", + "podcasts.noEpisodes": "This feed has no episodes.", + "podcasts.episodes": { + one: "{count} episode", + other: "{count} episodes", + }, + "podcasts.failed": "last fetch failed: {reason}", + "podcasts.open": "Episodes", + "podcasts.hide": "Hide", + "podcasts.unsubscribe": "Unsubscribe", + "podcasts.download": "Download", + "podcasts.play": "Play {title}", + "podcasts.pause": "Pause {title}", + "podcasts.resumeAt": "resumes at {time}", + "podcasts.status.new": "not downloaded", + "podcasts.status.downloading": "downloading", + "podcasts.status.completed": "ready", + "podcasts.status.error": "download failed", + "podcasts.status.deleted": "deleted", + "podcasts.conflict": "This server already follows that feed.", + "podcasts.refused": + "The feed could not be fetched or read, or its address is not one this server may reach.", + "podcasts.subscribeError": "The feed could not be added.", + "podcasts.mutationError": "That did not work. Try again.", + "podcasts.playbackError": "This episode could not be played.", +} as const; diff --git a/webapp/src/state/i18n/en/quality.ts b/webapp/src/state/i18n/en/quality.ts new file mode 100644 index 00000000..c8997764 --- /dev/null +++ b/webapp/src/state/i18n/en/quality.ts @@ -0,0 +1,120 @@ +// The attention lists and the audio quality analysis. +export const quality = { + "attention.lists": "Lists", + "attention.total": { + one: "{count} thing to look at", + other: "{count} things to look at", + }, + "attention.nothing": "Nothing needs attention.", + "attention.notYours": + "Only the owners and managers of this library see what needs attention in it.", + "attention.empty": "Nothing in this list.", + "attention.tracksNote": + "Fix a track from its menu: once corrected, it leaves the list.", + "attention.creditsNote": + "Each of these credits names several artists in one string. Split it into the artists it names, or keep it as one when it is a single name. Every later scan applies the decision.", + "attention.pending": "To decide", + "attention.decided": "Decided", + "attention.split": "Split", + "attention.confirmSplit": "Confirm: split into {count}", + "attention.keep": "Keep as one", + "attention.splitDone": "{name} is split.", + "attention.keepDone": "{name} stays as one.", + "attention.undone": "The decision about {name} is undone.", + "attention.alreadyDecided": + "This credit was decided in the meantime. The list has been reloaded.", + "attention.failed": "That did not work. Try again.", + "attention.noCredits": "No credit is waiting for a decision.", + "attention.noDecisions": "Nothing has been decided yet.", + "attention.fragments": "What {name} would split into", + "attention.knownFragment": "Already an artist in this library", + "attention.decidedSplit": "Split into {parts}", + "attention.decidedKeep": "Kept as one", + "attention.decidedBy": "By {who}, {when}", + "attention.undo": "Undo", + "attention.duplicateGroup": "Probable duplicate {group}", + "attention.split_credits": "Artists to split", + "attention.missing_artist": "Missing artist", + "attention.missing_album": "Missing album", + "attention.missing_year": "Missing year", + "attention.missing_track_number": "Missing track number", + "attention.missing_cover": "Missing cover", + "attention.album_year_conflict": "Albums with several years", + "attention.compilation_no_album_artist": + "Compilations without an album artist", + "attention.duplicate_track_number": "Duplicate track numbers", + "attention.track_number_gap": "Gaps in track numbers", + "attention.probable_duplicate": "Probable duplicates", + "attention.probable_fake_lossless": "Probable fake lossless", + "quality.help": + "Measured from the audio rather than read from the container: where the highs stop, and how many bits the samples use. A lossless file whose highs stop around 16 kHz was a 128 kbps MP3 before it was a FLAC. “No sign of a lossy source” is not proof: an encoder that keeps the full band leaves nothing to find.", + "quality.progress": "{measured} of {lossless} lossless files analysed", + "quality.disabled": "The analysis is turned off on this server.", + "quality.noLossless": "No lossless file in this library.", + "quality.status.running": + "Analysing now: {measured} of {lossless} done, {inFlight} in progress.", + "quality.status.waitingForScan": + "Waiting for the scan to finish before analysing.", + "quality.status.queued": + "Queued: the analysis will reach these files shortly.", + "quality.status.idle": "Idle. Last file analysed {when}.", + "quality.status.idleNever": "Idle. No file has been analysed yet.", + "quality.filters": "Verdicts", + "quality.all": "All files", + "quality.format": "Format", + "quality.allFormats": "All formats", + "quality.empty": "No file with this verdict.", + "quality.column.track": "Track", + "quality.column.container": "What the file says", + "quality.column.highs": "Highs stop at", + "quality.column.verdict": "What the audio says", + "quality.fullBand": "Full band", + "quality.khz": "{value} kHz", + "quality.db": "{value} dB", + "quality.analyseAgain": "Analyse again", + "quality.confirmAnalyse": "Confirm: analyse the whole library again", + "quality.requested": "The library will be analysed again in the background.", + "quality.requestFailed": "That did not work. Try again.", + "quality.section": "Real quality", + "quality.verdictLabel": "Verdict", + "quality.highsStop": "Highs stop at", + "quality.wall": "Fall at that point", + "quality.bitsUsed": "Bits in use, measured", + "quality.bitsOf": "{used} of {declared}", + "quality.bitsNote": + "A measurement of the samples, not proof of genuine hi-res: a file that uses every bit it declares was not padded, which does not mean it was recorded at that depth.", + "quality.analysed": "Analysed", + "quality.chart": "Averaged spectrum: the highs stop at {where}.", + "quality.chartFullBand": "Averaged spectrum: the highs run on with no wall.", + "quality.showSpectrogram": "Show the spectrogram", + "quality.drawing": "Drawing…", + "quality.spectrogramAlt": "Spectrogram of {title}, frequency against time", + "quality.spectrogramBusy": + "A spectrogram is already being drawn for you. Try again in a moment.", + "quality.spectrogramFailed": "The spectrogram could not be drawn.", + "quality.verdict.lossy_source": "Probably from a lossy file", + "quality.verdict.upsampled": "Probably upsampled", + "quality.verdict.padded": "Padded bit depth", + "quality.verdict.unreadable": "Could not be read", + "quality.verdict.silent": "Silent", + "quality.verdict.pending": "Waiting for analysis", + "quality.verdict.consistent": "No sign of a lossy source", + "quality.verdict.lossy": "Lossy", + "quality.verdict.not_measured": "Not measured", + "quality.explain.lossy_source": + "The highs stop at a wall where a lossy encoder cuts: this file was probably decoded from an MP3, AAC or Opus file and saved as lossless.", + "quality.explain.upsampled": + "The rate is above 48 kHz, but the highs stop where a CD or a 48 kHz recording ends: it was probably resampled from one.", + "quality.explain.padded": + "The container declares more bits than the samples use: a 16-bit recording stored at 24 bits, for instance.", + "quality.explain.unreadable": + "FFmpeg could not decode this file. It will be tried again when the file changes.", + "quality.explain.silent": "Nothing in this file is loud enough to measure.", + "quality.explain.pending": + "This file has not been analysed yet. The analysis runs in the background.", + "quality.explain.consistent": + "Nothing in the audio contradicts the container. That is not proof: an encoder that keeps the full band leaves nothing to find.", + "quality.explain.lossy": + "A lossy file says what it is; its quality is its encoder and its bitrate. It is not analysed.", + "quality.explain.not_measured": "This format is not analysed.", +} as const; diff --git a/webapp/src/state/i18n/en/scrobbling.ts b/webapp/src/state/i18n/en/scrobbling.ts new file mode 100644 index 00000000..048414fe --- /dev/null +++ b/webapp/src/state/i18n/en/scrobbling.ts @@ -0,0 +1,95 @@ +// Offline listening and scrobbling. +export const scrobbling = { + "offline.kicker": "In this browser", + "offline.title": "Kept for offline", + "offline.detail": + "These tracks were kept in this browser for the account that last signed in, and play without a connection. Signing out deletes them from this browser. Keep more from a track's menu.", + "offline.unsupported": + "This browser cannot keep tracks for offline listening.", + "offline.back": "Back to the library", + "offline.empty": "Nothing is kept yet. A track's menu offers to keep it.", + "offline.player": "Offline player", + "offline.gone": "That copy is no longer in this browser.", + "offline.forget": "Remove the offline copy of {title}", + "offline.forgetShort": "Remove", + "offline.forgetDetail": "Frees {size}", + "login.offline": { + one: "Listen to the {count} track kept in this browser", + other: "Listen to the {count} tracks kept in this browser", + }, + "scrobbling.title": "External scrobbling", + "scrobbling.detail": + "Forward what you listen to here to ListenBrainz, Maloja or Last.fm.", + "scrobbling.none": + "This server offers no destination. Its operator decides that.", + "scrobbling.provider.listenbrainz": "ListenBrainz", + "scrobbling.provider.maloja": "Maloja", + "scrobbling.provider.lastfm": "Last.fm", + "scrobbling.instance": "instance “{name}”", + "scrobbling.unavailable": "Unavailable here: {reason}", + "scrobbling.unavailable.no_application_configured": + "no Last.fm application is declared on this server", + "scrobbling.unavailable.browser_journey_needs_https": + "the browser journey needs this server’s public address to be an https one — an operator can still link an account from the command line", + "scrobbling.unavailable.unknown": "this server did not say why", + "scrobbling.retired": + "This server no longer offers this instance. Nothing more will leave for it — you can still withdraw the authorisation.", + "scrobbling.notLinked": "Not linked.", + "scrobbling.secret": "API key", + "scrobbling.secretHint": "Taken from your account on that destination.", + "scrobbling.link": "Link", + "scrobbling.working": "Working…", + "scrobbling.linkError": + "That authorisation was refused. Check the key and try again.", + "scrobbling.unlink": "Unlink", + "scrobbling.unlinkError": "That link could not be withdrawn.", + "scrobbling.connect": "Connect to Last.fm", + "scrobbling.connectDetail": + "Last.fm hands out no key you can paste, so it asks you there and brings you back.", + "scrobbling.connectError": "The Last.fm journey could not be opened.", + "scrobbling.returned": "Last.fm is linked: {name}.", + "scrobbling.health.healthy": "Listens are leaving", + "scrobbling.health.degraded": "The queue is not moving", + "scrobbling.health.broken": "Broken", + "scrobbling.healthDetail.healthy": "Nothing is stuck here.", + "scrobbling.healthDetail.degraded": + "The link answers, but listens are not getting through — either one is waiting on you, or the oldest has waited too long.", + "scrobbling.healthDetail.broken": + "The credential was refused, or this server can no longer reach that instance under the name the link was made against.", + "scrobbling.pending": { one: "{count} waiting", other: "{count} waiting" }, + "scrobbling.retrying": { + one: "{count} being retried", + other: "{count} being retried", + }, + "scrobbling.uncertainCount": { + one: "{count} waiting on you", + other: "{count} waiting on you", + }, + "scrobbling.oldest": "Oldest has waited since {when}.", + "scrobbling.lastSuccess": "Last accepted {when}.", + "scrobbling.lastFailure": "Last refusal: {reason}.", + "scrobbling.uncertain": "Listens only you can decide about", + "scrobbling.uncertainDetail": + "The connection broke after these left, so nobody knows whether the destination recorded them. Sending one again may put it twice in a listening history other people read; throwing it away leaves a gap. This server will not choose for you, and it will ask once.", + "scrobbling.uncertainNone": "Nothing is waiting on you.", + "scrobbling.playedAt": "Played {when}", + "scrobbling.attempts": { one: "{count} attempt", other: "{count} attempts" }, + "scrobbling.retry": "Send it again", + "scrobbling.discard": "Throw it away", + "scrobbling.answered": "Answered.", + "scrobbling.answerError": + "That listen could not be answered. Reload the page before trying again.", + "scrobbling.unnamedTrack": "Not in the history this account still holds", + "scrobbling.reason.rate_limited": "the destination asked for room", + "scrobbling.reason.retryable": "the destination did not take it", + "scrobbling.reason.auth_broken": "the authorisation was refused", + "scrobbling.reason.rejected": "the destination read it and refused it", + "scrobbling.reason.destination_gone": + "this server no longer reaches that instance under that name", + "scrobbling.reason.credential_unreadable": + "this server could not read the stored credential", + "scrobbling.reason.attempts_exhausted": "it was tried too many times", + "scrobbling.reason.unlinked": "the link was withdrawn", + "scrobbling.reason.ambiguous": "the answer never arrived", + "scrobbling.reason.interrupted": "the server stopped mid-flight", +} as const; diff --git a/webapp/src/state/i18n/en/settings.ts b/webapp/src/state/i18n/en/settings.ts new file mode 100644 index 00000000..2f0720c1 --- /dev/null +++ b/webapp/src/state/i18n/en/settings.ts @@ -0,0 +1,53 @@ +// Settings, registered clients, the Subsonic password and quality labels. +export const settings = { + "nav.settings": "Settings", + "settings.detail": + "Appearance, language, and the clients allowed to read your library.", + "settings.appearance": "Appearance", + "settings.playback": "Playback", + "settings.sections": "Settings sections", + "settings.section.account": "Account", + "settings.playbackDetail": + "How this browser plays your tracks. Kept in this browser, not on your account.", + "settings.levelling": "Volume levelling", + "clients.title": "Registered clients", + "clients.detail": + "Devices and scripts holding a credential on your library. Revoking one stops it at once.", + "clients.name": "What it is for", + "clients.register": "Register", + "clients.once": + "Copy this now. Only its hash is stored, so it is shown once and never again:", + "clients.revoke": "Revoke", + "clients.none": "No client holds a credential on your library.", + "clients.error": "That did not work. Nothing changed.", + "clients.registered": "registered {when}", + "clients.used": "last used {when}", + "clients.unused": "never used", + "subsonic.title": "Subsonic password", + "subsonic.detail": + "The separate password Subsonic clients sign in with. Changing it replaces the API key too, so a leaked one stops working.", + "subsonic.set": "set {when}", + "subsonic.changed": "last changed {when}", + "subsonic.password": "New password", + "subsonic.rotate": "Change it", + "subsonic.revoke": "Switch it off", + "subsonic.confirm": "Yes, switch it off", + "subsonic.once": + "Copy this API key now. Only its hash is stored, so it is shown once and never again:", + "subsonic.gone": + "Your Subsonic clients will stop working until you set it again in each of them.", + "subsonic.none": + "You have no Subsonic password, so no Subsonic client can read your library. Ask the administrator for one.", + "subsonic.short": "Use at least 12 characters.", + "subsonic.error": "That did not work. Nothing changed.", + "quality.label": "File quality", + "quality.unknown": "Unknown", + "quality.low": "Low", + "quality.standard": "Standard", + "quality.high": "High", + "quality.lossless": "Lossless", + "quality.hiRes": "Hi-res", + "file.channels": { one: "mono", other: "{count} ch" }, + "common.delete": "Delete", + "common.cancel": "Cancel", +} as const; diff --git a/webapp/src/state/i18n/en/shell.ts b/webapp/src/state/i18n/en/shell.ts new file mode 100644 index 00000000..f137e72d --- /dev/null +++ b/webapp/src/state/i18n/en/shell.ts @@ -0,0 +1,80 @@ +// Navigation, preferences, ReplayGain, shared words, album and song views. +export const shell = { + "nav.albums": "Albums", + "nav.artists": "Artists", + "nav.songs": "Songs", + "nav.search": "Search", + "nav.favourites": "Favourites", + "nav.playlists": "Playlists", + "nav.queue": "Queue", + "nav.shares": "Shares", + "nav.offline": "Offline", + "nav.radio": "Radio", + "nav.scrobbling": "Scrobbling", + "nav.admin": "Admin", + "nav.signOut": "Sign out", + "nav.skip": "Skip to content", + "nav.home": "WaveFlow server, home", + "nav.server": "server", + "nav.more": "More", + "nav.moreTitle": "Everything else", + "nav.moreDetail": + "The bar below holds what you reach for while listening. The rest is here.", + "nav.moreClose": "Close", + "preferences.theme": "Theme", + "preferences.language": "Language", + "preferences.themeLight": "Light", + "preferences.themeDark": "Dark", + "preferences.followSystem": "Follow the system", + "preferences.followSystemDetail": + "Switch between a theme’s light and dark versions when your device does.", + "preferences.followingLight": + "Your device is in light mode, so the light version is shown. Picking a theme picks both of its versions.", + "preferences.followingDark": + "Your device is in dark mode, so the dark version is shown. Picking a theme picks both of its versions.", + "preferences.standInLight": + "{theme} has no light version, so {fallback} stands in while your device is in light mode.", + "preferences.standInDark": + "{theme} has no dark version, so {fallback} stands in while your device is in dark mode.", + "replayGain.label": "ReplayGain", + "replayGain.off": "Off", + "replayGain.track": "Track gain", + "replayGain.album": "Album gain", + "replayGain.preamp": "Preamp", + "replayGain.detail": + "Plays every track at the loudness its tags measured, so one record is not louder than the next. Album gain keeps the differences inside an album. A track without tags plays as it is. It routes playback through Web Audio, which some phones pause with the screen.", + "common.loading": "Loading", + "common.loadError": "We could not load this view", + "common.noAlbums": "No albums yet.", + "common.noArtists": "No artists yet.", + "common.variousArtists": "Various artists", + "common.unknownArtist": "Unknown artist", + "common.album": "Album", + "album.play": "Play album", + "album.addQueue": "Add to queue", + "album.title": "Title", + "album.duration": "Duration", + "album.trackActions": "Track actions", + "album.moreBy": "More by {artist}", + "common.artist": "Artist", + "common.albums": { one: "{count} album", other: "{count} albums" }, + "common.tracks": { one: "{count} track", other: "{count} tracks" }, + "songs.loadedSoFar": { + one: "{count} track loaded", + other: "{count} tracks loaded", + }, + "songs.playLoaded": { + one: "Play {count} loaded track", + other: "Play {count} loaded tracks", + }, + "songs.playAll": { + one: "Play the track", + other: "Play all {count} tracks", + }, + "songs.loadMore": "Load more", + "songs.loadMoreError": "That page could not be loaded. Try again.", + "songs.loading": "Loading…", + "songs.empty": "This library holds no tracks yet.", + "file.megabytes": "{size} MB", + "file.kilobytes": "{size} kB", +} as const; diff --git a/webapp/src/state/i18n/en/smart.ts b/webapp/src/state/i18n/en/smart.ts new file mode 100644 index 00000000..80de60ab --- /dev/null +++ b/webapp/src/state/i18n/en/smart.ts @@ -0,0 +1,83 @@ +// Smart playlists. +export const smart = { + // Smart playlists and podcasts (RFC-025), kept together at the end. + "smart.title": "Smart playlists", + "smart.detail": + "Defined by rules and worked out again each time they are opened, so they keep up with the library.", + "smart.new": "New smart playlist", + "smart.edit": "Edit rules", + "smart.save": "Save smart playlist", + "smart.badge": "Smart", + "smart.empty": "No smart playlists yet.", + "smart.notCounted": "Counted when opened", + "smart.chooseLibrary": "Choose a library", + "smart.name": "Name", + "smart.match": "Tracks must match", + "smart.match.all": "every condition", + "smart.match.any": "at least one condition", + "smart.fieldOf": "Field of condition {position}", + "smart.operatorOf": "Comparison of condition {position}", + "smart.valueOf": "Value of condition {position}", + "smart.upperOf": "Upper bound of condition {position}", + "smart.removeCondition": "Remove condition {position}", + "smart.addCondition": "Add a condition", + "smart.sort": "Sort by", + "smart.sort.none": "Title (default)", + "smart.order": "Order", + "smart.order.asc": "Ascending", + "smart.order.desc": "Descending", + "smart.limit": "At most (tracks)", + "smart.days": "days", + "smart.yes": "yes", + "smart.no": "no", + "smart.preview": "Preview", + "smart.previewCount": { + one: "{count} track matches", + other: "{count} tracks match", + }, + "smart.unfinished": "Finish every condition to preview or save.", + "smart.refused": "The server refused these rules.", + "smart.full": "This account already holds as many smart playlists as it may.", + "smart.saveError": "The smart playlist could not be saved.", + "smart.deleteError": "The smart playlist could not be deleted.", + "smart.loadError": "The smart playlists could not be loaded.", + "smart.field.title": "Title", + "smart.field.artist": "Artist", + "smart.field.album_artist": "Album artist", + "smart.field.album": "Album", + "smart.field.genre": "Genre", + "smart.field.year": "Year", + "smart.field.rating": "Rating", + "smart.field.starred": "Favourite", + "smart.field.play_count": "Play count", + "smart.field.last_played": "Last played", + "smart.field.date_added": "Date added", + "smart.field.duration": "Duration (seconds)", + "smart.field.bitrate": "Bitrate (kbit/s)", + "smart.field.format": "Format", + "smart.field.library": "Library", + "smart.op.is": "is", + "smart.op.is_not": "is not", + "smart.op.contains": "contains", + "smart.op.not_contains": "does not contain", + "smart.op.starts_with": "starts with", + "smart.op.ends_with": "ends with", + "smart.op.greater_than": "is more than", + "smart.op.less_than": "is less than", + "smart.op.between": "is between", + "smart.op.in_the_last": "in the last (days)", + "smart.op.not_in_the_last": "not in the last (days)", + "smart.op.before": "before", + "smart.op.after": "after", + "smart.sort.title": "Title", + "smart.sort.artist": "Artist", + "smart.sort.album": "Album", + "smart.sort.year": "Year", + "smart.sort.rating": "Rating", + "smart.sort.play_count": "Play count", + "smart.sort.last_played": "Last played", + "smart.sort.date_added": "Date added", + "smart.sort.duration": "Duration", + "smart.sort.bitrate": "Bitrate", + "smart.sort.random": "Random", +} as const; diff --git a/webapp/src/state/i18n/en/stats.ts b/webapp/src/state/i18n/en/stats.ts new file mode 100644 index 00000000..2d2a6904 --- /dev/null +++ b/webapp/src/state/i18n/en/stats.ts @@ -0,0 +1,53 @@ +// Lyrics from LRCLIB, listening statistics and mixes. +export const stats = { + // RFC-024: lyrics from LRCLIB, statistics and mixes. + "lyrics.source.lrclib": "From LRCLIB", + "lyrics.fromLrclib": "Lyrics from LRCLIB", + "nav.stats": "Statistics", + "stats.detail": "What this account listened to, counted from its own plays.", + "stats.period": "Period", + "stats.period7d": "Last 7 days", + "stats.period30d": "Last 30 days", + "stats.period365d": "Last 12 months", + "stats.periodAll": "All time", + "stats.empty": "Nothing was listened to in this period.", + "stats.plays": "Listens", + "stats.listeningTime": "Listening time", + "stats.tracks": "Tracks", + "stats.artists": "Artists", + "stats.albums": "Albums", + "stats.estimate": + "Listening time adds up the length of each track listened to; no player says how much of it was heard.", + "stats.timelineDays": "Listens per day", + "stats.timelineMonths": "Listens per month", + "stats.bucket": { + one: "{label}: {count} listen", + other: "{label}: {count} listens", + }, + "stats.topTracks": "Top tracks", + "stats.topArtists": "Top artists", + "stats.topAlbums": "Top albums", + "stats.topGenres": "Top genres", + "stats.playCount": { one: "{count} listen", other: "{count} listens" }, + "stats.firstPlay": "First listen of the period", + "stats.lastPlay": "Last listen", + "stats.moment": "{title}, {when}", + "stats.minutes": { one: "{count} minute", other: "{count} minutes" }, + "stats.hoursMinutes": "{hours} h {minutes} min", + "mixes.title": "Made for you today", + "mixes.detail": { + one: "{count} track, drawn for today", + other: "{count} tracks, drawn for today", + }, + "mixes.mostPlayed": "Most played", + "mixes.neverPlayed": "Never played", + "mixes.forgottenFavourites": "Forgotten favourites", + "mixes.recentlyAdded": "Recently added", + "mixes.genre": "{genre} mix", + "mixes.decade": "The {decade}s", + "mixes.failed": "“{title}” could not be opened. Try again.", + "mixes.shelfTitle": "Mixes for you", + "mixes.shelfDetail": + "Playlists drawn from your listening, new every day. Your albums are below.", + "mixes.badge": "Mix", +} as const; diff --git a/webapp/src/state/i18n/en/track.ts b/webapp/src/state/i18n/en/track.ts new file mode 100644 index 00000000..f1ed1835 --- /dev/null +++ b/webapp/src/state/i18n/en/track.ts @@ -0,0 +1,130 @@ +// The track menu and properties, lyrics sheets, tag corrections, canvases. +export const track = { + "correction.edit": "Correct tags", + "correction.editTrack": "Correct tags: {title}", + "trackMenu.label": "Actions for {title}", + "trackMenu.play": "Play", + "trackMenu.queue": "Add to queue", + "trackMenu.download": "Download", + "trackMenu.keepOffline": "Keep for offline", + "trackMenu.forgetOffline": "Remove the offline copy", + "trackMenu.downloadFailed": "The download could not start. Try again.", + "trackMenu.dismiss": "Dismiss", + "trackMenu.goToAlbum": "Go to album", + "trackMenu.goToArtist": "Go to {name}", + "trackMenu.properties": "Properties", + "trackMenu.edit": "Edit tags", + "properties.kicker": "Properties", + "properties.close": "Close properties", + "properties.loadError": "Some properties could not be loaded.", + "properties.file": "File", + "properties.tags": "Tags", + "properties.sound": "Loudness and tempo", + "properties.lyrics": "Lyrics", + "properties.path": "Path in library", + "properties.format": "Format", + "properties.bitrate": "Bit rate", + "properties.sampleRate": "Sample rate", + "properties.bitDepth": "Bit depth", + "properties.channels": "Channels", + "properties.size": "Size", + "properties.duration": "Duration", + "properties.added": "Added", + "properties.hash": "Content hash", + "properties.title": "Title", + "properties.artists": "Artists", + "properties.album": "Album", + "properties.albumArtist": "Album artist", + "properties.year": "Year", + "properties.trackNumber": "Track", + "properties.discNumber": "Disc", + "properties.genres": "Genres", + "properties.comment": "Comment", + "properties.isrc": "ISRC", + "properties.musicbrainz": "MusicBrainz recording", + "properties.trackGain": "ReplayGain (track)", + "properties.albumGain": "ReplayGain (album)", + "properties.bpm": "BPM", + "properties.lyricsSheet": "Sheet {number}", + "properties.synced": "Synced, {lang}", + "properties.plain": "Plain text, {lang}", + "properties.noLyrics": "None", + "lyrics.source.uploadTtml": "Uploaded TTML", + "lyrics.source.uploadLrc": "Uploaded LRC", + "lyrics.source.uploadTxt": "Uploaded text", + "lyrics.source.sidecarLrc": "LRC file beside the track", + "lyrics.source.sidecarText": "Text file beside the track", + "lyrics.source.embedded": "Embedded in the file", + "lyrics.shown": "Shown", + "lyrics.download": "Download lyrics", + "lyrics.upload": "Upload lyrics…", + "lyrics.remove": "Remove the {format}", + "lyrics.unsupported": "Choose a .lrc, .txt or .ttml file.", + "lyrics.empty": "That file holds no lyrics.", + "lyrics.tooLarge": "That file is larger than 1 MB.", + "lyrics.failed": "The lyrics could not be saved. Try again.", + "lyrics.downloadFailed": "The lyrics could not be downloaded. Try again.", + "properties.lyricsSheets": "Sheets", + "properties.kbps": "{value} kbps", + "properties.khz": "{value} kHz", + "properties.bits": "{value} bit", + "properties.mb": "{value} MB", + "properties.db": "{value} dB", + "properties.dbPeak": "{value} dB, peak {peak}", + "properties.corrected": "Corrected", + "properties.fileSaid": "Corrected — the file says {value}", + "correction.detail": + "The file is never rewritten. A correction is stored beside it, survives a rescan, and every member of the library sees it.", + "correction.fieldTitle": "Title", + "correction.fieldSortTitle": "Sort title", + "correction.fieldYear": "Year", + "correction.fieldTrackNumber": "Track number", + "correction.fieldDiscNumber": "Disc number", + "correction.fieldMusicbrainz": "MusicBrainz recording ID", + "correction.fieldComment": "Comment", + "correction.fieldArtists": "Artists", + "correction.fieldGenres": "Genres", + "correction.listHint": "One name per line. Leave it empty to credit nobody.", + "correction.corrected": "Corrected", + "correction.fromFile": "The file says: {value}", + "correction.fileEmpty": "The file says nothing here", + "correction.restore": "Restore the file’s value", + "correction.keep": "Keep the correction", + "correction.restoring": "Goes back to the file’s value when saved", + "correction.restoringList": + "Read back from the file when saved — it cannot be shown beforehand", + "correction.save": "Save", + "correction.saving": "Saving…", + "correction.saved": "Saved.", + "correction.nothing": "Nothing has changed.", + "correction.invalid": "Check {fields}.", + "correction.refused": "The server refused these values.", + "correction.notAllowed": + "Only an owner or a manager of this library can correct its tags.", + "correction.error": "The corrections could not be saved.", + "correction.stale": + "Saved, but the corrections could not be read back. Reload the page before saving again.", + "correction.back": "Back to {album}", + "canvas.title": "Canvas", + "canvas.detail": + "A short loop played without sound over the cover, on the Now playing page. The server takes mp4 or webm.", + "canvas.loading": "Looking for a canvas…", + "canvas.none": "This track has no canvas.", + "canvas.unreadable": "Whether this track has a canvas could not be read.", + "canvas.preview": "Canvas preview", + "canvas.choose": "Choose a loop", + "canvas.replace": "Replace the loop", + "canvas.remove": "Remove the canvas", + "canvas.closed": + "This library does not take canvases. Its operator decides that on the server.", + "canvas.saving": "Sending…", + "canvas.placed": "Canvas saved.", + "canvas.removed": "Canvas removed.", + "canvas.refused": + "The server refused this file: it takes a short mp4 or webm loop with a picture.", + "canvas.tooLarge": "This file is larger than the server takes for a canvas.", + "canvas.quota": "This library has no room left for canvases.", + "canvas.notAllowed": + "The server did not change this canvas: the track or its loop is gone, or the account may no longer change it.", + "canvas.error": "The canvas could not be changed.", +} as const; diff --git a/webapp/src/state/i18n/fr/browse.ts b/webapp/src/state/i18n/fr/browse.ts new file mode 100644 index 00000000..18790b57 --- /dev/null +++ b/webapp/src/state/i18n/fr/browse.ts @@ -0,0 +1,94 @@ +// Administration, search, player buttons, OAuth consent, themes, browsing. +import type { TranslationValue } from ".."; +import type { browse as en } from "../en/browse"; + +export const browse: Record = { + "admin.title": "Administration", + "admin.detail": "Bibliothèques, scans et comptes", + "admin.libraries": "Bibliothèques", + "admin.libraryName": "Nom de la bibliothèque", + "admin.libraryPath": "Chemin absolu du dossier sur le serveur", + "admin.registering": "Ajout en cours…", + "admin.register": "Ajouter et scanner", + "admin.scan": "Scanner", + "admin.accounts": "Comptes", + "admin.webPassword": "Mot de passe web, 12 caractères minimum", + "admin.creating": "Création…", + "admin.create": "Créer le compte", + "admin.enable": "Activer", + "admin.disable": "Désactiver", + "admin.currentAccount": "Vous ne pouvez pas désactiver votre compte actuel", + "admin.libraryError": "Le dossier n’a pas pu être enregistré.", + "admin.accountError": "Le compte n’a pas pu être créé.", + "admin.statusError": "Le statut du compte n’a pas pu être modifié.", + "admin.scanError": "Le scan n’a pas pu démarrer.", + "admin.scanQueued": "Scan {id} ajouté à la file.", + "admin.initialScan": "Scan initial {id} ajouté à la file.", + "search.detail": "Trouvez un titre, un album ou un artiste", + "search.placeholder": "Titre, album ou artiste", + "search.query": "Requête de recherche", + "search.tracks": "Pistes", + "search.empty": "Aucun résultat.", + "player.error": "Lecture impossible. Vérifiez le serveur puis réessayez.", + "player.previous": "Piste précédente", + "player.play": "Lire", + "player.pause": "Pause", + "player.next": "Piste suivante", + "player.seek": "Position de lecture", + "authorize.title": "Autorisation", + "authorize.invalid": + "Ce lien d’autorisation est incomplet ou mène vers une destination non fiable.", + "authorize.client": "Autoriser {client}", + "authorize.detail": + "Cette application pourra parcourir vos bibliothèques, lire votre musique et gérer vos playlists, favoris et notes, comme ce compte.", + "authorize.redirect": "Retour vers {url}", + "authorize.error": "La demande d’autorisation de l’application est invalide.", + "authorize.busy": "Autorisation…", + "authorize.approve": "Autoriser", + "notFound.title": "Cette salle est silencieuse", + "notFound.detail": "La page demandée n’existe pas sur ce serveur.", + "notFound.back": "Retour à la bibliothèque", + "appearance.themes.default": "Émeraude clair", + "appearance.themes.midnightLight": "Minuit clair", + "appearance.themes.sunsetLight": "Coucher de soleil clair", + "appearance.themes.lavenderLight": "Lavande clair", + "appearance.themes.crimsonLight": "Cramoisi clair", + "appearance.themes.oceanLight": "Océan clair", + "appearance.themes.defaultDark": "Émeraude sombre", + "appearance.themes.oled": "Noir OLED", + "appearance.themes.midnight": "Minuit", + "appearance.themes.sunset": "Coucher de soleil", + "appearance.themes.lavender": "Lavande", + "appearance.themes.crimson": "Cramoisi", + "appearance.themes.ocean": "Océan", + "appearance.themes.neon": "Néon", + "browse.sort": "Trier", + "browse.sortAlphabeticalByName": "Par titre", + "browse.sortAlphabeticalByArtist": "Par artiste", + "browse.sortAlphabeticalByAlbum": "Par album", + "browse.sortNewest": "Ajoutés récemment", + "browse.sortRecent": "Écoutés récemment", + "browse.sortFrequent": "Les plus écoutés", + "browse.sortStarred": "Favoris", + "browse.sortByYear": "Par année", + "browse.filterAlbums": "Filtrer par titre ou artiste", + "browse.filterArtists": "Filtrer par nom", + "browse.filterPlaceholder": "Filtrer…", + "browse.matches": { + one: "{count} sur {total} affiché", + other: "{count} sur {total} affichés", + }, + "browse.noMatch": "Aucun résultat pour ce filtre.", + "card.play": "Lire", + "card.queue": "Mettre en file", + "rating.label": "Note", + "rating.stars": { one: "{count} étoile", other: "{count} étoiles" }, + "nav.genres": "Genres", + "nav.history": "Écoutes récentes", + "nav.random": "Aléatoire", + "nav.playing": "En écoute", + "nav.upload": "Téléverser", + "nav.folders": "Dossiers", + "nav.attention": "À corriger", + "nav.quality": "Qualité", +}; diff --git a/webapp/src/state/i18n/fr/folders.ts b/webapp/src/state/i18n/fr/folders.ts new file mode 100644 index 00000000..c9bc40bc --- /dev/null +++ b/webapp/src/state/i18n/fr/folders.ts @@ -0,0 +1,142 @@ +// Artist splits and images, folders, filing and uploads. +import type { TranslationValue } from ".."; +import type { folders as en } from "../en/folders"; + +export const folders: Record = { + "artistSplit.offer": "Scinder en {count} artistes", + "artistSplit.confirm": "Confirmer : scinder en {count}", + "artistSplit.done": "Scindé en {parts}.", + "artistSplit.failed": "Ce crédit n’a pas pu être scindé. Réessayez.", + "common.back": "Retour", + "artistImage.changePortrait": "Changer le portrait", + "artistImage.changeBanner": "Changer la bannière", + "artistImage.resetPortrait": "Portrait automatique", + "artistImage.resetBanner": "Sans bannière", + "artistImage.portraitChanged": "Portrait changé.", + "artistImage.bannerChanged": "Bannière changée.", + "artistImage.portraitReverted": "Retour au portrait automatique.", + "artistImage.bannerReverted": "Bannière retirée.", + "artistImage.tooLarge": "Cette image dépasse 10 Mo.", + "artistImage.notAnImage": "Choisissez une image JPEG, PNG ou WebP.", + "artistImage.failed": "Cela n’a pas fonctionné. Réessayez.", + "artistImage.chosen": "Portrait choisi à la main", + "artistImage.searchDeezer": "Rechercher sur Deezer", + "artistImage.menu": "Modifier les images", + "artistImage.menuShort": "Modifier", + "artistImage.menuLabel": "Images de {name}", + "artistImage.deezerTitle": "Choisir un portrait sur Deezer", + "artistImage.deezerQuery": "Nom de l’artiste", + "artistImage.deezerSearch": "Rechercher", + "artistImage.deezerSearching": "Recherche sur Deezer…", + "artistImage.deezerResults": "Artistes Deezer", + "artistImage.deezerNone": "Deezer ne connaît aucun artiste de ce nom.", + "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, + "artistImage.deezerUnavailable": + "La recherche Deezer n’est pas activée sur ce serveur.", + "artistImage.deezerBusy": + "Votre recherche précédente est encore en cours. Réessayez dans un instant.", + "artistImage.deezerExpired": + "Ces résultats ont expiré. Relancez la recherche.", + "folders.count": { + one: "{count} dossier créé dans cette bibliothèque", + other: "{count} dossiers créés dans cette bibliothèque", + }, + "folders.where": "Chemin du dossier", + "folders.subfolders": "Dossiers", + "folders.noLibrary": "Aucune bibliothèque n’est accessible à ce compte.", + "folders.none": "Aucun dossier n’a encore été créé dans cette bibliothèque.", + "folders.system": "Tenu par WaveFlow", + "folders.systemNote": + "À ranger réunit toutes les pistes qui ne sont dans aucun dossier. WaveFlow le tient lui-même : il n’est pas compté parmi les dossiers ci-dessus, et il ne peut être ni renommé, ni déplacé, ni supprimé.", + "filed.unfiled": "À ranger", + "filed.automatic": "Automatique", + "filed.topLevel": "Premier niveau", + "filed.here": "Ici actuellement", + "filed.destinations": "Dossiers au choix", + "filed.loadError": "Les dossiers n’ont pas pu être chargés.", + "filed.moveFailed": "Le déplacement a échoué. Réessayez.", + "filed.moveTrack": "Déplacer « {title} » vers un dossier", + "filed.moveFolder": "Déplacer « {name} »", + "filed.note": + "Des dossiers qui n’existent que dans WaveFlow. Classer une piste ici ne déplace aucun fichier.", + "filed.newFolder": "Nouveau dossier", + "filed.name": "Nom du dossier", + "filed.renameLabel": "Nouveau nom", + "filed.create": "Créer", + "filed.save": "Enregistrer", + "filed.cancel": "Annuler", + "filed.rename": "Renommer", + "filed.move": "Déplacer…", + "filed.delete": "Supprimer", + "filed.confirmDelete": + "Supprimer « {name} » et ses sous-dossiers ? Leurs pistes reviennent dans À ranger ; aucun fichier n’est touché.", + "filed.created": "« {name} » créé.", + "filed.renamed": "Renommé en « {name} ».", + "filed.deleted": { + one: "Dossier supprimé : {count} piste revient dans À ranger.", + other: "Dossier supprimé : {count} pistes reviennent dans À ranger.", + }, + "filed.filed": { + one: "{count} piste classée dans {folder}.", + other: "{count} pistes classées dans {folder}.", + }, + "filed.folderMoved": "« {name} » déplacé.", + "filed.nameTaken": "Un dossier de ce nom existe déjà ici.", + "filed.refused": + "Ce dossier ne peut pas aller là : vérifiez son nom, ou l’arborescence est trop profonde ou pleine.", + "filed.failed": "Cela n’a pas fonctionné. Réessayez.", + "filed.missing": "Ce dossier n’existe plus.", + "filed.empty": "Rien n’est encore classé ici.", + "filed.allFiled": "Toutes les pistes sont classées.", + "filed.uploadHere": "Envoyer ici", + "filed.unfiledNote": + "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée, et elle ne peut être ni renommée ni supprimée. Pour ranger une piste, ouvrez son menu et choisissez « Déplacer vers un dossier… ».", + "filed.unfiledNoteReader": + "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée.", + "trackMenu.move": "Déplacer vers un dossier…", + "upload.into": "Les fichiers reçus vont dans le dossier {folder}.", + "upload.doneUnfiled": "Ajouté à la bibliothèque, dans À ranger", + "upload.folderMissing": + "Le dossier pour lequel cet envoi a été ouvert est introuvable : les fichiers reçus iront dans À ranger.", + "upload.folderUnchecked": + "Le dossier pour lequel cet envoi a été ouvert n’a pas pu être vérifié : aucun fichier n’est pris pour l’instant.", + "upload.checkAgain": "Vérifier à nouveau", + "nav.group.library": "Bibliothèque", + "nav.group.listen": "Écoute", + "upload.title": "Téléverser dans {library}", + "upload.detail": + "Chaque fichier est d’abord identifié dans votre navigateur : un fichier que la bibliothèque possède déjà n’est jamais renvoyé, et un transfert interrompu reprend là où il s’était arrêté.", + "upload.pick": "Choisir des fichiers audio", + "upload.formats": "Formats que la bibliothèque sait indexer : {extensions}", + "upload.noLibrary": "Choisissez d’abord une bibliothèque.", + "upload.notAllowed": + "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut y ajouter des fichiers.", + "upload.closed": + "Cette bibliothèque n’accepte pas de fichiers. C’est son opérateur qui en décide, sur le serveur.", + "upload.waiting": "En attente", + "upload.hashing": "Identification…", + "upload.negotiating": "Question au serveur…", + "upload.uploading": "Envoi…", + "upload.committing": "Ajout à la bibliothèque…", + "upload.done": "Ajouté à la bibliothèque", + "upload.present": "Déjà dans la bibliothèque", + "upload.refused": "Refusé : {reason}", + "upload.failed": "Échec : {reason}", + "upload.progress": "Progression : {name}", + "upload.reason.unsupportedFormat": + "format audio que la bibliothèque ne sait pas indexer", + "upload.reason.tooLarge": "plus gros que ce que le serveur accepte", + "upload.reason.quotaExceeded": "la bibliothèque n’a plus de place", + "upload.reason.libraryClosed": "la bibliothèque n’accepte pas de fichiers", + "upload.reason.tooManySessions": + "trop de transferts déjà ouverts — réessayez dans un instant", + "upload.reason.refusedFile": + "ce qui est arrivé ne correspond pas au fichier, ou n’est pas un audio que la bibliothèque sait lire", + "upload.reason.unavailable": + "le serveur n’a pas pu le prendre pour l’instant — réessayez", + "upload.reason.network": "la connexion a été perdue", + "upload.reason.notAllowed": + "vous ne pouvez plus ajouter de fichiers à cette bibliothèque", + "upload.reason.error": "quelque chose s’est mal passé", + "upload.reason.hashing": "ce navigateur n’a pas pu identifier le fichier", +}; diff --git a/webapp/src/state/i18n/fr/index.ts b/webapp/src/state/i18n/fr/index.ts new file mode 100644 index 00000000..a72e350e --- /dev/null +++ b/webapp/src/state/i18n/fr/index.ts @@ -0,0 +1,31 @@ +// The French strings, assembled in the same order as the English ones. +import type { TranslationKey, TranslationValue } from ".."; +import { browse } from "./browse"; +import { folders } from "./folders"; +import { library } from "./library"; +import { maintenance } from "./maintenance"; +import { playing } from "./playing"; +import { podcasts } from "./podcasts"; +import { quality } from "./quality"; +import { scrobbling } from "./scrobbling"; +import { settings } from "./settings"; +import { shell } from "./shell"; +import { smart } from "./smart"; +import { stats } from "./stats"; +import { track } from "./track"; + +export const fr: Record = { + ...shell, + ...settings, + ...track, + ...library, + ...browse, + ...quality, + ...folders, + ...playing, + ...maintenance, + ...scrobbling, + ...stats, + ...smart, + ...podcasts, +}; diff --git a/webapp/src/state/i18n/fr/library.ts b/webapp/src/state/i18n/fr/library.ts new file mode 100644 index 00000000..72d26f60 --- /dev/null +++ b/webapp/src/state/i18n/fr/library.ts @@ -0,0 +1,179 @@ +// Sign-in, the library pages, import, the queue, shares and radio. +import type { TranslationValue } from ".."; +import type { library as en } from "../en/library"; + +export const library: Record = { + "login.heroEyebrow": "Votre musique, selon vos règles", + "login.heroTitle": "La nuit tombe, la bibliothèque s’éveille.", + "login.heroDetail": "Streaming privé, un catalogue, tous vos écrans.", + "login.server": "Serveur WaveFlow", + "login.createAdmin": "Créer le compte administrateur", + "login.welcome": "Bon retour", + "login.setupDetail": + "Ce serveur est neuf. Choisissez son premier compte administrateur.", + "login.setupToken": "Jeton d’installation", + "login.setupTokenHint": + "Le serveur l’a écrit dans le fichier setup-token de son répertoire de données, dont son journal donne le chemin. Si vous avez défini WAVEFLOW_SETUP_TOKEN, saisissez cette valeur.", + "login.setupTokenError": + "Ce jeton d’installation n’est pas le bon. Copiez-le de nouveau depuis le fichier setup-token.", + "login.username": "Nom d’utilisateur", + "login.password": "Mot de passe", + "login.setupError": + "Configuration impossible. Utilisez un nom valide et un mot de passe d’au moins 12 caractères.", + "login.error": "Nom d’utilisateur ou mot de passe incorrect.", + "login.wait": "Veuillez patienter…", + "login.create": "Créer et se connecter", + "login.signIn": "Se connecter", + "albums.detail": { + one: "{count} disque dans votre bibliothèque", + other: "{count} disques dans votre bibliothèque", + }, + "albums.gridTitle": "Dans votre bibliothèque", + "artists.detail": { + one: "{count} voix ou collaboration", + other: "{count} voix et collaborations", + }, + "artists.libraryCount": { + one: "{count} album dans votre bibliothèque", + other: "{count} albums dans votre bibliothèque", + }, + "artists.mostPlayed": "Les plus écoutés ici", + "artists.section.album": "Albums", + "artists.section.ep": "EP", + "artists.section.single": "Singles", + "artists.section.live": "Live", + "artists.section.compilation": "Compilations", + "artists.section.appearsOn": "Apparaît sur", + "artists.about": "À propos", + "artists.readMore": "Lire la suite", + "artists.similar": "Artistes proches dans votre bibliothèque", + "artists.enrichmentPending": "Recherche d’informations sur cet artiste…", + "artists.bioSource": "Biographie fournie par {provider}, le {date}.", + "artists.similarSource": "Suggérés par {provider}, le {date}.", + "artists.portraitSource": "Portrait fourni par {provider}, le {date}.", + "artists.bannerSource": "Bannière fournie par {provider}, le {date}.", + "favourites.detail": { + one: "{count} piste enregistrée", + other: "{count} pistes enregistrées", + }, + "favourites.empty": + "Ajoutez une piste aux favoris depuis un album ou la recherche pour la retrouver ici.", + "favourites.add": "Ajouter aux favoris", + "favourites.remove": "Retirer des favoris", + "playlists.detail": { + one: "{count} collection", + other: "{count} collections", + }, + "playlists.newName": "Nom de la nouvelle playlist", + "playlists.create": "Créer la playlist", + "playlists.play": "Lire", + "playlists.addQueue": "Ajouter la file", + "playlists.emptyOne": "Cette playlist est vide.", + "playlists.export": "Enregistrer en .m3u8", + "playlists.exportError": "Cette playlist n’a pas pu être enregistrée.", + "import.title": "Importer des playlists d’un autre lecteur", + "import.detail": + "Un fichier .m3u ou .m3u8. Rien n’est créé avant que vous ayez vu ce qui serait perdu.", + "import.pick": "Choisir un fichier de playlist", + "import.name": "Nom de la playlist", + "import.found": + "{resolved} entrées sur {entries} sont dans votre bibliothèque.", + "import.encoding": + "Le fichier n’était pas en UTF-8 et a été lu en {encoding}. Vérifiez les noms ci-dessous avant d’importer.", + "import.missing": { + one: "{count} entrée ne peut pas être placée :", + other: "{count} entrées ne peuvent pas être placées :", + }, + "import.line": "Ligne {line}", + "import.noMatch": "aucune piste correspondante", + "import.ambiguous": "correspond à plusieurs pistes", + "import.notAPath": "n’est pas un fichier d’une bibliothèque", + "import.duplicate": "déjà dans cette playlist", + "import.confirm": "Importer", + "import.cancel": "Annuler", + "import.failed": "Ce fichier n’a pas pu être lu comme une playlist.", + "playlists.empty": "Créez une playlist pour réunir plusieurs pistes.", + "playlists.createError": "La playlist n’a pas pu être créée.", + "playlists.queueError": "La file n’a pas pu être ajoutée à cette playlist.", + "playlists.deleteError": "La playlist n’a pas pu être supprimée.", + "queue.detail": { + one: "{count} piste synchronisée avec votre compte", + other: "{count} pistes synchronisées avec votre compte", + }, + "queue.playStart": "Lire depuis le début", + "queue.clear": "Vider la file", + "queue.remove": "Retirer", + "queue.empty": "Lisez un album ou une playlist pour construire votre file.", + "shares.detail": "Liens publics vers votre musique", + "shares.description": "Description", + "shares.create": "Partager la file actuelle", + "shares.needQueue": "Ajoutez des pistes à la file avant de créer un lien.", + "shares.once": "Ce lien ne sera affiché qu’une fois :", + "shares.defaultName": "Partage musical", + "shares.empty": "Aucun lien public n’a été créé.", + "shares.createError": "Le partage n’a pas pu être créé.", + "shares.tooLong": + "Un partage contient au plus {limit} titres, et la file en contient {count}. Raccourcissez la file pour la partager.", + "shares.deleteError": "Le partage n’a pas pu être supprimé.", + "shares.maxVisits": "Limite de visites (facultative)", + "shares.visits": { one: "{count} visite", other: "{count} visites" }, + "shares.visitsOf": "{count} visites sur {max}", + "share.kicker": "Partagé avec vous", + "share.defaultTitle": "Musique partagée avec vous", + "share.until": "Disponible jusqu’au {date}", + "share.empty": "Aucune de ces pistes ne peut plus être lue.", + "share.player": "Lecteur", + "share.unavailableTitle": "Ce lien n’ouvre plus rien", + "share.unavailableDetail": + "Il a peut-être expiré, été retiré, ou été ouvert autant de fois qu’il le permet.", + "playlists.public": "Visible par tous les comptes", + "playlists.publicError": "Cette playlist n’a pas pu être publiée.", + "playlists.others": "D’autres comptes", + "playlists.othersDetail": + "Les playlists publiques des autres comptes, avec les pistes que vos bibliothèques contiennent.", + "playlists.by": "Par {owner}", + "playlists.moveUp": "Monter {title}", + "playlists.moveDown": "Descendre {title}", + "playlists.removeEntry": "Retirer {title} de cette playlist", + "playlists.moveError": + "Cette entrée n’a pas pu être déplacée. La playlist a peut-être changé ailleurs ; elle a été relue.", + "playlists.removeEntryError": "Cette entrée n’a pas pu être retirée.", + "radio.detail": "Des stations en ligne, attachées à votre compte", + "radio.name": "Nom de la station", + "radio.address": "Adresse du flux", + "radio.homepage": "Page de la station (facultatif)", + "radio.image": "Image de la station (facultatif)", + "radio.imageHint": + "Le serveur la télécharge une fois, quand vous l’enregistrez, et la garde avec les pochettes : votre navigateur ne contacte jamais l’hôte de l’image.", + "radio.imageMissing": + "L’image n’a pas pu être récupérée ; enregistrez la station pour réessayer.", + "radio.add": "Ajouter la station", + "radio.save": "Enregistrer la station", + "radio.edit": "Modifier", + "radio.replacing": + "Enregistrer remplace tous les champs : une page vide efface celle qui était retenue.", + "radio.listen": "Écouter", + "radio.stop": "Arrêter", + "radio.visit": "Page de la station", + "radio.empty": "Aucune station. Ajoutez-en une pour la retrouver ici.", + "radio.addressRefused": "Une adresse doit commencer par http:// ou https://.", + "radio.mixedContent": + "Cette page est servie en HTTPS : votre navigateur refusera un flux en clair.", + "radio.full": "Ce compte retient déjà le maximum de stations.", + "radio.saveError": "La station n’a pas pu être enregistrée.", + "radio.deleteError": "La station n’a pas pu être supprimée.", + "radio.playbackError": + "Cette station n’a pas pu être lue. Le serveur ne s’y connecte jamais : cela se passe entre votre navigateur et la station.", + "radio.stations": "Stations", + "radio.newTitle": "Ajouter une station", + "radio.editTitle": "Modifier « {name} »", + "radio.listenTo": "Écouter {name}", + "radio.stopNamed": "Arrêter {name}", + "radio.actions": "Actions pour {name}", + "radio.confirmDelete": + "Supprimer la station « {name} » ? Son adresse et son image sont oubliées.", + "credential.password": "Mot de passe Subsonic dédié", + "credential.rotate": "Renouveler l’identifiant", + "credential.copy": "Copiez cette clé API maintenant :", + "credential.error": "Le renouvellement de l’identifiant a échoué.", +}; diff --git a/webapp/src/state/i18n/fr/maintenance.ts b/webapp/src/state/i18n/fr/maintenance.ts new file mode 100644 index 00000000..ea9406e2 --- /dev/null +++ b/webapp/src/state/i18n/fr/maintenance.ts @@ -0,0 +1,107 @@ +// Scan progress, accounts and tokens, maintenance and missing files. +import type { TranslationValue } from ".."; +import type { maintenance as en } from "../en/maintenance"; + +export const maintenance: Record = { + "scan.connecting": "En attente du premier relevé…", + "scan.queued": "En attente", + "scan.running": "Analyse en cours", + "scan.completed": "Terminé", + "scan.failed": "Échec", + "scan.progress": "Progression de l’analyse", + "scan.counted": "{processed} fichiers sur {total}", + "scan.tally": + "{added} ajoutés · {updated} mis à jour · {moved} déplacés · {skipped} inchangés · {errors} erreurs", + "admin.scanning": "Analyse en cours", + "admin.nowPlaying": "Écoutes en cours", + "admin.nobodyListening": "Personne n’écoute en ce moment.", + "admin.tokens": "Jetons d’API de {username}", + "admin.tokenName": "À quoi sert-il ?", + "admin.tokenCreate": "Émettre", + "admin.tokenOnce": "Copiez-le maintenant. Il n’est montré qu’une fois :", + "admin.tokenError": "Le jeton n’a pas pu être émis.", + "admin.tokenRevoke": "Révoquer", + "admin.tokenUsed": "déjà utilisé", + "admin.tokenUnused": "jamais utilisé", + "admin.tokenNone": "Aucun jeton sur ce compte.", + "scan.lost": + "Le flux de progression n’a pas pu s’ouvrir. L’analyse elle-même se poursuit.", + "admin.members": "Qui peut voir cette bibliothèque", + "admin.memberRole": "Rôle", + "admin.memberRemove": "Retirer", + "admin.memberOwner": "propriétaire, et le reste", + "admin.memberAdd": "Donner accès à", + "admin.memberChoose": "Choisir un compte…", + "admin.memberError": "Cette appartenance n’a pas pu être modifiée.", + "admin.manage": "Nom, dossier et suppression", + "admin.rename": "Nom", + "admin.path": "Dossier sur le serveur", + "admin.followLinks": + "Suivre les liens symboliques dont la cible reste dans la bibliothèque", + "admin.watch": + "Surveiller le dossier et analyser ce qui change (à couper pour un partage réseau modifié par d’autres machines)", + "admin.save": "Enregistrer", + "admin.saved": "Enregistré.", + "admin.savedScan": "Enregistré. Scan {id} ajouté à la file.", + "admin.saveError": + "Ce changement n’a pas pu être enregistré. Le dossier doit exister sur le serveur et ne pas être un lien symbolique.", + "admin.saveBusy": + "La bibliothèque est en cours d’analyse, ou une autre bibliothèque utilise déjà ce dossier. Réessayez une fois l’analyse terminée.", + "admin.delete": "Supprimer cette bibliothèque", + "admin.deleteNote": + "Retire la bibliothèque du catalogue pour tous ses membres : ses titres, albums et artistes, avec leurs entrées de playlist, favoris et écoutes. Aucun fichier n’est touché sur le disque. Tapez le nom de la bibliothèque pour confirmer.", + "admin.deleteConfirm": "Le nom de la bibliothèque, pour confirmer", + "admin.deleteButton": "Supprimer définitivement", + "admin.deleted": + "{name} a été supprimée : {tracks} titres ont quitté le catalogue.", + "admin.deleteError": "La bibliothèque n’a pas pu être supprimée.", + "scan.leftOut": "{excluded} écartés · {links} liens non suivis", + "nav.missing": "Fichiers manquants", + "nav.maintenance": "Maintenance", + "maintenance.sections": "Sections de maintenance", + "maintenance.none": + "Rien ici n’est à vous d’entretenir dans cette bibliothèque.", + "missing.total": { + one: "{count} fichier que les scans ne trouvent plus", + other: "{count} fichiers que les scans ne trouvent plus", + }, + "missing.nothing": "Chaque fichier est là où le catalogue l’attend.", + "missing.notYours": + "Seuls les propriétaires et gestionnaires de cette bibliothèque, et les administrateurs, voient ses fichiers manquants.", + "missing.note": + "Ces titres restent au catalogue, avec leurs favoris, entrées de playlist et écoutes, jusqu’à leur purge ; un fichier qui revient avant garde son titre. Une purge peut être annulée pendant un temps, puis le titre, et tout ce qui s’y rattache pour chaque membre, disparaît pour de bon.", + "missing.since": "manquant depuis le {date}", + "missing.cost": + "{playlists} en playlist · {favorites} favoris · {plays} écoutes", + "missing.select": "Sélectionner {title}", + "missing.purgeSelected": "Purger la sélection ({count})", + "missing.purgeAll": "Tout purger", + "missing.confirm": "Confirmer : purger {count}", + "missing.purged": { + one: "{count} titre purgé.", + other: "{count} titres purgés.", + }, + "missing.scanning": + "La bibliothèque est en cours d’analyse. Réessayez une fois l’analyse terminée.", + "missing.failed": "La purge n’a pas abouti. Réessayez.", + "missing.more": "Afficher plus", + "missing.purgedUntil": { + one: "{count} titre purgé. Il peut être restauré jusqu’au {date}.", + other: "{count} titres purgés. Ils peuvent être restaurés jusqu’au {date}.", + }, + "missing.purgedTitle": { + one: "Purgés, encore restaurables ({count})", + other: "Purgés, encore restaurables ({count})", + }, + "missing.purgedNote": + "Rien n’en est encore perdu. Restauré, un titre retourne aux fichiers manquants avec tout ce qui s’y rattachait ; un fichier qui revient restaure son titre de lui-même.", + "missing.deletedAfter": "supprimé pour de bon après le {date}", + "missing.restore": "Restaurer", + "missing.restoreOne": "Restaurer {title}", + "missing.restoreAll": "Tout restaurer", + "missing.restored": { + one: "{count} titre restauré.", + other: "{count} titres restaurés.", + }, + "missing.restoreFailed": "La restauration n’a pas abouti. Réessayez.", +}; diff --git a/webapp/src/state/i18n/fr/playing.ts b/webapp/src/state/i18n/fr/playing.ts new file mode 100644 index 00000000..42abcb83 --- /dev/null +++ b/webapp/src/state/i18n/fr/playing.ts @@ -0,0 +1,55 @@ +// Genres, history, random, lyrics, now playing, bookmarks, the player bar. +import type { TranslationValue } from ".."; +import type { playing as en } from "../en/playing"; + +export const playing: Record = { + "browse.filterGenres": "Filtrer par genre", + "genres.detail": { one: "{count} genre", other: "{count} genres" }, + "genres.empty": "Aucun genre n’est encore étiqueté dans cette bibliothèque.", + "genres.emptyOne": "Plus rien ne porte ce genre.", + "history.detail": { + one: "{count} piste, la plus récente d’abord", + other: "{count} pistes, la plus récente d’abord", + }, + "history.empty": "Rien n’a encore été écouté depuis ce compte.", + "random.detail": { + one: "{count} piste tirée", + other: "{count} pistes tirées", + }, + "random.again": "Tirer à nouveau", + "random.fromGenre": "Lire ce genre au hasard", + "random.empty": "Il n’y a encore rien à tirer.", + "lyrics.label": "Paroles", + "lyrics.none": "Aucune parole ne voyage avec ce fichier.", + "playing.idle": "Rien n’est en cours de lecture.", + "playing.empty": "Lancez une piste et elle apparaîtra ici, avec ses paroles.", + "lyrics.seekHint": + "Cette feuille porte des minutages — appuyez sur une ligne pour y sauter.", + "lyrics.seekTo": "Sauter à {when}", + "lyrics.seekLine": "{line}, sauter à {when}", + "playing.viewLabel": "Comment regarder cette piste", + "playing.viewDetail": "Détail", + "playing.viewLyrics": "Paroles", + "playing.viewImmersive": "Immersif", + "playing.immersiveTitle": "Vue immersive", + "playing.leaveImmersive": "Quitter la vue immersive", + "bookmarks.title": "Positions enregistrées", + "bookmarks.detail": "Une par piste. Enregistrer à nouveau la déplace.", + "bookmarks.save": "Enregistrer cette position", + "bookmarks.forget": "Oublier", + "player.shuffle": "Lecture aléatoire", + "player.repeatOff": "Répétition : aucune", + "player.repeatAll": "Répétition : toute la file", + "player.repeatOne": "Répétition : cette piste", + "player.volume": "Volume", + "player.mute": "Couper le son", + "player.unmute": "Rétablir le son", + "player.openAlbum": "Ouvrir l’album", + "player.live": "Direct", + "player.liveRadio": "Radio en direct", + "player.stopLive": "Arrêter la station", + "player.liveError": "Cette station n’a pas pu être lue.", + "player.closeAside": "Arrêter et fermer", + "library.active": "Bibliothèque", + "search.thisLibrary": "Recherche dans {library}.", +}; diff --git a/webapp/src/state/i18n/fr/podcasts.ts b/webapp/src/state/i18n/fr/podcasts.ts new file mode 100644 index 00000000..c29f33f3 --- /dev/null +++ b/webapp/src/state/i18n/fr/podcasts.ts @@ -0,0 +1,47 @@ +// Podcast subscriptions and episodes. +import type { TranslationValue } from ".."; +import type { podcasts as en } from "../en/podcasts"; + +export const podcasts: Record = { + "nav.podcasts": "Podcasts", + "podcasts.detail": "Les émissions que suit ce serveur, pour tous les comptes", + "podcasts.off": + "Les podcasts sont désactivés sur ce serveur. Ce qui a été conservé reste lisible ; rien de nouveau n’est récupéré.", + "podcasts.url": "Adresse du flux", + "podcasts.shared": + "Visible par tous les comptes : chacun sur ce serveur voit l’émission et peut écouter les épisodes téléchargés. Seuls les administrateurs voient l’adresse du flux.", + "podcasts.subscribe": "S’abonner", + "podcasts.refresh": "Actualiser tous les flux", + "podcasts.refreshing": "Les flux sont en cours de récupération.", + "podcasts.reload": "Recharger", + "podcasts.empty": "Aucun podcast pour l’instant.", + "podcasts.emptyDetail": + "Un administrateur abonne le serveur au flux d’une émission ici. Le serveur télécharge ses épisodes, et tous les comptes de ce serveur peuvent les écouter.", + "podcasts.addFirst": "Ajouter votre premier podcast", + "podcasts.channels": "Émissions", + "podcasts.showing": "Épisodes de {title} affichés.", + "podcasts.noEpisodes": "Ce flux ne contient aucun épisode.", + "podcasts.episodes": { + one: "{count} épisode", + other: "{count} épisodes", + }, + "podcasts.failed": "dernière récupération en échec : {reason}", + "podcasts.open": "Épisodes", + "podcasts.hide": "Masquer", + "podcasts.unsubscribe": "Se désabonner", + "podcasts.download": "Télécharger", + "podcasts.play": "Lire {title}", + "podcasts.pause": "Mettre en pause {title}", + "podcasts.resumeAt": "reprise à {time}", + "podcasts.status.new": "non téléchargé", + "podcasts.status.downloading": "téléchargement en cours", + "podcasts.status.completed": "prêt", + "podcasts.status.error": "échec du téléchargement", + "podcasts.status.deleted": "supprimé", + "podcasts.conflict": "Ce serveur suit déjà ce flux.", + "podcasts.refused": + "Le flux n’a pas pu être récupéré ou lu, ou son adresse n’est pas joignable par ce serveur.", + "podcasts.subscribeError": "Le flux n’a pas pu être ajouté.", + "podcasts.mutationError": "Cela n’a pas fonctionné. Réessayez.", + "podcasts.playbackError": "Cet épisode n’a pas pu être lu.", +}; diff --git a/webapp/src/state/i18n/fr/quality.ts b/webapp/src/state/i18n/fr/quality.ts new file mode 100644 index 00000000..9d2789a1 --- /dev/null +++ b/webapp/src/state/i18n/fr/quality.ts @@ -0,0 +1,124 @@ +// The attention lists and the audio quality analysis. +import type { TranslationValue } from ".."; +import type { quality as en } from "../en/quality"; + +export const quality: Record = { + "attention.lists": "Listes", + "attention.total": { + one: "{count} point à vérifier", + other: "{count} points à vérifier", + }, + "attention.nothing": "Rien à corriger.", + "attention.notYours": + "Seuls les propriétaires et les gestionnaires de cette bibliothèque voient ce qui y est à corriger.", + "attention.empty": "Rien dans cette liste.", + "attention.tracksNote": + "Corrigez une piste depuis son menu : une fois corrigée, elle quitte la liste.", + "attention.creditsNote": + "Chacun de ces crédits nomme plusieurs artistes dans une seule chaîne. Scindez-le en ces artistes, ou gardez-le tel quel s’il s’agit d’un seul nom. Chaque scan suivant applique la décision.", + "attention.pending": "À décider", + "attention.decided": "Décidés", + "attention.split": "Scinder", + "attention.confirmSplit": "Confirmer : scinder en {count}", + "attention.keep": "Garder tel quel", + "attention.splitDone": "{name} est scindé.", + "attention.keepDone": "{name} reste tel quel.", + "attention.undone": "La décision sur {name} est annulée.", + "attention.alreadyDecided": + "Ce crédit a été décidé entre-temps. La liste a été rechargée.", + "attention.failed": "Cela n’a pas fonctionné. Réessayez.", + "attention.noCredits": "Aucun crédit n’attend de décision.", + "attention.noDecisions": "Aucune décision pour l’instant.", + "attention.fragments": "Ce en quoi {name} serait scindé", + "attention.knownFragment": "Déjà un artiste de cette bibliothèque", + "attention.decidedSplit": "Scindé en {parts}", + "attention.decidedKeep": "Gardé tel quel", + "attention.decidedBy": "Par {who}, le {when}", + "attention.undo": "Annuler", + "attention.duplicateGroup": "Doublon probable {group}", + "attention.split_credits": "Artistes à scinder", + "attention.missing_artist": "Sans artiste", + "attention.missing_album": "Sans album", + "attention.missing_year": "Sans année", + "attention.missing_track_number": "Sans numéro de piste", + "attention.missing_cover": "Sans pochette", + "attention.album_year_conflict": "Albums à plusieurs années", + "attention.compilation_no_album_artist": "Compilations sans artiste d’album", + "attention.duplicate_track_number": "Numéros de piste en double", + "attention.track_number_gap": "Trous dans la numérotation", + "attention.probable_duplicate": "Doublons probables", + "attention.probable_fake_lossless": "Faux lossless probables", + "quality.help": + "Mesuré dans le son plutôt que lu dans le conteneur : où s’arrêtent les aigus, et combien de bits les échantillons utilisent. Un fichier sans perte dont les aigus s’arrêtent vers 16 kHz était un MP3 à 128 kb/s avant d’être un FLAC. « Aucun signe de source avec perte » n’est pas une preuve : un encodeur qui garde toute la bande ne laisse rien à trouver.", + "quality.progress": "{measured} fichiers sans perte analysés sur {lossless}", + "quality.disabled": "L’analyse est désactivée sur ce serveur.", + "quality.noLossless": "Aucun fichier sans perte dans cette bibliothèque.", + "quality.status.running": + "Analyse en cours : {measured} sur {lossless} terminés, {inFlight} en traitement.", + "quality.status.waitingForScan": + "En attente de la fin du scan avant d’analyser.", + "quality.status.queued": + "En file d’attente : l’analyse arrivera bientôt à ces fichiers.", + "quality.status.idle": "Au repos. Dernier fichier analysé le {when}.", + "quality.status.idleNever": "Au repos. Aucun fichier n’a encore été analysé.", + "quality.filters": "Verdicts", + "quality.all": "Tous les fichiers", + "quality.format": "Format", + "quality.allFormats": "Tous les formats", + "quality.empty": "Aucun fichier avec ce verdict.", + "quality.column.track": "Morceau", + "quality.column.container": "Ce que dit le fichier", + "quality.column.highs": "Les aigus s’arrêtent à", + "quality.column.verdict": "Ce que dit le son", + "quality.fullBand": "Bande complète", + "quality.khz": "{value} kHz", + "quality.db": "{value} dB", + "quality.analyseAgain": "Analyser à nouveau", + "quality.confirmAnalyse": "Confirmer : réanalyser toute la bibliothèque", + "quality.requested": "La bibliothèque sera réanalysée en arrière-plan.", + "quality.requestFailed": "Ça n’a pas marché. Réessayez.", + "quality.section": "Qualité réelle", + "quality.verdictLabel": "Verdict", + "quality.highsStop": "Les aigus s’arrêtent à", + "quality.wall": "Chute à cet endroit", + "quality.bitsUsed": "Bits utilisés, mesurés", + "quality.bitsOf": "{used} sur {declared}", + "quality.bitsNote": + "Une mesure des échantillons, pas une preuve de vraie haute résolution : un fichier qui utilise tous les bits qu’il annonce n’a pas été gonflé, ce qui ne veut pas dire qu’il a été enregistré à cette profondeur.", + "quality.analysed": "Analysé le", + "quality.chart": "Spectre moyen : les aigus s’arrêtent à {where}.", + "quality.chartFullBand": "Spectre moyen : les aigus continuent sans mur.", + "quality.showSpectrogram": "Afficher le spectrogramme", + "quality.drawing": "Dessin en cours…", + "quality.spectrogramAlt": + "Spectrogramme de {title}, fréquence en fonction du temps", + "quality.spectrogramBusy": + "Un spectrogramme est déjà en cours de dessin pour vous. Réessayez dans un instant.", + "quality.spectrogramFailed": "Le spectrogramme n’a pas pu être dessiné.", + "quality.verdict.lossy_source": "Probablement issu d’un fichier avec perte", + "quality.verdict.upsampled": "Probablement suréchantillonné", + "quality.verdict.padded": "Profondeur gonflée", + "quality.verdict.unreadable": "Illisible", + "quality.verdict.silent": "Silencieux", + "quality.verdict.pending": "En attente d’analyse", + "quality.verdict.consistent": "Aucun signe de source avec perte", + "quality.verdict.lossy": "Avec perte", + "quality.verdict.not_measured": "Non mesuré", + "quality.explain.lossy_source": + "Les aigus s’arrêtent net là où coupe un encodeur avec perte : ce fichier a probablement été décodé depuis un MP3, un AAC ou un Opus, puis enregistré sans perte.", + "quality.explain.upsampled": + "La fréquence dépasse 48 kHz, mais les aigus s’arrêtent là où finit un CD ou un enregistrement à 48 kHz : il a probablement été rééchantillonné depuis l’un d’eux.", + "quality.explain.padded": + "Le conteneur annonce plus de bits que les échantillons n’en utilisent : un enregistrement 16 bits rangé en 24 bits, par exemple.", + "quality.explain.unreadable": + "FFmpeg n’a pas pu décoder ce fichier. Il sera réessayé quand le fichier changera.", + "quality.explain.silent": + "Rien dans ce fichier n’est assez fort pour être mesuré.", + "quality.explain.pending": + "Ce fichier n’a pas encore été analysé. L’analyse tourne en arrière-plan.", + "quality.explain.consistent": + "Rien dans le son ne contredit le conteneur. Ce n’est pas une preuve : un encodeur qui garde toute la bande ne laisse rien à trouver.", + "quality.explain.lossy": + "Un fichier avec perte dit ce qu’il est ; sa qualité est celle de son encodeur et de son débit. Il n’est pas analysé.", + "quality.explain.not_measured": "Ce format n’est pas analysé.", +}; diff --git a/webapp/src/state/i18n/fr/scrobbling.ts b/webapp/src/state/i18n/fr/scrobbling.ts new file mode 100644 index 00000000..305012d8 --- /dev/null +++ b/webapp/src/state/i18n/fr/scrobbling.ts @@ -0,0 +1,106 @@ +// Offline listening and scrobbling. +import type { TranslationValue } from ".."; +import type { scrobbling as en } from "../en/scrobbling"; + +export const scrobbling: Record = { + "offline.kicker": "Dans ce navigateur", + "offline.title": "Gardé hors ligne", + "offline.detail": + "Ces titres sont gardés dans ce navigateur pour le compte qui s’y est connecté en dernier, et se lisent sans connexion. Se déconnecter les supprime de ce navigateur. Gardez-en d’autres depuis le menu d’un titre.", + "offline.unsupported": + "Ce navigateur ne peut pas garder de titres pour l’écoute hors ligne.", + "offline.back": "Retour à la bibliothèque", + "offline.empty": + "Rien n’est encore gardé. Le menu d’un titre propose de le garder.", + "offline.player": "Lecteur hors ligne", + "offline.gone": "Cette copie n’est plus dans ce navigateur.", + "offline.forget": "Retirer la copie hors ligne de {title}", + "offline.forgetShort": "Retirer", + "offline.forgetDetail": "Libère {size}", + "login.offline": { + one: "Écouter le {count} titre gardé dans ce navigateur", + other: "Écouter les {count} titres gardés dans ce navigateur", + }, + "scrobbling.title": "Scrobbling externe", + "scrobbling.detail": + "Transmettre ce que vous écoutez ici à ListenBrainz, Maloja ou Last.fm.", + "scrobbling.none": + "Ce serveur ne propose aucune destination. C’est son exploitant qui en décide.", + "scrobbling.provider.listenbrainz": "ListenBrainz", + "scrobbling.provider.maloja": "Maloja", + "scrobbling.provider.lastfm": "Last.fm", + "scrobbling.instance": "instance « {name} »", + "scrobbling.unavailable": "Indisponible ici : {reason}", + "scrobbling.unavailable.no_application_configured": + "aucune application Last.fm n’est déclarée sur ce serveur", + "scrobbling.unavailable.browser_journey_needs_https": + "le parcours par le navigateur exige que l’adresse publique de ce serveur soit en https — un exploitant peut encore lier un compte en ligne de commande", + "scrobbling.unavailable.unknown": "ce serveur n’a pas dit pourquoi", + "scrobbling.retired": + "Ce serveur ne propose plus cette instance. Plus rien ne partira vers elle — vous pouvez encore retirer l’autorisation.", + "scrobbling.notLinked": "Non liée.", + "scrobbling.secret": "Clé d’API", + "scrobbling.secretHint": "Prise sur votre compte chez cette destination.", + "scrobbling.link": "Lier", + "scrobbling.working": "En cours…", + "scrobbling.linkError": + "Cette autorisation a été refusée. Vérifiez la clé et recommencez.", + "scrobbling.unlink": "Délier", + "scrobbling.unlinkError": "Ce lien n’a pas pu être retiré.", + "scrobbling.connect": "Se connecter à Last.fm", + "scrobbling.connectDetail": + "Last.fm ne délivre aucune clé à recopier : il vous interroge chez lui et vous ramène ici.", + "scrobbling.connectError": "Le parcours Last.fm n’a pas pu s’ouvrir.", + "scrobbling.returned": "Last.fm est liée : {name}.", + "scrobbling.health.healthy": "Les écoutes partent", + "scrobbling.health.degraded": "La file n’avance pas", + "scrobbling.health.broken": "Rompu", + "scrobbling.healthDetail.healthy": "Rien n’est bloqué ici.", + "scrobbling.healthDetail.degraded": + "Le lien répond, mais les écoutes ne passent pas — soit l’une attend votre réponse, soit la plus ancienne attend depuis trop longtemps.", + "scrobbling.healthDetail.broken": + "L’autorisation a été refusée, ou ce serveur ne joint plus cette instance sous le nom qui a servi à créer le lien.", + "scrobbling.pending": { + one: "{count} en attente", + other: "{count} en attente", + }, + "scrobbling.retrying": { + one: "{count} en reprise", + other: "{count} en reprise", + }, + "scrobbling.uncertainCount": { + one: "{count} attend votre réponse", + other: "{count} attendent votre réponse", + }, + "scrobbling.oldest": "La plus ancienne attend depuis {when}.", + "scrobbling.lastSuccess": "Dernière acceptée {when}.", + "scrobbling.lastFailure": "Dernier refus : {reason}.", + "scrobbling.uncertain": "Des écoutes que vous seul pouvez trancher", + "scrobbling.uncertainDetail": + "La connexion a été coupée après leur départ : personne ne sait si la destination les a enregistrées. La renvoyer peut la faire figurer deux fois dans un historique que d’autres lisent ; la jeter laisse un trou. Ce serveur ne choisira pas à votre place, et il ne le demandera qu’une fois.", + "scrobbling.uncertainNone": "Rien n’attend votre réponse.", + "scrobbling.playedAt": "Écoutée {when}", + "scrobbling.attempts": { + one: "{count} tentative", + other: "{count} tentatives", + }, + "scrobbling.retry": "La renvoyer", + "scrobbling.discard": "La jeter", + "scrobbling.answered": "Répondu.", + "scrobbling.answerError": + "Cette écoute n’a pas pu être traitée. Rechargez la page avant de réessayer.", + "scrobbling.unnamedTrack": + "Absente de l’historique que ce compte conserve encore", + "scrobbling.reason.rate_limited": "la destination a demandé du répit", + "scrobbling.reason.retryable": "la destination ne l’a pas prise", + "scrobbling.reason.auth_broken": "l’autorisation a été refusée", + "scrobbling.reason.rejected": "la destination l’a lue et l’a refusée", + "scrobbling.reason.destination_gone": + "ce serveur ne joint plus cette instance sous ce nom", + "scrobbling.reason.credential_unreadable": + "ce serveur n’a pas pu lire l’identifiant enregistré", + "scrobbling.reason.attempts_exhausted": "elle a été tentée trop de fois", + "scrobbling.reason.unlinked": "le lien a été retiré", + "scrobbling.reason.ambiguous": "la réponse n’est jamais arrivée", + "scrobbling.reason.interrupted": "le serveur s’est arrêté en plein vol", +}; diff --git a/webapp/src/state/i18n/fr/settings.ts b/webapp/src/state/i18n/fr/settings.ts new file mode 100644 index 00000000..a69191d2 --- /dev/null +++ b/webapp/src/state/i18n/fr/settings.ts @@ -0,0 +1,57 @@ +// Settings, registered clients, the Subsonic password and quality labels. +import type { TranslationValue } from ".."; +import type { settings as en } from "../en/settings"; + +export const settings: Record = { + "nav.settings": "Réglages", + "settings.detail": + "Apparence, langue, et les clients autorisés à lire votre bibliothèque.", + "settings.appearance": "Apparence", + "settings.playback": "Lecture", + "settings.sections": "Sections des réglages", + "settings.section.account": "Compte", + "settings.playbackDetail": + "Comment ce navigateur lit vos titres. Gardé dans ce navigateur, pas sur votre compte.", + "settings.levelling": "Normalisation du volume", + "clients.title": "Clients enregistrés", + "clients.detail": + "Appareils et scripts détenant un justificatif sur votre bibliothèque. En révoquer un l’arrête aussitôt.", + "clients.name": "À quoi il sert", + "clients.register": "Enregistrer", + "clients.once": + "Copiez-le maintenant. Seule son empreinte est conservée : il est montré une fois et jamais plus :", + "clients.revoke": "Révoquer", + "clients.none": + "Aucun client ne détient de justificatif sur votre bibliothèque.", + "clients.error": "Cela n’a pas fonctionné. Rien n’a changé.", + "clients.registered": "enregistré le {when}", + "clients.used": "dernier usage le {when}", + "clients.unused": "jamais utilisé", + "subsonic.title": "Mot de passe Subsonic", + "subsonic.detail": + "Le mot de passe distinct avec lequel les clients Subsonic se connectent. Le changer remplace aussi la clé d’API, donc une clé divulguée cesse de fonctionner.", + "subsonic.set": "défini le {when}", + "subsonic.changed": "dernier changement le {when}", + "subsonic.password": "Nouveau mot de passe", + "subsonic.rotate": "Le changer", + "subsonic.revoke": "Le désactiver", + "subsonic.confirm": "Oui, le désactiver", + "subsonic.once": + "Copiez cette clé d’API maintenant. Seule son empreinte est conservée : elle est montrée une fois et jamais plus :", + "subsonic.gone": + "Vos clients Subsonic cesseront de fonctionner jusqu’à ce que vous le redéfinissiez dans chacun d’eux.", + "subsonic.none": + "Vous n’avez pas de mot de passe Subsonic, donc aucun client Subsonic ne peut lire votre bibliothèque. Demandez-en un à l’administrateur.", + "subsonic.short": "Utilisez au moins 12 caractères.", + "subsonic.error": "Cela n’a pas fonctionné. Rien n’a changé.", + "quality.label": "Qualité du fichier", + "quality.unknown": "Inconnue", + "quality.low": "Faible", + "quality.standard": "Standard", + "quality.high": "Élevée", + "quality.lossless": "Sans perte", + "quality.hiRes": "Haute résolution", + "file.channels": { one: "mono", other: "{count} canaux" }, + "common.delete": "Supprimer", + "common.cancel": "Annuler", +}; diff --git a/webapp/src/state/i18n/fr/shell.ts b/webapp/src/state/i18n/fr/shell.ts new file mode 100644 index 00000000..34ce70bb --- /dev/null +++ b/webapp/src/state/i18n/fr/shell.ts @@ -0,0 +1,83 @@ +// Navigation, preferences, ReplayGain, shared words, album and song views. +import type { TranslationValue } from ".."; +import type { shell as en } from "../en/shell"; + +export const shell: Record = { + "nav.albums": "Albums", + "nav.artists": "Artistes", + "nav.songs": "Titres", + "nav.search": "Recherche", + "nav.favourites": "Favoris", + "nav.playlists": "Playlists", + "nav.queue": "File d’attente", + "nav.shares": "Partages", + "nav.offline": "Hors ligne", + "nav.radio": "Radio", + "nav.scrobbling": "Scrobbling", + "nav.admin": "Administration", + "nav.signOut": "Se déconnecter", + "nav.skip": "Aller au contenu", + "nav.home": "WaveFlow serveur, accueil", + "nav.server": "serveur", + "nav.more": "Plus", + "nav.moreTitle": "Tout le reste", + "nav.moreDetail": + "La barre du bas porte ce qu’on attrape en écoutant. Le reste est ici.", + "nav.moreClose": "Fermer", + "preferences.theme": "Thème", + "preferences.language": "Langue", + "preferences.themeLight": "Clair", + "preferences.themeDark": "Sombre", + "preferences.followSystem": "Suivre le système", + "preferences.followSystemDetail": + "Passer de la version claire d’un thème à sa version sombre en même temps que votre appareil.", + "preferences.followingLight": + "Votre appareil est en mode clair : la version claire est affichée. Choisir un thème choisit ses deux versions.", + "preferences.followingDark": + "Votre appareil est en mode sombre : la version sombre est affichée. Choisir un thème choisit ses deux versions.", + "preferences.standInLight": + "{theme} n’a pas de version claire : {fallback} le remplace tant que votre appareil est en mode clair.", + "preferences.standInDark": + "{theme} n’a pas de version sombre : {fallback} le remplace tant que votre appareil est en mode sombre.", + "replayGain.label": "ReplayGain", + "replayGain.off": "Désactivé", + "replayGain.track": "Gain par piste", + "replayGain.album": "Gain par album", + "replayGain.preamp": "Préampli", + "replayGain.detail": + "Joue chaque piste au volume mesuré dans ses tags, pour qu’un disque ne sonne pas plus fort que le suivant. Le gain par album garde les écarts à l’intérieur d’un album. Une piste sans tags est jouée telle quelle. La lecture passe alors par Web Audio, que certains téléphones mettent en pause avec l’écran.", + "common.loading": "Chargement", + "common.loadError": "Impossible de charger cette vue", + "common.noAlbums": "Aucun album pour le moment.", + "common.noArtists": "Aucun artiste pour le moment.", + "common.variousArtists": "Artistes variés", + "common.unknownArtist": "Artiste inconnu", + "common.album": "Album", + "album.play": "Lire l’album", + "album.addQueue": "Ajouter à la file", + "album.title": "Titre", + "album.duration": "Durée", + "album.trackActions": "Actions sur les titres", + "album.moreBy": "Autres albums de {artist}", + "common.artist": "Artiste", + "common.albums": { one: "{count} album", other: "{count} albums" }, + "common.tracks": { one: "{count} piste", other: "{count} pistes" }, + "songs.loadedSoFar": { + one: "{count} piste chargée", + other: "{count} pistes chargées", + }, + "songs.playLoaded": { + one: "Lire la piste chargée", + other: "Lire les {count} pistes chargées", + }, + "songs.playAll": { + one: "Lire la piste", + other: "Lire les {count} pistes", + }, + "songs.loadMore": "Charger la suite", + "songs.loadMoreError": "Cette page n’a pas pu être chargée. Réessayez.", + "songs.loading": "Chargement…", + "songs.empty": "Cette bibliothèque ne contient encore aucune piste.", + "file.megabytes": "{size} Mo", + "file.kilobytes": "{size} ko", +}; diff --git a/webapp/src/state/i18n/fr/smart.ts b/webapp/src/state/i18n/fr/smart.ts new file mode 100644 index 00000000..12f5899f --- /dev/null +++ b/webapp/src/state/i18n/fr/smart.ts @@ -0,0 +1,86 @@ +// Smart playlists. +import type { TranslationValue } from ".."; +import type { smart as en } from "../en/smart"; + +export const smart: Record = { + "smart.title": "Listes intelligentes", + "smart.detail": + "Définies par des règles et recalculées à chaque ouverture : elles suivent la bibliothèque.", + "smart.new": "Nouvelle liste intelligente", + "smart.edit": "Modifier les règles", + "smart.save": "Enregistrer la liste intelligente", + "smart.badge": "Intelligente", + "smart.empty": "Aucune liste intelligente pour l’instant.", + "smart.notCounted": "Comptée à l’ouverture", + "smart.chooseLibrary": "Choisir une bibliothèque", + "smart.name": "Nom", + "smart.match": "Les pistes doivent remplir", + "smart.match.all": "toutes les conditions", + "smart.match.any": "au moins une condition", + "smart.fieldOf": "Champ de la condition {position}", + "smart.operatorOf": "Comparaison de la condition {position}", + "smart.valueOf": "Valeur de la condition {position}", + "smart.upperOf": "Borne haute de la condition {position}", + "smart.removeCondition": "Retirer la condition {position}", + "smart.addCondition": "Ajouter une condition", + "smart.sort": "Trier par", + "smart.sort.none": "Titre (par défaut)", + "smart.order": "Ordre", + "smart.order.asc": "Croissant", + "smart.order.desc": "Décroissant", + "smart.limit": "Au plus (pistes)", + "smart.days": "jours", + "smart.yes": "oui", + "smart.no": "non", + "smart.preview": "Aperçu", + "smart.previewCount": { + one: "{count} piste correspond", + other: "{count} pistes correspondent", + }, + "smart.unfinished": + "Complétez chaque condition pour voir l’aperçu ou enregistrer.", + "smart.refused": "Le serveur a refusé ces règles.", + "smart.full": "Ce compte retient déjà le maximum de listes intelligentes.", + "smart.saveError": "La liste intelligente n’a pas pu être enregistrée.", + "smart.deleteError": "La liste intelligente n’a pas pu être supprimée.", + "smart.loadError": "Les listes intelligentes n’ont pas pu être chargées.", + "smart.field.title": "Titre", + "smart.field.artist": "Artiste", + "smart.field.album_artist": "Artiste de l’album", + "smart.field.album": "Album", + "smart.field.genre": "Genre", + "smart.field.year": "Année", + "smart.field.rating": "Note", + "smart.field.starred": "Favori", + "smart.field.play_count": "Nombre d’écoutes", + "smart.field.last_played": "Dernière écoute", + "smart.field.date_added": "Date d’ajout", + "smart.field.duration": "Durée (secondes)", + "smart.field.bitrate": "Débit (kbit/s)", + "smart.field.format": "Format", + "smart.field.library": "Bibliothèque", + "smart.op.is": "est", + "smart.op.is_not": "n’est pas", + "smart.op.contains": "contient", + "smart.op.not_contains": "ne contient pas", + "smart.op.starts_with": "commence par", + "smart.op.ends_with": "finit par", + "smart.op.greater_than": "est supérieur à", + "smart.op.less_than": "est inférieur à", + "smart.op.between": "est entre", + "smart.op.in_the_last": "dans les derniers (jours)", + "smart.op.not_in_the_last": "pas dans les derniers (jours)", + "smart.op.before": "avant le", + "smart.op.after": "après le", + "smart.sort.title": "Titre", + "smart.sort.artist": "Artiste", + "smart.sort.album": "Album", + "smart.sort.year": "Année", + "smart.sort.rating": "Note", + "smart.sort.play_count": "Nombre d’écoutes", + "smart.sort.last_played": "Dernière écoute", + "smart.sort.date_added": "Date d’ajout", + "smart.sort.duration": "Durée", + "smart.sort.bitrate": "Débit", + "smart.sort.random": "Au hasard", +}; diff --git a/webapp/src/state/i18n/fr/stats.ts b/webapp/src/state/i18n/fr/stats.ts new file mode 100644 index 00000000..9ab392c1 --- /dev/null +++ b/webapp/src/state/i18n/fr/stats.ts @@ -0,0 +1,56 @@ +// Lyrics from LRCLIB, listening statistics and mixes. +import type { TranslationValue } from ".."; +import type { stats as en } from "../en/stats"; + +export const stats: Record = { + // RFC-024 : paroles de LRCLIB, statistiques et mix. + "lyrics.source.lrclib": "Depuis LRCLIB", + "lyrics.fromLrclib": "Paroles fournies par LRCLIB", + "nav.stats": "Statistiques", + "stats.detail": "Ce que ce compte a écouté, compté à partir de ses écoutes.", + "stats.period": "Période", + "stats.period7d": "7 derniers jours", + "stats.period30d": "30 derniers jours", + "stats.period365d": "12 derniers mois", + "stats.periodAll": "Depuis toujours", + "stats.empty": "Rien n’a été écouté pendant cette période.", + "stats.plays": "Écoutes", + "stats.listeningTime": "Temps d’écoute", + "stats.tracks": "Titres", + "stats.artists": "Artistes", + "stats.albums": "Albums", + "stats.estimate": + "Le temps d’écoute additionne la durée de chaque titre écouté ; aucun lecteur ne dit quelle part en a été entendue.", + "stats.timelineDays": "Écoutes par jour", + "stats.timelineMonths": "Écoutes par mois", + "stats.bucket": { + one: "{label} : {count} écoute", + other: "{label} : {count} écoutes", + }, + "stats.topTracks": "Titres les plus écoutés", + "stats.topArtists": "Artistes les plus écoutés", + "stats.topAlbums": "Albums les plus écoutés", + "stats.topGenres": "Genres les plus écoutés", + "stats.playCount": { one: "{count} écoute", other: "{count} écoutes" }, + "stats.firstPlay": "Première écoute de la période", + "stats.lastPlay": "Dernière écoute", + "stats.moment": "{title}, {when}", + "stats.minutes": { one: "{count} minute", other: "{count} minutes" }, + "stats.hoursMinutes": "{hours} h {minutes} min", + "mixes.title": "Pour vous aujourd’hui", + "mixes.detail": { + one: "{count} titre, tiré pour aujourd’hui", + other: "{count} titres, tirés pour aujourd’hui", + }, + "mixes.mostPlayed": "Les plus écoutés", + "mixes.neverPlayed": "Jamais écoutés", + "mixes.forgottenFavourites": "Favoris oubliés", + "mixes.recentlyAdded": "Ajouts récents", + "mixes.genre": "Mix {genre}", + "mixes.decade": "Les années {decade}", + "mixes.failed": "Impossible d’ouvrir « {title} ». Réessayez.", + "mixes.shelfTitle": "Mix pour vous", + "mixes.shelfDetail": + "Des listes tirées de vos écoutes, renouvelées chaque jour. Vos albums sont en dessous.", + "mixes.badge": "Mix", +}; diff --git a/webapp/src/state/i18n/fr/track.ts b/webapp/src/state/i18n/fr/track.ts new file mode 100644 index 00000000..b1d281c9 --- /dev/null +++ b/webapp/src/state/i18n/fr/track.ts @@ -0,0 +1,137 @@ +// The track menu and properties, lyrics sheets, tag corrections, canvases. +import type { TranslationValue } from ".."; +import type { track as en } from "../en/track"; + +export const track: Record = { + "correction.edit": "Corriger les tags", + "correction.editTrack": "Corriger les tags : {title}", + "trackMenu.label": "Actions pour {title}", + "trackMenu.play": "Lire", + "trackMenu.queue": "Ajouter à la file", + "trackMenu.download": "Télécharger", + "trackMenu.keepOffline": "Garder hors ligne", + "trackMenu.forgetOffline": "Retirer la copie hors ligne", + "trackMenu.downloadFailed": + "Le téléchargement n’a pas pu démarrer. Réessayez.", + "trackMenu.dismiss": "Fermer", + "trackMenu.goToAlbum": "Aller à l’album", + "trackMenu.goToArtist": "Aller à {name}", + "trackMenu.properties": "Propriétés", + "trackMenu.edit": "Corriger les tags", + "properties.kicker": "Propriétés", + "properties.close": "Fermer les propriétés", + "properties.loadError": "Certaines propriétés n’ont pas pu être chargées.", + "properties.file": "Fichier", + "properties.tags": "Tags", + "properties.sound": "Volume et tempo", + "properties.lyrics": "Paroles", + "properties.path": "Chemin dans la bibliothèque", + "properties.format": "Format", + "properties.bitrate": "Débit", + "properties.sampleRate": "Fréquence d’échantillonnage", + "properties.bitDepth": "Profondeur", + "properties.channels": "Canaux", + "properties.size": "Taille", + "properties.duration": "Durée", + "properties.added": "Ajouté le", + "properties.hash": "Empreinte du contenu", + "properties.title": "Titre", + "properties.artists": "Artistes", + "properties.album": "Album", + "properties.albumArtist": "Artiste de l’album", + "properties.year": "Année", + "properties.trackNumber": "Piste", + "properties.discNumber": "Disque", + "properties.genres": "Genres", + "properties.comment": "Commentaire", + "properties.isrc": "ISRC", + "properties.musicbrainz": "Enregistrement MusicBrainz", + "properties.trackGain": "ReplayGain (piste)", + "properties.albumGain": "ReplayGain (album)", + "properties.bpm": "BPM", + "properties.lyricsSheet": "Feuille {number}", + "properties.synced": "Synchronisées, {lang}", + "properties.plain": "Texte brut, {lang}", + "properties.noLyrics": "Aucune", + "lyrics.source.uploadTtml": "TTML envoyé", + "lyrics.source.uploadLrc": "LRC envoyé", + "lyrics.source.uploadTxt": "Texte envoyé", + "lyrics.source.sidecarLrc": "Fichier LRC à côté du titre", + "lyrics.source.sidecarText": "Fichier texte à côté du titre", + "lyrics.source.embedded": "Intégrées au fichier", + "lyrics.shown": "Affichées", + "lyrics.download": "Télécharger les paroles", + "lyrics.upload": "Envoyer des paroles…", + "lyrics.remove": "Retirer le {format}", + "lyrics.unsupported": "Choisissez un fichier .lrc, .txt ou .ttml.", + "lyrics.empty": "Ce fichier ne contient pas de paroles.", + "lyrics.tooLarge": "Ce fichier dépasse 1 Mo.", + "lyrics.failed": "Les paroles n’ont pas pu être enregistrées. Réessayez.", + "lyrics.downloadFailed": + "Les paroles n’ont pas pu être téléchargées. Réessayez.", + "properties.lyricsSheets": "Feuilles", + "properties.kbps": "{value} kbit/s", + "properties.khz": "{value} kHz", + "properties.bits": "{value} bits", + "properties.mb": "{value} Mo", + "properties.db": "{value} dB", + "properties.dbPeak": "{value} dB, crête {peak}", + "properties.corrected": "Corrigé", + "properties.fileSaid": "Corrigé — le fichier indique {value}", + "correction.detail": + "Le fichier n’est jamais réécrit. La correction est rangée à côté, survit à un rescan, et tous les membres de la bibliothèque la voient.", + "correction.fieldTitle": "Titre", + "correction.fieldSortTitle": "Titre de tri", + "correction.fieldYear": "Année", + "correction.fieldTrackNumber": "Numéro de piste", + "correction.fieldDiscNumber": "Numéro de disque", + "correction.fieldMusicbrainz": "Identifiant d’enregistrement MusicBrainz", + "correction.fieldComment": "Commentaire", + "correction.fieldArtists": "Artistes", + "correction.fieldGenres": "Genres", + "correction.listHint": + "Un nom par ligne. Laissez vide pour ne créditer personne.", + "correction.corrected": "Corrigé", + "correction.fromFile": "Le fichier indique : {value}", + "correction.fileEmpty": "Le fichier n’indique rien ici", + "correction.restore": "Rétablir la valeur du fichier", + "correction.keep": "Garder la correction", + "correction.restoring": "Reprend la valeur du fichier à l’enregistrement", + "correction.restoringList": + "Relu depuis le fichier à l’enregistrement — impossible de l’afficher avant", + "correction.save": "Enregistrer", + "correction.saving": "Enregistrement…", + "correction.saved": "Enregistré.", + "correction.nothing": "Rien n’a changé.", + "correction.invalid": "Vérifiez : {fields}.", + "correction.refused": "Le serveur a refusé ces valeurs.", + "correction.notAllowed": + "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut corriger ses tags.", + "correction.error": "Les corrections n’ont pas pu être enregistrées.", + "correction.stale": + "Enregistré, mais les corrections n’ont pas pu être relues. Rechargez la page avant d’enregistrer à nouveau.", + "correction.back": "Retour à {album}", + "canvas.title": "Canvas", + "canvas.detail": + "Une courte boucle jouée sans le son par-dessus la pochette, sur la page En écoute. Le serveur prend le mp4 ou le webm.", + "canvas.loading": "Recherche d’un canvas…", + "canvas.none": "Cette piste n’a pas de canvas.", + "canvas.unreadable": "Impossible de savoir si cette piste a un canvas.", + "canvas.preview": "Aperçu du canvas", + "canvas.choose": "Choisir une boucle", + "canvas.replace": "Remplacer la boucle", + "canvas.remove": "Retirer le canvas", + "canvas.closed": + "Cette bibliothèque n’accepte pas de canvas. C’est son opérateur qui en décide, sur le serveur.", + "canvas.saving": "Envoi…", + "canvas.placed": "Canvas enregistré.", + "canvas.removed": "Canvas retiré.", + "canvas.refused": + "Le serveur a refusé ce fichier : il prend une courte boucle mp4 ou webm, avec une image.", + "canvas.tooLarge": + "Ce fichier dépasse la taille que le serveur accepte pour un canvas.", + "canvas.quota": "Cette bibliothèque n’a plus de place pour les canvas.", + "canvas.notAllowed": + "Le serveur n’a pas modifié ce canvas : la piste ou sa boucle a disparu, ou le compte n’en a plus le droit.", + "canvas.error": "Le canvas n’a pas pu être modifié.", +}; diff --git a/webapp/src/state/i18n/index.tsx b/webapp/src/state/i18n/index.tsx index d76fc606..277ba1ce 100644 --- a/webapp/src/state/i18n/index.tsx +++ b/webapp/src/state/i18n/index.tsx @@ -7,2385 +7,11 @@ import { useState, } from "react"; -const en = { - "nav.albums": "Albums", - "nav.artists": "Artists", - "nav.songs": "Songs", - "nav.search": "Search", - "nav.favourites": "Favourites", - "nav.playlists": "Playlists", - "nav.queue": "Queue", - "nav.shares": "Shares", - "nav.offline": "Offline", - "nav.radio": "Radio", - "nav.scrobbling": "Scrobbling", - "nav.admin": "Admin", - "nav.signOut": "Sign out", - "nav.skip": "Skip to content", - "nav.home": "WaveFlow server, home", - "nav.server": "server", - "nav.more": "More", - "nav.moreTitle": "Everything else", - "nav.moreDetail": - "The bar below holds what you reach for while listening. The rest is here.", - "nav.moreClose": "Close", - "preferences.theme": "Theme", - "preferences.language": "Language", - "preferences.themeLight": "Light", - "preferences.themeDark": "Dark", - "preferences.followSystem": "Follow the system", - "preferences.followSystemDetail": - "Switch between a theme’s light and dark versions when your device does.", - "preferences.followingLight": - "Your device is in light mode, so the light version is shown. Picking a theme picks both of its versions.", - "preferences.followingDark": - "Your device is in dark mode, so the dark version is shown. Picking a theme picks both of its versions.", - "preferences.standInLight": - "{theme} has no light version, so {fallback} stands in while your device is in light mode.", - "preferences.standInDark": - "{theme} has no dark version, so {fallback} stands in while your device is in dark mode.", - "replayGain.label": "ReplayGain", - "replayGain.off": "Off", - "replayGain.track": "Track gain", - "replayGain.album": "Album gain", - "replayGain.preamp": "Preamp", - "replayGain.detail": - "Plays every track at the loudness its tags measured, so one record is not louder than the next. Album gain keeps the differences inside an album. A track without tags plays as it is. It routes playback through Web Audio, which some phones pause with the screen.", - "common.loading": "Loading", - "common.loadError": "We could not load this view", - "common.noAlbums": "No albums yet.", - "common.noArtists": "No artists yet.", - "common.variousArtists": "Various artists", - "common.unknownArtist": "Unknown artist", - "common.album": "Album", - "album.play": "Play album", - "album.addQueue": "Add to queue", - "album.title": "Title", - "album.duration": "Duration", - "album.trackActions": "Track actions", - "album.moreBy": "More by {artist}", - "common.artist": "Artist", - "common.albums": { one: "{count} album", other: "{count} albums" }, - "common.tracks": { one: "{count} track", other: "{count} tracks" }, - "songs.loadedSoFar": { - one: "{count} track loaded", - other: "{count} tracks loaded", - }, - "songs.playLoaded": { - one: "Play {count} loaded track", - other: "Play {count} loaded tracks", - }, - "songs.playAll": { - one: "Play the track", - other: "Play all {count} tracks", - }, - "songs.loadMore": "Load more", - "songs.loadMoreError": "That page could not be loaded. Try again.", - "songs.loading": "Loading…", - "songs.empty": "This library holds no tracks yet.", - "file.megabytes": "{size} MB", - "file.kilobytes": "{size} kB", - "nav.settings": "Settings", - "settings.detail": - "Appearance, language, and the clients allowed to read your library.", - "settings.appearance": "Appearance", - "settings.playback": "Playback", - "settings.sections": "Settings sections", - "settings.section.account": "Account", - "settings.playbackDetail": - "How this browser plays your tracks. Kept in this browser, not on your account.", - "settings.levelling": "Volume levelling", - "clients.title": "Registered clients", - "clients.detail": - "Devices and scripts holding a credential on your library. Revoking one stops it at once.", - "clients.name": "What it is for", - "clients.register": "Register", - "clients.once": - "Copy this now. Only its hash is stored, so it is shown once and never again:", - "clients.revoke": "Revoke", - "clients.none": "No client holds a credential on your library.", - "clients.error": "That did not work. Nothing changed.", - "clients.registered": "registered {when}", - "clients.used": "last used {when}", - "clients.unused": "never used", - "subsonic.title": "Subsonic password", - "subsonic.detail": - "The separate password Subsonic clients sign in with. Changing it replaces the API key too, so a leaked one stops working.", - "subsonic.set": "set {when}", - "subsonic.changed": "last changed {when}", - "subsonic.password": "New password", - "subsonic.rotate": "Change it", - "subsonic.revoke": "Switch it off", - "subsonic.confirm": "Yes, switch it off", - "subsonic.once": - "Copy this API key now. Only its hash is stored, so it is shown once and never again:", - "subsonic.gone": - "Your Subsonic clients will stop working until you set it again in each of them.", - "subsonic.none": - "You have no Subsonic password, so no Subsonic client can read your library. Ask the administrator for one.", - "subsonic.short": "Use at least 12 characters.", - "subsonic.error": "That did not work. Nothing changed.", - "quality.label": "File quality", - "quality.unknown": "Unknown", - "quality.low": "Low", - "quality.standard": "Standard", - "quality.high": "High", - "quality.lossless": "Lossless", - "quality.hiRes": "Hi-res", - "file.channels": { one: "mono", other: "{count} ch" }, - "common.delete": "Delete", - "common.cancel": "Cancel", - "correction.edit": "Correct tags", - "correction.editTrack": "Correct tags: {title}", - "trackMenu.label": "Actions for {title}", - "trackMenu.play": "Play", - "trackMenu.queue": "Add to queue", - "trackMenu.download": "Download", - "trackMenu.keepOffline": "Keep for offline", - "trackMenu.forgetOffline": "Remove the offline copy", - "trackMenu.downloadFailed": "The download could not start. Try again.", - "trackMenu.dismiss": "Dismiss", - "trackMenu.goToAlbum": "Go to album", - "trackMenu.goToArtist": "Go to {name}", - "trackMenu.properties": "Properties", - "trackMenu.edit": "Edit tags", - "properties.kicker": "Properties", - "properties.close": "Close properties", - "properties.loadError": "Some properties could not be loaded.", - "properties.file": "File", - "properties.tags": "Tags", - "properties.sound": "Loudness and tempo", - "properties.lyrics": "Lyrics", - "properties.path": "Path in library", - "properties.format": "Format", - "properties.bitrate": "Bit rate", - "properties.sampleRate": "Sample rate", - "properties.bitDepth": "Bit depth", - "properties.channels": "Channels", - "properties.size": "Size", - "properties.duration": "Duration", - "properties.added": "Added", - "properties.hash": "Content hash", - "properties.title": "Title", - "properties.artists": "Artists", - "properties.album": "Album", - "properties.albumArtist": "Album artist", - "properties.year": "Year", - "properties.trackNumber": "Track", - "properties.discNumber": "Disc", - "properties.genres": "Genres", - "properties.comment": "Comment", - "properties.isrc": "ISRC", - "properties.musicbrainz": "MusicBrainz recording", - "properties.trackGain": "ReplayGain (track)", - "properties.albumGain": "ReplayGain (album)", - "properties.bpm": "BPM", - "properties.lyricsSheet": "Sheet {number}", - "properties.synced": "Synced, {lang}", - "properties.plain": "Plain text, {lang}", - "properties.noLyrics": "None", - "lyrics.source.uploadTtml": "Uploaded TTML", - "lyrics.source.uploadLrc": "Uploaded LRC", - "lyrics.source.uploadTxt": "Uploaded text", - "lyrics.source.sidecarLrc": "LRC file beside the track", - "lyrics.source.sidecarText": "Text file beside the track", - "lyrics.source.embedded": "Embedded in the file", - "lyrics.shown": "Shown", - "lyrics.download": "Download lyrics", - "lyrics.upload": "Upload lyrics…", - "lyrics.remove": "Remove the {format}", - "lyrics.unsupported": "Choose a .lrc, .txt or .ttml file.", - "lyrics.empty": "That file holds no lyrics.", - "lyrics.tooLarge": "That file is larger than 1 MB.", - "lyrics.failed": "The lyrics could not be saved. Try again.", - "lyrics.downloadFailed": "The lyrics could not be downloaded. Try again.", - "properties.lyricsSheets": "Sheets", - "properties.kbps": "{value} kbps", - "properties.khz": "{value} kHz", - "properties.bits": "{value} bit", - "properties.mb": "{value} MB", - "properties.db": "{value} dB", - "properties.dbPeak": "{value} dB, peak {peak}", - "properties.corrected": "Corrected", - "properties.fileSaid": "Corrected — the file says {value}", - "correction.detail": - "The file is never rewritten. A correction is stored beside it, survives a rescan, and every member of the library sees it.", - "correction.fieldTitle": "Title", - "correction.fieldSortTitle": "Sort title", - "correction.fieldYear": "Year", - "correction.fieldTrackNumber": "Track number", - "correction.fieldDiscNumber": "Disc number", - "correction.fieldMusicbrainz": "MusicBrainz recording ID", - "correction.fieldComment": "Comment", - "correction.fieldArtists": "Artists", - "correction.fieldGenres": "Genres", - "correction.listHint": "One name per line. Leave it empty to credit nobody.", - "correction.corrected": "Corrected", - "correction.fromFile": "The file says: {value}", - "correction.fileEmpty": "The file says nothing here", - "correction.restore": "Restore the file’s value", - "correction.keep": "Keep the correction", - "correction.restoring": "Goes back to the file’s value when saved", - "correction.restoringList": - "Read back from the file when saved — it cannot be shown beforehand", - "correction.save": "Save", - "correction.saving": "Saving…", - "correction.saved": "Saved.", - "correction.nothing": "Nothing has changed.", - "correction.invalid": "Check {fields}.", - "correction.refused": "The server refused these values.", - "correction.notAllowed": - "Only an owner or a manager of this library can correct its tags.", - "correction.error": "The corrections could not be saved.", - "correction.stale": - "Saved, but the corrections could not be read back. Reload the page before saving again.", - "correction.back": "Back to {album}", - "canvas.title": "Canvas", - "canvas.detail": - "A short loop played without sound over the cover, on the Now playing page. The server takes mp4 or webm.", - "canvas.loading": "Looking for a canvas…", - "canvas.none": "This track has no canvas.", - "canvas.unreadable": "Whether this track has a canvas could not be read.", - "canvas.preview": "Canvas preview", - "canvas.choose": "Choose a loop", - "canvas.replace": "Replace the loop", - "canvas.remove": "Remove the canvas", - "canvas.closed": - "This library does not take canvases. Its operator decides that on the server.", - "canvas.saving": "Sending…", - "canvas.placed": "Canvas saved.", - "canvas.removed": "Canvas removed.", - "canvas.refused": - "The server refused this file: it takes a short mp4 or webm loop with a picture.", - "canvas.tooLarge": "This file is larger than the server takes for a canvas.", - "canvas.quota": "This library has no room left for canvases.", - "canvas.notAllowed": - "The server did not change this canvas: the track or its loop is gone, or the account may no longer change it.", - "canvas.error": "The canvas could not be changed.", - "login.heroEyebrow": "Your music, on your terms", - "login.heroTitle": "After dark, the library comes alive.", - "login.heroDetail": "Private streaming, one catalogue, every screen.", - "login.server": "WaveFlow server", - "login.createAdmin": "Create the administrator", - "login.welcome": "Welcome back", - "login.setupDetail": - "This is a new server. Choose the first administrator account.", - "login.setupToken": "Setup token", - "login.setupTokenHint": - "The server wrote it to the file setup-token in its data directory, and its log names the path. If you set WAVEFLOW_SETUP_TOKEN, enter that value.", - "login.setupTokenError": - "This setup token is not the right one. Copy it again from the setup-token file.", - "login.username": "Username", - "login.password": "Password", - "login.setupError": - "Setup failed. Use a valid username and at least 12 password characters.", - "login.error": "Wrong username or password.", - "login.wait": "Please wait…", - "login.create": "Create and sign in", - "login.signIn": "Sign in", - "albums.detail": { - one: "{count} record in your library", - other: "{count} records in your library", - }, - "albums.gridTitle": "In your library", - "artists.detail": { - one: "{count} voice or collaborator", - other: "{count} voices and collaborators", - }, - "artists.libraryCount": { - one: "{count} album in your library", - other: "{count} albums in your library", - }, - "artists.mostPlayed": "Most played here", - "artists.section.album": "Albums", - "artists.section.ep": "EPs", - "artists.section.single": "Singles", - "artists.section.live": "Live", - "artists.section.compilation": "Compilations", - "artists.section.appearsOn": "Appears on", - "artists.about": "About", - "artists.readMore": "Read more", - "artists.similar": "Similar artists in your library", - "artists.enrichmentPending": "Looking this artist up…", - "artists.bioSource": "Biography from {provider}, {date}.", - "artists.similarSource": "Suggested by {provider}, {date}.", - "artists.portraitSource": "Portrait from {provider}, {date}.", - "artists.bannerSource": "Banner from {provider}, {date}.", - "favourites.detail": { - one: "{count} saved track", - other: "{count} saved tracks", - }, - "favourites.empty": - "Star a track from an album or search result and it will appear here.", - "favourites.add": "Add favourite", - "favourites.remove": "Remove favourite", - "playlists.detail": { - one: "{count} collection", - other: "{count} collections", - }, - "playlists.newName": "New playlist name", - "playlists.create": "Create playlist", - "playlists.play": "Play", - "playlists.addQueue": "Add queue", - "playlists.emptyOne": "This playlist is empty.", - "playlists.export": "Save as .m3u8", - "playlists.exportError": "That playlist could not be saved.", - "import.title": "Bring playlists from another player", - "import.detail": - "A .m3u or .m3u8 file. Nothing is created until you have seen what it would lose.", - "import.pick": "Choose a playlist file", - "import.name": "Playlist name", - "import.found": "{resolved} of {entries} entries are in your library.", - "import.encoding": - "The file was not UTF-8 and was read as {encoding}. Check the names below before importing.", - "import.missing": { - one: "{count} entry cannot be placed:", - other: "{count} entries cannot be placed:", - }, - "import.line": "Line {line}", - "import.noMatch": "no matching track", - "import.ambiguous": "matches several tracks", - "import.notAPath": "not a file in a library", - "import.duplicate": "already in this playlist", - "import.confirm": "Import", - "import.cancel": "Cancel", - "import.failed": "That file could not be read as a playlist.", - "playlists.empty": "Create a playlist to keep a set of tracks together.", - "playlists.createError": "The playlist could not be created.", - "playlists.queueError": "The queue could not be added to this playlist.", - "playlists.deleteError": "The playlist could not be deleted.", - "queue.detail": { - one: "{count} track synchronized with your account", - other: "{count} tracks synchronized with your account", - }, - "queue.playStart": "Play from start", - "queue.clear": "Clear queue", - "queue.remove": "Remove", - "queue.empty": "Play an album or playlist to build your synchronized queue.", - "shares.detail": "Public links for selected music", - "shares.description": "Description", - "shares.create": "Share current queue", - "shares.needQueue": "Add tracks to the queue before creating a link.", - "shares.once": "This link is shown only once:", - "shares.defaultName": "Music share", - "shares.empty": "No public links have been created.", - "shares.createError": "The share could not be created.", - "shares.tooLong": - "A share holds up to {limit} tracks, and the queue holds {count}. Shorten the queue to share it.", - "shares.deleteError": "The share could not be deleted.", - "shares.maxVisits": "Visit limit (optional)", - "shares.visits": { one: "{count} visit", other: "{count} visits" }, - "shares.visitsOf": "{count} of {max} visits", - "share.kicker": "Shared with you", - "share.defaultTitle": "Music shared with you", - "share.until": "Available until {date}", - "share.empty": "None of these tracks can be played any more.", - "share.player": "Player", - "share.unavailableTitle": "This link opens nothing", - "share.unavailableDetail": - "It may have expired, been withdrawn, or been opened as many times as it allows.", - "playlists.public": "Visible to every account", - "playlists.publicError": "That playlist could not be published.", - "playlists.others": "From other accounts", - "playlists.othersDetail": - "Public playlists of other accounts, with the tracks your libraries hold.", - "playlists.by": "By {owner}", - "playlists.moveUp": "Move {title} up", - "playlists.moveDown": "Move {title} down", - "playlists.removeEntry": "Remove {title} from this playlist", - "playlists.moveError": - "That entry could not be moved. The playlist may have changed elsewhere; it has been read again.", - "playlists.removeEntryError": "That entry could not be removed.", - "radio.detail": "Internet stations, kept with your account", - "radio.name": "Station name", - "radio.address": "Stream address", - "radio.homepage": "Station page (optional)", - "radio.image": "Station image (optional)", - "radio.imageHint": - "The server downloads it once, when you save, and keeps it with the covers: your browser never contacts the image’s host.", - "radio.imageMissing": - "The image could not be fetched; save the station to try again.", - "radio.add": "Add station", - "radio.save": "Save station", - "radio.edit": "Edit", - "radio.replacing": - "Saving replaces every field, so an empty page address clears the stored one.", - "radio.listen": "Listen", - "radio.stop": "Stop", - "radio.visit": "Station page", - "radio.empty": "No stations yet. Add one to keep it here.", - "radio.addressRefused": "An address must start with http:// or https://.", - "radio.mixedContent": - "This page is served over HTTPS, so your browser will refuse a clear-text stream.", - "radio.full": "This account already holds as many stations as it may.", - "radio.saveError": "The station could not be saved.", - "radio.deleteError": "The station could not be deleted.", - "radio.playbackError": - "That station could not be played. The server never connects to it, so this is between your browser and the station.", - "radio.stations": "Stations", - "radio.newTitle": "Add a station", - "radio.editTitle": "Edit “{name}”", - "radio.listenTo": "Listen to {name}", - "radio.stopNamed": "Stop {name}", - "radio.actions": "Actions for {name}", - "radio.confirmDelete": - "Delete the station “{name}”? Its address and image are forgotten.", - "credential.password": "Dedicated Subsonic password", - "credential.rotate": "Rotate credential", - "credential.copy": "Copy this API key now:", - "credential.error": "Credential rotation failed.", - "admin.title": "Administration", - "admin.detail": "Libraries, scans and accounts", - "admin.libraries": "Libraries", - "admin.libraryName": "Library name", - "admin.libraryPath": "Absolute server folder path", - "admin.registering": "Registering…", - "admin.register": "Register and scan", - "admin.scan": "Scan now", - "admin.accounts": "Accounts", - "admin.webPassword": "Web password, 12 characters minimum", - "admin.creating": "Creating…", - "admin.create": "Create account", - "admin.enable": "Enable", - "admin.disable": "Disable", - "admin.currentAccount": "You cannot disable your current account", - "admin.libraryError": "The library path could not be registered.", - "admin.accountError": "The account could not be created.", - "admin.statusError": "The account status could not be changed.", - "admin.scanError": "The scan could not be started.", - "admin.scanQueued": "Scan {id} queued.", - "admin.initialScan": "Initial scan {id} queued.", - "search.detail": "Find a title, album or artist", - "search.placeholder": "Title, album or artist", - "search.query": "Search query", - "search.tracks": "Tracks", - "search.empty": "Nothing found.", - "player.error": "Playback could not start. Check the server and try again.", - "player.previous": "Previous track", - "player.play": "Play", - "player.pause": "Pause", - "player.next": "Next track", - "player.seek": "Seek", - "authorize.title": "Authorisation", - "authorize.invalid": - "This authorisation link is incomplete or asks to send you somewhere we do not trust.", - "authorize.client": "Authorise {client}", - "authorize.detail": - "It will be able to browse your libraries, play your music and manage your playlists, favourites and ratings, everything this account can do.", - "authorize.redirect": "Sending you back to {url}", - "authorize.error": - "The application sent an authorisation request we cannot honour.", - "authorize.busy": "Authorising…", - "authorize.approve": "Authorise", - "notFound.title": "This room is silent", - "notFound.detail": "The page you asked for does not exist on this server.", - "notFound.back": "Back to the library", - "appearance.themes.default": "Emerald light", - "appearance.themes.midnightLight": "Midnight light", - "appearance.themes.sunsetLight": "Sunset light", - "appearance.themes.lavenderLight": "Lavender light", - "appearance.themes.crimsonLight": "Crimson light", - "appearance.themes.oceanLight": "Ocean light", - "appearance.themes.defaultDark": "Emerald dark", - "appearance.themes.oled": "OLED black", - "appearance.themes.midnight": "Midnight", - "appearance.themes.sunset": "Sunset", - "appearance.themes.lavender": "Lavender", - "appearance.themes.crimson": "Crimson", - "appearance.themes.ocean": "Ocean", - "appearance.themes.neon": "Neon", - "browse.sort": "Sort", - "browse.sortAlphabeticalByName": "By title", - "browse.sortAlphabeticalByArtist": "By artist", - "browse.sortAlphabeticalByAlbum": "By album", - "browse.sortNewest": "Recently added", - "browse.sortRecent": "Recently played", - "browse.sortFrequent": "Most played", - "browse.sortStarred": "Favourites", - "browse.sortByYear": "By year", - "browse.filterAlbums": "Filter by title or artist", - "browse.filterArtists": "Filter by name", - "browse.filterPlaceholder": "Filter…", - "browse.matches": { - one: "{count} of {total} shown", - other: "{count} of {total} shown", - }, - "browse.noMatch": "Nothing matches that filter.", - "card.play": "Play", - "card.queue": "Add to queue", - "rating.label": "Rating", - "rating.stars": { one: "{count} star", other: "{count} stars" }, - "nav.genres": "Genres", - "nav.history": "Recently played", - "nav.random": "Random", - "nav.playing": "Now playing", - "nav.upload": "Upload", - "nav.folders": "Folders", - "nav.attention": "Needs attention", - "nav.quality": "Quality", - "attention.lists": "Lists", - "attention.total": { - one: "{count} thing to look at", - other: "{count} things to look at", - }, - "attention.nothing": "Nothing needs attention.", - "attention.notYours": - "Only the owners and managers of this library see what needs attention in it.", - "attention.empty": "Nothing in this list.", - "attention.tracksNote": - "Fix a track from its menu: once corrected, it leaves the list.", - "attention.creditsNote": - "Each of these credits names several artists in one string. Split it into the artists it names, or keep it as one when it is a single name. Every later scan applies the decision.", - "attention.pending": "To decide", - "attention.decided": "Decided", - "attention.split": "Split", - "attention.confirmSplit": "Confirm: split into {count}", - "attention.keep": "Keep as one", - "attention.splitDone": "{name} is split.", - "attention.keepDone": "{name} stays as one.", - "attention.undone": "The decision about {name} is undone.", - "attention.alreadyDecided": - "This credit was decided in the meantime. The list has been reloaded.", - "attention.failed": "That did not work. Try again.", - "attention.noCredits": "No credit is waiting for a decision.", - "attention.noDecisions": "Nothing has been decided yet.", - "attention.fragments": "What {name} would split into", - "attention.knownFragment": "Already an artist in this library", - "attention.decidedSplit": "Split into {parts}", - "attention.decidedKeep": "Kept as one", - "attention.decidedBy": "By {who}, {when}", - "attention.undo": "Undo", - "attention.duplicateGroup": "Probable duplicate {group}", - "attention.split_credits": "Artists to split", - "attention.missing_artist": "Missing artist", - "attention.missing_album": "Missing album", - "attention.missing_year": "Missing year", - "attention.missing_track_number": "Missing track number", - "attention.missing_cover": "Missing cover", - "attention.album_year_conflict": "Albums with several years", - "attention.compilation_no_album_artist": - "Compilations without an album artist", - "attention.duplicate_track_number": "Duplicate track numbers", - "attention.track_number_gap": "Gaps in track numbers", - "attention.probable_duplicate": "Probable duplicates", - "attention.probable_fake_lossless": "Probable fake lossless", - "quality.help": - "Measured from the audio rather than read from the container: where the highs stop, and how many bits the samples use. A lossless file whose highs stop around 16 kHz was a 128 kbps MP3 before it was a FLAC. “No sign of a lossy source” is not proof: an encoder that keeps the full band leaves nothing to find.", - "quality.progress": "{measured} of {lossless} lossless files analysed", - "quality.disabled": "The analysis is turned off on this server.", - "quality.noLossless": "No lossless file in this library.", - "quality.status.running": - "Analysing now: {measured} of {lossless} done, {inFlight} in progress.", - "quality.status.waitingForScan": - "Waiting for the scan to finish before analysing.", - "quality.status.queued": - "Queued: the analysis will reach these files shortly.", - "quality.status.idle": "Idle. Last file analysed {when}.", - "quality.status.idleNever": "Idle. No file has been analysed yet.", - "quality.filters": "Verdicts", - "quality.all": "All files", - "quality.format": "Format", - "quality.allFormats": "All formats", - "quality.empty": "No file with this verdict.", - "quality.column.track": "Track", - "quality.column.container": "What the file says", - "quality.column.highs": "Highs stop at", - "quality.column.verdict": "What the audio says", - "quality.fullBand": "Full band", - "quality.khz": "{value} kHz", - "quality.db": "{value} dB", - "quality.analyseAgain": "Analyse again", - "quality.confirmAnalyse": "Confirm: analyse the whole library again", - "quality.requested": "The library will be analysed again in the background.", - "quality.requestFailed": "That did not work. Try again.", - "quality.section": "Real quality", - "quality.verdictLabel": "Verdict", - "quality.highsStop": "Highs stop at", - "quality.wall": "Fall at that point", - "quality.bitsUsed": "Bits in use, measured", - "quality.bitsOf": "{used} of {declared}", - "quality.bitsNote": - "A measurement of the samples, not proof of genuine hi-res: a file that uses every bit it declares was not padded, which does not mean it was recorded at that depth.", - "quality.analysed": "Analysed", - "quality.chart": "Averaged spectrum: the highs stop at {where}.", - "quality.chartFullBand": "Averaged spectrum: the highs run on with no wall.", - "quality.showSpectrogram": "Show the spectrogram", - "quality.drawing": "Drawing…", - "quality.spectrogramAlt": "Spectrogram of {title}, frequency against time", - "quality.spectrogramBusy": - "A spectrogram is already being drawn for you. Try again in a moment.", - "quality.spectrogramFailed": "The spectrogram could not be drawn.", - "quality.verdict.lossy_source": "Probably from a lossy file", - "quality.verdict.upsampled": "Probably upsampled", - "quality.verdict.padded": "Padded bit depth", - "quality.verdict.unreadable": "Could not be read", - "quality.verdict.silent": "Silent", - "quality.verdict.pending": "Waiting for analysis", - "quality.verdict.consistent": "No sign of a lossy source", - "quality.verdict.lossy": "Lossy", - "quality.verdict.not_measured": "Not measured", - "quality.explain.lossy_source": - "The highs stop at a wall where a lossy encoder cuts: this file was probably decoded from an MP3, AAC or Opus file and saved as lossless.", - "quality.explain.upsampled": - "The rate is above 48 kHz, but the highs stop where a CD or a 48 kHz recording ends: it was probably resampled from one.", - "quality.explain.padded": - "The container declares more bits than the samples use: a 16-bit recording stored at 24 bits, for instance.", - "quality.explain.unreadable": - "FFmpeg could not decode this file. It will be tried again when the file changes.", - "quality.explain.silent": "Nothing in this file is loud enough to measure.", - "quality.explain.pending": - "This file has not been analysed yet. The analysis runs in the background.", - "quality.explain.consistent": - "Nothing in the audio contradicts the container. That is not proof: an encoder that keeps the full band leaves nothing to find.", - "quality.explain.lossy": - "A lossy file says what it is; its quality is its encoder and its bitrate. It is not analysed.", - "quality.explain.not_measured": "This format is not analysed.", - "artistSplit.offer": "Split into {count} artists", - "artistSplit.confirm": "Confirm: split into {count}", - "artistSplit.done": "Split into {parts}.", - "artistSplit.failed": "This credit could not be split. Try again.", - "common.back": "Back", - "artistImage.changePortrait": "Change portrait", - "artistImage.changeBanner": "Change banner", - "artistImage.resetPortrait": "Automatic portrait", - "artistImage.resetBanner": "No banner", - "artistImage.portraitChanged": "Portrait changed.", - "artistImage.bannerChanged": "Banner changed.", - "artistImage.portraitReverted": "Back to the automatic portrait.", - "artistImage.bannerReverted": "Banner removed.", - "artistImage.tooLarge": "This image is larger than 10 MB.", - "artistImage.notAnImage": "Choose a JPEG, PNG or WebP image.", - "artistImage.failed": "That did not work. Try again.", - "artistImage.chosen": "Portrait chosen by hand", - "artistImage.searchDeezer": "Search Deezer", - "artistImage.menu": "Edit images", - "artistImage.menuShort": "Edit", - "artistImage.menuLabel": "Images of {name}", - "artistImage.deezerTitle": "Choose a portrait from Deezer", - "artistImage.deezerQuery": "Artist name", - "artistImage.deezerSearch": "Search", - "artistImage.deezerSearching": "Searching Deezer…", - "artistImage.deezerResults": "Deezer artists", - "artistImage.deezerNone": "Deezer knows no artist by that name.", - "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, - "artistImage.deezerUnavailable": - "Searching Deezer is not enabled on this server.", - "artistImage.deezerBusy": - "Your previous search is still running. Try again in a moment.", - "artistImage.deezerExpired": "These results have expired. Search again.", - "folders.count": { - one: "{count} folder created in this library", - other: "{count} folders created in this library", - }, - "folders.where": "Folder path", - "folders.subfolders": "Folders", - "folders.noLibrary": "No library is available to this account.", - "folders.none": "No folder has been created in this library yet.", - "folders.system": "Kept by WaveFlow", - "folders.systemNote": - "To sort gathers every track that is in no folder. WaveFlow keeps it itself: it is not counted among the folders above, and it cannot be renamed, moved or deleted.", - "filed.unfiled": "To sort", - "filed.automatic": "Automatic", - "filed.topLevel": "Top level", - "filed.here": "Here now", - "filed.destinations": "Folders to choose from", - "filed.loadError": "The folders could not be loaded.", - "filed.moveFailed": "The move failed. Try again.", - "filed.moveTrack": "Move “{title}” to a folder", - "filed.moveFolder": "Move “{name}”", - "filed.note": - "Folders that exist only in WaveFlow. Filing a track here moves no file.", - "filed.newFolder": "New folder", - "filed.name": "Folder name", - "filed.renameLabel": "New name", - "filed.create": "Create", - "filed.save": "Save", - "filed.cancel": "Cancel", - "filed.rename": "Rename", - "filed.move": "Move…", - "filed.delete": "Delete", - "filed.confirmDelete": - "Delete “{name}” and its sub-folders? Their tracks go back to To sort; no file is touched.", - "filed.created": "“{name}” created.", - "filed.renamed": "Renamed to “{name}”.", - "filed.deleted": { - one: "Folder deleted: {count} track is back in To sort.", - other: "Folder deleted: {count} tracks are back in To sort.", - }, - "filed.filed": { - one: "{count} track filed in {folder}.", - other: "{count} tracks filed in {folder}.", - }, - "filed.folderMoved": "“{name}” moved.", - "filed.nameTaken": "A folder of that name is already here.", - "filed.refused": - "That folder cannot go there: check its name, or the tree is too deep or full.", - "filed.failed": "That did not work. Try again.", - "filed.missing": "This folder no longer exists.", - "filed.empty": "Nothing is filed here yet.", - "filed.allFiled": "Every track is filed.", - "filed.uploadHere": "Upload here", - "filed.unfiledNote": - "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed, and it cannot be renamed or deleted. To file a track, open its menu and choose “Move to folder…”.", - "filed.unfiledNoteReader": - "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed.", - "trackMenu.move": "Move to folder…", - "upload.into": "Received files go into the folder {folder}.", - "upload.doneUnfiled": "Added to the library, in To sort", - "upload.folderMissing": - "The folder this upload was opened for cannot be found: received files will go to To sort.", - "upload.folderUnchecked": - "The folder this upload was opened for could not be checked, so no file is taken yet.", - "upload.checkAgain": "Check again", - "nav.group.library": "Library", - "nav.group.listen": "Listening", - "upload.title": "Upload to {library}", - "upload.detail": - "Each file is fingerprinted in your browser first, so a file the library already holds is never sent again, and an interrupted transfer picks up where it stopped.", - "upload.pick": "Choose audio files", - "upload.formats": "Formats the library can index: {extensions}", - "upload.noLibrary": "Choose a library first.", - "upload.notAllowed": - "Only an owner or a manager of this library can add files to it.", - "upload.closed": - "This library does not take files. Its operator decides that on the server.", - "upload.waiting": "Waiting", - "upload.hashing": "Fingerprinting…", - "upload.negotiating": "Asking the server…", - "upload.uploading": "Sending…", - "upload.committing": "Adding to the library…", - "upload.done": "Added to the library", - "upload.present": "Already in the library", - "upload.refused": "Refused: {reason}", - "upload.failed": "Failed: {reason}", - "upload.progress": "Progress: {name}", - "upload.reason.unsupportedFormat": - "not an audio format the library can index", - "upload.reason.tooLarge": "larger than the server accepts", - "upload.reason.quotaExceeded": "the library has no room left", - "upload.reason.libraryClosed": "the library does not take files", - "upload.reason.tooManySessions": - "too many transfers already open — try again shortly", - "upload.reason.refusedFile": - "what arrived did not match the file, or is not audio the library can read", - "upload.reason.unavailable": - "the server could not take it just now — try again", - "upload.reason.network": "the connection was lost", - "upload.reason.notAllowed": "you may no longer add files to this library", - "upload.reason.error": "something went wrong", - "upload.reason.hashing": "this browser could not fingerprint the file", - "browse.filterGenres": "Filter by genre", - "genres.detail": { one: "{count} genre", other: "{count} genres" }, - "genres.empty": "No genre is tagged in this library yet.", - "genres.emptyOne": "Nothing is tagged with this genre any more.", - "history.detail": { - one: "{count} track, most recent first", - other: "{count} tracks, most recent first", - }, - "history.empty": "Nothing has been played from this account yet.", - "random.detail": { - one: "{count} track drawn", - other: "{count} tracks drawn", - }, - "random.again": "Draw again", - "random.fromGenre": "Play this genre at random", - "random.empty": "There is nothing to draw from yet.", - "lyrics.label": "Lyrics", - "lyrics.none": "No lyrics travel with this file.", - "playing.idle": "Nothing is playing.", - "playing.empty": "Start a track and it will appear here, with its lyrics.", - "lyrics.seekHint": "This sheet carries timings — press a line to jump to it.", - "lyrics.seekTo": "Jump to {when}", - "lyrics.seekLine": "{line}, jump to {when}", - "playing.viewLabel": "How to look at this track", - "playing.viewDetail": "Detail", - "playing.viewLyrics": "Lyrics", - "playing.viewImmersive": "Immersive", - "playing.immersiveTitle": "Immersive view", - "playing.leaveImmersive": "Leave the immersive view", - "bookmarks.title": "Saved positions", - "bookmarks.detail": "One per track. Saving again moves it.", - "bookmarks.save": "Save this position", - "bookmarks.forget": "Forget", - "player.shuffle": "Shuffle", - "player.repeatOff": "Repeat: off", - "player.repeatAll": "Repeat: the whole queue", - "player.repeatOne": "Repeat: this track", - "player.volume": "Volume", - "player.mute": "Mute", - "player.unmute": "Unmute", - "player.openAlbum": "Open the album", - "player.live": "Live", - "player.liveRadio": "Live radio", - "player.stopLive": "Stop the station", - "player.liveError": "This station could not be played.", - "player.closeAside": "Stop and close", - "library.active": "Library", - "search.thisLibrary": "Searching {library}.", - "scan.connecting": "Waiting for the first reading…", - "scan.queued": "Queued", - "scan.running": "Scanning", - "scan.completed": "Finished", - "scan.failed": "Failed", - "scan.progress": "Scan progress", - "scan.counted": "{processed} of {total} files", - "scan.tally": - "{added} added · {updated} updated · {moved} moved · {skipped} unchanged · {errors} errors", - "admin.scanning": "Scan in progress", - "admin.nowPlaying": "Playing now", - "admin.nobodyListening": "Nobody is listening right now.", - "admin.tokens": "API tokens for {username}", - "admin.tokenName": "What is it for?", - "admin.tokenCreate": "Issue", - "admin.tokenOnce": "Copy it now. It is shown once and never again:", - "admin.tokenError": "The token could not be issued.", - "admin.tokenRevoke": "Revoke", - "admin.tokenUsed": "already used", - "admin.tokenUnused": "never used", - "admin.tokenNone": "No token on this account.", - "scan.lost": - "The progress stream could not be opened. The scan itself is unaffected.", - "admin.members": "Who may see this library", - "admin.memberRole": "Role", - "admin.memberRemove": "Remove", - "admin.memberOwner": "owner, and stays one", - "admin.memberAdd": "Give access to", - "admin.memberChoose": "Choose an account…", - "admin.memberError": "That membership could not be changed.", - "admin.manage": "Name, folder and deletion", - "admin.rename": "Name", - "admin.path": "Folder on the server", - "admin.followLinks": - "Follow symbolic links whose target stays inside the library", - "admin.watch": - "Watch the folder and scan what changes (off for a network share changed by other machines)", - "admin.save": "Save", - "admin.saved": "Saved.", - "admin.savedScan": "Saved. Scan {id} queued.", - "admin.saveError": - "That change could not be saved. A folder must exist on the server and must not be a symbolic link.", - "admin.saveBusy": - "The library is being scanned, or another library already uses that folder. Try again once the scan is over.", - "admin.delete": "Delete this library", - "admin.deleteNote": - "Removes the library from the catalogue for every member: its tracks, albums and artists, with their playlist entries, favourites and plays. No file is touched on disk. Type the library’s name to confirm.", - "admin.deleteConfirm": "The library’s name, to confirm", - "admin.deleteButton": "Delete for good", - "admin.deleted": "{name} was deleted: {tracks} tracks left the catalogue.", - "admin.deleteError": "The library could not be deleted.", - "scan.leftOut": "{excluded} left out · {links} links not followed", - "nav.missing": "Missing files", - "nav.maintenance": "Maintenance", - "maintenance.sections": "Maintenance sections", - "maintenance.none": "Nothing here is yours to maintain in this library.", - "missing.total": { - one: "{count} file the scans can no longer find", - other: "{count} files the scans can no longer find", - }, - "missing.nothing": "Every file is where the catalogue expects it.", - "missing.notYours": - "Only the owners and managers of this library, and administrators, see its missing files.", - "missing.note": - "These tracks stay in the catalogue, with their favourites, playlist entries and plays, until they are purged; a file that comes back first keeps its track. A purge can be taken back for a while, then the track, and everything that hangs off it for every member, goes for good.", - "missing.since": "missing since {date}", - "missing.cost": - "{playlists} in playlists · {favorites} favourites · {plays} plays", - "missing.select": "Select {title}", - "missing.purgeSelected": "Purge the selected ({count})", - "missing.purgeAll": "Purge them all", - "missing.confirm": "Confirm: purge {count}", - "missing.purged": { - one: "{count} track purged.", - other: "{count} tracks purged.", - }, - "missing.scanning": - "The library is being scanned. Try again once the scan is over.", - "missing.failed": "The purge did not work. Try again.", - "missing.more": "Show more", - "missing.purgedUntil": { - one: "{count} track purged. It can be restored until {date}.", - other: "{count} tracks purged. They can be restored until {date}.", - }, - "missing.purgedTitle": { - one: "Purged, still restorable ({count})", - other: "Purged, still restorable ({count})", - }, - "missing.purgedNote": - "Nothing of these is lost yet. Restored, a track goes back to the missing files with everything that hung off it; a file that comes back restores its track by itself.", - "missing.deletedAfter": "deleted for good after {date}", - "missing.restore": "Restore", - "missing.restoreOne": "Restore {title}", - "missing.restoreAll": "Restore them all", - "missing.restored": { - one: "{count} track restored.", - other: "{count} tracks restored.", - }, - "missing.restoreFailed": "The restoration did not work. Try again.", - "offline.kicker": "In this browser", - "offline.title": "Kept for offline", - "offline.detail": - "These tracks were kept in this browser for the account that last signed in, and play without a connection. Signing out deletes them from this browser. Keep more from a track's menu.", - "offline.unsupported": - "This browser cannot keep tracks for offline listening.", - "offline.back": "Back to the library", - "offline.empty": "Nothing is kept yet. A track's menu offers to keep it.", - "offline.player": "Offline player", - "offline.gone": "That copy is no longer in this browser.", - "offline.forget": "Remove the offline copy of {title}", - "offline.forgetShort": "Remove", - "offline.forgetDetail": "Frees {size}", - "login.offline": { - one: "Listen to the {count} track kept in this browser", - other: "Listen to the {count} tracks kept in this browser", - }, - "scrobbling.title": "External scrobbling", - "scrobbling.detail": - "Forward what you listen to here to ListenBrainz, Maloja or Last.fm.", - "scrobbling.none": - "This server offers no destination. Its operator decides that.", - "scrobbling.provider.listenbrainz": "ListenBrainz", - "scrobbling.provider.maloja": "Maloja", - "scrobbling.provider.lastfm": "Last.fm", - "scrobbling.instance": "instance “{name}”", - "scrobbling.unavailable": "Unavailable here: {reason}", - "scrobbling.unavailable.no_application_configured": - "no Last.fm application is declared on this server", - "scrobbling.unavailable.browser_journey_needs_https": - "the browser journey needs this server’s public address to be an https one — an operator can still link an account from the command line", - "scrobbling.unavailable.unknown": "this server did not say why", - "scrobbling.retired": - "This server no longer offers this instance. Nothing more will leave for it — you can still withdraw the authorisation.", - "scrobbling.notLinked": "Not linked.", - "scrobbling.secret": "API key", - "scrobbling.secretHint": "Taken from your account on that destination.", - "scrobbling.link": "Link", - "scrobbling.working": "Working…", - "scrobbling.linkError": - "That authorisation was refused. Check the key and try again.", - "scrobbling.unlink": "Unlink", - "scrobbling.unlinkError": "That link could not be withdrawn.", - "scrobbling.connect": "Connect to Last.fm", - "scrobbling.connectDetail": - "Last.fm hands out no key you can paste, so it asks you there and brings you back.", - "scrobbling.connectError": "The Last.fm journey could not be opened.", - "scrobbling.returned": "Last.fm is linked: {name}.", - "scrobbling.health.healthy": "Listens are leaving", - "scrobbling.health.degraded": "The queue is not moving", - "scrobbling.health.broken": "Broken", - "scrobbling.healthDetail.healthy": "Nothing is stuck here.", - "scrobbling.healthDetail.degraded": - "The link answers, but listens are not getting through — either one is waiting on you, or the oldest has waited too long.", - "scrobbling.healthDetail.broken": - "The credential was refused, or this server can no longer reach that instance under the name the link was made against.", - "scrobbling.pending": { one: "{count} waiting", other: "{count} waiting" }, - "scrobbling.retrying": { - one: "{count} being retried", - other: "{count} being retried", - }, - "scrobbling.uncertainCount": { - one: "{count} waiting on you", - other: "{count} waiting on you", - }, - "scrobbling.oldest": "Oldest has waited since {when}.", - "scrobbling.lastSuccess": "Last accepted {when}.", - "scrobbling.lastFailure": "Last refusal: {reason}.", - "scrobbling.uncertain": "Listens only you can decide about", - "scrobbling.uncertainDetail": - "The connection broke after these left, so nobody knows whether the destination recorded them. Sending one again may put it twice in a listening history other people read; throwing it away leaves a gap. This server will not choose for you, and it will ask once.", - "scrobbling.uncertainNone": "Nothing is waiting on you.", - "scrobbling.playedAt": "Played {when}", - "scrobbling.attempts": { one: "{count} attempt", other: "{count} attempts" }, - "scrobbling.retry": "Send it again", - "scrobbling.discard": "Throw it away", - "scrobbling.answered": "Answered.", - "scrobbling.answerError": - "That listen could not be answered. Reload the page before trying again.", - "scrobbling.unnamedTrack": "Not in the history this account still holds", - "scrobbling.reason.rate_limited": "the destination asked for room", - "scrobbling.reason.retryable": "the destination did not take it", - "scrobbling.reason.auth_broken": "the authorisation was refused", - "scrobbling.reason.rejected": "the destination read it and refused it", - "scrobbling.reason.destination_gone": - "this server no longer reaches that instance under that name", - "scrobbling.reason.credential_unreadable": - "this server could not read the stored credential", - "scrobbling.reason.attempts_exhausted": "it was tried too many times", - "scrobbling.reason.unlinked": "the link was withdrawn", - "scrobbling.reason.ambiguous": "the answer never arrived", - "scrobbling.reason.interrupted": "the server stopped mid-flight", - // RFC-024: lyrics from LRCLIB, statistics and mixes. - "lyrics.source.lrclib": "From LRCLIB", - "lyrics.fromLrclib": "Lyrics from LRCLIB", - "nav.stats": "Statistics", - "stats.detail": "What this account listened to, counted from its own plays.", - "stats.period": "Period", - "stats.period7d": "Last 7 days", - "stats.period30d": "Last 30 days", - "stats.period365d": "Last 12 months", - "stats.periodAll": "All time", - "stats.empty": "Nothing was listened to in this period.", - "stats.plays": "Listens", - "stats.listeningTime": "Listening time", - "stats.tracks": "Tracks", - "stats.artists": "Artists", - "stats.albums": "Albums", - "stats.estimate": - "Listening time adds up the length of each track listened to; no player says how much of it was heard.", - "stats.timelineDays": "Listens per day", - "stats.timelineMonths": "Listens per month", - "stats.bucket": { - one: "{label}: {count} listen", - other: "{label}: {count} listens", - }, - "stats.topTracks": "Top tracks", - "stats.topArtists": "Top artists", - "stats.topAlbums": "Top albums", - "stats.topGenres": "Top genres", - "stats.playCount": { one: "{count} listen", other: "{count} listens" }, - "stats.firstPlay": "First listen of the period", - "stats.lastPlay": "Last listen", - "stats.moment": "{title}, {when}", - "stats.minutes": { one: "{count} minute", other: "{count} minutes" }, - "stats.hoursMinutes": "{hours} h {minutes} min", - "mixes.title": "Made for you today", - "mixes.detail": { - one: "{count} track, drawn for today", - other: "{count} tracks, drawn for today", - }, - "mixes.mostPlayed": "Most played", - "mixes.neverPlayed": "Never played", - "mixes.forgottenFavourites": "Forgotten favourites", - "mixes.recentlyAdded": "Recently added", - "mixes.genre": "{genre} mix", - "mixes.decade": "The {decade}s", - "mixes.failed": "“{title}” could not be opened. Try again.", - "mixes.shelfTitle": "Mixes for you", - "mixes.shelfDetail": - "Playlists drawn from your listening, new every day. Your albums are below.", - "mixes.badge": "Mix", - // Smart playlists and podcasts (RFC-025), kept together at the end. - "smart.title": "Smart playlists", - "smart.detail": - "Defined by rules and worked out again each time they are opened, so they keep up with the library.", - "smart.new": "New smart playlist", - "smart.edit": "Edit rules", - "smart.save": "Save smart playlist", - "smart.badge": "Smart", - "smart.empty": "No smart playlists yet.", - "smart.notCounted": "Counted when opened", - "smart.chooseLibrary": "Choose a library", - "smart.name": "Name", - "smart.match": "Tracks must match", - "smart.match.all": "every condition", - "smart.match.any": "at least one condition", - "smart.fieldOf": "Field of condition {position}", - "smart.operatorOf": "Comparison of condition {position}", - "smart.valueOf": "Value of condition {position}", - "smart.upperOf": "Upper bound of condition {position}", - "smart.removeCondition": "Remove condition {position}", - "smart.addCondition": "Add a condition", - "smart.sort": "Sort by", - "smart.sort.none": "Title (default)", - "smart.order": "Order", - "smart.order.asc": "Ascending", - "smart.order.desc": "Descending", - "smart.limit": "At most (tracks)", - "smart.days": "days", - "smart.yes": "yes", - "smart.no": "no", - "smart.preview": "Preview", - "smart.previewCount": { - one: "{count} track matches", - other: "{count} tracks match", - }, - "smart.unfinished": "Finish every condition to preview or save.", - "smart.refused": "The server refused these rules.", - "smart.full": "This account already holds as many smart playlists as it may.", - "smart.saveError": "The smart playlist could not be saved.", - "smart.deleteError": "The smart playlist could not be deleted.", - "smart.loadError": "The smart playlists could not be loaded.", - "smart.field.title": "Title", - "smart.field.artist": "Artist", - "smart.field.album_artist": "Album artist", - "smart.field.album": "Album", - "smart.field.genre": "Genre", - "smart.field.year": "Year", - "smart.field.rating": "Rating", - "smart.field.starred": "Favourite", - "smart.field.play_count": "Play count", - "smart.field.last_played": "Last played", - "smart.field.date_added": "Date added", - "smart.field.duration": "Duration (seconds)", - "smart.field.bitrate": "Bitrate (kbit/s)", - "smart.field.format": "Format", - "smart.field.library": "Library", - "smart.op.is": "is", - "smart.op.is_not": "is not", - "smart.op.contains": "contains", - "smart.op.not_contains": "does not contain", - "smart.op.starts_with": "starts with", - "smart.op.ends_with": "ends with", - "smart.op.greater_than": "is more than", - "smart.op.less_than": "is less than", - "smart.op.between": "is between", - "smart.op.in_the_last": "in the last (days)", - "smart.op.not_in_the_last": "not in the last (days)", - "smart.op.before": "before", - "smart.op.after": "after", - "smart.sort.title": "Title", - "smart.sort.artist": "Artist", - "smart.sort.album": "Album", - "smart.sort.year": "Year", - "smart.sort.rating": "Rating", - "smart.sort.play_count": "Play count", - "smart.sort.last_played": "Last played", - "smart.sort.date_added": "Date added", - "smart.sort.duration": "Duration", - "smart.sort.bitrate": "Bitrate", - "smart.sort.random": "Random", - "nav.podcasts": "Podcasts", - "podcasts.detail": "Shows this server follows, for every account", - "podcasts.off": - "Podcasts are switched off on this server. What was kept before stays readable; nothing new is fetched.", - "podcasts.url": "Feed address", - "podcasts.shared": - "Visible to every account: anyone on this server sees the show and can play the episodes it downloads. Only administrators see the feed address.", - "podcasts.subscribe": "Subscribe", - "podcasts.refresh": "Refresh every feed", - "podcasts.refreshing": "The feeds are being fetched again.", - "podcasts.reload": "Reload", - "podcasts.empty": "No podcasts yet.", - "podcasts.emptyDetail": - "An administrator subscribes to a show’s feed here. The server downloads its episodes, and every account on this server can listen to them.", - "podcasts.addFirst": "Add your first podcast", - "podcasts.channels": "Shows", - "podcasts.showing": "Showing the episodes of {title}.", - "podcasts.noEpisodes": "This feed has no episodes.", - "podcasts.episodes": { - one: "{count} episode", - other: "{count} episodes", - }, - "podcasts.failed": "last fetch failed: {reason}", - "podcasts.open": "Episodes", - "podcasts.hide": "Hide", - "podcasts.unsubscribe": "Unsubscribe", - "podcasts.download": "Download", - "podcasts.play": "Play {title}", - "podcasts.pause": "Pause {title}", - "podcasts.resumeAt": "resumes at {time}", - "podcasts.status.new": "not downloaded", - "podcasts.status.downloading": "downloading", - "podcasts.status.completed": "ready", - "podcasts.status.error": "download failed", - "podcasts.status.deleted": "deleted", - "podcasts.conflict": "This server already follows that feed.", - "podcasts.refused": - "The feed could not be fetched or read, or its address is not one this server may reach.", - "podcasts.subscribeError": "The feed could not be added.", - "podcasts.mutationError": "That did not work. Try again.", - "podcasts.playbackError": "This episode could not be played.", -} as const; +import { en } from "./en"; +import { fr } from "./fr"; export type TranslationKey = keyof typeof en; -type TranslationValue = string | { one: string; other: string }; - -const fr: Record = { - "nav.albums": "Albums", - "nav.artists": "Artistes", - "nav.songs": "Titres", - "nav.search": "Recherche", - "nav.favourites": "Favoris", - "nav.playlists": "Playlists", - "nav.queue": "File d’attente", - "nav.shares": "Partages", - "nav.offline": "Hors ligne", - "nav.radio": "Radio", - "nav.scrobbling": "Scrobbling", - "nav.admin": "Administration", - "nav.signOut": "Se déconnecter", - "nav.skip": "Aller au contenu", - "nav.home": "WaveFlow serveur, accueil", - "nav.server": "serveur", - "nav.more": "Plus", - "nav.moreTitle": "Tout le reste", - "nav.moreDetail": - "La barre du bas porte ce qu’on attrape en écoutant. Le reste est ici.", - "nav.moreClose": "Fermer", - "preferences.theme": "Thème", - "preferences.language": "Langue", - "preferences.themeLight": "Clair", - "preferences.themeDark": "Sombre", - "preferences.followSystem": "Suivre le système", - "preferences.followSystemDetail": - "Passer de la version claire d’un thème à sa version sombre en même temps que votre appareil.", - "preferences.followingLight": - "Votre appareil est en mode clair : la version claire est affichée. Choisir un thème choisit ses deux versions.", - "preferences.followingDark": - "Votre appareil est en mode sombre : la version sombre est affichée. Choisir un thème choisit ses deux versions.", - "preferences.standInLight": - "{theme} n’a pas de version claire : {fallback} le remplace tant que votre appareil est en mode clair.", - "preferences.standInDark": - "{theme} n’a pas de version sombre : {fallback} le remplace tant que votre appareil est en mode sombre.", - "replayGain.label": "ReplayGain", - "replayGain.off": "Désactivé", - "replayGain.track": "Gain par piste", - "replayGain.album": "Gain par album", - "replayGain.preamp": "Préampli", - "replayGain.detail": - "Joue chaque piste au volume mesuré dans ses tags, pour qu’un disque ne sonne pas plus fort que le suivant. Le gain par album garde les écarts à l’intérieur d’un album. Une piste sans tags est jouée telle quelle. La lecture passe alors par Web Audio, que certains téléphones mettent en pause avec l’écran.", - "common.loading": "Chargement", - "common.loadError": "Impossible de charger cette vue", - "common.noAlbums": "Aucun album pour le moment.", - "common.noArtists": "Aucun artiste pour le moment.", - "common.variousArtists": "Artistes variés", - "common.unknownArtist": "Artiste inconnu", - "common.album": "Album", - "album.play": "Lire l’album", - "album.addQueue": "Ajouter à la file", - "album.title": "Titre", - "album.duration": "Durée", - "album.trackActions": "Actions sur les titres", - "album.moreBy": "Autres albums de {artist}", - "common.artist": "Artiste", - "common.albums": { one: "{count} album", other: "{count} albums" }, - "common.tracks": { one: "{count} piste", other: "{count} pistes" }, - "songs.loadedSoFar": { - one: "{count} piste chargée", - other: "{count} pistes chargées", - }, - "songs.playLoaded": { - one: "Lire la piste chargée", - other: "Lire les {count} pistes chargées", - }, - "songs.playAll": { - one: "Lire la piste", - other: "Lire les {count} pistes", - }, - "songs.loadMore": "Charger la suite", - "songs.loadMoreError": "Cette page n’a pas pu être chargée. Réessayez.", - "songs.loading": "Chargement…", - "songs.empty": "Cette bibliothèque ne contient encore aucune piste.", - "file.megabytes": "{size} Mo", - "file.kilobytes": "{size} ko", - "nav.settings": "Réglages", - "settings.detail": - "Apparence, langue, et les clients autorisés à lire votre bibliothèque.", - "settings.appearance": "Apparence", - "settings.playback": "Lecture", - "settings.sections": "Sections des réglages", - "settings.section.account": "Compte", - "settings.playbackDetail": - "Comment ce navigateur lit vos titres. Gardé dans ce navigateur, pas sur votre compte.", - "settings.levelling": "Normalisation du volume", - "clients.title": "Clients enregistrés", - "clients.detail": - "Appareils et scripts détenant un justificatif sur votre bibliothèque. En révoquer un l’arrête aussitôt.", - "clients.name": "À quoi il sert", - "clients.register": "Enregistrer", - "clients.once": - "Copiez-le maintenant. Seule son empreinte est conservée : il est montré une fois et jamais plus :", - "clients.revoke": "Révoquer", - "clients.none": - "Aucun client ne détient de justificatif sur votre bibliothèque.", - "clients.error": "Cela n’a pas fonctionné. Rien n’a changé.", - "clients.registered": "enregistré le {when}", - "clients.used": "dernier usage le {when}", - "clients.unused": "jamais utilisé", - "subsonic.title": "Mot de passe Subsonic", - "subsonic.detail": - "Le mot de passe distinct avec lequel les clients Subsonic se connectent. Le changer remplace aussi la clé d’API, donc une clé divulguée cesse de fonctionner.", - "subsonic.set": "défini le {when}", - "subsonic.changed": "dernier changement le {when}", - "subsonic.password": "Nouveau mot de passe", - "subsonic.rotate": "Le changer", - "subsonic.revoke": "Le désactiver", - "subsonic.confirm": "Oui, le désactiver", - "subsonic.once": - "Copiez cette clé d’API maintenant. Seule son empreinte est conservée : elle est montrée une fois et jamais plus :", - "subsonic.gone": - "Vos clients Subsonic cesseront de fonctionner jusqu’à ce que vous le redéfinissiez dans chacun d’eux.", - "subsonic.none": - "Vous n’avez pas de mot de passe Subsonic, donc aucun client Subsonic ne peut lire votre bibliothèque. Demandez-en un à l’administrateur.", - "subsonic.short": "Utilisez au moins 12 caractères.", - "subsonic.error": "Cela n’a pas fonctionné. Rien n’a changé.", - "quality.label": "Qualité du fichier", - "quality.unknown": "Inconnue", - "quality.low": "Faible", - "quality.standard": "Standard", - "quality.high": "Élevée", - "quality.lossless": "Sans perte", - "quality.hiRes": "Haute résolution", - "file.channels": { one: "mono", other: "{count} canaux" }, - "common.delete": "Supprimer", - "common.cancel": "Annuler", - "correction.edit": "Corriger les tags", - "correction.editTrack": "Corriger les tags : {title}", - "trackMenu.label": "Actions pour {title}", - "trackMenu.play": "Lire", - "trackMenu.queue": "Ajouter à la file", - "trackMenu.download": "Télécharger", - "trackMenu.keepOffline": "Garder hors ligne", - "trackMenu.forgetOffline": "Retirer la copie hors ligne", - "trackMenu.downloadFailed": - "Le téléchargement n’a pas pu démarrer. Réessayez.", - "trackMenu.dismiss": "Fermer", - "trackMenu.goToAlbum": "Aller à l’album", - "trackMenu.goToArtist": "Aller à {name}", - "trackMenu.properties": "Propriétés", - "trackMenu.edit": "Corriger les tags", - "properties.kicker": "Propriétés", - "properties.close": "Fermer les propriétés", - "properties.loadError": "Certaines propriétés n’ont pas pu être chargées.", - "properties.file": "Fichier", - "properties.tags": "Tags", - "properties.sound": "Volume et tempo", - "properties.lyrics": "Paroles", - "properties.path": "Chemin dans la bibliothèque", - "properties.format": "Format", - "properties.bitrate": "Débit", - "properties.sampleRate": "Fréquence d’échantillonnage", - "properties.bitDepth": "Profondeur", - "properties.channels": "Canaux", - "properties.size": "Taille", - "properties.duration": "Durée", - "properties.added": "Ajouté le", - "properties.hash": "Empreinte du contenu", - "properties.title": "Titre", - "properties.artists": "Artistes", - "properties.album": "Album", - "properties.albumArtist": "Artiste de l’album", - "properties.year": "Année", - "properties.trackNumber": "Piste", - "properties.discNumber": "Disque", - "properties.genres": "Genres", - "properties.comment": "Commentaire", - "properties.isrc": "ISRC", - "properties.musicbrainz": "Enregistrement MusicBrainz", - "properties.trackGain": "ReplayGain (piste)", - "properties.albumGain": "ReplayGain (album)", - "properties.bpm": "BPM", - "properties.lyricsSheet": "Feuille {number}", - "properties.synced": "Synchronisées, {lang}", - "properties.plain": "Texte brut, {lang}", - "properties.noLyrics": "Aucune", - "lyrics.source.uploadTtml": "TTML envoyé", - "lyrics.source.uploadLrc": "LRC envoyé", - "lyrics.source.uploadTxt": "Texte envoyé", - "lyrics.source.sidecarLrc": "Fichier LRC à côté du titre", - "lyrics.source.sidecarText": "Fichier texte à côté du titre", - "lyrics.source.embedded": "Intégrées au fichier", - "lyrics.shown": "Affichées", - "lyrics.download": "Télécharger les paroles", - "lyrics.upload": "Envoyer des paroles…", - "lyrics.remove": "Retirer le {format}", - "lyrics.unsupported": "Choisissez un fichier .lrc, .txt ou .ttml.", - "lyrics.empty": "Ce fichier ne contient pas de paroles.", - "lyrics.tooLarge": "Ce fichier dépasse 1 Mo.", - "lyrics.failed": "Les paroles n’ont pas pu être enregistrées. Réessayez.", - "lyrics.downloadFailed": - "Les paroles n’ont pas pu être téléchargées. Réessayez.", - "properties.lyricsSheets": "Feuilles", - "properties.kbps": "{value} kbit/s", - "properties.khz": "{value} kHz", - "properties.bits": "{value} bits", - "properties.mb": "{value} Mo", - "properties.db": "{value} dB", - "properties.dbPeak": "{value} dB, crête {peak}", - "properties.corrected": "Corrigé", - "properties.fileSaid": "Corrigé — le fichier indique {value}", - "correction.detail": - "Le fichier n’est jamais réécrit. La correction est rangée à côté, survit à un rescan, et tous les membres de la bibliothèque la voient.", - "correction.fieldTitle": "Titre", - "correction.fieldSortTitle": "Titre de tri", - "correction.fieldYear": "Année", - "correction.fieldTrackNumber": "Numéro de piste", - "correction.fieldDiscNumber": "Numéro de disque", - "correction.fieldMusicbrainz": "Identifiant d’enregistrement MusicBrainz", - "correction.fieldComment": "Commentaire", - "correction.fieldArtists": "Artistes", - "correction.fieldGenres": "Genres", - "correction.listHint": - "Un nom par ligne. Laissez vide pour ne créditer personne.", - "correction.corrected": "Corrigé", - "correction.fromFile": "Le fichier indique : {value}", - "correction.fileEmpty": "Le fichier n’indique rien ici", - "correction.restore": "Rétablir la valeur du fichier", - "correction.keep": "Garder la correction", - "correction.restoring": "Reprend la valeur du fichier à l’enregistrement", - "correction.restoringList": - "Relu depuis le fichier à l’enregistrement — impossible de l’afficher avant", - "correction.save": "Enregistrer", - "correction.saving": "Enregistrement…", - "correction.saved": "Enregistré.", - "correction.nothing": "Rien n’a changé.", - "correction.invalid": "Vérifiez : {fields}.", - "correction.refused": "Le serveur a refusé ces valeurs.", - "correction.notAllowed": - "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut corriger ses tags.", - "correction.error": "Les corrections n’ont pas pu être enregistrées.", - "correction.stale": - "Enregistré, mais les corrections n’ont pas pu être relues. Rechargez la page avant d’enregistrer à nouveau.", - "correction.back": "Retour à {album}", - "canvas.title": "Canvas", - "canvas.detail": - "Une courte boucle jouée sans le son par-dessus la pochette, sur la page En écoute. Le serveur prend le mp4 ou le webm.", - "canvas.loading": "Recherche d’un canvas…", - "canvas.none": "Cette piste n’a pas de canvas.", - "canvas.unreadable": "Impossible de savoir si cette piste a un canvas.", - "canvas.preview": "Aperçu du canvas", - "canvas.choose": "Choisir une boucle", - "canvas.replace": "Remplacer la boucle", - "canvas.remove": "Retirer le canvas", - "canvas.closed": - "Cette bibliothèque n’accepte pas de canvas. C’est son opérateur qui en décide, sur le serveur.", - "canvas.saving": "Envoi…", - "canvas.placed": "Canvas enregistré.", - "canvas.removed": "Canvas retiré.", - "canvas.refused": - "Le serveur a refusé ce fichier : il prend une courte boucle mp4 ou webm, avec une image.", - "canvas.tooLarge": - "Ce fichier dépasse la taille que le serveur accepte pour un canvas.", - "canvas.quota": "Cette bibliothèque n’a plus de place pour les canvas.", - "canvas.notAllowed": - "Le serveur n’a pas modifié ce canvas : la piste ou sa boucle a disparu, ou le compte n’en a plus le droit.", - "canvas.error": "Le canvas n’a pas pu être modifié.", - "login.heroEyebrow": "Votre musique, selon vos règles", - "login.heroTitle": "La nuit tombe, la bibliothèque s’éveille.", - "login.heroDetail": "Streaming privé, un catalogue, tous vos écrans.", - "login.server": "Serveur WaveFlow", - "login.createAdmin": "Créer le compte administrateur", - "login.welcome": "Bon retour", - "login.setupDetail": - "Ce serveur est neuf. Choisissez son premier compte administrateur.", - "login.setupToken": "Jeton d’installation", - "login.setupTokenHint": - "Le serveur l’a écrit dans le fichier setup-token de son répertoire de données, dont son journal donne le chemin. Si vous avez défini WAVEFLOW_SETUP_TOKEN, saisissez cette valeur.", - "login.setupTokenError": - "Ce jeton d’installation n’est pas le bon. Copiez-le de nouveau depuis le fichier setup-token.", - "login.username": "Nom d’utilisateur", - "login.password": "Mot de passe", - "login.setupError": - "Configuration impossible. Utilisez un nom valide et un mot de passe d’au moins 12 caractères.", - "login.error": "Nom d’utilisateur ou mot de passe incorrect.", - "login.wait": "Veuillez patienter…", - "login.create": "Créer et se connecter", - "login.signIn": "Se connecter", - "albums.detail": { - one: "{count} disque dans votre bibliothèque", - other: "{count} disques dans votre bibliothèque", - }, - "albums.gridTitle": "Dans votre bibliothèque", - "artists.detail": { - one: "{count} voix ou collaboration", - other: "{count} voix et collaborations", - }, - "artists.libraryCount": { - one: "{count} album dans votre bibliothèque", - other: "{count} albums dans votre bibliothèque", - }, - "artists.mostPlayed": "Les plus écoutés ici", - "artists.section.album": "Albums", - "artists.section.ep": "EP", - "artists.section.single": "Singles", - "artists.section.live": "Live", - "artists.section.compilation": "Compilations", - "artists.section.appearsOn": "Apparaît sur", - "artists.about": "À propos", - "artists.readMore": "Lire la suite", - "artists.similar": "Artistes proches dans votre bibliothèque", - "artists.enrichmentPending": "Recherche d’informations sur cet artiste…", - "artists.bioSource": "Biographie fournie par {provider}, le {date}.", - "artists.similarSource": "Suggérés par {provider}, le {date}.", - "artists.portraitSource": "Portrait fourni par {provider}, le {date}.", - "artists.bannerSource": "Bannière fournie par {provider}, le {date}.", - "favourites.detail": { - one: "{count} piste enregistrée", - other: "{count} pistes enregistrées", - }, - "favourites.empty": - "Ajoutez une piste aux favoris depuis un album ou la recherche pour la retrouver ici.", - "favourites.add": "Ajouter aux favoris", - "favourites.remove": "Retirer des favoris", - "playlists.detail": { - one: "{count} collection", - other: "{count} collections", - }, - "playlists.newName": "Nom de la nouvelle playlist", - "playlists.create": "Créer la playlist", - "playlists.play": "Lire", - "playlists.addQueue": "Ajouter la file", - "playlists.emptyOne": "Cette playlist est vide.", - "playlists.export": "Enregistrer en .m3u8", - "playlists.exportError": "Cette playlist n’a pas pu être enregistrée.", - "import.title": "Importer des playlists d’un autre lecteur", - "import.detail": - "Un fichier .m3u ou .m3u8. Rien n’est créé avant que vous ayez vu ce qui serait perdu.", - "import.pick": "Choisir un fichier de playlist", - "import.name": "Nom de la playlist", - "import.found": - "{resolved} entrées sur {entries} sont dans votre bibliothèque.", - "import.encoding": - "Le fichier n’était pas en UTF-8 et a été lu en {encoding}. Vérifiez les noms ci-dessous avant d’importer.", - "import.missing": { - one: "{count} entrée ne peut pas être placée :", - other: "{count} entrées ne peuvent pas être placées :", - }, - "import.line": "Ligne {line}", - "import.noMatch": "aucune piste correspondante", - "import.ambiguous": "correspond à plusieurs pistes", - "import.notAPath": "n’est pas un fichier d’une bibliothèque", - "import.duplicate": "déjà dans cette playlist", - "import.confirm": "Importer", - "import.cancel": "Annuler", - "import.failed": "Ce fichier n’a pas pu être lu comme une playlist.", - "playlists.empty": "Créez une playlist pour réunir plusieurs pistes.", - "playlists.createError": "La playlist n’a pas pu être créée.", - "playlists.queueError": "La file n’a pas pu être ajoutée à cette playlist.", - "playlists.deleteError": "La playlist n’a pas pu être supprimée.", - "queue.detail": { - one: "{count} piste synchronisée avec votre compte", - other: "{count} pistes synchronisées avec votre compte", - }, - "queue.playStart": "Lire depuis le début", - "queue.clear": "Vider la file", - "queue.remove": "Retirer", - "queue.empty": "Lisez un album ou une playlist pour construire votre file.", - "shares.detail": "Liens publics vers votre musique", - "shares.description": "Description", - "shares.create": "Partager la file actuelle", - "shares.needQueue": "Ajoutez des pistes à la file avant de créer un lien.", - "shares.once": "Ce lien ne sera affiché qu’une fois :", - "shares.defaultName": "Partage musical", - "shares.empty": "Aucun lien public n’a été créé.", - "shares.createError": "Le partage n’a pas pu être créé.", - "shares.tooLong": - "Un partage contient au plus {limit} titres, et la file en contient {count}. Raccourcissez la file pour la partager.", - "shares.deleteError": "Le partage n’a pas pu être supprimé.", - "shares.maxVisits": "Limite de visites (facultative)", - "shares.visits": { one: "{count} visite", other: "{count} visites" }, - "shares.visitsOf": "{count} visites sur {max}", - "share.kicker": "Partagé avec vous", - "share.defaultTitle": "Musique partagée avec vous", - "share.until": "Disponible jusqu’au {date}", - "share.empty": "Aucune de ces pistes ne peut plus être lue.", - "share.player": "Lecteur", - "share.unavailableTitle": "Ce lien n’ouvre plus rien", - "share.unavailableDetail": - "Il a peut-être expiré, été retiré, ou été ouvert autant de fois qu’il le permet.", - "playlists.public": "Visible par tous les comptes", - "playlists.publicError": "Cette playlist n’a pas pu être publiée.", - "playlists.others": "D’autres comptes", - "playlists.othersDetail": - "Les playlists publiques des autres comptes, avec les pistes que vos bibliothèques contiennent.", - "playlists.by": "Par {owner}", - "playlists.moveUp": "Monter {title}", - "playlists.moveDown": "Descendre {title}", - "playlists.removeEntry": "Retirer {title} de cette playlist", - "playlists.moveError": - "Cette entrée n’a pas pu être déplacée. La playlist a peut-être changé ailleurs ; elle a été relue.", - "playlists.removeEntryError": "Cette entrée n’a pas pu être retirée.", - "radio.detail": "Des stations en ligne, attachées à votre compte", - "radio.name": "Nom de la station", - "radio.address": "Adresse du flux", - "radio.homepage": "Page de la station (facultatif)", - "radio.image": "Image de la station (facultatif)", - "radio.imageHint": - "Le serveur la télécharge une fois, quand vous l’enregistrez, et la garde avec les pochettes : votre navigateur ne contacte jamais l’hôte de l’image.", - "radio.imageMissing": - "L’image n’a pas pu être récupérée ; enregistrez la station pour réessayer.", - "radio.add": "Ajouter la station", - "radio.save": "Enregistrer la station", - "radio.edit": "Modifier", - "radio.replacing": - "Enregistrer remplace tous les champs : une page vide efface celle qui était retenue.", - "radio.listen": "Écouter", - "radio.stop": "Arrêter", - "radio.visit": "Page de la station", - "radio.empty": "Aucune station. Ajoutez-en une pour la retrouver ici.", - "radio.addressRefused": "Une adresse doit commencer par http:// ou https://.", - "radio.mixedContent": - "Cette page est servie en HTTPS : votre navigateur refusera un flux en clair.", - "radio.full": "Ce compte retient déjà le maximum de stations.", - "radio.saveError": "La station n’a pas pu être enregistrée.", - "radio.deleteError": "La station n’a pas pu être supprimée.", - "radio.playbackError": - "Cette station n’a pas pu être lue. Le serveur ne s’y connecte jamais : cela se passe entre votre navigateur et la station.", - "radio.stations": "Stations", - "radio.newTitle": "Ajouter une station", - "radio.editTitle": "Modifier « {name} »", - "radio.listenTo": "Écouter {name}", - "radio.stopNamed": "Arrêter {name}", - "radio.actions": "Actions pour {name}", - "radio.confirmDelete": - "Supprimer la station « {name} » ? Son adresse et son image sont oubliées.", - "credential.password": "Mot de passe Subsonic dédié", - "credential.rotate": "Renouveler l’identifiant", - "credential.copy": "Copiez cette clé API maintenant :", - "credential.error": "Le renouvellement de l’identifiant a échoué.", - "admin.title": "Administration", - "admin.detail": "Bibliothèques, scans et comptes", - "admin.libraries": "Bibliothèques", - "admin.libraryName": "Nom de la bibliothèque", - "admin.libraryPath": "Chemin absolu du dossier sur le serveur", - "admin.registering": "Ajout en cours…", - "admin.register": "Ajouter et scanner", - "admin.scan": "Scanner", - "admin.accounts": "Comptes", - "admin.webPassword": "Mot de passe web, 12 caractères minimum", - "admin.creating": "Création…", - "admin.create": "Créer le compte", - "admin.enable": "Activer", - "admin.disable": "Désactiver", - "admin.currentAccount": "Vous ne pouvez pas désactiver votre compte actuel", - "admin.libraryError": "Le dossier n’a pas pu être enregistré.", - "admin.accountError": "Le compte n’a pas pu être créé.", - "admin.statusError": "Le statut du compte n’a pas pu être modifié.", - "admin.scanError": "Le scan n’a pas pu démarrer.", - "admin.scanQueued": "Scan {id} ajouté à la file.", - "admin.initialScan": "Scan initial {id} ajouté à la file.", - "search.detail": "Trouvez un titre, un album ou un artiste", - "search.placeholder": "Titre, album ou artiste", - "search.query": "Requête de recherche", - "search.tracks": "Pistes", - "search.empty": "Aucun résultat.", - "player.error": "Lecture impossible. Vérifiez le serveur puis réessayez.", - "player.previous": "Piste précédente", - "player.play": "Lire", - "player.pause": "Pause", - "player.next": "Piste suivante", - "player.seek": "Position de lecture", - "authorize.title": "Autorisation", - "authorize.invalid": - "Ce lien d’autorisation est incomplet ou mène vers une destination non fiable.", - "authorize.client": "Autoriser {client}", - "authorize.detail": - "Cette application pourra parcourir vos bibliothèques, lire votre musique et gérer vos playlists, favoris et notes, comme ce compte.", - "authorize.redirect": "Retour vers {url}", - "authorize.error": "La demande d’autorisation de l’application est invalide.", - "authorize.busy": "Autorisation…", - "authorize.approve": "Autoriser", - "notFound.title": "Cette salle est silencieuse", - "notFound.detail": "La page demandée n’existe pas sur ce serveur.", - "notFound.back": "Retour à la bibliothèque", - "appearance.themes.default": "Émeraude clair", - "appearance.themes.midnightLight": "Minuit clair", - "appearance.themes.sunsetLight": "Coucher de soleil clair", - "appearance.themes.lavenderLight": "Lavande clair", - "appearance.themes.crimsonLight": "Cramoisi clair", - "appearance.themes.oceanLight": "Océan clair", - "appearance.themes.defaultDark": "Émeraude sombre", - "appearance.themes.oled": "Noir OLED", - "appearance.themes.midnight": "Minuit", - "appearance.themes.sunset": "Coucher de soleil", - "appearance.themes.lavender": "Lavande", - "appearance.themes.crimson": "Cramoisi", - "appearance.themes.ocean": "Océan", - "appearance.themes.neon": "Néon", - "browse.sort": "Trier", - "browse.sortAlphabeticalByName": "Par titre", - "browse.sortAlphabeticalByArtist": "Par artiste", - "browse.sortAlphabeticalByAlbum": "Par album", - "browse.sortNewest": "Ajoutés récemment", - "browse.sortRecent": "Écoutés récemment", - "browse.sortFrequent": "Les plus écoutés", - "browse.sortStarred": "Favoris", - "browse.sortByYear": "Par année", - "browse.filterAlbums": "Filtrer par titre ou artiste", - "browse.filterArtists": "Filtrer par nom", - "browse.filterPlaceholder": "Filtrer…", - "browse.matches": { - one: "{count} sur {total} affiché", - other: "{count} sur {total} affichés", - }, - "browse.noMatch": "Aucun résultat pour ce filtre.", - "card.play": "Lire", - "card.queue": "Mettre en file", - "rating.label": "Note", - "rating.stars": { one: "{count} étoile", other: "{count} étoiles" }, - "nav.genres": "Genres", - "nav.history": "Écoutes récentes", - "nav.random": "Aléatoire", - "nav.playing": "En écoute", - "nav.upload": "Téléverser", - "nav.folders": "Dossiers", - "nav.attention": "À corriger", - "nav.quality": "Qualité", - "attention.lists": "Listes", - "attention.total": { - one: "{count} point à vérifier", - other: "{count} points à vérifier", - }, - "attention.nothing": "Rien à corriger.", - "attention.notYours": - "Seuls les propriétaires et les gestionnaires de cette bibliothèque voient ce qui y est à corriger.", - "attention.empty": "Rien dans cette liste.", - "attention.tracksNote": - "Corrigez une piste depuis son menu : une fois corrigée, elle quitte la liste.", - "attention.creditsNote": - "Chacun de ces crédits nomme plusieurs artistes dans une seule chaîne. Scindez-le en ces artistes, ou gardez-le tel quel s’il s’agit d’un seul nom. Chaque scan suivant applique la décision.", - "attention.pending": "À décider", - "attention.decided": "Décidés", - "attention.split": "Scinder", - "attention.confirmSplit": "Confirmer : scinder en {count}", - "attention.keep": "Garder tel quel", - "attention.splitDone": "{name} est scindé.", - "attention.keepDone": "{name} reste tel quel.", - "attention.undone": "La décision sur {name} est annulée.", - "attention.alreadyDecided": - "Ce crédit a été décidé entre-temps. La liste a été rechargée.", - "attention.failed": "Cela n’a pas fonctionné. Réessayez.", - "attention.noCredits": "Aucun crédit n’attend de décision.", - "attention.noDecisions": "Aucune décision pour l’instant.", - "attention.fragments": "Ce en quoi {name} serait scindé", - "attention.knownFragment": "Déjà un artiste de cette bibliothèque", - "attention.decidedSplit": "Scindé en {parts}", - "attention.decidedKeep": "Gardé tel quel", - "attention.decidedBy": "Par {who}, le {when}", - "attention.undo": "Annuler", - "attention.duplicateGroup": "Doublon probable {group}", - "attention.split_credits": "Artistes à scinder", - "attention.missing_artist": "Sans artiste", - "attention.missing_album": "Sans album", - "attention.missing_year": "Sans année", - "attention.missing_track_number": "Sans numéro de piste", - "attention.missing_cover": "Sans pochette", - "attention.album_year_conflict": "Albums à plusieurs années", - "attention.compilation_no_album_artist": "Compilations sans artiste d’album", - "attention.duplicate_track_number": "Numéros de piste en double", - "attention.track_number_gap": "Trous dans la numérotation", - "attention.probable_duplicate": "Doublons probables", - "attention.probable_fake_lossless": "Faux lossless probables", - "quality.help": - "Mesuré dans le son plutôt que lu dans le conteneur : où s’arrêtent les aigus, et combien de bits les échantillons utilisent. Un fichier sans perte dont les aigus s’arrêtent vers 16 kHz était un MP3 à 128 kb/s avant d’être un FLAC. « Aucun signe de source avec perte » n’est pas une preuve : un encodeur qui garde toute la bande ne laisse rien à trouver.", - "quality.progress": "{measured} fichiers sans perte analysés sur {lossless}", - "quality.disabled": "L’analyse est désactivée sur ce serveur.", - "quality.noLossless": "Aucun fichier sans perte dans cette bibliothèque.", - "quality.status.running": - "Analyse en cours : {measured} sur {lossless} terminés, {inFlight} en traitement.", - "quality.status.waitingForScan": - "En attente de la fin du scan avant d’analyser.", - "quality.status.queued": - "En file d’attente : l’analyse arrivera bientôt à ces fichiers.", - "quality.status.idle": "Au repos. Dernier fichier analysé le {when}.", - "quality.status.idleNever": "Au repos. Aucun fichier n’a encore été analysé.", - "quality.filters": "Verdicts", - "quality.all": "Tous les fichiers", - "quality.format": "Format", - "quality.allFormats": "Tous les formats", - "quality.empty": "Aucun fichier avec ce verdict.", - "quality.column.track": "Morceau", - "quality.column.container": "Ce que dit le fichier", - "quality.column.highs": "Les aigus s’arrêtent à", - "quality.column.verdict": "Ce que dit le son", - "quality.fullBand": "Bande complète", - "quality.khz": "{value} kHz", - "quality.db": "{value} dB", - "quality.analyseAgain": "Analyser à nouveau", - "quality.confirmAnalyse": "Confirmer : réanalyser toute la bibliothèque", - "quality.requested": "La bibliothèque sera réanalysée en arrière-plan.", - "quality.requestFailed": "Ça n’a pas marché. Réessayez.", - "quality.section": "Qualité réelle", - "quality.verdictLabel": "Verdict", - "quality.highsStop": "Les aigus s’arrêtent à", - "quality.wall": "Chute à cet endroit", - "quality.bitsUsed": "Bits utilisés, mesurés", - "quality.bitsOf": "{used} sur {declared}", - "quality.bitsNote": - "Une mesure des échantillons, pas une preuve de vraie haute résolution : un fichier qui utilise tous les bits qu’il annonce n’a pas été gonflé, ce qui ne veut pas dire qu’il a été enregistré à cette profondeur.", - "quality.analysed": "Analysé le", - "quality.chart": "Spectre moyen : les aigus s’arrêtent à {where}.", - "quality.chartFullBand": "Spectre moyen : les aigus continuent sans mur.", - "quality.showSpectrogram": "Afficher le spectrogramme", - "quality.drawing": "Dessin en cours…", - "quality.spectrogramAlt": - "Spectrogramme de {title}, fréquence en fonction du temps", - "quality.spectrogramBusy": - "Un spectrogramme est déjà en cours de dessin pour vous. Réessayez dans un instant.", - "quality.spectrogramFailed": "Le spectrogramme n’a pas pu être dessiné.", - "quality.verdict.lossy_source": "Probablement issu d’un fichier avec perte", - "quality.verdict.upsampled": "Probablement suréchantillonné", - "quality.verdict.padded": "Profondeur gonflée", - "quality.verdict.unreadable": "Illisible", - "quality.verdict.silent": "Silencieux", - "quality.verdict.pending": "En attente d’analyse", - "quality.verdict.consistent": "Aucun signe de source avec perte", - "quality.verdict.lossy": "Avec perte", - "quality.verdict.not_measured": "Non mesuré", - "quality.explain.lossy_source": - "Les aigus s’arrêtent net là où coupe un encodeur avec perte : ce fichier a probablement été décodé depuis un MP3, un AAC ou un Opus, puis enregistré sans perte.", - "quality.explain.upsampled": - "La fréquence dépasse 48 kHz, mais les aigus s’arrêtent là où finit un CD ou un enregistrement à 48 kHz : il a probablement été rééchantillonné depuis l’un d’eux.", - "quality.explain.padded": - "Le conteneur annonce plus de bits que les échantillons n’en utilisent : un enregistrement 16 bits rangé en 24 bits, par exemple.", - "quality.explain.unreadable": - "FFmpeg n’a pas pu décoder ce fichier. Il sera réessayé quand le fichier changera.", - "quality.explain.silent": - "Rien dans ce fichier n’est assez fort pour être mesuré.", - "quality.explain.pending": - "Ce fichier n’a pas encore été analysé. L’analyse tourne en arrière-plan.", - "quality.explain.consistent": - "Rien dans le son ne contredit le conteneur. Ce n’est pas une preuve : un encodeur qui garde toute la bande ne laisse rien à trouver.", - "quality.explain.lossy": - "Un fichier avec perte dit ce qu’il est ; sa qualité est celle de son encodeur et de son débit. Il n’est pas analysé.", - "quality.explain.not_measured": "Ce format n’est pas analysé.", - "artistSplit.offer": "Scinder en {count} artistes", - "artistSplit.confirm": "Confirmer : scinder en {count}", - "artistSplit.done": "Scindé en {parts}.", - "artistSplit.failed": "Ce crédit n’a pas pu être scindé. Réessayez.", - "common.back": "Retour", - "artistImage.changePortrait": "Changer le portrait", - "artistImage.changeBanner": "Changer la bannière", - "artistImage.resetPortrait": "Portrait automatique", - "artistImage.resetBanner": "Sans bannière", - "artistImage.portraitChanged": "Portrait changé.", - "artistImage.bannerChanged": "Bannière changée.", - "artistImage.portraitReverted": "Retour au portrait automatique.", - "artistImage.bannerReverted": "Bannière retirée.", - "artistImage.tooLarge": "Cette image dépasse 10 Mo.", - "artistImage.notAnImage": "Choisissez une image JPEG, PNG ou WebP.", - "artistImage.failed": "Cela n’a pas fonctionné. Réessayez.", - "artistImage.chosen": "Portrait choisi à la main", - "artistImage.searchDeezer": "Rechercher sur Deezer", - "artistImage.menu": "Modifier les images", - "artistImage.menuShort": "Modifier", - "artistImage.menuLabel": "Images de {name}", - "artistImage.deezerTitle": "Choisir un portrait sur Deezer", - "artistImage.deezerQuery": "Nom de l’artiste", - "artistImage.deezerSearch": "Rechercher", - "artistImage.deezerSearching": "Recherche sur Deezer…", - "artistImage.deezerResults": "Artistes Deezer", - "artistImage.deezerNone": "Deezer ne connaît aucun artiste de ce nom.", - "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, - "artistImage.deezerUnavailable": - "La recherche Deezer n’est pas activée sur ce serveur.", - "artistImage.deezerBusy": - "Votre recherche précédente est encore en cours. Réessayez dans un instant.", - "artistImage.deezerExpired": - "Ces résultats ont expiré. Relancez la recherche.", - "folders.count": { - one: "{count} dossier créé dans cette bibliothèque", - other: "{count} dossiers créés dans cette bibliothèque", - }, - "folders.where": "Chemin du dossier", - "folders.subfolders": "Dossiers", - "folders.noLibrary": "Aucune bibliothèque n’est accessible à ce compte.", - "folders.none": "Aucun dossier n’a encore été créé dans cette bibliothèque.", - "folders.system": "Tenu par WaveFlow", - "folders.systemNote": - "À ranger réunit toutes les pistes qui ne sont dans aucun dossier. WaveFlow le tient lui-même : il n’est pas compté parmi les dossiers ci-dessus, et il ne peut être ni renommé, ni déplacé, ni supprimé.", - "filed.unfiled": "À ranger", - "filed.automatic": "Automatique", - "filed.topLevel": "Premier niveau", - "filed.here": "Ici actuellement", - "filed.destinations": "Dossiers au choix", - "filed.loadError": "Les dossiers n’ont pas pu être chargés.", - "filed.moveFailed": "Le déplacement a échoué. Réessayez.", - "filed.moveTrack": "Déplacer « {title} » vers un dossier", - "filed.moveFolder": "Déplacer « {name} »", - "filed.note": - "Des dossiers qui n’existent que dans WaveFlow. Classer une piste ici ne déplace aucun fichier.", - "filed.newFolder": "Nouveau dossier", - "filed.name": "Nom du dossier", - "filed.renameLabel": "Nouveau nom", - "filed.create": "Créer", - "filed.save": "Enregistrer", - "filed.cancel": "Annuler", - "filed.rename": "Renommer", - "filed.move": "Déplacer…", - "filed.delete": "Supprimer", - "filed.confirmDelete": - "Supprimer « {name} » et ses sous-dossiers ? Leurs pistes reviennent dans À ranger ; aucun fichier n’est touché.", - "filed.created": "« {name} » créé.", - "filed.renamed": "Renommé en « {name} ».", - "filed.deleted": { - one: "Dossier supprimé : {count} piste revient dans À ranger.", - other: "Dossier supprimé : {count} pistes reviennent dans À ranger.", - }, - "filed.filed": { - one: "{count} piste classée dans {folder}.", - other: "{count} pistes classées dans {folder}.", - }, - "filed.folderMoved": "« {name} » déplacé.", - "filed.nameTaken": "Un dossier de ce nom existe déjà ici.", - "filed.refused": - "Ce dossier ne peut pas aller là : vérifiez son nom, ou l’arborescence est trop profonde ou pleine.", - "filed.failed": "Cela n’a pas fonctionné. Réessayez.", - "filed.missing": "Ce dossier n’existe plus.", - "filed.empty": "Rien n’est encore classé ici.", - "filed.allFiled": "Toutes les pistes sont classées.", - "filed.uploadHere": "Envoyer ici", - "filed.unfiledNote": - "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée, et elle ne peut être ni renommée ni supprimée. Pour ranger une piste, ouvrez son menu et choisissez « Déplacer vers un dossier… ».", - "filed.unfiledNoteReader": - "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée.", - "trackMenu.move": "Déplacer vers un dossier…", - "upload.into": "Les fichiers reçus vont dans le dossier {folder}.", - "upload.doneUnfiled": "Ajouté à la bibliothèque, dans À ranger", - "upload.folderMissing": - "Le dossier pour lequel cet envoi a été ouvert est introuvable : les fichiers reçus iront dans À ranger.", - "upload.folderUnchecked": - "Le dossier pour lequel cet envoi a été ouvert n’a pas pu être vérifié : aucun fichier n’est pris pour l’instant.", - "upload.checkAgain": "Vérifier à nouveau", - "nav.group.library": "Bibliothèque", - "nav.group.listen": "Écoute", - "upload.title": "Téléverser dans {library}", - "upload.detail": - "Chaque fichier est d’abord identifié dans votre navigateur : un fichier que la bibliothèque possède déjà n’est jamais renvoyé, et un transfert interrompu reprend là où il s’était arrêté.", - "upload.pick": "Choisir des fichiers audio", - "upload.formats": "Formats que la bibliothèque sait indexer : {extensions}", - "upload.noLibrary": "Choisissez d’abord une bibliothèque.", - "upload.notAllowed": - "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut y ajouter des fichiers.", - "upload.closed": - "Cette bibliothèque n’accepte pas de fichiers. C’est son opérateur qui en décide, sur le serveur.", - "upload.waiting": "En attente", - "upload.hashing": "Identification…", - "upload.negotiating": "Question au serveur…", - "upload.uploading": "Envoi…", - "upload.committing": "Ajout à la bibliothèque…", - "upload.done": "Ajouté à la bibliothèque", - "upload.present": "Déjà dans la bibliothèque", - "upload.refused": "Refusé : {reason}", - "upload.failed": "Échec : {reason}", - "upload.progress": "Progression : {name}", - "upload.reason.unsupportedFormat": - "format audio que la bibliothèque ne sait pas indexer", - "upload.reason.tooLarge": "plus gros que ce que le serveur accepte", - "upload.reason.quotaExceeded": "la bibliothèque n’a plus de place", - "upload.reason.libraryClosed": "la bibliothèque n’accepte pas de fichiers", - "upload.reason.tooManySessions": - "trop de transferts déjà ouverts — réessayez dans un instant", - "upload.reason.refusedFile": - "ce qui est arrivé ne correspond pas au fichier, ou n’est pas un audio que la bibliothèque sait lire", - "upload.reason.unavailable": - "le serveur n’a pas pu le prendre pour l’instant — réessayez", - "upload.reason.network": "la connexion a été perdue", - "upload.reason.notAllowed": - "vous ne pouvez plus ajouter de fichiers à cette bibliothèque", - "upload.reason.error": "quelque chose s’est mal passé", - "upload.reason.hashing": "ce navigateur n’a pas pu identifier le fichier", - "browse.filterGenres": "Filtrer par genre", - "genres.detail": { one: "{count} genre", other: "{count} genres" }, - "genres.empty": "Aucun genre n’est encore étiqueté dans cette bibliothèque.", - "genres.emptyOne": "Plus rien ne porte ce genre.", - "history.detail": { - one: "{count} piste, la plus récente d’abord", - other: "{count} pistes, la plus récente d’abord", - }, - "history.empty": "Rien n’a encore été écouté depuis ce compte.", - "random.detail": { - one: "{count} piste tirée", - other: "{count} pistes tirées", - }, - "random.again": "Tirer à nouveau", - "random.fromGenre": "Lire ce genre au hasard", - "random.empty": "Il n’y a encore rien à tirer.", - "lyrics.label": "Paroles", - "lyrics.none": "Aucune parole ne voyage avec ce fichier.", - "playing.idle": "Rien n’est en cours de lecture.", - "playing.empty": "Lancez une piste et elle apparaîtra ici, avec ses paroles.", - "lyrics.seekHint": - "Cette feuille porte des minutages — appuyez sur une ligne pour y sauter.", - "lyrics.seekTo": "Sauter à {when}", - "lyrics.seekLine": "{line}, sauter à {when}", - "playing.viewLabel": "Comment regarder cette piste", - "playing.viewDetail": "Détail", - "playing.viewLyrics": "Paroles", - "playing.viewImmersive": "Immersif", - "playing.immersiveTitle": "Vue immersive", - "playing.leaveImmersive": "Quitter la vue immersive", - "bookmarks.title": "Positions enregistrées", - "bookmarks.detail": "Une par piste. Enregistrer à nouveau la déplace.", - "bookmarks.save": "Enregistrer cette position", - "bookmarks.forget": "Oublier", - "player.shuffle": "Lecture aléatoire", - "player.repeatOff": "Répétition : aucune", - "player.repeatAll": "Répétition : toute la file", - "player.repeatOne": "Répétition : cette piste", - "player.volume": "Volume", - "player.mute": "Couper le son", - "player.unmute": "Rétablir le son", - "player.openAlbum": "Ouvrir l’album", - "player.live": "Direct", - "player.liveRadio": "Radio en direct", - "player.stopLive": "Arrêter la station", - "player.liveError": "Cette station n’a pas pu être lue.", - "player.closeAside": "Arrêter et fermer", - "library.active": "Bibliothèque", - "search.thisLibrary": "Recherche dans {library}.", - "scan.connecting": "En attente du premier relevé…", - "scan.queued": "En attente", - "scan.running": "Analyse en cours", - "scan.completed": "Terminé", - "scan.failed": "Échec", - "scan.progress": "Progression de l’analyse", - "scan.counted": "{processed} fichiers sur {total}", - "scan.tally": - "{added} ajoutés · {updated} mis à jour · {moved} déplacés · {skipped} inchangés · {errors} erreurs", - "admin.scanning": "Analyse en cours", - "admin.nowPlaying": "Écoutes en cours", - "admin.nobodyListening": "Personne n’écoute en ce moment.", - "admin.tokens": "Jetons d’API de {username}", - "admin.tokenName": "À quoi sert-il ?", - "admin.tokenCreate": "Émettre", - "admin.tokenOnce": "Copiez-le maintenant. Il n’est montré qu’une fois :", - "admin.tokenError": "Le jeton n’a pas pu être émis.", - "admin.tokenRevoke": "Révoquer", - "admin.tokenUsed": "déjà utilisé", - "admin.tokenUnused": "jamais utilisé", - "admin.tokenNone": "Aucun jeton sur ce compte.", - "scan.lost": - "Le flux de progression n’a pas pu s’ouvrir. L’analyse elle-même se poursuit.", - "admin.members": "Qui peut voir cette bibliothèque", - "admin.memberRole": "Rôle", - "admin.memberRemove": "Retirer", - "admin.memberOwner": "propriétaire, et le reste", - "admin.memberAdd": "Donner accès à", - "admin.memberChoose": "Choisir un compte…", - "admin.memberError": "Cette appartenance n’a pas pu être modifiée.", - "admin.manage": "Nom, dossier et suppression", - "admin.rename": "Nom", - "admin.path": "Dossier sur le serveur", - "admin.followLinks": - "Suivre les liens symboliques dont la cible reste dans la bibliothèque", - "admin.watch": - "Surveiller le dossier et analyser ce qui change (à couper pour un partage réseau modifié par d’autres machines)", - "admin.save": "Enregistrer", - "admin.saved": "Enregistré.", - "admin.savedScan": "Enregistré. Scan {id} ajouté à la file.", - "admin.saveError": - "Ce changement n’a pas pu être enregistré. Le dossier doit exister sur le serveur et ne pas être un lien symbolique.", - "admin.saveBusy": - "La bibliothèque est en cours d’analyse, ou une autre bibliothèque utilise déjà ce dossier. Réessayez une fois l’analyse terminée.", - "admin.delete": "Supprimer cette bibliothèque", - "admin.deleteNote": - "Retire la bibliothèque du catalogue pour tous ses membres : ses titres, albums et artistes, avec leurs entrées de playlist, favoris et écoutes. Aucun fichier n’est touché sur le disque. Tapez le nom de la bibliothèque pour confirmer.", - "admin.deleteConfirm": "Le nom de la bibliothèque, pour confirmer", - "admin.deleteButton": "Supprimer définitivement", - "admin.deleted": - "{name} a été supprimée : {tracks} titres ont quitté le catalogue.", - "admin.deleteError": "La bibliothèque n’a pas pu être supprimée.", - "scan.leftOut": "{excluded} écartés · {links} liens non suivis", - "nav.missing": "Fichiers manquants", - "nav.maintenance": "Maintenance", - "maintenance.sections": "Sections de maintenance", - "maintenance.none": - "Rien ici n’est à vous d’entretenir dans cette bibliothèque.", - "missing.total": { - one: "{count} fichier que les scans ne trouvent plus", - other: "{count} fichiers que les scans ne trouvent plus", - }, - "missing.nothing": "Chaque fichier est là où le catalogue l’attend.", - "missing.notYours": - "Seuls les propriétaires et gestionnaires de cette bibliothèque, et les administrateurs, voient ses fichiers manquants.", - "missing.note": - "Ces titres restent au catalogue, avec leurs favoris, entrées de playlist et écoutes, jusqu’à leur purge ; un fichier qui revient avant garde son titre. Une purge peut être annulée pendant un temps, puis le titre, et tout ce qui s’y rattache pour chaque membre, disparaît pour de bon.", - "missing.since": "manquant depuis le {date}", - "missing.cost": - "{playlists} en playlist · {favorites} favoris · {plays} écoutes", - "missing.select": "Sélectionner {title}", - "missing.purgeSelected": "Purger la sélection ({count})", - "missing.purgeAll": "Tout purger", - "missing.confirm": "Confirmer : purger {count}", - "missing.purged": { - one: "{count} titre purgé.", - other: "{count} titres purgés.", - }, - "missing.scanning": - "La bibliothèque est en cours d’analyse. Réessayez une fois l’analyse terminée.", - "missing.failed": "La purge n’a pas abouti. Réessayez.", - "missing.more": "Afficher plus", - "missing.purgedUntil": { - one: "{count} titre purgé. Il peut être restauré jusqu’au {date}.", - other: "{count} titres purgés. Ils peuvent être restaurés jusqu’au {date}.", - }, - "missing.purgedTitle": { - one: "Purgés, encore restaurables ({count})", - other: "Purgés, encore restaurables ({count})", - }, - "missing.purgedNote": - "Rien n’en est encore perdu. Restauré, un titre retourne aux fichiers manquants avec tout ce qui s’y rattachait ; un fichier qui revient restaure son titre de lui-même.", - "missing.deletedAfter": "supprimé pour de bon après le {date}", - "missing.restore": "Restaurer", - "missing.restoreOne": "Restaurer {title}", - "missing.restoreAll": "Tout restaurer", - "missing.restored": { - one: "{count} titre restauré.", - other: "{count} titres restaurés.", - }, - "missing.restoreFailed": "La restauration n’a pas abouti. Réessayez.", - "offline.kicker": "Dans ce navigateur", - "offline.title": "Gardé hors ligne", - "offline.detail": - "Ces titres sont gardés dans ce navigateur pour le compte qui s’y est connecté en dernier, et se lisent sans connexion. Se déconnecter les supprime de ce navigateur. Gardez-en d’autres depuis le menu d’un titre.", - "offline.unsupported": - "Ce navigateur ne peut pas garder de titres pour l’écoute hors ligne.", - "offline.back": "Retour à la bibliothèque", - "offline.empty": - "Rien n’est encore gardé. Le menu d’un titre propose de le garder.", - "offline.player": "Lecteur hors ligne", - "offline.gone": "Cette copie n’est plus dans ce navigateur.", - "offline.forget": "Retirer la copie hors ligne de {title}", - "offline.forgetShort": "Retirer", - "offline.forgetDetail": "Libère {size}", - "login.offline": { - one: "Écouter le {count} titre gardé dans ce navigateur", - other: "Écouter les {count} titres gardés dans ce navigateur", - }, - "scrobbling.title": "Scrobbling externe", - "scrobbling.detail": - "Transmettre ce que vous écoutez ici à ListenBrainz, Maloja ou Last.fm.", - "scrobbling.none": - "Ce serveur ne propose aucune destination. C’est son exploitant qui en décide.", - "scrobbling.provider.listenbrainz": "ListenBrainz", - "scrobbling.provider.maloja": "Maloja", - "scrobbling.provider.lastfm": "Last.fm", - "scrobbling.instance": "instance « {name} »", - "scrobbling.unavailable": "Indisponible ici : {reason}", - "scrobbling.unavailable.no_application_configured": - "aucune application Last.fm n’est déclarée sur ce serveur", - "scrobbling.unavailable.browser_journey_needs_https": - "le parcours par le navigateur exige que l’adresse publique de ce serveur soit en https — un exploitant peut encore lier un compte en ligne de commande", - "scrobbling.unavailable.unknown": "ce serveur n’a pas dit pourquoi", - "scrobbling.retired": - "Ce serveur ne propose plus cette instance. Plus rien ne partira vers elle — vous pouvez encore retirer l’autorisation.", - "scrobbling.notLinked": "Non liée.", - "scrobbling.secret": "Clé d’API", - "scrobbling.secretHint": "Prise sur votre compte chez cette destination.", - "scrobbling.link": "Lier", - "scrobbling.working": "En cours…", - "scrobbling.linkError": - "Cette autorisation a été refusée. Vérifiez la clé et recommencez.", - "scrobbling.unlink": "Délier", - "scrobbling.unlinkError": "Ce lien n’a pas pu être retiré.", - "scrobbling.connect": "Se connecter à Last.fm", - "scrobbling.connectDetail": - "Last.fm ne délivre aucune clé à recopier : il vous interroge chez lui et vous ramène ici.", - "scrobbling.connectError": "Le parcours Last.fm n’a pas pu s’ouvrir.", - "scrobbling.returned": "Last.fm est liée : {name}.", - "scrobbling.health.healthy": "Les écoutes partent", - "scrobbling.health.degraded": "La file n’avance pas", - "scrobbling.health.broken": "Rompu", - "scrobbling.healthDetail.healthy": "Rien n’est bloqué ici.", - "scrobbling.healthDetail.degraded": - "Le lien répond, mais les écoutes ne passent pas — soit l’une attend votre réponse, soit la plus ancienne attend depuis trop longtemps.", - "scrobbling.healthDetail.broken": - "L’autorisation a été refusée, ou ce serveur ne joint plus cette instance sous le nom qui a servi à créer le lien.", - "scrobbling.pending": { - one: "{count} en attente", - other: "{count} en attente", - }, - "scrobbling.retrying": { - one: "{count} en reprise", - other: "{count} en reprise", - }, - "scrobbling.uncertainCount": { - one: "{count} attend votre réponse", - other: "{count} attendent votre réponse", - }, - "scrobbling.oldest": "La plus ancienne attend depuis {when}.", - "scrobbling.lastSuccess": "Dernière acceptée {when}.", - "scrobbling.lastFailure": "Dernier refus : {reason}.", - "scrobbling.uncertain": "Des écoutes que vous seul pouvez trancher", - "scrobbling.uncertainDetail": - "La connexion a été coupée après leur départ : personne ne sait si la destination les a enregistrées. La renvoyer peut la faire figurer deux fois dans un historique que d’autres lisent ; la jeter laisse un trou. Ce serveur ne choisira pas à votre place, et il ne le demandera qu’une fois.", - "scrobbling.uncertainNone": "Rien n’attend votre réponse.", - "scrobbling.playedAt": "Écoutée {when}", - "scrobbling.attempts": { - one: "{count} tentative", - other: "{count} tentatives", - }, - "scrobbling.retry": "La renvoyer", - "scrobbling.discard": "La jeter", - "scrobbling.answered": "Répondu.", - "scrobbling.answerError": - "Cette écoute n’a pas pu être traitée. Rechargez la page avant de réessayer.", - "scrobbling.unnamedTrack": - "Absente de l’historique que ce compte conserve encore", - "scrobbling.reason.rate_limited": "la destination a demandé du répit", - "scrobbling.reason.retryable": "la destination ne l’a pas prise", - "scrobbling.reason.auth_broken": "l’autorisation a été refusée", - "scrobbling.reason.rejected": "la destination l’a lue et l’a refusée", - "scrobbling.reason.destination_gone": - "ce serveur ne joint plus cette instance sous ce nom", - "scrobbling.reason.credential_unreadable": - "ce serveur n’a pas pu lire l’identifiant enregistré", - "scrobbling.reason.attempts_exhausted": "elle a été tentée trop de fois", - "scrobbling.reason.unlinked": "le lien a été retiré", - "scrobbling.reason.ambiguous": "la réponse n’est jamais arrivée", - "scrobbling.reason.interrupted": "le serveur s’est arrêté en plein vol", - // RFC-024 : paroles de LRCLIB, statistiques et mix. - "lyrics.source.lrclib": "Depuis LRCLIB", - "lyrics.fromLrclib": "Paroles fournies par LRCLIB", - "nav.stats": "Statistiques", - "stats.detail": "Ce que ce compte a écouté, compté à partir de ses écoutes.", - "stats.period": "Période", - "stats.period7d": "7 derniers jours", - "stats.period30d": "30 derniers jours", - "stats.period365d": "12 derniers mois", - "stats.periodAll": "Depuis toujours", - "stats.empty": "Rien n’a été écouté pendant cette période.", - "stats.plays": "Écoutes", - "stats.listeningTime": "Temps d’écoute", - "stats.tracks": "Titres", - "stats.artists": "Artistes", - "stats.albums": "Albums", - "stats.estimate": - "Le temps d’écoute additionne la durée de chaque titre écouté ; aucun lecteur ne dit quelle part en a été entendue.", - "stats.timelineDays": "Écoutes par jour", - "stats.timelineMonths": "Écoutes par mois", - "stats.bucket": { - one: "{label} : {count} écoute", - other: "{label} : {count} écoutes", - }, - "stats.topTracks": "Titres les plus écoutés", - "stats.topArtists": "Artistes les plus écoutés", - "stats.topAlbums": "Albums les plus écoutés", - "stats.topGenres": "Genres les plus écoutés", - "stats.playCount": { one: "{count} écoute", other: "{count} écoutes" }, - "stats.firstPlay": "Première écoute de la période", - "stats.lastPlay": "Dernière écoute", - "stats.moment": "{title}, {when}", - "stats.minutes": { one: "{count} minute", other: "{count} minutes" }, - "stats.hoursMinutes": "{hours} h {minutes} min", - "mixes.title": "Pour vous aujourd’hui", - "mixes.detail": { - one: "{count} titre, tiré pour aujourd’hui", - other: "{count} titres, tirés pour aujourd’hui", - }, - "mixes.mostPlayed": "Les plus écoutés", - "mixes.neverPlayed": "Jamais écoutés", - "mixes.forgottenFavourites": "Favoris oubliés", - "mixes.recentlyAdded": "Ajouts récents", - "mixes.genre": "Mix {genre}", - "mixes.decade": "Les années {decade}", - "mixes.failed": "Impossible d’ouvrir « {title} ». Réessayez.", - "mixes.shelfTitle": "Mix pour vous", - "mixes.shelfDetail": - "Des listes tirées de vos écoutes, renouvelées chaque jour. Vos albums sont en dessous.", - "mixes.badge": "Mix", - "smart.title": "Listes intelligentes", - "smart.detail": - "Définies par des règles et recalculées à chaque ouverture : elles suivent la bibliothèque.", - "smart.new": "Nouvelle liste intelligente", - "smart.edit": "Modifier les règles", - "smart.save": "Enregistrer la liste intelligente", - "smart.badge": "Intelligente", - "smart.empty": "Aucune liste intelligente pour l’instant.", - "smart.notCounted": "Comptée à l’ouverture", - "smart.chooseLibrary": "Choisir une bibliothèque", - "smart.name": "Nom", - "smart.match": "Les pistes doivent remplir", - "smart.match.all": "toutes les conditions", - "smart.match.any": "au moins une condition", - "smart.fieldOf": "Champ de la condition {position}", - "smart.operatorOf": "Comparaison de la condition {position}", - "smart.valueOf": "Valeur de la condition {position}", - "smart.upperOf": "Borne haute de la condition {position}", - "smart.removeCondition": "Retirer la condition {position}", - "smart.addCondition": "Ajouter une condition", - "smart.sort": "Trier par", - "smart.sort.none": "Titre (par défaut)", - "smart.order": "Ordre", - "smart.order.asc": "Croissant", - "smart.order.desc": "Décroissant", - "smart.limit": "Au plus (pistes)", - "smart.days": "jours", - "smart.yes": "oui", - "smart.no": "non", - "smart.preview": "Aperçu", - "smart.previewCount": { - one: "{count} piste correspond", - other: "{count} pistes correspondent", - }, - "smart.unfinished": - "Complétez chaque condition pour voir l’aperçu ou enregistrer.", - "smart.refused": "Le serveur a refusé ces règles.", - "smart.full": "Ce compte retient déjà le maximum de listes intelligentes.", - "smart.saveError": "La liste intelligente n’a pas pu être enregistrée.", - "smart.deleteError": "La liste intelligente n’a pas pu être supprimée.", - "smart.loadError": "Les listes intelligentes n’ont pas pu être chargées.", - "smart.field.title": "Titre", - "smart.field.artist": "Artiste", - "smart.field.album_artist": "Artiste de l’album", - "smart.field.album": "Album", - "smart.field.genre": "Genre", - "smart.field.year": "Année", - "smart.field.rating": "Note", - "smart.field.starred": "Favori", - "smart.field.play_count": "Nombre d’écoutes", - "smart.field.last_played": "Dernière écoute", - "smart.field.date_added": "Date d’ajout", - "smart.field.duration": "Durée (secondes)", - "smart.field.bitrate": "Débit (kbit/s)", - "smart.field.format": "Format", - "smart.field.library": "Bibliothèque", - "smart.op.is": "est", - "smart.op.is_not": "n’est pas", - "smart.op.contains": "contient", - "smart.op.not_contains": "ne contient pas", - "smart.op.starts_with": "commence par", - "smart.op.ends_with": "finit par", - "smart.op.greater_than": "est supérieur à", - "smart.op.less_than": "est inférieur à", - "smart.op.between": "est entre", - "smart.op.in_the_last": "dans les derniers (jours)", - "smart.op.not_in_the_last": "pas dans les derniers (jours)", - "smart.op.before": "avant le", - "smart.op.after": "après le", - "smart.sort.title": "Titre", - "smart.sort.artist": "Artiste", - "smart.sort.album": "Album", - "smart.sort.year": "Année", - "smart.sort.rating": "Note", - "smart.sort.play_count": "Nombre d’écoutes", - "smart.sort.last_played": "Dernière écoute", - "smart.sort.date_added": "Date d’ajout", - "smart.sort.duration": "Durée", - "smart.sort.bitrate": "Débit", - "smart.sort.random": "Au hasard", - "nav.podcasts": "Podcasts", - "podcasts.detail": "Les émissions que suit ce serveur, pour tous les comptes", - "podcasts.off": - "Les podcasts sont désactivés sur ce serveur. Ce qui a été conservé reste lisible ; rien de nouveau n’est récupéré.", - "podcasts.url": "Adresse du flux", - "podcasts.shared": - "Visible par tous les comptes : chacun sur ce serveur voit l’émission et peut écouter les épisodes téléchargés. Seuls les administrateurs voient l’adresse du flux.", - "podcasts.subscribe": "S’abonner", - "podcasts.refresh": "Actualiser tous les flux", - "podcasts.refreshing": "Les flux sont en cours de récupération.", - "podcasts.reload": "Recharger", - "podcasts.empty": "Aucun podcast pour l’instant.", - "podcasts.emptyDetail": - "Un administrateur abonne le serveur au flux d’une émission ici. Le serveur télécharge ses épisodes, et tous les comptes de ce serveur peuvent les écouter.", - "podcasts.addFirst": "Ajouter votre premier podcast", - "podcasts.channels": "Émissions", - "podcasts.showing": "Épisodes de {title} affichés.", - "podcasts.noEpisodes": "Ce flux ne contient aucun épisode.", - "podcasts.episodes": { - one: "{count} épisode", - other: "{count} épisodes", - }, - "podcasts.failed": "dernière récupération en échec : {reason}", - "podcasts.open": "Épisodes", - "podcasts.hide": "Masquer", - "podcasts.unsubscribe": "Se désabonner", - "podcasts.download": "Télécharger", - "podcasts.play": "Lire {title}", - "podcasts.pause": "Mettre en pause {title}", - "podcasts.resumeAt": "reprise à {time}", - "podcasts.status.new": "non téléchargé", - "podcasts.status.downloading": "téléchargement en cours", - "podcasts.status.completed": "prêt", - "podcasts.status.error": "échec du téléchargement", - "podcasts.status.deleted": "supprimé", - "podcasts.conflict": "Ce serveur suit déjà ce flux.", - "podcasts.refused": - "Le flux n’a pas pu être récupéré ou lu, ou son adresse n’est pas joignable par ce serveur.", - "podcasts.subscribeError": "Le flux n’a pas pu être ajouté.", - "podcasts.mutationError": "Cela n’a pas fonctionné. Réessayez.", - "podcasts.playbackError": "Cet épisode n’a pas pu être lu.", -}; +export type TranslationValue = string | { one: string; other: string }; export type Locale = "en" | "fr"; From 3dfd8a9631b1ef95fd442051713535a730cbafc8 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 26 Oct 2026 23:34:22 +0100 Subject: [PATCH 3/4] refactor(webapp): group the translations by key prefix and test that no key is defined twice Signed-off-by: InstaZDLL --- .../i18n/en/{maintenance.ts => admin.ts} | 49 ++-- webapp/src/state/i18n/en/attention.ts | 53 +++++ webapp/src/state/i18n/en/browse.ts | 92 ------- .../state/i18n/en/{settings.ts => clients.ts} | 39 ++- webapp/src/state/i18n/en/common.ts | 23 ++ webapp/src/state/i18n/en/folders.ts | 38 +-- webapp/src/state/i18n/en/index.ts | 32 ++- webapp/src/state/i18n/en/library.ts | 222 +++++++---------- webapp/src/state/i18n/en/login.ts | 28 +++ webapp/src/state/i18n/en/nav.ts | 42 ++++ webapp/src/state/i18n/en/player.ts | 77 ++++++ webapp/src/state/i18n/en/playing.ts | 51 ---- webapp/src/state/i18n/en/playlists.ts | 84 +++++++ webapp/src/state/i18n/en/podcasts.ts | 1 - webapp/src/state/i18n/en/preferences.ts | 48 ++++ webapp/src/state/i18n/en/quality.ts | 55 +---- webapp/src/state/i18n/en/scrobbling.ts | 19 +- webapp/src/state/i18n/en/shell.ts | 80 ------- webapp/src/state/i18n/en/stats.ts | 6 +- webapp/src/state/i18n/en/track.ts | 93 ++++---- .../i18n/fr/{maintenance.ts => admin.ts} | 51 ++-- webapp/src/state/i18n/fr/attention.ts | 55 +++++ webapp/src/state/i18n/fr/browse.ts | 94 -------- .../state/i18n/fr/{settings.ts => clients.ts} | 40 ++-- webapp/src/state/i18n/fr/common.ts | 26 ++ webapp/src/state/i18n/fr/folders.ts | 39 +-- webapp/src/state/i18n/fr/index.ts | 32 ++- webapp/src/state/i18n/fr/library.ts | 224 +++++++----------- webapp/src/state/i18n/fr/login.ts | 31 +++ webapp/src/state/i18n/fr/nav.ts | 45 ++++ webapp/src/state/i18n/fr/player.ts | 81 +++++++ webapp/src/state/i18n/fr/playing.ts | 55 ----- webapp/src/state/i18n/fr/playlists.ts | 88 +++++++ webapp/src/state/i18n/fr/podcasts.ts | 1 - webapp/src/state/i18n/fr/preferences.ts | 51 ++++ webapp/src/state/i18n/fr/quality.ts | 54 +---- webapp/src/state/i18n/fr/scrobbling.ts | 20 +- webapp/src/state/i18n/fr/shell.ts | 83 ------- webapp/src/state/i18n/fr/stats.ts | 6 +- webapp/src/state/i18n/fr/track.ts | 96 ++++---- webapp/src/state/i18n/keys.test.ts | 43 ++++ 41 files changed, 1221 insertions(+), 1126 deletions(-) rename webapp/src/state/i18n/en/{maintenance.ts => admin.ts} (82%) create mode 100644 webapp/src/state/i18n/en/attention.ts delete mode 100644 webapp/src/state/i18n/en/browse.ts rename webapp/src/state/i18n/en/{settings.ts => clients.ts} (63%) create mode 100644 webapp/src/state/i18n/en/common.ts create mode 100644 webapp/src/state/i18n/en/login.ts create mode 100644 webapp/src/state/i18n/en/nav.ts create mode 100644 webapp/src/state/i18n/en/player.ts delete mode 100644 webapp/src/state/i18n/en/playing.ts create mode 100644 webapp/src/state/i18n/en/playlists.ts create mode 100644 webapp/src/state/i18n/en/preferences.ts delete mode 100644 webapp/src/state/i18n/en/shell.ts rename webapp/src/state/i18n/fr/{maintenance.ts => admin.ts} (81%) create mode 100644 webapp/src/state/i18n/fr/attention.ts delete mode 100644 webapp/src/state/i18n/fr/browse.ts rename webapp/src/state/i18n/fr/{settings.ts => clients.ts} (63%) create mode 100644 webapp/src/state/i18n/fr/common.ts create mode 100644 webapp/src/state/i18n/fr/login.ts create mode 100644 webapp/src/state/i18n/fr/nav.ts create mode 100644 webapp/src/state/i18n/fr/player.ts delete mode 100644 webapp/src/state/i18n/fr/playing.ts create mode 100644 webapp/src/state/i18n/fr/playlists.ts create mode 100644 webapp/src/state/i18n/fr/preferences.ts delete mode 100644 webapp/src/state/i18n/fr/shell.ts create mode 100644 webapp/src/state/i18n/keys.test.ts diff --git a/webapp/src/state/i18n/en/maintenance.ts b/webapp/src/state/i18n/en/admin.ts similarity index 82% rename from webapp/src/state/i18n/en/maintenance.ts rename to webapp/src/state/i18n/en/admin.ts index 5283f9bb..de59581a 100644 --- a/webapp/src/state/i18n/en/maintenance.ts +++ b/webapp/src/state/i18n/en/admin.ts @@ -1,14 +1,26 @@ -// Scan progress, accounts and tokens, maintenance and missing files. -export const maintenance = { - "scan.connecting": "Waiting for the first reading…", - "scan.queued": "Queued", - "scan.running": "Scanning", - "scan.completed": "Finished", - "scan.failed": "Failed", - "scan.progress": "Scan progress", - "scan.counted": "{processed} of {total} files", - "scan.tally": - "{added} added · {updated} updated · {moved} moved · {skipped} unchanged · {errors} errors", +// Administration, scans, maintenance and missing files. +export const admin = { + "admin.title": "Administration", + "admin.detail": "Libraries, scans and accounts", + "admin.libraries": "Libraries", + "admin.libraryName": "Library name", + "admin.libraryPath": "Absolute server folder path", + "admin.registering": "Registering…", + "admin.register": "Register and scan", + "admin.scan": "Scan now", + "admin.accounts": "Accounts", + "admin.webPassword": "Web password, 12 characters minimum", + "admin.creating": "Creating…", + "admin.create": "Create account", + "admin.enable": "Enable", + "admin.disable": "Disable", + "admin.currentAccount": "You cannot disable your current account", + "admin.libraryError": "The library path could not be registered.", + "admin.accountError": "The account could not be created.", + "admin.statusError": "The account status could not be changed.", + "admin.scanError": "The scan could not be started.", + "admin.scanQueued": "Scan {id} queued.", + "admin.initialScan": "Initial scan {id} queued.", "admin.scanning": "Scan in progress", "admin.nowPlaying": "Playing now", "admin.nobodyListening": "Nobody is listening right now.", @@ -21,8 +33,6 @@ export const maintenance = { "admin.tokenUsed": "already used", "admin.tokenUnused": "never used", "admin.tokenNone": "No token on this account.", - "scan.lost": - "The progress stream could not be opened. The scan itself is unaffected.", "admin.members": "Who may see this library", "admin.memberRole": "Role", "admin.memberRemove": "Remove", @@ -51,9 +61,18 @@ export const maintenance = { "admin.deleteButton": "Delete for good", "admin.deleted": "{name} was deleted: {tracks} tracks left the catalogue.", "admin.deleteError": "The library could not be deleted.", + "scan.connecting": "Waiting for the first reading…", + "scan.queued": "Queued", + "scan.running": "Scanning", + "scan.completed": "Finished", + "scan.failed": "Failed", + "scan.progress": "Scan progress", + "scan.counted": "{processed} of {total} files", + "scan.tally": + "{added} added · {updated} updated · {moved} moved · {skipped} unchanged · {errors} errors", + "scan.lost": + "The progress stream could not be opened. The scan itself is unaffected.", "scan.leftOut": "{excluded} left out · {links} links not followed", - "nav.missing": "Missing files", - "nav.maintenance": "Maintenance", "maintenance.sections": "Maintenance sections", "maintenance.none": "Nothing here is yours to maintain in this library.", "missing.total": { diff --git a/webapp/src/state/i18n/en/attention.ts b/webapp/src/state/i18n/en/attention.ts new file mode 100644 index 00000000..a8400a12 --- /dev/null +++ b/webapp/src/state/i18n/en/attention.ts @@ -0,0 +1,53 @@ +// The attention lists and artist splits. +export const attention = { + "attention.lists": "Lists", + "attention.total": { + one: "{count} thing to look at", + other: "{count} things to look at", + }, + "attention.nothing": "Nothing needs attention.", + "attention.notYours": + "Only the owners and managers of this library see what needs attention in it.", + "attention.empty": "Nothing in this list.", + "attention.tracksNote": + "Fix a track from its menu: once corrected, it leaves the list.", + "attention.creditsNote": + "Each of these credits names several artists in one string. Split it into the artists it names, or keep it as one when it is a single name. Every later scan applies the decision.", + "attention.pending": "To decide", + "attention.decided": "Decided", + "attention.split": "Split", + "attention.confirmSplit": "Confirm: split into {count}", + "attention.keep": "Keep as one", + "attention.splitDone": "{name} is split.", + "attention.keepDone": "{name} stays as one.", + "attention.undone": "The decision about {name} is undone.", + "attention.alreadyDecided": + "This credit was decided in the meantime. The list has been reloaded.", + "attention.failed": "That did not work. Try again.", + "attention.noCredits": "No credit is waiting for a decision.", + "attention.noDecisions": "Nothing has been decided yet.", + "attention.fragments": "What {name} would split into", + "attention.knownFragment": "Already an artist in this library", + "attention.decidedSplit": "Split into {parts}", + "attention.decidedKeep": "Kept as one", + "attention.decidedBy": "By {who}, {when}", + "attention.undo": "Undo", + "attention.duplicateGroup": "Probable duplicate {group}", + "attention.split_credits": "Artists to split", + "attention.missing_artist": "Missing artist", + "attention.missing_album": "Missing album", + "attention.missing_year": "Missing year", + "attention.missing_track_number": "Missing track number", + "attention.missing_cover": "Missing cover", + "attention.album_year_conflict": "Albums with several years", + "attention.compilation_no_album_artist": + "Compilations without an album artist", + "attention.duplicate_track_number": "Duplicate track numbers", + "attention.track_number_gap": "Gaps in track numbers", + "attention.probable_duplicate": "Probable duplicates", + "attention.probable_fake_lossless": "Probable fake lossless", + "artistSplit.offer": "Split into {count} artists", + "artistSplit.confirm": "Confirm: split into {count}", + "artistSplit.done": "Split into {parts}.", + "artistSplit.failed": "This credit could not be split. Try again.", +} as const; diff --git a/webapp/src/state/i18n/en/browse.ts b/webapp/src/state/i18n/en/browse.ts deleted file mode 100644 index 090cb9a1..00000000 --- a/webapp/src/state/i18n/en/browse.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Administration, search, player buttons, OAuth consent, themes, browsing. -export const browse = { - "admin.title": "Administration", - "admin.detail": "Libraries, scans and accounts", - "admin.libraries": "Libraries", - "admin.libraryName": "Library name", - "admin.libraryPath": "Absolute server folder path", - "admin.registering": "Registering…", - "admin.register": "Register and scan", - "admin.scan": "Scan now", - "admin.accounts": "Accounts", - "admin.webPassword": "Web password, 12 characters minimum", - "admin.creating": "Creating…", - "admin.create": "Create account", - "admin.enable": "Enable", - "admin.disable": "Disable", - "admin.currentAccount": "You cannot disable your current account", - "admin.libraryError": "The library path could not be registered.", - "admin.accountError": "The account could not be created.", - "admin.statusError": "The account status could not be changed.", - "admin.scanError": "The scan could not be started.", - "admin.scanQueued": "Scan {id} queued.", - "admin.initialScan": "Initial scan {id} queued.", - "search.detail": "Find a title, album or artist", - "search.placeholder": "Title, album or artist", - "search.query": "Search query", - "search.tracks": "Tracks", - "search.empty": "Nothing found.", - "player.error": "Playback could not start. Check the server and try again.", - "player.previous": "Previous track", - "player.play": "Play", - "player.pause": "Pause", - "player.next": "Next track", - "player.seek": "Seek", - "authorize.title": "Authorisation", - "authorize.invalid": - "This authorisation link is incomplete or asks to send you somewhere we do not trust.", - "authorize.client": "Authorise {client}", - "authorize.detail": - "It will be able to browse your libraries, play your music and manage your playlists, favourites and ratings, everything this account can do.", - "authorize.redirect": "Sending you back to {url}", - "authorize.error": - "The application sent an authorisation request we cannot honour.", - "authorize.busy": "Authorising…", - "authorize.approve": "Authorise", - "notFound.title": "This room is silent", - "notFound.detail": "The page you asked for does not exist on this server.", - "notFound.back": "Back to the library", - "appearance.themes.default": "Emerald light", - "appearance.themes.midnightLight": "Midnight light", - "appearance.themes.sunsetLight": "Sunset light", - "appearance.themes.lavenderLight": "Lavender light", - "appearance.themes.crimsonLight": "Crimson light", - "appearance.themes.oceanLight": "Ocean light", - "appearance.themes.defaultDark": "Emerald dark", - "appearance.themes.oled": "OLED black", - "appearance.themes.midnight": "Midnight", - "appearance.themes.sunset": "Sunset", - "appearance.themes.lavender": "Lavender", - "appearance.themes.crimson": "Crimson", - "appearance.themes.ocean": "Ocean", - "appearance.themes.neon": "Neon", - "browse.sort": "Sort", - "browse.sortAlphabeticalByName": "By title", - "browse.sortAlphabeticalByArtist": "By artist", - "browse.sortAlphabeticalByAlbum": "By album", - "browse.sortNewest": "Recently added", - "browse.sortRecent": "Recently played", - "browse.sortFrequent": "Most played", - "browse.sortStarred": "Favourites", - "browse.sortByYear": "By year", - "browse.filterAlbums": "Filter by title or artist", - "browse.filterArtists": "Filter by name", - "browse.filterPlaceholder": "Filter…", - "browse.matches": { - one: "{count} of {total} shown", - other: "{count} of {total} shown", - }, - "browse.noMatch": "Nothing matches that filter.", - "card.play": "Play", - "card.queue": "Add to queue", - "rating.label": "Rating", - "rating.stars": { one: "{count} star", other: "{count} stars" }, - "nav.genres": "Genres", - "nav.history": "Recently played", - "nav.random": "Random", - "nav.playing": "Now playing", - "nav.upload": "Upload", - "nav.folders": "Folders", - "nav.attention": "Needs attention", - "nav.quality": "Quality", -} as const; diff --git a/webapp/src/state/i18n/en/settings.ts b/webapp/src/state/i18n/en/clients.ts similarity index 63% rename from webapp/src/state/i18n/en/settings.ts rename to webapp/src/state/i18n/en/clients.ts index 2f0720c1..66b84e8d 100644 --- a/webapp/src/state/i18n/en/settings.ts +++ b/webapp/src/state/i18n/en/clients.ts @@ -1,15 +1,5 @@ -// Settings, registered clients, the Subsonic password and quality labels. -export const settings = { - "nav.settings": "Settings", - "settings.detail": - "Appearance, language, and the clients allowed to read your library.", - "settings.appearance": "Appearance", - "settings.playback": "Playback", - "settings.sections": "Settings sections", - "settings.section.account": "Account", - "settings.playbackDetail": - "How this browser plays your tracks. Kept in this browser, not on your account.", - "settings.levelling": "Volume levelling", +// Registered clients, the Subsonic password and OAuth consent. +export const clients = { "clients.title": "Registered clients", "clients.detail": "Devices and scripts holding a credential on your library. Revoking one stops it at once.", @@ -40,14 +30,19 @@ export const settings = { "You have no Subsonic password, so no Subsonic client can read your library. Ask the administrator for one.", "subsonic.short": "Use at least 12 characters.", "subsonic.error": "That did not work. Nothing changed.", - "quality.label": "File quality", - "quality.unknown": "Unknown", - "quality.low": "Low", - "quality.standard": "Standard", - "quality.high": "High", - "quality.lossless": "Lossless", - "quality.hiRes": "Hi-res", - "file.channels": { one: "mono", other: "{count} ch" }, - "common.delete": "Delete", - "common.cancel": "Cancel", + "credential.password": "Dedicated Subsonic password", + "credential.rotate": "Rotate credential", + "credential.copy": "Copy this API key now:", + "credential.error": "Credential rotation failed.", + "authorize.title": "Authorisation", + "authorize.invalid": + "This authorisation link is incomplete or asks to send you somewhere we do not trust.", + "authorize.client": "Authorise {client}", + "authorize.detail": + "It will be able to browse your libraries, play your music and manage your playlists, favourites and ratings, everything this account can do.", + "authorize.redirect": "Sending you back to {url}", + "authorize.error": + "The application sent an authorisation request we cannot honour.", + "authorize.busy": "Authorising…", + "authorize.approve": "Authorise", } as const; diff --git a/webapp/src/state/i18n/en/common.ts b/webapp/src/state/i18n/en/common.ts new file mode 100644 index 00000000..8b77701f --- /dev/null +++ b/webapp/src/state/i18n/en/common.ts @@ -0,0 +1,23 @@ +// Words shared across screens, file sizes, card buttons and ratings. +export const common = { + "common.loading": "Loading", + "common.loadError": "We could not load this view", + "common.noAlbums": "No albums yet.", + "common.noArtists": "No artists yet.", + "common.variousArtists": "Various artists", + "common.unknownArtist": "Unknown artist", + "common.album": "Album", + "common.artist": "Artist", + "common.albums": { one: "{count} album", other: "{count} albums" }, + "common.tracks": { one: "{count} track", other: "{count} tracks" }, + "common.delete": "Delete", + "common.cancel": "Cancel", + "common.back": "Back", + "file.megabytes": "{size} MB", + "file.kilobytes": "{size} kB", + "file.channels": { one: "mono", other: "{count} ch" }, + "card.play": "Play", + "card.queue": "Add to queue", + "rating.label": "Rating", + "rating.stars": { one: "{count} star", other: "{count} stars" }, +} as const; diff --git a/webapp/src/state/i18n/en/folders.ts b/webapp/src/state/i18n/en/folders.ts index 341e6d21..fe39a36d 100644 --- a/webapp/src/state/i18n/en/folders.ts +++ b/webapp/src/state/i18n/en/folders.ts @@ -1,38 +1,5 @@ -// Artist splits and images, folders, filing and uploads. +// Folders, filing and uploads. export const folders = { - "artistSplit.offer": "Split into {count} artists", - "artistSplit.confirm": "Confirm: split into {count}", - "artistSplit.done": "Split into {parts}.", - "artistSplit.failed": "This credit could not be split. Try again.", - "common.back": "Back", - "artistImage.changePortrait": "Change portrait", - "artistImage.changeBanner": "Change banner", - "artistImage.resetPortrait": "Automatic portrait", - "artistImage.resetBanner": "No banner", - "artistImage.portraitChanged": "Portrait changed.", - "artistImage.bannerChanged": "Banner changed.", - "artistImage.portraitReverted": "Back to the automatic portrait.", - "artistImage.bannerReverted": "Banner removed.", - "artistImage.tooLarge": "This image is larger than 10 MB.", - "artistImage.notAnImage": "Choose a JPEG, PNG or WebP image.", - "artistImage.failed": "That did not work. Try again.", - "artistImage.chosen": "Portrait chosen by hand", - "artistImage.searchDeezer": "Search Deezer", - "artistImage.menu": "Edit images", - "artistImage.menuShort": "Edit", - "artistImage.menuLabel": "Images of {name}", - "artistImage.deezerTitle": "Choose a portrait from Deezer", - "artistImage.deezerQuery": "Artist name", - "artistImage.deezerSearch": "Search", - "artistImage.deezerSearching": "Searching Deezer…", - "artistImage.deezerResults": "Deezer artists", - "artistImage.deezerNone": "Deezer knows no artist by that name.", - "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, - "artistImage.deezerUnavailable": - "Searching Deezer is not enabled on this server.", - "artistImage.deezerBusy": - "Your previous search is still running. Try again in a moment.", - "artistImage.deezerExpired": "These results have expired. Search again.", "folders.count": { one: "{count} folder created in this library", other: "{count} folders created in this library", @@ -89,7 +56,6 @@ export const folders = { "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed, and it cannot be renamed or deleted. To file a track, open its menu and choose “Move to folder…”.", "filed.unfiledNoteReader": "Every track that is in no folder yet. WaveFlow keeps this list itself: a track leaves it as soon as it is filed.", - "trackMenu.move": "Move to folder…", "upload.into": "Received files go into the folder {folder}.", "upload.doneUnfiled": "Added to the library, in To sort", "upload.folderMissing": @@ -97,8 +63,6 @@ export const folders = { "upload.folderUnchecked": "The folder this upload was opened for could not be checked, so no file is taken yet.", "upload.checkAgain": "Check again", - "nav.group.library": "Library", - "nav.group.listen": "Listening", "upload.title": "Upload to {library}", "upload.detail": "Each file is fingerprinted in your browser first, so a file the library already holds is never sent again, and an interrupted transfer picks up where it stopped.", diff --git a/webapp/src/state/i18n/en/index.ts b/webapp/src/state/i18n/en/index.ts index 3cce8f15..63fded7e 100644 --- a/webapp/src/state/i18n/en/index.ts +++ b/webapp/src/state/i18n/en/index.ts @@ -1,28 +1,36 @@ -// The English strings, one file per domain, in their original order. -import { browse } from "./browse"; +// The English strings, assembled from one file per domain. +import { admin } from "./admin"; +import { attention } from "./attention"; +import { clients } from "./clients"; +import { common } from "./common"; import { folders } from "./folders"; import { library } from "./library"; -import { maintenance } from "./maintenance"; -import { playing } from "./playing"; +import { login } from "./login"; +import { nav } from "./nav"; +import { player } from "./player"; +import { playlists } from "./playlists"; import { podcasts } from "./podcasts"; +import { preferences } from "./preferences"; import { quality } from "./quality"; import { scrobbling } from "./scrobbling"; -import { settings } from "./settings"; -import { shell } from "./shell"; import { smart } from "./smart"; import { stats } from "./stats"; import { track } from "./track"; export const en = { - ...shell, - ...settings, - ...track, + ...nav, + ...preferences, + ...common, ...library, - ...browse, + ...clients, ...quality, + ...track, + ...login, + ...playlists, + ...player, + ...admin, + ...attention, ...folders, - ...playing, - ...maintenance, ...scrobbling, ...stats, ...smart, diff --git a/webapp/src/state/i18n/en/library.ts b/webapp/src/state/i18n/en/library.ts index 71ff3857..26c4518b 100644 --- a/webapp/src/state/i18n/en/library.ts +++ b/webapp/src/state/i18n/en/library.ts @@ -1,26 +1,27 @@ -// Sign-in, the library pages, import, the queue, shares and radio. +// The library pages: albums, artists, songs, favourites, browsing, search. export const library = { - "login.heroEyebrow": "Your music, on your terms", - "login.heroTitle": "After dark, the library comes alive.", - "login.heroDetail": "Private streaming, one catalogue, every screen.", - "login.server": "WaveFlow server", - "login.createAdmin": "Create the administrator", - "login.welcome": "Welcome back", - "login.setupDetail": - "This is a new server. Choose the first administrator account.", - "login.setupToken": "Setup token", - "login.setupTokenHint": - "The server wrote it to the file setup-token in its data directory, and its log names the path. If you set WAVEFLOW_SETUP_TOKEN, enter that value.", - "login.setupTokenError": - "This setup token is not the right one. Copy it again from the setup-token file.", - "login.username": "Username", - "login.password": "Password", - "login.setupError": - "Setup failed. Use a valid username and at least 12 password characters.", - "login.error": "Wrong username or password.", - "login.wait": "Please wait…", - "login.create": "Create and sign in", - "login.signIn": "Sign in", + "album.play": "Play album", + "album.addQueue": "Add to queue", + "album.title": "Title", + "album.duration": "Duration", + "album.trackActions": "Track actions", + "album.moreBy": "More by {artist}", + "songs.loadedSoFar": { + one: "{count} track loaded", + other: "{count} tracks loaded", + }, + "songs.playLoaded": { + one: "Play {count} loaded track", + other: "Play {count} loaded tracks", + }, + "songs.playAll": { + one: "Play the track", + other: "Play all {count} tracks", + }, + "songs.loadMore": "Load more", + "songs.loadMoreError": "That page could not be loaded. Try again.", + "songs.loading": "Loading…", + "songs.empty": "This library holds no tracks yet.", "albums.detail": { one: "{count} record in your library", other: "{count} records in your library", @@ -49,6 +50,34 @@ export const library = { "artists.similarSource": "Suggested by {provider}, {date}.", "artists.portraitSource": "Portrait from {provider}, {date}.", "artists.bannerSource": "Banner from {provider}, {date}.", + "artistImage.changePortrait": "Change portrait", + "artistImage.changeBanner": "Change banner", + "artistImage.resetPortrait": "Automatic portrait", + "artistImage.resetBanner": "No banner", + "artistImage.portraitChanged": "Portrait changed.", + "artistImage.bannerChanged": "Banner changed.", + "artistImage.portraitReverted": "Back to the automatic portrait.", + "artistImage.bannerReverted": "Banner removed.", + "artistImage.tooLarge": "This image is larger than 10 MB.", + "artistImage.notAnImage": "Choose a JPEG, PNG or WebP image.", + "artistImage.failed": "That did not work. Try again.", + "artistImage.chosen": "Portrait chosen by hand", + "artistImage.searchDeezer": "Search Deezer", + "artistImage.menu": "Edit images", + "artistImage.menuShort": "Edit", + "artistImage.menuLabel": "Images of {name}", + "artistImage.deezerTitle": "Choose a portrait from Deezer", + "artistImage.deezerQuery": "Artist name", + "artistImage.deezerSearch": "Search", + "artistImage.deezerSearching": "Searching Deezer…", + "artistImage.deezerResults": "Deezer artists", + "artistImage.deezerNone": "Deezer knows no artist by that name.", + "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, + "artistImage.deezerUnavailable": + "Searching Deezer is not enabled on this server.", + "artistImage.deezerBusy": + "Your previous search is still running. Try again in a moment.", + "artistImage.deezerExpired": "These results have expired. Search again.", "favourites.detail": { one: "{count} saved track", other: "{count} saved tracks", @@ -57,119 +86,44 @@ export const library = { "Star a track from an album or search result and it will appear here.", "favourites.add": "Add favourite", "favourites.remove": "Remove favourite", - "playlists.detail": { - one: "{count} collection", - other: "{count} collections", + "browse.sort": "Sort", + "browse.sortAlphabeticalByName": "By title", + "browse.sortAlphabeticalByArtist": "By artist", + "browse.sortAlphabeticalByAlbum": "By album", + "browse.sortNewest": "Recently added", + "browse.sortRecent": "Recently played", + "browse.sortFrequent": "Most played", + "browse.sortStarred": "Favourites", + "browse.sortByYear": "By year", + "browse.filterAlbums": "Filter by title or artist", + "browse.filterArtists": "Filter by name", + "browse.filterPlaceholder": "Filter…", + "browse.matches": { + one: "{count} of {total} shown", + other: "{count} of {total} shown", }, - "playlists.newName": "New playlist name", - "playlists.create": "Create playlist", - "playlists.play": "Play", - "playlists.addQueue": "Add queue", - "playlists.emptyOne": "This playlist is empty.", - "playlists.export": "Save as .m3u8", - "playlists.exportError": "That playlist could not be saved.", - "import.title": "Bring playlists from another player", - "import.detail": - "A .m3u or .m3u8 file. Nothing is created until you have seen what it would lose.", - "import.pick": "Choose a playlist file", - "import.name": "Playlist name", - "import.found": "{resolved} of {entries} entries are in your library.", - "import.encoding": - "The file was not UTF-8 and was read as {encoding}. Check the names below before importing.", - "import.missing": { - one: "{count} entry cannot be placed:", - other: "{count} entries cannot be placed:", + "browse.noMatch": "Nothing matches that filter.", + "browse.filterGenres": "Filter by genre", + "search.detail": "Find a title, album or artist", + "search.placeholder": "Title, album or artist", + "search.query": "Search query", + "search.tracks": "Tracks", + "search.empty": "Nothing found.", + "search.thisLibrary": "Searching {library}.", + "genres.detail": { one: "{count} genre", other: "{count} genres" }, + "genres.empty": "No genre is tagged in this library yet.", + "genres.emptyOne": "Nothing is tagged with this genre any more.", + "history.detail": { + one: "{count} track, most recent first", + other: "{count} tracks, most recent first", }, - "import.line": "Line {line}", - "import.noMatch": "no matching track", - "import.ambiguous": "matches several tracks", - "import.notAPath": "not a file in a library", - "import.duplicate": "already in this playlist", - "import.confirm": "Import", - "import.cancel": "Cancel", - "import.failed": "That file could not be read as a playlist.", - "playlists.empty": "Create a playlist to keep a set of tracks together.", - "playlists.createError": "The playlist could not be created.", - "playlists.queueError": "The queue could not be added to this playlist.", - "playlists.deleteError": "The playlist could not be deleted.", - "queue.detail": { - one: "{count} track synchronized with your account", - other: "{count} tracks synchronized with your account", + "history.empty": "Nothing has been played from this account yet.", + "random.detail": { + one: "{count} track drawn", + other: "{count} tracks drawn", }, - "queue.playStart": "Play from start", - "queue.clear": "Clear queue", - "queue.remove": "Remove", - "queue.empty": "Play an album or playlist to build your synchronized queue.", - "shares.detail": "Public links for selected music", - "shares.description": "Description", - "shares.create": "Share current queue", - "shares.needQueue": "Add tracks to the queue before creating a link.", - "shares.once": "This link is shown only once:", - "shares.defaultName": "Music share", - "shares.empty": "No public links have been created.", - "shares.createError": "The share could not be created.", - "shares.tooLong": - "A share holds up to {limit} tracks, and the queue holds {count}. Shorten the queue to share it.", - "shares.deleteError": "The share could not be deleted.", - "shares.maxVisits": "Visit limit (optional)", - "shares.visits": { one: "{count} visit", other: "{count} visits" }, - "shares.visitsOf": "{count} of {max} visits", - "share.kicker": "Shared with you", - "share.defaultTitle": "Music shared with you", - "share.until": "Available until {date}", - "share.empty": "None of these tracks can be played any more.", - "share.player": "Player", - "share.unavailableTitle": "This link opens nothing", - "share.unavailableDetail": - "It may have expired, been withdrawn, or been opened as many times as it allows.", - "playlists.public": "Visible to every account", - "playlists.publicError": "That playlist could not be published.", - "playlists.others": "From other accounts", - "playlists.othersDetail": - "Public playlists of other accounts, with the tracks your libraries hold.", - "playlists.by": "By {owner}", - "playlists.moveUp": "Move {title} up", - "playlists.moveDown": "Move {title} down", - "playlists.removeEntry": "Remove {title} from this playlist", - "playlists.moveError": - "That entry could not be moved. The playlist may have changed elsewhere; it has been read again.", - "playlists.removeEntryError": "That entry could not be removed.", - "radio.detail": "Internet stations, kept with your account", - "radio.name": "Station name", - "radio.address": "Stream address", - "radio.homepage": "Station page (optional)", - "radio.image": "Station image (optional)", - "radio.imageHint": - "The server downloads it once, when you save, and keeps it with the covers: your browser never contacts the image’s host.", - "radio.imageMissing": - "The image could not be fetched; save the station to try again.", - "radio.add": "Add station", - "radio.save": "Save station", - "radio.edit": "Edit", - "radio.replacing": - "Saving replaces every field, so an empty page address clears the stored one.", - "radio.listen": "Listen", - "radio.stop": "Stop", - "radio.visit": "Station page", - "radio.empty": "No stations yet. Add one to keep it here.", - "radio.addressRefused": "An address must start with http:// or https://.", - "radio.mixedContent": - "This page is served over HTTPS, so your browser will refuse a clear-text stream.", - "radio.full": "This account already holds as many stations as it may.", - "radio.saveError": "The station could not be saved.", - "radio.deleteError": "The station could not be deleted.", - "radio.playbackError": - "That station could not be played. The server never connects to it, so this is between your browser and the station.", - "radio.stations": "Stations", - "radio.newTitle": "Add a station", - "radio.editTitle": "Edit “{name}”", - "radio.listenTo": "Listen to {name}", - "radio.stopNamed": "Stop {name}", - "radio.actions": "Actions for {name}", - "radio.confirmDelete": - "Delete the station “{name}”? Its address and image are forgotten.", - "credential.password": "Dedicated Subsonic password", - "credential.rotate": "Rotate credential", - "credential.copy": "Copy this API key now:", - "credential.error": "Credential rotation failed.", + "random.again": "Draw again", + "random.fromGenre": "Play this genre at random", + "random.empty": "There is nothing to draw from yet.", + "library.active": "Library", } as const; diff --git a/webapp/src/state/i18n/en/login.ts b/webapp/src/state/i18n/en/login.ts new file mode 100644 index 00000000..2821cfd2 --- /dev/null +++ b/webapp/src/state/i18n/en/login.ts @@ -0,0 +1,28 @@ +// Sign-in and first-run setup. +export const login = { + "login.heroEyebrow": "Your music, on your terms", + "login.heroTitle": "After dark, the library comes alive.", + "login.heroDetail": "Private streaming, one catalogue, every screen.", + "login.server": "WaveFlow server", + "login.createAdmin": "Create the administrator", + "login.welcome": "Welcome back", + "login.setupDetail": + "This is a new server. Choose the first administrator account.", + "login.setupToken": "Setup token", + "login.setupTokenHint": + "The server wrote it to the file setup-token in its data directory, and its log names the path. If you set WAVEFLOW_SETUP_TOKEN, enter that value.", + "login.setupTokenError": + "This setup token is not the right one. Copy it again from the setup-token file.", + "login.username": "Username", + "login.password": "Password", + "login.setupError": + "Setup failed. Use a valid username and at least 12 password characters.", + "login.error": "Wrong username or password.", + "login.wait": "Please wait…", + "login.create": "Create and sign in", + "login.signIn": "Sign in", + "login.offline": { + one: "Listen to the {count} track kept in this browser", + other: "Listen to the {count} tracks kept in this browser", + }, +} as const; diff --git a/webapp/src/state/i18n/en/nav.ts b/webapp/src/state/i18n/en/nav.ts new file mode 100644 index 00000000..fd104066 --- /dev/null +++ b/webapp/src/state/i18n/en/nav.ts @@ -0,0 +1,42 @@ +// Navigation and the page for unknown routes. +export const nav = { + "nav.albums": "Albums", + "nav.artists": "Artists", + "nav.songs": "Songs", + "nav.search": "Search", + "nav.favourites": "Favourites", + "nav.playlists": "Playlists", + "nav.queue": "Queue", + "nav.shares": "Shares", + "nav.offline": "Offline", + "nav.radio": "Radio", + "nav.scrobbling": "Scrobbling", + "nav.admin": "Admin", + "nav.signOut": "Sign out", + "nav.skip": "Skip to content", + "nav.home": "WaveFlow server, home", + "nav.server": "server", + "nav.more": "More", + "nav.moreTitle": "Everything else", + "nav.moreDetail": + "The bar below holds what you reach for while listening. The rest is here.", + "nav.moreClose": "Close", + "nav.settings": "Settings", + "nav.genres": "Genres", + "nav.history": "Recently played", + "nav.random": "Random", + "nav.playing": "Now playing", + "nav.upload": "Upload", + "nav.folders": "Folders", + "nav.attention": "Needs attention", + "nav.quality": "Quality", + "nav.group.library": "Library", + "nav.group.listen": "Listening", + "nav.missing": "Missing files", + "nav.maintenance": "Maintenance", + "nav.stats": "Statistics", + "nav.podcasts": "Podcasts", + "notFound.title": "This room is silent", + "notFound.detail": "The page you asked for does not exist on this server.", + "notFound.back": "Back to the library", +} as const; diff --git a/webapp/src/state/i18n/en/player.ts b/webapp/src/state/i18n/en/player.ts new file mode 100644 index 00000000..48317f96 --- /dev/null +++ b/webapp/src/state/i18n/en/player.ts @@ -0,0 +1,77 @@ +// The player bar, now playing, radio and offline listening. +export const player = { + "player.error": "Playback could not start. Check the server and try again.", + "player.previous": "Previous track", + "player.play": "Play", + "player.pause": "Pause", + "player.next": "Next track", + "player.seek": "Seek", + "player.shuffle": "Shuffle", + "player.repeatOff": "Repeat: off", + "player.repeatAll": "Repeat: the whole queue", + "player.repeatOne": "Repeat: this track", + "player.volume": "Volume", + "player.mute": "Mute", + "player.unmute": "Unmute", + "player.openAlbum": "Open the album", + "player.live": "Live", + "player.liveRadio": "Live radio", + "player.stopLive": "Stop the station", + "player.liveError": "This station could not be played.", + "player.closeAside": "Stop and close", + "playing.idle": "Nothing is playing.", + "playing.empty": "Start a track and it will appear here, with its lyrics.", + "playing.viewLabel": "How to look at this track", + "playing.viewDetail": "Detail", + "playing.viewLyrics": "Lyrics", + "playing.viewImmersive": "Immersive", + "playing.immersiveTitle": "Immersive view", + "playing.leaveImmersive": "Leave the immersive view", + "radio.detail": "Internet stations, kept with your account", + "radio.name": "Station name", + "radio.address": "Stream address", + "radio.homepage": "Station page (optional)", + "radio.image": "Station image (optional)", + "radio.imageHint": + "The server downloads it once, when you save, and keeps it with the covers: your browser never contacts the image’s host.", + "radio.imageMissing": + "The image could not be fetched; save the station to try again.", + "radio.add": "Add station", + "radio.save": "Save station", + "radio.edit": "Edit", + "radio.replacing": + "Saving replaces every field, so an empty page address clears the stored one.", + "radio.listen": "Listen", + "radio.stop": "Stop", + "radio.visit": "Station page", + "radio.empty": "No stations yet. Add one to keep it here.", + "radio.addressRefused": "An address must start with http:// or https://.", + "radio.mixedContent": + "This page is served over HTTPS, so your browser will refuse a clear-text stream.", + "radio.full": "This account already holds as many stations as it may.", + "radio.saveError": "The station could not be saved.", + "radio.deleteError": "The station could not be deleted.", + "radio.playbackError": + "That station could not be played. The server never connects to it, so this is between your browser and the station.", + "radio.stations": "Stations", + "radio.newTitle": "Add a station", + "radio.editTitle": "Edit “{name}”", + "radio.listenTo": "Listen to {name}", + "radio.stopNamed": "Stop {name}", + "radio.actions": "Actions for {name}", + "radio.confirmDelete": + "Delete the station “{name}”? Its address and image are forgotten.", + "offline.kicker": "In this browser", + "offline.title": "Kept for offline", + "offline.detail": + "These tracks were kept in this browser for the account that last signed in, and play without a connection. Signing out deletes them from this browser. Keep more from a track's menu.", + "offline.unsupported": + "This browser cannot keep tracks for offline listening.", + "offline.back": "Back to the library", + "offline.empty": "Nothing is kept yet. A track's menu offers to keep it.", + "offline.player": "Offline player", + "offline.gone": "That copy is no longer in this browser.", + "offline.forget": "Remove the offline copy of {title}", + "offline.forgetShort": "Remove", + "offline.forgetDetail": "Frees {size}", +} as const; diff --git a/webapp/src/state/i18n/en/playing.ts b/webapp/src/state/i18n/en/playing.ts deleted file mode 100644 index f03fd67c..00000000 --- a/webapp/src/state/i18n/en/playing.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Genres, history, random, lyrics, now playing, bookmarks, the player bar. -export const playing = { - "browse.filterGenres": "Filter by genre", - "genres.detail": { one: "{count} genre", other: "{count} genres" }, - "genres.empty": "No genre is tagged in this library yet.", - "genres.emptyOne": "Nothing is tagged with this genre any more.", - "history.detail": { - one: "{count} track, most recent first", - other: "{count} tracks, most recent first", - }, - "history.empty": "Nothing has been played from this account yet.", - "random.detail": { - one: "{count} track drawn", - other: "{count} tracks drawn", - }, - "random.again": "Draw again", - "random.fromGenre": "Play this genre at random", - "random.empty": "There is nothing to draw from yet.", - "lyrics.label": "Lyrics", - "lyrics.none": "No lyrics travel with this file.", - "playing.idle": "Nothing is playing.", - "playing.empty": "Start a track and it will appear here, with its lyrics.", - "lyrics.seekHint": "This sheet carries timings — press a line to jump to it.", - "lyrics.seekTo": "Jump to {when}", - "lyrics.seekLine": "{line}, jump to {when}", - "playing.viewLabel": "How to look at this track", - "playing.viewDetail": "Detail", - "playing.viewLyrics": "Lyrics", - "playing.viewImmersive": "Immersive", - "playing.immersiveTitle": "Immersive view", - "playing.leaveImmersive": "Leave the immersive view", - "bookmarks.title": "Saved positions", - "bookmarks.detail": "One per track. Saving again moves it.", - "bookmarks.save": "Save this position", - "bookmarks.forget": "Forget", - "player.shuffle": "Shuffle", - "player.repeatOff": "Repeat: off", - "player.repeatAll": "Repeat: the whole queue", - "player.repeatOne": "Repeat: this track", - "player.volume": "Volume", - "player.mute": "Mute", - "player.unmute": "Unmute", - "player.openAlbum": "Open the album", - "player.live": "Live", - "player.liveRadio": "Live radio", - "player.stopLive": "Stop the station", - "player.liveError": "This station could not be played.", - "player.closeAside": "Stop and close", - "library.active": "Library", - "search.thisLibrary": "Searching {library}.", -} as const; diff --git a/webapp/src/state/i18n/en/playlists.ts b/webapp/src/state/i18n/en/playlists.ts new file mode 100644 index 00000000..b15ef2ef --- /dev/null +++ b/webapp/src/state/i18n/en/playlists.ts @@ -0,0 +1,84 @@ +// Playlists and their import, the queue, shares and bookmarks. +export const playlists = { + "playlists.detail": { + one: "{count} collection", + other: "{count} collections", + }, + "playlists.newName": "New playlist name", + "playlists.create": "Create playlist", + "playlists.play": "Play", + "playlists.addQueue": "Add queue", + "playlists.emptyOne": "This playlist is empty.", + "playlists.export": "Save as .m3u8", + "playlists.exportError": "That playlist could not be saved.", + "playlists.empty": "Create a playlist to keep a set of tracks together.", + "playlists.createError": "The playlist could not be created.", + "playlists.queueError": "The queue could not be added to this playlist.", + "playlists.deleteError": "The playlist could not be deleted.", + "playlists.public": "Visible to every account", + "playlists.publicError": "That playlist could not be published.", + "playlists.others": "From other accounts", + "playlists.othersDetail": + "Public playlists of other accounts, with the tracks your libraries hold.", + "playlists.by": "By {owner}", + "playlists.moveUp": "Move {title} up", + "playlists.moveDown": "Move {title} down", + "playlists.removeEntry": "Remove {title} from this playlist", + "playlists.moveError": + "That entry could not be moved. The playlist may have changed elsewhere; it has been read again.", + "playlists.removeEntryError": "That entry could not be removed.", + "import.title": "Bring playlists from another player", + "import.detail": + "A .m3u or .m3u8 file. Nothing is created until you have seen what it would lose.", + "import.pick": "Choose a playlist file", + "import.name": "Playlist name", + "import.found": "{resolved} of {entries} entries are in your library.", + "import.encoding": + "The file was not UTF-8 and was read as {encoding}. Check the names below before importing.", + "import.missing": { + one: "{count} entry cannot be placed:", + other: "{count} entries cannot be placed:", + }, + "import.line": "Line {line}", + "import.noMatch": "no matching track", + "import.ambiguous": "matches several tracks", + "import.notAPath": "not a file in a library", + "import.duplicate": "already in this playlist", + "import.confirm": "Import", + "import.cancel": "Cancel", + "import.failed": "That file could not be read as a playlist.", + "queue.detail": { + one: "{count} track synchronized with your account", + other: "{count} tracks synchronized with your account", + }, + "queue.playStart": "Play from start", + "queue.clear": "Clear queue", + "queue.remove": "Remove", + "queue.empty": "Play an album or playlist to build your synchronized queue.", + "shares.detail": "Public links for selected music", + "shares.description": "Description", + "shares.create": "Share current queue", + "shares.needQueue": "Add tracks to the queue before creating a link.", + "shares.once": "This link is shown only once:", + "shares.defaultName": "Music share", + "shares.empty": "No public links have been created.", + "shares.createError": "The share could not be created.", + "shares.tooLong": + "A share holds up to {limit} tracks, and the queue holds {count}. Shorten the queue to share it.", + "shares.deleteError": "The share could not be deleted.", + "shares.maxVisits": "Visit limit (optional)", + "shares.visits": { one: "{count} visit", other: "{count} visits" }, + "shares.visitsOf": "{count} of {max} visits", + "share.kicker": "Shared with you", + "share.defaultTitle": "Music shared with you", + "share.until": "Available until {date}", + "share.empty": "None of these tracks can be played any more.", + "share.player": "Player", + "share.unavailableTitle": "This link opens nothing", + "share.unavailableDetail": + "It may have expired, been withdrawn, or been opened as many times as it allows.", + "bookmarks.title": "Saved positions", + "bookmarks.detail": "One per track. Saving again moves it.", + "bookmarks.save": "Save this position", + "bookmarks.forget": "Forget", +} as const; diff --git a/webapp/src/state/i18n/en/podcasts.ts b/webapp/src/state/i18n/en/podcasts.ts index 3578a1f4..bdad5da0 100644 --- a/webapp/src/state/i18n/en/podcasts.ts +++ b/webapp/src/state/i18n/en/podcasts.ts @@ -1,6 +1,5 @@ // Podcast subscriptions and episodes. export const podcasts = { - "nav.podcasts": "Podcasts", "podcasts.detail": "Shows this server follows, for every account", "podcasts.off": "Podcasts are switched off on this server. What was kept before stays readable; nothing new is fetched.", diff --git a/webapp/src/state/i18n/en/preferences.ts b/webapp/src/state/i18n/en/preferences.ts new file mode 100644 index 00000000..bab29308 --- /dev/null +++ b/webapp/src/state/i18n/en/preferences.ts @@ -0,0 +1,48 @@ +// Preferences, ReplayGain, the settings page and themes. +export const preferences = { + "preferences.theme": "Theme", + "preferences.language": "Language", + "preferences.themeLight": "Light", + "preferences.themeDark": "Dark", + "preferences.followSystem": "Follow the system", + "preferences.followSystemDetail": + "Switch between a theme’s light and dark versions when your device does.", + "preferences.followingLight": + "Your device is in light mode, so the light version is shown. Picking a theme picks both of its versions.", + "preferences.followingDark": + "Your device is in dark mode, so the dark version is shown. Picking a theme picks both of its versions.", + "preferences.standInLight": + "{theme} has no light version, so {fallback} stands in while your device is in light mode.", + "preferences.standInDark": + "{theme} has no dark version, so {fallback} stands in while your device is in dark mode.", + "replayGain.label": "ReplayGain", + "replayGain.off": "Off", + "replayGain.track": "Track gain", + "replayGain.album": "Album gain", + "replayGain.preamp": "Preamp", + "replayGain.detail": + "Plays every track at the loudness its tags measured, so one record is not louder than the next. Album gain keeps the differences inside an album. A track without tags plays as it is. It routes playback through Web Audio, which some phones pause with the screen.", + "settings.detail": + "Appearance, language, and the clients allowed to read your library.", + "settings.appearance": "Appearance", + "settings.playback": "Playback", + "settings.sections": "Settings sections", + "settings.section.account": "Account", + "settings.playbackDetail": + "How this browser plays your tracks. Kept in this browser, not on your account.", + "settings.levelling": "Volume levelling", + "appearance.themes.default": "Emerald light", + "appearance.themes.midnightLight": "Midnight light", + "appearance.themes.sunsetLight": "Sunset light", + "appearance.themes.lavenderLight": "Lavender light", + "appearance.themes.crimsonLight": "Crimson light", + "appearance.themes.oceanLight": "Ocean light", + "appearance.themes.defaultDark": "Emerald dark", + "appearance.themes.oled": "OLED black", + "appearance.themes.midnight": "Midnight", + "appearance.themes.sunset": "Sunset", + "appearance.themes.lavender": "Lavender", + "appearance.themes.crimson": "Crimson", + "appearance.themes.ocean": "Ocean", + "appearance.themes.neon": "Neon", +} as const; diff --git a/webapp/src/state/i18n/en/quality.ts b/webapp/src/state/i18n/en/quality.ts index c8997764..c51de9fa 100644 --- a/webapp/src/state/i18n/en/quality.ts +++ b/webapp/src/state/i18n/en/quality.ts @@ -1,51 +1,12 @@ -// The attention lists and the audio quality analysis. +// Audio quality labels and the quality analysis. export const quality = { - "attention.lists": "Lists", - "attention.total": { - one: "{count} thing to look at", - other: "{count} things to look at", - }, - "attention.nothing": "Nothing needs attention.", - "attention.notYours": - "Only the owners and managers of this library see what needs attention in it.", - "attention.empty": "Nothing in this list.", - "attention.tracksNote": - "Fix a track from its menu: once corrected, it leaves the list.", - "attention.creditsNote": - "Each of these credits names several artists in one string. Split it into the artists it names, or keep it as one when it is a single name. Every later scan applies the decision.", - "attention.pending": "To decide", - "attention.decided": "Decided", - "attention.split": "Split", - "attention.confirmSplit": "Confirm: split into {count}", - "attention.keep": "Keep as one", - "attention.splitDone": "{name} is split.", - "attention.keepDone": "{name} stays as one.", - "attention.undone": "The decision about {name} is undone.", - "attention.alreadyDecided": - "This credit was decided in the meantime. The list has been reloaded.", - "attention.failed": "That did not work. Try again.", - "attention.noCredits": "No credit is waiting for a decision.", - "attention.noDecisions": "Nothing has been decided yet.", - "attention.fragments": "What {name} would split into", - "attention.knownFragment": "Already an artist in this library", - "attention.decidedSplit": "Split into {parts}", - "attention.decidedKeep": "Kept as one", - "attention.decidedBy": "By {who}, {when}", - "attention.undo": "Undo", - "attention.duplicateGroup": "Probable duplicate {group}", - "attention.split_credits": "Artists to split", - "attention.missing_artist": "Missing artist", - "attention.missing_album": "Missing album", - "attention.missing_year": "Missing year", - "attention.missing_track_number": "Missing track number", - "attention.missing_cover": "Missing cover", - "attention.album_year_conflict": "Albums with several years", - "attention.compilation_no_album_artist": - "Compilations without an album artist", - "attention.duplicate_track_number": "Duplicate track numbers", - "attention.track_number_gap": "Gaps in track numbers", - "attention.probable_duplicate": "Probable duplicates", - "attention.probable_fake_lossless": "Probable fake lossless", + "quality.label": "File quality", + "quality.unknown": "Unknown", + "quality.low": "Low", + "quality.standard": "Standard", + "quality.high": "High", + "quality.lossless": "Lossless", + "quality.hiRes": "Hi-res", "quality.help": "Measured from the audio rather than read from the container: where the highs stop, and how many bits the samples use. A lossless file whose highs stop around 16 kHz was a 128 kbps MP3 before it was a FLAC. “No sign of a lossy source” is not proof: an encoder that keeps the full band leaves nothing to find.", "quality.progress": "{measured} of {lossless} lossless files analysed", diff --git a/webapp/src/state/i18n/en/scrobbling.ts b/webapp/src/state/i18n/en/scrobbling.ts index 048414fe..e4b41b04 100644 --- a/webapp/src/state/i18n/en/scrobbling.ts +++ b/webapp/src/state/i18n/en/scrobbling.ts @@ -1,22 +1,5 @@ -// Offline listening and scrobbling. +// Scrobbling. export const scrobbling = { - "offline.kicker": "In this browser", - "offline.title": "Kept for offline", - "offline.detail": - "These tracks were kept in this browser for the account that last signed in, and play without a connection. Signing out deletes them from this browser. Keep more from a track's menu.", - "offline.unsupported": - "This browser cannot keep tracks for offline listening.", - "offline.back": "Back to the library", - "offline.empty": "Nothing is kept yet. A track's menu offers to keep it.", - "offline.player": "Offline player", - "offline.gone": "That copy is no longer in this browser.", - "offline.forget": "Remove the offline copy of {title}", - "offline.forgetShort": "Remove", - "offline.forgetDetail": "Frees {size}", - "login.offline": { - one: "Listen to the {count} track kept in this browser", - other: "Listen to the {count} tracks kept in this browser", - }, "scrobbling.title": "External scrobbling", "scrobbling.detail": "Forward what you listen to here to ListenBrainz, Maloja or Last.fm.", diff --git a/webapp/src/state/i18n/en/shell.ts b/webapp/src/state/i18n/en/shell.ts deleted file mode 100644 index f137e72d..00000000 --- a/webapp/src/state/i18n/en/shell.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Navigation, preferences, ReplayGain, shared words, album and song views. -export const shell = { - "nav.albums": "Albums", - "nav.artists": "Artists", - "nav.songs": "Songs", - "nav.search": "Search", - "nav.favourites": "Favourites", - "nav.playlists": "Playlists", - "nav.queue": "Queue", - "nav.shares": "Shares", - "nav.offline": "Offline", - "nav.radio": "Radio", - "nav.scrobbling": "Scrobbling", - "nav.admin": "Admin", - "nav.signOut": "Sign out", - "nav.skip": "Skip to content", - "nav.home": "WaveFlow server, home", - "nav.server": "server", - "nav.more": "More", - "nav.moreTitle": "Everything else", - "nav.moreDetail": - "The bar below holds what you reach for while listening. The rest is here.", - "nav.moreClose": "Close", - "preferences.theme": "Theme", - "preferences.language": "Language", - "preferences.themeLight": "Light", - "preferences.themeDark": "Dark", - "preferences.followSystem": "Follow the system", - "preferences.followSystemDetail": - "Switch between a theme’s light and dark versions when your device does.", - "preferences.followingLight": - "Your device is in light mode, so the light version is shown. Picking a theme picks both of its versions.", - "preferences.followingDark": - "Your device is in dark mode, so the dark version is shown. Picking a theme picks both of its versions.", - "preferences.standInLight": - "{theme} has no light version, so {fallback} stands in while your device is in light mode.", - "preferences.standInDark": - "{theme} has no dark version, so {fallback} stands in while your device is in dark mode.", - "replayGain.label": "ReplayGain", - "replayGain.off": "Off", - "replayGain.track": "Track gain", - "replayGain.album": "Album gain", - "replayGain.preamp": "Preamp", - "replayGain.detail": - "Plays every track at the loudness its tags measured, so one record is not louder than the next. Album gain keeps the differences inside an album. A track without tags plays as it is. It routes playback through Web Audio, which some phones pause with the screen.", - "common.loading": "Loading", - "common.loadError": "We could not load this view", - "common.noAlbums": "No albums yet.", - "common.noArtists": "No artists yet.", - "common.variousArtists": "Various artists", - "common.unknownArtist": "Unknown artist", - "common.album": "Album", - "album.play": "Play album", - "album.addQueue": "Add to queue", - "album.title": "Title", - "album.duration": "Duration", - "album.trackActions": "Track actions", - "album.moreBy": "More by {artist}", - "common.artist": "Artist", - "common.albums": { one: "{count} album", other: "{count} albums" }, - "common.tracks": { one: "{count} track", other: "{count} tracks" }, - "songs.loadedSoFar": { - one: "{count} track loaded", - other: "{count} tracks loaded", - }, - "songs.playLoaded": { - one: "Play {count} loaded track", - other: "Play {count} loaded tracks", - }, - "songs.playAll": { - one: "Play the track", - other: "Play all {count} tracks", - }, - "songs.loadMore": "Load more", - "songs.loadMoreError": "That page could not be loaded. Try again.", - "songs.loading": "Loading…", - "songs.empty": "This library holds no tracks yet.", - "file.megabytes": "{size} MB", - "file.kilobytes": "{size} kB", -} as const; diff --git a/webapp/src/state/i18n/en/stats.ts b/webapp/src/state/i18n/en/stats.ts index 2d2a6904..f820fd68 100644 --- a/webapp/src/state/i18n/en/stats.ts +++ b/webapp/src/state/i18n/en/stats.ts @@ -1,9 +1,5 @@ -// Lyrics from LRCLIB, listening statistics and mixes. +// Listening statistics and mixes. export const stats = { - // RFC-024: lyrics from LRCLIB, statistics and mixes. - "lyrics.source.lrclib": "From LRCLIB", - "lyrics.fromLrclib": "Lyrics from LRCLIB", - "nav.stats": "Statistics", "stats.detail": "What this account listened to, counted from its own plays.", "stats.period": "Period", "stats.period7d": "Last 7 days", diff --git a/webapp/src/state/i18n/en/track.ts b/webapp/src/state/i18n/en/track.ts index f1ed1835..32a6b866 100644 --- a/webapp/src/state/i18n/en/track.ts +++ b/webapp/src/state/i18n/en/track.ts @@ -1,7 +1,39 @@ -// The track menu and properties, lyrics sheets, tag corrections, canvases. +// The track menu, track properties, tag corrections, lyrics and canvases. export const track = { "correction.edit": "Correct tags", "correction.editTrack": "Correct tags: {title}", + "correction.detail": + "The file is never rewritten. A correction is stored beside it, survives a rescan, and every member of the library sees it.", + "correction.fieldTitle": "Title", + "correction.fieldSortTitle": "Sort title", + "correction.fieldYear": "Year", + "correction.fieldTrackNumber": "Track number", + "correction.fieldDiscNumber": "Disc number", + "correction.fieldMusicbrainz": "MusicBrainz recording ID", + "correction.fieldComment": "Comment", + "correction.fieldArtists": "Artists", + "correction.fieldGenres": "Genres", + "correction.listHint": "One name per line. Leave it empty to credit nobody.", + "correction.corrected": "Corrected", + "correction.fromFile": "The file says: {value}", + "correction.fileEmpty": "The file says nothing here", + "correction.restore": "Restore the file’s value", + "correction.keep": "Keep the correction", + "correction.restoring": "Goes back to the file’s value when saved", + "correction.restoringList": + "Read back from the file when saved — it cannot be shown beforehand", + "correction.save": "Save", + "correction.saving": "Saving…", + "correction.saved": "Saved.", + "correction.nothing": "Nothing has changed.", + "correction.invalid": "Check {fields}.", + "correction.refused": "The server refused these values.", + "correction.notAllowed": + "Only an owner or a manager of this library can correct its tags.", + "correction.error": "The corrections could not be saved.", + "correction.stale": + "Saved, but the corrections could not be read back. Reload the page before saving again.", + "correction.back": "Back to {album}", "trackMenu.label": "Actions for {title}", "trackMenu.play": "Play", "trackMenu.queue": "Add to queue", @@ -14,6 +46,7 @@ export const track = { "trackMenu.goToArtist": "Go to {name}", "trackMenu.properties": "Properties", "trackMenu.edit": "Edit tags", + "trackMenu.move": "Move to folder…", "properties.kicker": "Properties", "properties.close": "Close properties", "properties.loadError": "Some properties could not be loaded.", @@ -49,6 +82,15 @@ export const track = { "properties.synced": "Synced, {lang}", "properties.plain": "Plain text, {lang}", "properties.noLyrics": "None", + "properties.lyricsSheets": "Sheets", + "properties.kbps": "{value} kbps", + "properties.khz": "{value} kHz", + "properties.bits": "{value} bit", + "properties.mb": "{value} MB", + "properties.db": "{value} dB", + "properties.dbPeak": "{value} dB, peak {peak}", + "properties.corrected": "Corrected", + "properties.fileSaid": "Corrected — the file says {value}", "lyrics.source.uploadTtml": "Uploaded TTML", "lyrics.source.uploadLrc": "Uploaded LRC", "lyrics.source.uploadTxt": "Uploaded text", @@ -64,47 +106,14 @@ export const track = { "lyrics.tooLarge": "That file is larger than 1 MB.", "lyrics.failed": "The lyrics could not be saved. Try again.", "lyrics.downloadFailed": "The lyrics could not be downloaded. Try again.", - "properties.lyricsSheets": "Sheets", - "properties.kbps": "{value} kbps", - "properties.khz": "{value} kHz", - "properties.bits": "{value} bit", - "properties.mb": "{value} MB", - "properties.db": "{value} dB", - "properties.dbPeak": "{value} dB, peak {peak}", - "properties.corrected": "Corrected", - "properties.fileSaid": "Corrected — the file says {value}", - "correction.detail": - "The file is never rewritten. A correction is stored beside it, survives a rescan, and every member of the library sees it.", - "correction.fieldTitle": "Title", - "correction.fieldSortTitle": "Sort title", - "correction.fieldYear": "Year", - "correction.fieldTrackNumber": "Track number", - "correction.fieldDiscNumber": "Disc number", - "correction.fieldMusicbrainz": "MusicBrainz recording ID", - "correction.fieldComment": "Comment", - "correction.fieldArtists": "Artists", - "correction.fieldGenres": "Genres", - "correction.listHint": "One name per line. Leave it empty to credit nobody.", - "correction.corrected": "Corrected", - "correction.fromFile": "The file says: {value}", - "correction.fileEmpty": "The file says nothing here", - "correction.restore": "Restore the file’s value", - "correction.keep": "Keep the correction", - "correction.restoring": "Goes back to the file’s value when saved", - "correction.restoringList": - "Read back from the file when saved — it cannot be shown beforehand", - "correction.save": "Save", - "correction.saving": "Saving…", - "correction.saved": "Saved.", - "correction.nothing": "Nothing has changed.", - "correction.invalid": "Check {fields}.", - "correction.refused": "The server refused these values.", - "correction.notAllowed": - "Only an owner or a manager of this library can correct its tags.", - "correction.error": "The corrections could not be saved.", - "correction.stale": - "Saved, but the corrections could not be read back. Reload the page before saving again.", - "correction.back": "Back to {album}", + "lyrics.label": "Lyrics", + "lyrics.none": "No lyrics travel with this file.", + "lyrics.seekHint": "This sheet carries timings — press a line to jump to it.", + "lyrics.seekTo": "Jump to {when}", + "lyrics.seekLine": "{line}, jump to {when}", + // RFC-024: lyrics from LRCLIB, statistics and mixes. + "lyrics.source.lrclib": "From LRCLIB", + "lyrics.fromLrclib": "Lyrics from LRCLIB", "canvas.title": "Canvas", "canvas.detail": "A short loop played without sound over the cover, on the Now playing page. The server takes mp4 or webm.", diff --git a/webapp/src/state/i18n/fr/maintenance.ts b/webapp/src/state/i18n/fr/admin.ts similarity index 81% rename from webapp/src/state/i18n/fr/maintenance.ts rename to webapp/src/state/i18n/fr/admin.ts index ea9406e2..b5159510 100644 --- a/webapp/src/state/i18n/fr/maintenance.ts +++ b/webapp/src/state/i18n/fr/admin.ts @@ -1,17 +1,29 @@ -// Scan progress, accounts and tokens, maintenance and missing files. +// Administration, scans, maintenance and missing files. import type { TranslationValue } from ".."; -import type { maintenance as en } from "../en/maintenance"; +import type { admin as en } from "../en/admin"; -export const maintenance: Record = { - "scan.connecting": "En attente du premier relevé…", - "scan.queued": "En attente", - "scan.running": "Analyse en cours", - "scan.completed": "Terminé", - "scan.failed": "Échec", - "scan.progress": "Progression de l’analyse", - "scan.counted": "{processed} fichiers sur {total}", - "scan.tally": - "{added} ajoutés · {updated} mis à jour · {moved} déplacés · {skipped} inchangés · {errors} erreurs", +export const admin: Record = { + "admin.title": "Administration", + "admin.detail": "Bibliothèques, scans et comptes", + "admin.libraries": "Bibliothèques", + "admin.libraryName": "Nom de la bibliothèque", + "admin.libraryPath": "Chemin absolu du dossier sur le serveur", + "admin.registering": "Ajout en cours…", + "admin.register": "Ajouter et scanner", + "admin.scan": "Scanner", + "admin.accounts": "Comptes", + "admin.webPassword": "Mot de passe web, 12 caractères minimum", + "admin.creating": "Création…", + "admin.create": "Créer le compte", + "admin.enable": "Activer", + "admin.disable": "Désactiver", + "admin.currentAccount": "Vous ne pouvez pas désactiver votre compte actuel", + "admin.libraryError": "Le dossier n’a pas pu être enregistré.", + "admin.accountError": "Le compte n’a pas pu être créé.", + "admin.statusError": "Le statut du compte n’a pas pu être modifié.", + "admin.scanError": "Le scan n’a pas pu démarrer.", + "admin.scanQueued": "Scan {id} ajouté à la file.", + "admin.initialScan": "Scan initial {id} ajouté à la file.", "admin.scanning": "Analyse en cours", "admin.nowPlaying": "Écoutes en cours", "admin.nobodyListening": "Personne n’écoute en ce moment.", @@ -24,8 +36,6 @@ export const maintenance: Record = { "admin.tokenUsed": "déjà utilisé", "admin.tokenUnused": "jamais utilisé", "admin.tokenNone": "Aucun jeton sur ce compte.", - "scan.lost": - "Le flux de progression n’a pas pu s’ouvrir. L’analyse elle-même se poursuit.", "admin.members": "Qui peut voir cette bibliothèque", "admin.memberRole": "Rôle", "admin.memberRemove": "Retirer", @@ -55,9 +65,18 @@ export const maintenance: Record = { "admin.deleted": "{name} a été supprimée : {tracks} titres ont quitté le catalogue.", "admin.deleteError": "La bibliothèque n’a pas pu être supprimée.", + "scan.connecting": "En attente du premier relevé…", + "scan.queued": "En attente", + "scan.running": "Analyse en cours", + "scan.completed": "Terminé", + "scan.failed": "Échec", + "scan.progress": "Progression de l’analyse", + "scan.counted": "{processed} fichiers sur {total}", + "scan.tally": + "{added} ajoutés · {updated} mis à jour · {moved} déplacés · {skipped} inchangés · {errors} erreurs", + "scan.lost": + "Le flux de progression n’a pas pu s’ouvrir. L’analyse elle-même se poursuit.", "scan.leftOut": "{excluded} écartés · {links} liens non suivis", - "nav.missing": "Fichiers manquants", - "nav.maintenance": "Maintenance", "maintenance.sections": "Sections de maintenance", "maintenance.none": "Rien ici n’est à vous d’entretenir dans cette bibliothèque.", diff --git a/webapp/src/state/i18n/fr/attention.ts b/webapp/src/state/i18n/fr/attention.ts new file mode 100644 index 00000000..68e2eaac --- /dev/null +++ b/webapp/src/state/i18n/fr/attention.ts @@ -0,0 +1,55 @@ +// The attention lists and artist splits. +import type { TranslationValue } from ".."; +import type { attention as en } from "../en/attention"; + +export const attention: Record = { + "attention.lists": "Listes", + "attention.total": { + one: "{count} point à vérifier", + other: "{count} points à vérifier", + }, + "attention.nothing": "Rien à corriger.", + "attention.notYours": + "Seuls les propriétaires et les gestionnaires de cette bibliothèque voient ce qui y est à corriger.", + "attention.empty": "Rien dans cette liste.", + "attention.tracksNote": + "Corrigez une piste depuis son menu : une fois corrigée, elle quitte la liste.", + "attention.creditsNote": + "Chacun de ces crédits nomme plusieurs artistes dans une seule chaîne. Scindez-le en ces artistes, ou gardez-le tel quel s’il s’agit d’un seul nom. Chaque scan suivant applique la décision.", + "attention.pending": "À décider", + "attention.decided": "Décidés", + "attention.split": "Scinder", + "attention.confirmSplit": "Confirmer : scinder en {count}", + "attention.keep": "Garder tel quel", + "attention.splitDone": "{name} est scindé.", + "attention.keepDone": "{name} reste tel quel.", + "attention.undone": "La décision sur {name} est annulée.", + "attention.alreadyDecided": + "Ce crédit a été décidé entre-temps. La liste a été rechargée.", + "attention.failed": "Cela n’a pas fonctionné. Réessayez.", + "attention.noCredits": "Aucun crédit n’attend de décision.", + "attention.noDecisions": "Aucune décision pour l’instant.", + "attention.fragments": "Ce en quoi {name} serait scindé", + "attention.knownFragment": "Déjà un artiste de cette bibliothèque", + "attention.decidedSplit": "Scindé en {parts}", + "attention.decidedKeep": "Gardé tel quel", + "attention.decidedBy": "Par {who}, le {when}", + "attention.undo": "Annuler", + "attention.duplicateGroup": "Doublon probable {group}", + "attention.split_credits": "Artistes à scinder", + "attention.missing_artist": "Sans artiste", + "attention.missing_album": "Sans album", + "attention.missing_year": "Sans année", + "attention.missing_track_number": "Sans numéro de piste", + "attention.missing_cover": "Sans pochette", + "attention.album_year_conflict": "Albums à plusieurs années", + "attention.compilation_no_album_artist": "Compilations sans artiste d’album", + "attention.duplicate_track_number": "Numéros de piste en double", + "attention.track_number_gap": "Trous dans la numérotation", + "attention.probable_duplicate": "Doublons probables", + "attention.probable_fake_lossless": "Faux lossless probables", + "artistSplit.offer": "Scinder en {count} artistes", + "artistSplit.confirm": "Confirmer : scinder en {count}", + "artistSplit.done": "Scindé en {parts}.", + "artistSplit.failed": "Ce crédit n’a pas pu être scindé. Réessayez.", +}; diff --git a/webapp/src/state/i18n/fr/browse.ts b/webapp/src/state/i18n/fr/browse.ts deleted file mode 100644 index 18790b57..00000000 --- a/webapp/src/state/i18n/fr/browse.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Administration, search, player buttons, OAuth consent, themes, browsing. -import type { TranslationValue } from ".."; -import type { browse as en } from "../en/browse"; - -export const browse: Record = { - "admin.title": "Administration", - "admin.detail": "Bibliothèques, scans et comptes", - "admin.libraries": "Bibliothèques", - "admin.libraryName": "Nom de la bibliothèque", - "admin.libraryPath": "Chemin absolu du dossier sur le serveur", - "admin.registering": "Ajout en cours…", - "admin.register": "Ajouter et scanner", - "admin.scan": "Scanner", - "admin.accounts": "Comptes", - "admin.webPassword": "Mot de passe web, 12 caractères minimum", - "admin.creating": "Création…", - "admin.create": "Créer le compte", - "admin.enable": "Activer", - "admin.disable": "Désactiver", - "admin.currentAccount": "Vous ne pouvez pas désactiver votre compte actuel", - "admin.libraryError": "Le dossier n’a pas pu être enregistré.", - "admin.accountError": "Le compte n’a pas pu être créé.", - "admin.statusError": "Le statut du compte n’a pas pu être modifié.", - "admin.scanError": "Le scan n’a pas pu démarrer.", - "admin.scanQueued": "Scan {id} ajouté à la file.", - "admin.initialScan": "Scan initial {id} ajouté à la file.", - "search.detail": "Trouvez un titre, un album ou un artiste", - "search.placeholder": "Titre, album ou artiste", - "search.query": "Requête de recherche", - "search.tracks": "Pistes", - "search.empty": "Aucun résultat.", - "player.error": "Lecture impossible. Vérifiez le serveur puis réessayez.", - "player.previous": "Piste précédente", - "player.play": "Lire", - "player.pause": "Pause", - "player.next": "Piste suivante", - "player.seek": "Position de lecture", - "authorize.title": "Autorisation", - "authorize.invalid": - "Ce lien d’autorisation est incomplet ou mène vers une destination non fiable.", - "authorize.client": "Autoriser {client}", - "authorize.detail": - "Cette application pourra parcourir vos bibliothèques, lire votre musique et gérer vos playlists, favoris et notes, comme ce compte.", - "authorize.redirect": "Retour vers {url}", - "authorize.error": "La demande d’autorisation de l’application est invalide.", - "authorize.busy": "Autorisation…", - "authorize.approve": "Autoriser", - "notFound.title": "Cette salle est silencieuse", - "notFound.detail": "La page demandée n’existe pas sur ce serveur.", - "notFound.back": "Retour à la bibliothèque", - "appearance.themes.default": "Émeraude clair", - "appearance.themes.midnightLight": "Minuit clair", - "appearance.themes.sunsetLight": "Coucher de soleil clair", - "appearance.themes.lavenderLight": "Lavande clair", - "appearance.themes.crimsonLight": "Cramoisi clair", - "appearance.themes.oceanLight": "Océan clair", - "appearance.themes.defaultDark": "Émeraude sombre", - "appearance.themes.oled": "Noir OLED", - "appearance.themes.midnight": "Minuit", - "appearance.themes.sunset": "Coucher de soleil", - "appearance.themes.lavender": "Lavande", - "appearance.themes.crimson": "Cramoisi", - "appearance.themes.ocean": "Océan", - "appearance.themes.neon": "Néon", - "browse.sort": "Trier", - "browse.sortAlphabeticalByName": "Par titre", - "browse.sortAlphabeticalByArtist": "Par artiste", - "browse.sortAlphabeticalByAlbum": "Par album", - "browse.sortNewest": "Ajoutés récemment", - "browse.sortRecent": "Écoutés récemment", - "browse.sortFrequent": "Les plus écoutés", - "browse.sortStarred": "Favoris", - "browse.sortByYear": "Par année", - "browse.filterAlbums": "Filtrer par titre ou artiste", - "browse.filterArtists": "Filtrer par nom", - "browse.filterPlaceholder": "Filtrer…", - "browse.matches": { - one: "{count} sur {total} affiché", - other: "{count} sur {total} affichés", - }, - "browse.noMatch": "Aucun résultat pour ce filtre.", - "card.play": "Lire", - "card.queue": "Mettre en file", - "rating.label": "Note", - "rating.stars": { one: "{count} étoile", other: "{count} étoiles" }, - "nav.genres": "Genres", - "nav.history": "Écoutes récentes", - "nav.random": "Aléatoire", - "nav.playing": "En écoute", - "nav.upload": "Téléverser", - "nav.folders": "Dossiers", - "nav.attention": "À corriger", - "nav.quality": "Qualité", -}; diff --git a/webapp/src/state/i18n/fr/settings.ts b/webapp/src/state/i18n/fr/clients.ts similarity index 63% rename from webapp/src/state/i18n/fr/settings.ts rename to webapp/src/state/i18n/fr/clients.ts index a69191d2..702306bf 100644 --- a/webapp/src/state/i18n/fr/settings.ts +++ b/webapp/src/state/i18n/fr/clients.ts @@ -1,18 +1,8 @@ -// Settings, registered clients, the Subsonic password and quality labels. +// Registered clients, the Subsonic password and OAuth consent. import type { TranslationValue } from ".."; -import type { settings as en } from "../en/settings"; +import type { clients as en } from "../en/clients"; -export const settings: Record = { - "nav.settings": "Réglages", - "settings.detail": - "Apparence, langue, et les clients autorisés à lire votre bibliothèque.", - "settings.appearance": "Apparence", - "settings.playback": "Lecture", - "settings.sections": "Sections des réglages", - "settings.section.account": "Compte", - "settings.playbackDetail": - "Comment ce navigateur lit vos titres. Gardé dans ce navigateur, pas sur votre compte.", - "settings.levelling": "Normalisation du volume", +export const clients: Record = { "clients.title": "Clients enregistrés", "clients.detail": "Appareils et scripts détenant un justificatif sur votre bibliothèque. En révoquer un l’arrête aussitôt.", @@ -44,14 +34,18 @@ export const settings: Record = { "Vous n’avez pas de mot de passe Subsonic, donc aucun client Subsonic ne peut lire votre bibliothèque. Demandez-en un à l’administrateur.", "subsonic.short": "Utilisez au moins 12 caractères.", "subsonic.error": "Cela n’a pas fonctionné. Rien n’a changé.", - "quality.label": "Qualité du fichier", - "quality.unknown": "Inconnue", - "quality.low": "Faible", - "quality.standard": "Standard", - "quality.high": "Élevée", - "quality.lossless": "Sans perte", - "quality.hiRes": "Haute résolution", - "file.channels": { one: "mono", other: "{count} canaux" }, - "common.delete": "Supprimer", - "common.cancel": "Annuler", + "credential.password": "Mot de passe Subsonic dédié", + "credential.rotate": "Renouveler l’identifiant", + "credential.copy": "Copiez cette clé API maintenant :", + "credential.error": "Le renouvellement de l’identifiant a échoué.", + "authorize.title": "Autorisation", + "authorize.invalid": + "Ce lien d’autorisation est incomplet ou mène vers une destination non fiable.", + "authorize.client": "Autoriser {client}", + "authorize.detail": + "Cette application pourra parcourir vos bibliothèques, lire votre musique et gérer vos playlists, favoris et notes, comme ce compte.", + "authorize.redirect": "Retour vers {url}", + "authorize.error": "La demande d’autorisation de l’application est invalide.", + "authorize.busy": "Autorisation…", + "authorize.approve": "Autoriser", }; diff --git a/webapp/src/state/i18n/fr/common.ts b/webapp/src/state/i18n/fr/common.ts new file mode 100644 index 00000000..a750a79c --- /dev/null +++ b/webapp/src/state/i18n/fr/common.ts @@ -0,0 +1,26 @@ +// Words shared across screens, file sizes, card buttons and ratings. +import type { TranslationValue } from ".."; +import type { common as en } from "../en/common"; + +export const common: Record = { + "common.loading": "Chargement", + "common.loadError": "Impossible de charger cette vue", + "common.noAlbums": "Aucun album pour le moment.", + "common.noArtists": "Aucun artiste pour le moment.", + "common.variousArtists": "Artistes variés", + "common.unknownArtist": "Artiste inconnu", + "common.album": "Album", + "common.artist": "Artiste", + "common.albums": { one: "{count} album", other: "{count} albums" }, + "common.tracks": { one: "{count} piste", other: "{count} pistes" }, + "common.delete": "Supprimer", + "common.cancel": "Annuler", + "common.back": "Retour", + "file.megabytes": "{size} Mo", + "file.kilobytes": "{size} ko", + "file.channels": { one: "mono", other: "{count} canaux" }, + "card.play": "Lire", + "card.queue": "Mettre en file", + "rating.label": "Note", + "rating.stars": { one: "{count} étoile", other: "{count} étoiles" }, +}; diff --git a/webapp/src/state/i18n/fr/folders.ts b/webapp/src/state/i18n/fr/folders.ts index c9bc40bc..0c33d23f 100644 --- a/webapp/src/state/i18n/fr/folders.ts +++ b/webapp/src/state/i18n/fr/folders.ts @@ -1,42 +1,8 @@ -// Artist splits and images, folders, filing and uploads. +// Folders, filing and uploads. import type { TranslationValue } from ".."; import type { folders as en } from "../en/folders"; export const folders: Record = { - "artistSplit.offer": "Scinder en {count} artistes", - "artistSplit.confirm": "Confirmer : scinder en {count}", - "artistSplit.done": "Scindé en {parts}.", - "artistSplit.failed": "Ce crédit n’a pas pu être scindé. Réessayez.", - "common.back": "Retour", - "artistImage.changePortrait": "Changer le portrait", - "artistImage.changeBanner": "Changer la bannière", - "artistImage.resetPortrait": "Portrait automatique", - "artistImage.resetBanner": "Sans bannière", - "artistImage.portraitChanged": "Portrait changé.", - "artistImage.bannerChanged": "Bannière changée.", - "artistImage.portraitReverted": "Retour au portrait automatique.", - "artistImage.bannerReverted": "Bannière retirée.", - "artistImage.tooLarge": "Cette image dépasse 10 Mo.", - "artistImage.notAnImage": "Choisissez une image JPEG, PNG ou WebP.", - "artistImage.failed": "Cela n’a pas fonctionné. Réessayez.", - "artistImage.chosen": "Portrait choisi à la main", - "artistImage.searchDeezer": "Rechercher sur Deezer", - "artistImage.menu": "Modifier les images", - "artistImage.menuShort": "Modifier", - "artistImage.menuLabel": "Images de {name}", - "artistImage.deezerTitle": "Choisir un portrait sur Deezer", - "artistImage.deezerQuery": "Nom de l’artiste", - "artistImage.deezerSearch": "Rechercher", - "artistImage.deezerSearching": "Recherche sur Deezer…", - "artistImage.deezerResults": "Artistes Deezer", - "artistImage.deezerNone": "Deezer ne connaît aucun artiste de ce nom.", - "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, - "artistImage.deezerUnavailable": - "La recherche Deezer n’est pas activée sur ce serveur.", - "artistImage.deezerBusy": - "Votre recherche précédente est encore en cours. Réessayez dans un instant.", - "artistImage.deezerExpired": - "Ces résultats ont expiré. Relancez la recherche.", "folders.count": { one: "{count} dossier créé dans cette bibliothèque", other: "{count} dossiers créés dans cette bibliothèque", @@ -93,7 +59,6 @@ export const folders: Record = { "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée, et elle ne peut être ni renommée ni supprimée. Pour ranger une piste, ouvrez son menu et choisissez « Déplacer vers un dossier… ».", "filed.unfiledNoteReader": "Toutes les pistes qui ne sont encore dans aucun dossier. WaveFlow tient cette liste lui-même : une piste en sort dès qu’elle est rangée.", - "trackMenu.move": "Déplacer vers un dossier…", "upload.into": "Les fichiers reçus vont dans le dossier {folder}.", "upload.doneUnfiled": "Ajouté à la bibliothèque, dans À ranger", "upload.folderMissing": @@ -101,8 +66,6 @@ export const folders: Record = { "upload.folderUnchecked": "Le dossier pour lequel cet envoi a été ouvert n’a pas pu être vérifié : aucun fichier n’est pris pour l’instant.", "upload.checkAgain": "Vérifier à nouveau", - "nav.group.library": "Bibliothèque", - "nav.group.listen": "Écoute", "upload.title": "Téléverser dans {library}", "upload.detail": "Chaque fichier est d’abord identifié dans votre navigateur : un fichier que la bibliothèque possède déjà n’est jamais renvoyé, et un transfert interrompu reprend là où il s’était arrêté.", diff --git a/webapp/src/state/i18n/fr/index.ts b/webapp/src/state/i18n/fr/index.ts index a72e350e..a64e7a5d 100644 --- a/webapp/src/state/i18n/fr/index.ts +++ b/webapp/src/state/i18n/fr/index.ts @@ -1,29 +1,37 @@ -// The French strings, assembled in the same order as the English ones. +// The French strings, assembled from the same domains as the English ones. import type { TranslationKey, TranslationValue } from ".."; -import { browse } from "./browse"; +import { admin } from "./admin"; +import { attention } from "./attention"; +import { clients } from "./clients"; +import { common } from "./common"; import { folders } from "./folders"; import { library } from "./library"; -import { maintenance } from "./maintenance"; -import { playing } from "./playing"; +import { login } from "./login"; +import { nav } from "./nav"; +import { player } from "./player"; +import { playlists } from "./playlists"; import { podcasts } from "./podcasts"; +import { preferences } from "./preferences"; import { quality } from "./quality"; import { scrobbling } from "./scrobbling"; -import { settings } from "./settings"; -import { shell } from "./shell"; import { smart } from "./smart"; import { stats } from "./stats"; import { track } from "./track"; export const fr: Record = { - ...shell, - ...settings, - ...track, + ...nav, + ...preferences, + ...common, ...library, - ...browse, + ...clients, ...quality, + ...track, + ...login, + ...playlists, + ...player, + ...admin, + ...attention, ...folders, - ...playing, - ...maintenance, ...scrobbling, ...stats, ...smart, diff --git a/webapp/src/state/i18n/fr/library.ts b/webapp/src/state/i18n/fr/library.ts index 72d26f60..d758756b 100644 --- a/webapp/src/state/i18n/fr/library.ts +++ b/webapp/src/state/i18n/fr/library.ts @@ -1,29 +1,30 @@ -// Sign-in, the library pages, import, the queue, shares and radio. +// The library pages: albums, artists, songs, favourites, browsing, search. import type { TranslationValue } from ".."; import type { library as en } from "../en/library"; export const library: Record = { - "login.heroEyebrow": "Votre musique, selon vos règles", - "login.heroTitle": "La nuit tombe, la bibliothèque s’éveille.", - "login.heroDetail": "Streaming privé, un catalogue, tous vos écrans.", - "login.server": "Serveur WaveFlow", - "login.createAdmin": "Créer le compte administrateur", - "login.welcome": "Bon retour", - "login.setupDetail": - "Ce serveur est neuf. Choisissez son premier compte administrateur.", - "login.setupToken": "Jeton d’installation", - "login.setupTokenHint": - "Le serveur l’a écrit dans le fichier setup-token de son répertoire de données, dont son journal donne le chemin. Si vous avez défini WAVEFLOW_SETUP_TOKEN, saisissez cette valeur.", - "login.setupTokenError": - "Ce jeton d’installation n’est pas le bon. Copiez-le de nouveau depuis le fichier setup-token.", - "login.username": "Nom d’utilisateur", - "login.password": "Mot de passe", - "login.setupError": - "Configuration impossible. Utilisez un nom valide et un mot de passe d’au moins 12 caractères.", - "login.error": "Nom d’utilisateur ou mot de passe incorrect.", - "login.wait": "Veuillez patienter…", - "login.create": "Créer et se connecter", - "login.signIn": "Se connecter", + "album.play": "Lire l’album", + "album.addQueue": "Ajouter à la file", + "album.title": "Titre", + "album.duration": "Durée", + "album.trackActions": "Actions sur les titres", + "album.moreBy": "Autres albums de {artist}", + "songs.loadedSoFar": { + one: "{count} piste chargée", + other: "{count} pistes chargées", + }, + "songs.playLoaded": { + one: "Lire la piste chargée", + other: "Lire les {count} pistes chargées", + }, + "songs.playAll": { + one: "Lire la piste", + other: "Lire les {count} pistes", + }, + "songs.loadMore": "Charger la suite", + "songs.loadMoreError": "Cette page n’a pas pu être chargée. Réessayez.", + "songs.loading": "Chargement…", + "songs.empty": "Cette bibliothèque ne contient encore aucune piste.", "albums.detail": { one: "{count} disque dans votre bibliothèque", other: "{count} disques dans votre bibliothèque", @@ -52,6 +53,35 @@ export const library: Record = { "artists.similarSource": "Suggérés par {provider}, le {date}.", "artists.portraitSource": "Portrait fourni par {provider}, le {date}.", "artists.bannerSource": "Bannière fournie par {provider}, le {date}.", + "artistImage.changePortrait": "Changer le portrait", + "artistImage.changeBanner": "Changer la bannière", + "artistImage.resetPortrait": "Portrait automatique", + "artistImage.resetBanner": "Sans bannière", + "artistImage.portraitChanged": "Portrait changé.", + "artistImage.bannerChanged": "Bannière changée.", + "artistImage.portraitReverted": "Retour au portrait automatique.", + "artistImage.bannerReverted": "Bannière retirée.", + "artistImage.tooLarge": "Cette image dépasse 10 Mo.", + "artistImage.notAnImage": "Choisissez une image JPEG, PNG ou WebP.", + "artistImage.failed": "Cela n’a pas fonctionné. Réessayez.", + "artistImage.chosen": "Portrait choisi à la main", + "artistImage.searchDeezer": "Rechercher sur Deezer", + "artistImage.menu": "Modifier les images", + "artistImage.menuShort": "Modifier", + "artistImage.menuLabel": "Images de {name}", + "artistImage.deezerTitle": "Choisir un portrait sur Deezer", + "artistImage.deezerQuery": "Nom de l’artiste", + "artistImage.deezerSearch": "Rechercher", + "artistImage.deezerSearching": "Recherche sur Deezer…", + "artistImage.deezerResults": "Artistes Deezer", + "artistImage.deezerNone": "Deezer ne connaît aucun artiste de ce nom.", + "artistImage.deezerFans": { one: "{count} fan", other: "{count} fans" }, + "artistImage.deezerUnavailable": + "La recherche Deezer n’est pas activée sur ce serveur.", + "artistImage.deezerBusy": + "Votre recherche précédente est encore en cours. Réessayez dans un instant.", + "artistImage.deezerExpired": + "Ces résultats ont expiré. Relancez la recherche.", "favourites.detail": { one: "{count} piste enregistrée", other: "{count} pistes enregistrées", @@ -60,120 +90,44 @@ export const library: Record = { "Ajoutez une piste aux favoris depuis un album ou la recherche pour la retrouver ici.", "favourites.add": "Ajouter aux favoris", "favourites.remove": "Retirer des favoris", - "playlists.detail": { - one: "{count} collection", - other: "{count} collections", + "browse.sort": "Trier", + "browse.sortAlphabeticalByName": "Par titre", + "browse.sortAlphabeticalByArtist": "Par artiste", + "browse.sortAlphabeticalByAlbum": "Par album", + "browse.sortNewest": "Ajoutés récemment", + "browse.sortRecent": "Écoutés récemment", + "browse.sortFrequent": "Les plus écoutés", + "browse.sortStarred": "Favoris", + "browse.sortByYear": "Par année", + "browse.filterAlbums": "Filtrer par titre ou artiste", + "browse.filterArtists": "Filtrer par nom", + "browse.filterPlaceholder": "Filtrer…", + "browse.matches": { + one: "{count} sur {total} affiché", + other: "{count} sur {total} affichés", }, - "playlists.newName": "Nom de la nouvelle playlist", - "playlists.create": "Créer la playlist", - "playlists.play": "Lire", - "playlists.addQueue": "Ajouter la file", - "playlists.emptyOne": "Cette playlist est vide.", - "playlists.export": "Enregistrer en .m3u8", - "playlists.exportError": "Cette playlist n’a pas pu être enregistrée.", - "import.title": "Importer des playlists d’un autre lecteur", - "import.detail": - "Un fichier .m3u ou .m3u8. Rien n’est créé avant que vous ayez vu ce qui serait perdu.", - "import.pick": "Choisir un fichier de playlist", - "import.name": "Nom de la playlist", - "import.found": - "{resolved} entrées sur {entries} sont dans votre bibliothèque.", - "import.encoding": - "Le fichier n’était pas en UTF-8 et a été lu en {encoding}. Vérifiez les noms ci-dessous avant d’importer.", - "import.missing": { - one: "{count} entrée ne peut pas être placée :", - other: "{count} entrées ne peuvent pas être placées :", + "browse.noMatch": "Aucun résultat pour ce filtre.", + "browse.filterGenres": "Filtrer par genre", + "search.detail": "Trouvez un titre, un album ou un artiste", + "search.placeholder": "Titre, album ou artiste", + "search.query": "Requête de recherche", + "search.tracks": "Pistes", + "search.empty": "Aucun résultat.", + "search.thisLibrary": "Recherche dans {library}.", + "genres.detail": { one: "{count} genre", other: "{count} genres" }, + "genres.empty": "Aucun genre n’est encore étiqueté dans cette bibliothèque.", + "genres.emptyOne": "Plus rien ne porte ce genre.", + "history.detail": { + one: "{count} piste, la plus récente d’abord", + other: "{count} pistes, la plus récente d’abord", }, - "import.line": "Ligne {line}", - "import.noMatch": "aucune piste correspondante", - "import.ambiguous": "correspond à plusieurs pistes", - "import.notAPath": "n’est pas un fichier d’une bibliothèque", - "import.duplicate": "déjà dans cette playlist", - "import.confirm": "Importer", - "import.cancel": "Annuler", - "import.failed": "Ce fichier n’a pas pu être lu comme une playlist.", - "playlists.empty": "Créez une playlist pour réunir plusieurs pistes.", - "playlists.createError": "La playlist n’a pas pu être créée.", - "playlists.queueError": "La file n’a pas pu être ajoutée à cette playlist.", - "playlists.deleteError": "La playlist n’a pas pu être supprimée.", - "queue.detail": { - one: "{count} piste synchronisée avec votre compte", - other: "{count} pistes synchronisées avec votre compte", + "history.empty": "Rien n’a encore été écouté depuis ce compte.", + "random.detail": { + one: "{count} piste tirée", + other: "{count} pistes tirées", }, - "queue.playStart": "Lire depuis le début", - "queue.clear": "Vider la file", - "queue.remove": "Retirer", - "queue.empty": "Lisez un album ou une playlist pour construire votre file.", - "shares.detail": "Liens publics vers votre musique", - "shares.description": "Description", - "shares.create": "Partager la file actuelle", - "shares.needQueue": "Ajoutez des pistes à la file avant de créer un lien.", - "shares.once": "Ce lien ne sera affiché qu’une fois :", - "shares.defaultName": "Partage musical", - "shares.empty": "Aucun lien public n’a été créé.", - "shares.createError": "Le partage n’a pas pu être créé.", - "shares.tooLong": - "Un partage contient au plus {limit} titres, et la file en contient {count}. Raccourcissez la file pour la partager.", - "shares.deleteError": "Le partage n’a pas pu être supprimé.", - "shares.maxVisits": "Limite de visites (facultative)", - "shares.visits": { one: "{count} visite", other: "{count} visites" }, - "shares.visitsOf": "{count} visites sur {max}", - "share.kicker": "Partagé avec vous", - "share.defaultTitle": "Musique partagée avec vous", - "share.until": "Disponible jusqu’au {date}", - "share.empty": "Aucune de ces pistes ne peut plus être lue.", - "share.player": "Lecteur", - "share.unavailableTitle": "Ce lien n’ouvre plus rien", - "share.unavailableDetail": - "Il a peut-être expiré, été retiré, ou été ouvert autant de fois qu’il le permet.", - "playlists.public": "Visible par tous les comptes", - "playlists.publicError": "Cette playlist n’a pas pu être publiée.", - "playlists.others": "D’autres comptes", - "playlists.othersDetail": - "Les playlists publiques des autres comptes, avec les pistes que vos bibliothèques contiennent.", - "playlists.by": "Par {owner}", - "playlists.moveUp": "Monter {title}", - "playlists.moveDown": "Descendre {title}", - "playlists.removeEntry": "Retirer {title} de cette playlist", - "playlists.moveError": - "Cette entrée n’a pas pu être déplacée. La playlist a peut-être changé ailleurs ; elle a été relue.", - "playlists.removeEntryError": "Cette entrée n’a pas pu être retirée.", - "radio.detail": "Des stations en ligne, attachées à votre compte", - "radio.name": "Nom de la station", - "radio.address": "Adresse du flux", - "radio.homepage": "Page de la station (facultatif)", - "radio.image": "Image de la station (facultatif)", - "radio.imageHint": - "Le serveur la télécharge une fois, quand vous l’enregistrez, et la garde avec les pochettes : votre navigateur ne contacte jamais l’hôte de l’image.", - "radio.imageMissing": - "L’image n’a pas pu être récupérée ; enregistrez la station pour réessayer.", - "radio.add": "Ajouter la station", - "radio.save": "Enregistrer la station", - "radio.edit": "Modifier", - "radio.replacing": - "Enregistrer remplace tous les champs : une page vide efface celle qui était retenue.", - "radio.listen": "Écouter", - "radio.stop": "Arrêter", - "radio.visit": "Page de la station", - "radio.empty": "Aucune station. Ajoutez-en une pour la retrouver ici.", - "radio.addressRefused": "Une adresse doit commencer par http:// ou https://.", - "radio.mixedContent": - "Cette page est servie en HTTPS : votre navigateur refusera un flux en clair.", - "radio.full": "Ce compte retient déjà le maximum de stations.", - "radio.saveError": "La station n’a pas pu être enregistrée.", - "radio.deleteError": "La station n’a pas pu être supprimée.", - "radio.playbackError": - "Cette station n’a pas pu être lue. Le serveur ne s’y connecte jamais : cela se passe entre votre navigateur et la station.", - "radio.stations": "Stations", - "radio.newTitle": "Ajouter une station", - "radio.editTitle": "Modifier « {name} »", - "radio.listenTo": "Écouter {name}", - "radio.stopNamed": "Arrêter {name}", - "radio.actions": "Actions pour {name}", - "radio.confirmDelete": - "Supprimer la station « {name} » ? Son adresse et son image sont oubliées.", - "credential.password": "Mot de passe Subsonic dédié", - "credential.rotate": "Renouveler l’identifiant", - "credential.copy": "Copiez cette clé API maintenant :", - "credential.error": "Le renouvellement de l’identifiant a échoué.", + "random.again": "Tirer à nouveau", + "random.fromGenre": "Lire ce genre au hasard", + "random.empty": "Il n’y a encore rien à tirer.", + "library.active": "Bibliothèque", }; diff --git a/webapp/src/state/i18n/fr/login.ts b/webapp/src/state/i18n/fr/login.ts new file mode 100644 index 00000000..35efe811 --- /dev/null +++ b/webapp/src/state/i18n/fr/login.ts @@ -0,0 +1,31 @@ +// Sign-in and first-run setup. +import type { TranslationValue } from ".."; +import type { login as en } from "../en/login"; + +export const login: Record = { + "login.heroEyebrow": "Votre musique, selon vos règles", + "login.heroTitle": "La nuit tombe, la bibliothèque s’éveille.", + "login.heroDetail": "Streaming privé, un catalogue, tous vos écrans.", + "login.server": "Serveur WaveFlow", + "login.createAdmin": "Créer le compte administrateur", + "login.welcome": "Bon retour", + "login.setupDetail": + "Ce serveur est neuf. Choisissez son premier compte administrateur.", + "login.setupToken": "Jeton d’installation", + "login.setupTokenHint": + "Le serveur l’a écrit dans le fichier setup-token de son répertoire de données, dont son journal donne le chemin. Si vous avez défini WAVEFLOW_SETUP_TOKEN, saisissez cette valeur.", + "login.setupTokenError": + "Ce jeton d’installation n’est pas le bon. Copiez-le de nouveau depuis le fichier setup-token.", + "login.username": "Nom d’utilisateur", + "login.password": "Mot de passe", + "login.setupError": + "Configuration impossible. Utilisez un nom valide et un mot de passe d’au moins 12 caractères.", + "login.error": "Nom d’utilisateur ou mot de passe incorrect.", + "login.wait": "Veuillez patienter…", + "login.create": "Créer et se connecter", + "login.signIn": "Se connecter", + "login.offline": { + one: "Écouter le {count} titre gardé dans ce navigateur", + other: "Écouter les {count} titres gardés dans ce navigateur", + }, +}; diff --git a/webapp/src/state/i18n/fr/nav.ts b/webapp/src/state/i18n/fr/nav.ts new file mode 100644 index 00000000..4c333eaa --- /dev/null +++ b/webapp/src/state/i18n/fr/nav.ts @@ -0,0 +1,45 @@ +// Navigation and the page for unknown routes. +import type { TranslationValue } from ".."; +import type { nav as en } from "../en/nav"; + +export const nav: Record = { + "nav.albums": "Albums", + "nav.artists": "Artistes", + "nav.songs": "Titres", + "nav.search": "Recherche", + "nav.favourites": "Favoris", + "nav.playlists": "Playlists", + "nav.queue": "File d’attente", + "nav.shares": "Partages", + "nav.offline": "Hors ligne", + "nav.radio": "Radio", + "nav.scrobbling": "Scrobbling", + "nav.admin": "Administration", + "nav.signOut": "Se déconnecter", + "nav.skip": "Aller au contenu", + "nav.home": "WaveFlow serveur, accueil", + "nav.server": "serveur", + "nav.more": "Plus", + "nav.moreTitle": "Tout le reste", + "nav.moreDetail": + "La barre du bas porte ce qu’on attrape en écoutant. Le reste est ici.", + "nav.moreClose": "Fermer", + "nav.settings": "Réglages", + "nav.genres": "Genres", + "nav.history": "Écoutes récentes", + "nav.random": "Aléatoire", + "nav.playing": "En écoute", + "nav.upload": "Téléverser", + "nav.folders": "Dossiers", + "nav.attention": "À corriger", + "nav.quality": "Qualité", + "nav.group.library": "Bibliothèque", + "nav.group.listen": "Écoute", + "nav.missing": "Fichiers manquants", + "nav.maintenance": "Maintenance", + "nav.stats": "Statistiques", + "nav.podcasts": "Podcasts", + "notFound.title": "Cette salle est silencieuse", + "notFound.detail": "La page demandée n’existe pas sur ce serveur.", + "notFound.back": "Retour à la bibliothèque", +}; diff --git a/webapp/src/state/i18n/fr/player.ts b/webapp/src/state/i18n/fr/player.ts new file mode 100644 index 00000000..7ee32766 --- /dev/null +++ b/webapp/src/state/i18n/fr/player.ts @@ -0,0 +1,81 @@ +// The player bar, now playing, radio and offline listening. +import type { TranslationValue } from ".."; +import type { player as en } from "../en/player"; + +export const player: Record = { + "player.error": "Lecture impossible. Vérifiez le serveur puis réessayez.", + "player.previous": "Piste précédente", + "player.play": "Lire", + "player.pause": "Pause", + "player.next": "Piste suivante", + "player.seek": "Position de lecture", + "player.shuffle": "Lecture aléatoire", + "player.repeatOff": "Répétition : aucune", + "player.repeatAll": "Répétition : toute la file", + "player.repeatOne": "Répétition : cette piste", + "player.volume": "Volume", + "player.mute": "Couper le son", + "player.unmute": "Rétablir le son", + "player.openAlbum": "Ouvrir l’album", + "player.live": "Direct", + "player.liveRadio": "Radio en direct", + "player.stopLive": "Arrêter la station", + "player.liveError": "Cette station n’a pas pu être lue.", + "player.closeAside": "Arrêter et fermer", + "playing.idle": "Rien n’est en cours de lecture.", + "playing.empty": "Lancez une piste et elle apparaîtra ici, avec ses paroles.", + "playing.viewLabel": "Comment regarder cette piste", + "playing.viewDetail": "Détail", + "playing.viewLyrics": "Paroles", + "playing.viewImmersive": "Immersif", + "playing.immersiveTitle": "Vue immersive", + "playing.leaveImmersive": "Quitter la vue immersive", + "radio.detail": "Des stations en ligne, attachées à votre compte", + "radio.name": "Nom de la station", + "radio.address": "Adresse du flux", + "radio.homepage": "Page de la station (facultatif)", + "radio.image": "Image de la station (facultatif)", + "radio.imageHint": + "Le serveur la télécharge une fois, quand vous l’enregistrez, et la garde avec les pochettes : votre navigateur ne contacte jamais l’hôte de l’image.", + "radio.imageMissing": + "L’image n’a pas pu être récupérée ; enregistrez la station pour réessayer.", + "radio.add": "Ajouter la station", + "radio.save": "Enregistrer la station", + "radio.edit": "Modifier", + "radio.replacing": + "Enregistrer remplace tous les champs : une page vide efface celle qui était retenue.", + "radio.listen": "Écouter", + "radio.stop": "Arrêter", + "radio.visit": "Page de la station", + "radio.empty": "Aucune station. Ajoutez-en une pour la retrouver ici.", + "radio.addressRefused": "Une adresse doit commencer par http:// ou https://.", + "radio.mixedContent": + "Cette page est servie en HTTPS : votre navigateur refusera un flux en clair.", + "radio.full": "Ce compte retient déjà le maximum de stations.", + "radio.saveError": "La station n’a pas pu être enregistrée.", + "radio.deleteError": "La station n’a pas pu être supprimée.", + "radio.playbackError": + "Cette station n’a pas pu être lue. Le serveur ne s’y connecte jamais : cela se passe entre votre navigateur et la station.", + "radio.stations": "Stations", + "radio.newTitle": "Ajouter une station", + "radio.editTitle": "Modifier « {name} »", + "radio.listenTo": "Écouter {name}", + "radio.stopNamed": "Arrêter {name}", + "radio.actions": "Actions pour {name}", + "radio.confirmDelete": + "Supprimer la station « {name} » ? Son adresse et son image sont oubliées.", + "offline.kicker": "Dans ce navigateur", + "offline.title": "Gardé hors ligne", + "offline.detail": + "Ces titres sont gardés dans ce navigateur pour le compte qui s’y est connecté en dernier, et se lisent sans connexion. Se déconnecter les supprime de ce navigateur. Gardez-en d’autres depuis le menu d’un titre.", + "offline.unsupported": + "Ce navigateur ne peut pas garder de titres pour l’écoute hors ligne.", + "offline.back": "Retour à la bibliothèque", + "offline.empty": + "Rien n’est encore gardé. Le menu d’un titre propose de le garder.", + "offline.player": "Lecteur hors ligne", + "offline.gone": "Cette copie n’est plus dans ce navigateur.", + "offline.forget": "Retirer la copie hors ligne de {title}", + "offline.forgetShort": "Retirer", + "offline.forgetDetail": "Libère {size}", +}; diff --git a/webapp/src/state/i18n/fr/playing.ts b/webapp/src/state/i18n/fr/playing.ts deleted file mode 100644 index 42abcb83..00000000 --- a/webapp/src/state/i18n/fr/playing.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Genres, history, random, lyrics, now playing, bookmarks, the player bar. -import type { TranslationValue } from ".."; -import type { playing as en } from "../en/playing"; - -export const playing: Record = { - "browse.filterGenres": "Filtrer par genre", - "genres.detail": { one: "{count} genre", other: "{count} genres" }, - "genres.empty": "Aucun genre n’est encore étiqueté dans cette bibliothèque.", - "genres.emptyOne": "Plus rien ne porte ce genre.", - "history.detail": { - one: "{count} piste, la plus récente d’abord", - other: "{count} pistes, la plus récente d’abord", - }, - "history.empty": "Rien n’a encore été écouté depuis ce compte.", - "random.detail": { - one: "{count} piste tirée", - other: "{count} pistes tirées", - }, - "random.again": "Tirer à nouveau", - "random.fromGenre": "Lire ce genre au hasard", - "random.empty": "Il n’y a encore rien à tirer.", - "lyrics.label": "Paroles", - "lyrics.none": "Aucune parole ne voyage avec ce fichier.", - "playing.idle": "Rien n’est en cours de lecture.", - "playing.empty": "Lancez une piste et elle apparaîtra ici, avec ses paroles.", - "lyrics.seekHint": - "Cette feuille porte des minutages — appuyez sur une ligne pour y sauter.", - "lyrics.seekTo": "Sauter à {when}", - "lyrics.seekLine": "{line}, sauter à {when}", - "playing.viewLabel": "Comment regarder cette piste", - "playing.viewDetail": "Détail", - "playing.viewLyrics": "Paroles", - "playing.viewImmersive": "Immersif", - "playing.immersiveTitle": "Vue immersive", - "playing.leaveImmersive": "Quitter la vue immersive", - "bookmarks.title": "Positions enregistrées", - "bookmarks.detail": "Une par piste. Enregistrer à nouveau la déplace.", - "bookmarks.save": "Enregistrer cette position", - "bookmarks.forget": "Oublier", - "player.shuffle": "Lecture aléatoire", - "player.repeatOff": "Répétition : aucune", - "player.repeatAll": "Répétition : toute la file", - "player.repeatOne": "Répétition : cette piste", - "player.volume": "Volume", - "player.mute": "Couper le son", - "player.unmute": "Rétablir le son", - "player.openAlbum": "Ouvrir l’album", - "player.live": "Direct", - "player.liveRadio": "Radio en direct", - "player.stopLive": "Arrêter la station", - "player.liveError": "Cette station n’a pas pu être lue.", - "player.closeAside": "Arrêter et fermer", - "library.active": "Bibliothèque", - "search.thisLibrary": "Recherche dans {library}.", -}; diff --git a/webapp/src/state/i18n/fr/playlists.ts b/webapp/src/state/i18n/fr/playlists.ts new file mode 100644 index 00000000..67be1d16 --- /dev/null +++ b/webapp/src/state/i18n/fr/playlists.ts @@ -0,0 +1,88 @@ +// Playlists and their import, the queue, shares and bookmarks. +import type { TranslationValue } from ".."; +import type { playlists as en } from "../en/playlists"; + +export const playlists: Record = { + "playlists.detail": { + one: "{count} collection", + other: "{count} collections", + }, + "playlists.newName": "Nom de la nouvelle playlist", + "playlists.create": "Créer la playlist", + "playlists.play": "Lire", + "playlists.addQueue": "Ajouter la file", + "playlists.emptyOne": "Cette playlist est vide.", + "playlists.export": "Enregistrer en .m3u8", + "playlists.exportError": "Cette playlist n’a pas pu être enregistrée.", + "playlists.empty": "Créez une playlist pour réunir plusieurs pistes.", + "playlists.createError": "La playlist n’a pas pu être créée.", + "playlists.queueError": "La file n’a pas pu être ajoutée à cette playlist.", + "playlists.deleteError": "La playlist n’a pas pu être supprimée.", + "playlists.public": "Visible par tous les comptes", + "playlists.publicError": "Cette playlist n’a pas pu être publiée.", + "playlists.others": "D’autres comptes", + "playlists.othersDetail": + "Les playlists publiques des autres comptes, avec les pistes que vos bibliothèques contiennent.", + "playlists.by": "Par {owner}", + "playlists.moveUp": "Monter {title}", + "playlists.moveDown": "Descendre {title}", + "playlists.removeEntry": "Retirer {title} de cette playlist", + "playlists.moveError": + "Cette entrée n’a pas pu être déplacée. La playlist a peut-être changé ailleurs ; elle a été relue.", + "playlists.removeEntryError": "Cette entrée n’a pas pu être retirée.", + "import.title": "Importer des playlists d’un autre lecteur", + "import.detail": + "Un fichier .m3u ou .m3u8. Rien n’est créé avant que vous ayez vu ce qui serait perdu.", + "import.pick": "Choisir un fichier de playlist", + "import.name": "Nom de la playlist", + "import.found": + "{resolved} entrées sur {entries} sont dans votre bibliothèque.", + "import.encoding": + "Le fichier n’était pas en UTF-8 et a été lu en {encoding}. Vérifiez les noms ci-dessous avant d’importer.", + "import.missing": { + one: "{count} entrée ne peut pas être placée :", + other: "{count} entrées ne peuvent pas être placées :", + }, + "import.line": "Ligne {line}", + "import.noMatch": "aucune piste correspondante", + "import.ambiguous": "correspond à plusieurs pistes", + "import.notAPath": "n’est pas un fichier d’une bibliothèque", + "import.duplicate": "déjà dans cette playlist", + "import.confirm": "Importer", + "import.cancel": "Annuler", + "import.failed": "Ce fichier n’a pas pu être lu comme une playlist.", + "queue.detail": { + one: "{count} piste synchronisée avec votre compte", + other: "{count} pistes synchronisées avec votre compte", + }, + "queue.playStart": "Lire depuis le début", + "queue.clear": "Vider la file", + "queue.remove": "Retirer", + "queue.empty": "Lisez un album ou une playlist pour construire votre file.", + "shares.detail": "Liens publics vers votre musique", + "shares.description": "Description", + "shares.create": "Partager la file actuelle", + "shares.needQueue": "Ajoutez des pistes à la file avant de créer un lien.", + "shares.once": "Ce lien ne sera affiché qu’une fois :", + "shares.defaultName": "Partage musical", + "shares.empty": "Aucun lien public n’a été créé.", + "shares.createError": "Le partage n’a pas pu être créé.", + "shares.tooLong": + "Un partage contient au plus {limit} titres, et la file en contient {count}. Raccourcissez la file pour la partager.", + "shares.deleteError": "Le partage n’a pas pu être supprimé.", + "shares.maxVisits": "Limite de visites (facultative)", + "shares.visits": { one: "{count} visite", other: "{count} visites" }, + "shares.visitsOf": "{count} visites sur {max}", + "share.kicker": "Partagé avec vous", + "share.defaultTitle": "Musique partagée avec vous", + "share.until": "Disponible jusqu’au {date}", + "share.empty": "Aucune de ces pistes ne peut plus être lue.", + "share.player": "Lecteur", + "share.unavailableTitle": "Ce lien n’ouvre plus rien", + "share.unavailableDetail": + "Il a peut-être expiré, été retiré, ou été ouvert autant de fois qu’il le permet.", + "bookmarks.title": "Positions enregistrées", + "bookmarks.detail": "Une par piste. Enregistrer à nouveau la déplace.", + "bookmarks.save": "Enregistrer cette position", + "bookmarks.forget": "Oublier", +}; diff --git a/webapp/src/state/i18n/fr/podcasts.ts b/webapp/src/state/i18n/fr/podcasts.ts index c29f33f3..3a577301 100644 --- a/webapp/src/state/i18n/fr/podcasts.ts +++ b/webapp/src/state/i18n/fr/podcasts.ts @@ -3,7 +3,6 @@ import type { TranslationValue } from ".."; import type { podcasts as en } from "../en/podcasts"; export const podcasts: Record = { - "nav.podcasts": "Podcasts", "podcasts.detail": "Les émissions que suit ce serveur, pour tous les comptes", "podcasts.off": "Les podcasts sont désactivés sur ce serveur. Ce qui a été conservé reste lisible ; rien de nouveau n’est récupéré.", diff --git a/webapp/src/state/i18n/fr/preferences.ts b/webapp/src/state/i18n/fr/preferences.ts new file mode 100644 index 00000000..ddf4d13b --- /dev/null +++ b/webapp/src/state/i18n/fr/preferences.ts @@ -0,0 +1,51 @@ +// Preferences, ReplayGain, the settings page and themes. +import type { TranslationValue } from ".."; +import type { preferences as en } from "../en/preferences"; + +export const preferences: Record = { + "preferences.theme": "Thème", + "preferences.language": "Langue", + "preferences.themeLight": "Clair", + "preferences.themeDark": "Sombre", + "preferences.followSystem": "Suivre le système", + "preferences.followSystemDetail": + "Passer de la version claire d’un thème à sa version sombre en même temps que votre appareil.", + "preferences.followingLight": + "Votre appareil est en mode clair : la version claire est affichée. Choisir un thème choisit ses deux versions.", + "preferences.followingDark": + "Votre appareil est en mode sombre : la version sombre est affichée. Choisir un thème choisit ses deux versions.", + "preferences.standInLight": + "{theme} n’a pas de version claire : {fallback} le remplace tant que votre appareil est en mode clair.", + "preferences.standInDark": + "{theme} n’a pas de version sombre : {fallback} le remplace tant que votre appareil est en mode sombre.", + "replayGain.label": "ReplayGain", + "replayGain.off": "Désactivé", + "replayGain.track": "Gain par piste", + "replayGain.album": "Gain par album", + "replayGain.preamp": "Préampli", + "replayGain.detail": + "Joue chaque piste au volume mesuré dans ses tags, pour qu’un disque ne sonne pas plus fort que le suivant. Le gain par album garde les écarts à l’intérieur d’un album. Une piste sans tags est jouée telle quelle. La lecture passe alors par Web Audio, que certains téléphones mettent en pause avec l’écran.", + "settings.detail": + "Apparence, langue, et les clients autorisés à lire votre bibliothèque.", + "settings.appearance": "Apparence", + "settings.playback": "Lecture", + "settings.sections": "Sections des réglages", + "settings.section.account": "Compte", + "settings.playbackDetail": + "Comment ce navigateur lit vos titres. Gardé dans ce navigateur, pas sur votre compte.", + "settings.levelling": "Normalisation du volume", + "appearance.themes.default": "Émeraude clair", + "appearance.themes.midnightLight": "Minuit clair", + "appearance.themes.sunsetLight": "Coucher de soleil clair", + "appearance.themes.lavenderLight": "Lavande clair", + "appearance.themes.crimsonLight": "Cramoisi clair", + "appearance.themes.oceanLight": "Océan clair", + "appearance.themes.defaultDark": "Émeraude sombre", + "appearance.themes.oled": "Noir OLED", + "appearance.themes.midnight": "Minuit", + "appearance.themes.sunset": "Coucher de soleil", + "appearance.themes.lavender": "Lavande", + "appearance.themes.crimson": "Cramoisi", + "appearance.themes.ocean": "Océan", + "appearance.themes.neon": "Néon", +}; diff --git a/webapp/src/state/i18n/fr/quality.ts b/webapp/src/state/i18n/fr/quality.ts index 9d2789a1..7c0b0287 100644 --- a/webapp/src/state/i18n/fr/quality.ts +++ b/webapp/src/state/i18n/fr/quality.ts @@ -1,53 +1,15 @@ -// The attention lists and the audio quality analysis. +// Audio quality labels and the quality analysis. import type { TranslationValue } from ".."; import type { quality as en } from "../en/quality"; export const quality: Record = { - "attention.lists": "Listes", - "attention.total": { - one: "{count} point à vérifier", - other: "{count} points à vérifier", - }, - "attention.nothing": "Rien à corriger.", - "attention.notYours": - "Seuls les propriétaires et les gestionnaires de cette bibliothèque voient ce qui y est à corriger.", - "attention.empty": "Rien dans cette liste.", - "attention.tracksNote": - "Corrigez une piste depuis son menu : une fois corrigée, elle quitte la liste.", - "attention.creditsNote": - "Chacun de ces crédits nomme plusieurs artistes dans une seule chaîne. Scindez-le en ces artistes, ou gardez-le tel quel s’il s’agit d’un seul nom. Chaque scan suivant applique la décision.", - "attention.pending": "À décider", - "attention.decided": "Décidés", - "attention.split": "Scinder", - "attention.confirmSplit": "Confirmer : scinder en {count}", - "attention.keep": "Garder tel quel", - "attention.splitDone": "{name} est scindé.", - "attention.keepDone": "{name} reste tel quel.", - "attention.undone": "La décision sur {name} est annulée.", - "attention.alreadyDecided": - "Ce crédit a été décidé entre-temps. La liste a été rechargée.", - "attention.failed": "Cela n’a pas fonctionné. Réessayez.", - "attention.noCredits": "Aucun crédit n’attend de décision.", - "attention.noDecisions": "Aucune décision pour l’instant.", - "attention.fragments": "Ce en quoi {name} serait scindé", - "attention.knownFragment": "Déjà un artiste de cette bibliothèque", - "attention.decidedSplit": "Scindé en {parts}", - "attention.decidedKeep": "Gardé tel quel", - "attention.decidedBy": "Par {who}, le {when}", - "attention.undo": "Annuler", - "attention.duplicateGroup": "Doublon probable {group}", - "attention.split_credits": "Artistes à scinder", - "attention.missing_artist": "Sans artiste", - "attention.missing_album": "Sans album", - "attention.missing_year": "Sans année", - "attention.missing_track_number": "Sans numéro de piste", - "attention.missing_cover": "Sans pochette", - "attention.album_year_conflict": "Albums à plusieurs années", - "attention.compilation_no_album_artist": "Compilations sans artiste d’album", - "attention.duplicate_track_number": "Numéros de piste en double", - "attention.track_number_gap": "Trous dans la numérotation", - "attention.probable_duplicate": "Doublons probables", - "attention.probable_fake_lossless": "Faux lossless probables", + "quality.label": "Qualité du fichier", + "quality.unknown": "Inconnue", + "quality.low": "Faible", + "quality.standard": "Standard", + "quality.high": "Élevée", + "quality.lossless": "Sans perte", + "quality.hiRes": "Haute résolution", "quality.help": "Mesuré dans le son plutôt que lu dans le conteneur : où s’arrêtent les aigus, et combien de bits les échantillons utilisent. Un fichier sans perte dont les aigus s’arrêtent vers 16 kHz était un MP3 à 128 kb/s avant d’être un FLAC. « Aucun signe de source avec perte » n’est pas une preuve : un encodeur qui garde toute la bande ne laisse rien à trouver.", "quality.progress": "{measured} fichiers sans perte analysés sur {lossless}", diff --git a/webapp/src/state/i18n/fr/scrobbling.ts b/webapp/src/state/i18n/fr/scrobbling.ts index 305012d8..fb31451a 100644 --- a/webapp/src/state/i18n/fr/scrobbling.ts +++ b/webapp/src/state/i18n/fr/scrobbling.ts @@ -1,26 +1,8 @@ -// Offline listening and scrobbling. +// Scrobbling. import type { TranslationValue } from ".."; import type { scrobbling as en } from "../en/scrobbling"; export const scrobbling: Record = { - "offline.kicker": "Dans ce navigateur", - "offline.title": "Gardé hors ligne", - "offline.detail": - "Ces titres sont gardés dans ce navigateur pour le compte qui s’y est connecté en dernier, et se lisent sans connexion. Se déconnecter les supprime de ce navigateur. Gardez-en d’autres depuis le menu d’un titre.", - "offline.unsupported": - "Ce navigateur ne peut pas garder de titres pour l’écoute hors ligne.", - "offline.back": "Retour à la bibliothèque", - "offline.empty": - "Rien n’est encore gardé. Le menu d’un titre propose de le garder.", - "offline.player": "Lecteur hors ligne", - "offline.gone": "Cette copie n’est plus dans ce navigateur.", - "offline.forget": "Retirer la copie hors ligne de {title}", - "offline.forgetShort": "Retirer", - "offline.forgetDetail": "Libère {size}", - "login.offline": { - one: "Écouter le {count} titre gardé dans ce navigateur", - other: "Écouter les {count} titres gardés dans ce navigateur", - }, "scrobbling.title": "Scrobbling externe", "scrobbling.detail": "Transmettre ce que vous écoutez ici à ListenBrainz, Maloja ou Last.fm.", diff --git a/webapp/src/state/i18n/fr/shell.ts b/webapp/src/state/i18n/fr/shell.ts deleted file mode 100644 index 34ce70bb..00000000 --- a/webapp/src/state/i18n/fr/shell.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Navigation, preferences, ReplayGain, shared words, album and song views. -import type { TranslationValue } from ".."; -import type { shell as en } from "../en/shell"; - -export const shell: Record = { - "nav.albums": "Albums", - "nav.artists": "Artistes", - "nav.songs": "Titres", - "nav.search": "Recherche", - "nav.favourites": "Favoris", - "nav.playlists": "Playlists", - "nav.queue": "File d’attente", - "nav.shares": "Partages", - "nav.offline": "Hors ligne", - "nav.radio": "Radio", - "nav.scrobbling": "Scrobbling", - "nav.admin": "Administration", - "nav.signOut": "Se déconnecter", - "nav.skip": "Aller au contenu", - "nav.home": "WaveFlow serveur, accueil", - "nav.server": "serveur", - "nav.more": "Plus", - "nav.moreTitle": "Tout le reste", - "nav.moreDetail": - "La barre du bas porte ce qu’on attrape en écoutant. Le reste est ici.", - "nav.moreClose": "Fermer", - "preferences.theme": "Thème", - "preferences.language": "Langue", - "preferences.themeLight": "Clair", - "preferences.themeDark": "Sombre", - "preferences.followSystem": "Suivre le système", - "preferences.followSystemDetail": - "Passer de la version claire d’un thème à sa version sombre en même temps que votre appareil.", - "preferences.followingLight": - "Votre appareil est en mode clair : la version claire est affichée. Choisir un thème choisit ses deux versions.", - "preferences.followingDark": - "Votre appareil est en mode sombre : la version sombre est affichée. Choisir un thème choisit ses deux versions.", - "preferences.standInLight": - "{theme} n’a pas de version claire : {fallback} le remplace tant que votre appareil est en mode clair.", - "preferences.standInDark": - "{theme} n’a pas de version sombre : {fallback} le remplace tant que votre appareil est en mode sombre.", - "replayGain.label": "ReplayGain", - "replayGain.off": "Désactivé", - "replayGain.track": "Gain par piste", - "replayGain.album": "Gain par album", - "replayGain.preamp": "Préampli", - "replayGain.detail": - "Joue chaque piste au volume mesuré dans ses tags, pour qu’un disque ne sonne pas plus fort que le suivant. Le gain par album garde les écarts à l’intérieur d’un album. Une piste sans tags est jouée telle quelle. La lecture passe alors par Web Audio, que certains téléphones mettent en pause avec l’écran.", - "common.loading": "Chargement", - "common.loadError": "Impossible de charger cette vue", - "common.noAlbums": "Aucun album pour le moment.", - "common.noArtists": "Aucun artiste pour le moment.", - "common.variousArtists": "Artistes variés", - "common.unknownArtist": "Artiste inconnu", - "common.album": "Album", - "album.play": "Lire l’album", - "album.addQueue": "Ajouter à la file", - "album.title": "Titre", - "album.duration": "Durée", - "album.trackActions": "Actions sur les titres", - "album.moreBy": "Autres albums de {artist}", - "common.artist": "Artiste", - "common.albums": { one: "{count} album", other: "{count} albums" }, - "common.tracks": { one: "{count} piste", other: "{count} pistes" }, - "songs.loadedSoFar": { - one: "{count} piste chargée", - other: "{count} pistes chargées", - }, - "songs.playLoaded": { - one: "Lire la piste chargée", - other: "Lire les {count} pistes chargées", - }, - "songs.playAll": { - one: "Lire la piste", - other: "Lire les {count} pistes", - }, - "songs.loadMore": "Charger la suite", - "songs.loadMoreError": "Cette page n’a pas pu être chargée. Réessayez.", - "songs.loading": "Chargement…", - "songs.empty": "Cette bibliothèque ne contient encore aucune piste.", - "file.megabytes": "{size} Mo", - "file.kilobytes": "{size} ko", -}; diff --git a/webapp/src/state/i18n/fr/stats.ts b/webapp/src/state/i18n/fr/stats.ts index 9ab392c1..817af61c 100644 --- a/webapp/src/state/i18n/fr/stats.ts +++ b/webapp/src/state/i18n/fr/stats.ts @@ -1,12 +1,8 @@ -// Lyrics from LRCLIB, listening statistics and mixes. +// Listening statistics and mixes. import type { TranslationValue } from ".."; import type { stats as en } from "../en/stats"; export const stats: Record = { - // RFC-024 : paroles de LRCLIB, statistiques et mix. - "lyrics.source.lrclib": "Depuis LRCLIB", - "lyrics.fromLrclib": "Paroles fournies par LRCLIB", - "nav.stats": "Statistiques", "stats.detail": "Ce que ce compte a écouté, compté à partir de ses écoutes.", "stats.period": "Période", "stats.period7d": "7 derniers jours", diff --git a/webapp/src/state/i18n/fr/track.ts b/webapp/src/state/i18n/fr/track.ts index b1d281c9..e61242f2 100644 --- a/webapp/src/state/i18n/fr/track.ts +++ b/webapp/src/state/i18n/fr/track.ts @@ -1,10 +1,43 @@ -// The track menu and properties, lyrics sheets, tag corrections, canvases. +// The track menu, track properties, tag corrections, lyrics and canvases. import type { TranslationValue } from ".."; import type { track as en } from "../en/track"; export const track: Record = { "correction.edit": "Corriger les tags", "correction.editTrack": "Corriger les tags : {title}", + "correction.detail": + "Le fichier n’est jamais réécrit. La correction est rangée à côté, survit à un rescan, et tous les membres de la bibliothèque la voient.", + "correction.fieldTitle": "Titre", + "correction.fieldSortTitle": "Titre de tri", + "correction.fieldYear": "Année", + "correction.fieldTrackNumber": "Numéro de piste", + "correction.fieldDiscNumber": "Numéro de disque", + "correction.fieldMusicbrainz": "Identifiant d’enregistrement MusicBrainz", + "correction.fieldComment": "Commentaire", + "correction.fieldArtists": "Artistes", + "correction.fieldGenres": "Genres", + "correction.listHint": + "Un nom par ligne. Laissez vide pour ne créditer personne.", + "correction.corrected": "Corrigé", + "correction.fromFile": "Le fichier indique : {value}", + "correction.fileEmpty": "Le fichier n’indique rien ici", + "correction.restore": "Rétablir la valeur du fichier", + "correction.keep": "Garder la correction", + "correction.restoring": "Reprend la valeur du fichier à l’enregistrement", + "correction.restoringList": + "Relu depuis le fichier à l’enregistrement — impossible de l’afficher avant", + "correction.save": "Enregistrer", + "correction.saving": "Enregistrement…", + "correction.saved": "Enregistré.", + "correction.nothing": "Rien n’a changé.", + "correction.invalid": "Vérifiez : {fields}.", + "correction.refused": "Le serveur a refusé ces valeurs.", + "correction.notAllowed": + "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut corriger ses tags.", + "correction.error": "Les corrections n’ont pas pu être enregistrées.", + "correction.stale": + "Enregistré, mais les corrections n’ont pas pu être relues. Rechargez la page avant d’enregistrer à nouveau.", + "correction.back": "Retour à {album}", "trackMenu.label": "Actions pour {title}", "trackMenu.play": "Lire", "trackMenu.queue": "Ajouter à la file", @@ -18,6 +51,7 @@ export const track: Record = { "trackMenu.goToArtist": "Aller à {name}", "trackMenu.properties": "Propriétés", "trackMenu.edit": "Corriger les tags", + "trackMenu.move": "Déplacer vers un dossier…", "properties.kicker": "Propriétés", "properties.close": "Fermer les propriétés", "properties.loadError": "Certaines propriétés n’ont pas pu être chargées.", @@ -53,6 +87,15 @@ export const track: Record = { "properties.synced": "Synchronisées, {lang}", "properties.plain": "Texte brut, {lang}", "properties.noLyrics": "Aucune", + "properties.lyricsSheets": "Feuilles", + "properties.kbps": "{value} kbit/s", + "properties.khz": "{value} kHz", + "properties.bits": "{value} bits", + "properties.mb": "{value} Mo", + "properties.db": "{value} dB", + "properties.dbPeak": "{value} dB, crête {peak}", + "properties.corrected": "Corrigé", + "properties.fileSaid": "Corrigé — le fichier indique {value}", "lyrics.source.uploadTtml": "TTML envoyé", "lyrics.source.uploadLrc": "LRC envoyé", "lyrics.source.uploadTxt": "Texte envoyé", @@ -69,48 +112,15 @@ export const track: Record = { "lyrics.failed": "Les paroles n’ont pas pu être enregistrées. Réessayez.", "lyrics.downloadFailed": "Les paroles n’ont pas pu être téléchargées. Réessayez.", - "properties.lyricsSheets": "Feuilles", - "properties.kbps": "{value} kbit/s", - "properties.khz": "{value} kHz", - "properties.bits": "{value} bits", - "properties.mb": "{value} Mo", - "properties.db": "{value} dB", - "properties.dbPeak": "{value} dB, crête {peak}", - "properties.corrected": "Corrigé", - "properties.fileSaid": "Corrigé — le fichier indique {value}", - "correction.detail": - "Le fichier n’est jamais réécrit. La correction est rangée à côté, survit à un rescan, et tous les membres de la bibliothèque la voient.", - "correction.fieldTitle": "Titre", - "correction.fieldSortTitle": "Titre de tri", - "correction.fieldYear": "Année", - "correction.fieldTrackNumber": "Numéro de piste", - "correction.fieldDiscNumber": "Numéro de disque", - "correction.fieldMusicbrainz": "Identifiant d’enregistrement MusicBrainz", - "correction.fieldComment": "Commentaire", - "correction.fieldArtists": "Artistes", - "correction.fieldGenres": "Genres", - "correction.listHint": - "Un nom par ligne. Laissez vide pour ne créditer personne.", - "correction.corrected": "Corrigé", - "correction.fromFile": "Le fichier indique : {value}", - "correction.fileEmpty": "Le fichier n’indique rien ici", - "correction.restore": "Rétablir la valeur du fichier", - "correction.keep": "Garder la correction", - "correction.restoring": "Reprend la valeur du fichier à l’enregistrement", - "correction.restoringList": - "Relu depuis le fichier à l’enregistrement — impossible de l’afficher avant", - "correction.save": "Enregistrer", - "correction.saving": "Enregistrement…", - "correction.saved": "Enregistré.", - "correction.nothing": "Rien n’a changé.", - "correction.invalid": "Vérifiez : {fields}.", - "correction.refused": "Le serveur a refusé ces valeurs.", - "correction.notAllowed": - "Seul un propriétaire ou un gestionnaire de cette bibliothèque peut corriger ses tags.", - "correction.error": "Les corrections n’ont pas pu être enregistrées.", - "correction.stale": - "Enregistré, mais les corrections n’ont pas pu être relues. Rechargez la page avant d’enregistrer à nouveau.", - "correction.back": "Retour à {album}", + "lyrics.label": "Paroles", + "lyrics.none": "Aucune parole ne voyage avec ce fichier.", + "lyrics.seekHint": + "Cette feuille porte des minutages — appuyez sur une ligne pour y sauter.", + "lyrics.seekTo": "Sauter à {when}", + "lyrics.seekLine": "{line}, sauter à {when}", + // RFC-024 : paroles de LRCLIB, statistiques et mix. + "lyrics.source.lrclib": "Depuis LRCLIB", + "lyrics.fromLrclib": "Paroles fournies par LRCLIB", "canvas.title": "Canvas", "canvas.detail": "Une courte boucle jouée sans le son par-dessus la pochette, sur la page En écoute. Le serveur prend le mp4 ou le webm.", diff --git a/webapp/src/state/i18n/keys.test.ts b/webapp/src/state/i18n/keys.test.ts new file mode 100644 index 00000000..aa6ed27b --- /dev/null +++ b/webapp/src/state/i18n/keys.test.ts @@ -0,0 +1,43 @@ +// Every key lives in exactly one domain file, and every domain is assembled. +import { describe, expect, it } from "vitest"; + +import { en } from "./en"; +import { fr } from "./fr"; + +// A key repeated inside one object literal is a compile error, but the +// assembled objects are spreads, where a later domain silently overwrites an +// earlier one. The French domains are typed against their English +// counterparts, so a French key outside its domain fails to compile already. +const domains = { + en: import.meta.glob>( + ["./en/*.ts", "!./en/index.ts"], + { eager: true }, + ), + fr: import.meta.glob>( + ["./fr/*.ts", "!./fr/index.ts"], + { eager: true }, + ), +}; +const assembled = { en, fr }; + +describe.each(["en", "fr"] as const)("%s translation domains", (locale) => { + const owners = new Map(); + for (const [file, module] of Object.entries(domains[locale])) { + for (const strings of Object.values(module)) { + for (const key of Object.keys(strings)) { + owners.set(key, [...(owners.get(key) ?? []), file]); + } + } + } + + it("defines each key in one domain file only", () => { + const repeated = [...owners].filter(([, files]) => files.length > 1); + expect(repeated).toEqual([]); + }); + + it("assembles every domain file", () => { + expect([...owners.keys()].sort()).toEqual( + Object.keys(assembled[locale]).sort(), + ); + }); +}); From c3d1d25cd96f14be9dd9d9cb14fdf29ede4b5a5c Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 26 Oct 2026 23:36:49 +0100 Subject: [PATCH 4/4] refactor(webapp): reword the rfc section comments to match the files they now sit in Signed-off-by: InstaZDLL --- webapp/src/state/i18n/en/podcasts.ts | 1 + webapp/src/state/i18n/en/smart.ts | 2 +- webapp/src/state/i18n/en/stats.ts | 1 + webapp/src/state/i18n/en/track.ts | 2 +- webapp/src/state/i18n/fr/stats.ts | 1 + webapp/src/state/i18n/fr/track.ts | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webapp/src/state/i18n/en/podcasts.ts b/webapp/src/state/i18n/en/podcasts.ts index bdad5da0..488b2f8e 100644 --- a/webapp/src/state/i18n/en/podcasts.ts +++ b/webapp/src/state/i18n/en/podcasts.ts @@ -1,5 +1,6 @@ // Podcast subscriptions and episodes. export const podcasts = { + // Podcasts (RFC-025). "podcasts.detail": "Shows this server follows, for every account", "podcasts.off": "Podcasts are switched off on this server. What was kept before stays readable; nothing new is fetched.", diff --git a/webapp/src/state/i18n/en/smart.ts b/webapp/src/state/i18n/en/smart.ts index 80de60ab..ce0130b9 100644 --- a/webapp/src/state/i18n/en/smart.ts +++ b/webapp/src/state/i18n/en/smart.ts @@ -1,6 +1,6 @@ // Smart playlists. export const smart = { - // Smart playlists and podcasts (RFC-025), kept together at the end. + // Smart playlists (RFC-025). "smart.title": "Smart playlists", "smart.detail": "Defined by rules and worked out again each time they are opened, so they keep up with the library.", diff --git a/webapp/src/state/i18n/en/stats.ts b/webapp/src/state/i18n/en/stats.ts index f820fd68..554445cc 100644 --- a/webapp/src/state/i18n/en/stats.ts +++ b/webapp/src/state/i18n/en/stats.ts @@ -1,5 +1,6 @@ // Listening statistics and mixes. export const stats = { + // RFC-024: listening statistics and mixes. "stats.detail": "What this account listened to, counted from its own plays.", "stats.period": "Period", "stats.period7d": "Last 7 days", diff --git a/webapp/src/state/i18n/en/track.ts b/webapp/src/state/i18n/en/track.ts index 32a6b866..b80c7892 100644 --- a/webapp/src/state/i18n/en/track.ts +++ b/webapp/src/state/i18n/en/track.ts @@ -111,7 +111,7 @@ export const track = { "lyrics.seekHint": "This sheet carries timings — press a line to jump to it.", "lyrics.seekTo": "Jump to {when}", "lyrics.seekLine": "{line}, jump to {when}", - // RFC-024: lyrics from LRCLIB, statistics and mixes. + // RFC-024: lyrics from LRCLIB. "lyrics.source.lrclib": "From LRCLIB", "lyrics.fromLrclib": "Lyrics from LRCLIB", "canvas.title": "Canvas", diff --git a/webapp/src/state/i18n/fr/stats.ts b/webapp/src/state/i18n/fr/stats.ts index 817af61c..2db7fef2 100644 --- a/webapp/src/state/i18n/fr/stats.ts +++ b/webapp/src/state/i18n/fr/stats.ts @@ -3,6 +3,7 @@ import type { TranslationValue } from ".."; import type { stats as en } from "../en/stats"; export const stats: Record = { + // RFC-024: listening statistics and mixes. "stats.detail": "Ce que ce compte a écouté, compté à partir de ses écoutes.", "stats.period": "Période", "stats.period7d": "7 derniers jours", diff --git a/webapp/src/state/i18n/fr/track.ts b/webapp/src/state/i18n/fr/track.ts index e61242f2..462ce014 100644 --- a/webapp/src/state/i18n/fr/track.ts +++ b/webapp/src/state/i18n/fr/track.ts @@ -118,7 +118,7 @@ export const track: Record = { "Cette feuille porte des minutages — appuyez sur une ligne pour y sauter.", "lyrics.seekTo": "Sauter à {when}", "lyrics.seekLine": "{line}, sauter à {when}", - // RFC-024 : paroles de LRCLIB, statistiques et mix. + // RFC-024: lyrics from LRCLIB. "lyrics.source.lrclib": "Depuis LRCLIB", "lyrics.fromLrclib": "Paroles fournies par LRCLIB", "canvas.title": "Canvas",