diff --git a/package-lock.json b/package-lock.json index 7d13889f5..001eae968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "framer-motion": "^10.13.0", "fuse.js": "^6.6.2", "history": "^5.3.0", - "i18next": "^21.6.16", + "i18next": "^21.10.0", "idb-keyval": "^6.2.1", "immer": "^9.0.21", "is-electron": "^2.2.2", @@ -57,7 +57,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", - "react-i18next": "^11.16.7", + "react-i18next": "^11.18.6", "react-icons": "^4.10.1", "react-player": "^2.11.0", "react-router": "^6.16.0", @@ -125,7 +125,7 @@ "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", - "i18next-parser": "^6.3.0", + "i18next-parser": "^6.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "lint-staged": "^12.3.7", @@ -11871,7 +11871,8 @@ "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "node_modules/html-minifier-terser": { "version": "6.1.0", @@ -12129,9 +12130,9 @@ } }, "node_modules/i18next": { - "version": "21.6.16", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz", - "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==", + "version": "21.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", + "integrity": "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==", "funding": [ { "type": "individual", @@ -16968,12 +16969,11 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "node_modules/react-i18next": { - "version": "11.16.7", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz", - "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==", + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", "dependencies": { "@babel/runtime": "^7.14.5", - "html-escaper": "^2.0.2", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { @@ -30233,7 +30233,8 @@ "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "html-minifier-terser": { "version": "6.1.0", @@ -30419,9 +30420,9 @@ "dev": true }, "i18next": { - "version": "21.6.16", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz", - "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==", + "version": "21.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", + "integrity": "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==", "requires": { "@babel/runtime": "^7.17.2" } @@ -33952,12 +33953,11 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-i18next": { - "version": "11.16.7", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz", - "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==", + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", "requires": { "@babel/runtime": "^7.14.5", - "html-escaper": "^2.0.2", "html-parse-stringify": "^3.0.1" } }, diff --git a/package.json b/package.json index 2c0207cfd..719910246 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.web.ts", "test": "jest", "prepare": "husky install", - "i18next": "i18next -c src/renderer/i18n/i18next-parser.config.js", + "i18next": "i18next -c src/i18n/i18next-parser.config.js", "prod:buildserver": "pwsh -c \"./scripts/server-build.ps1\"", "prod:publishserver": "pwsh -c \"./scripts/server-publish.ps1\"" }, @@ -219,7 +219,7 @@ "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", - "i18next-parser": "^6.3.0", + "i18next-parser": "^6.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "lint-staged": "^12.3.7", @@ -288,7 +288,7 @@ "framer-motion": "^10.13.0", "fuse.js": "^6.6.2", "history": "^5.3.0", - "i18next": "^21.6.16", + "i18next": "^21.10.0", "idb-keyval": "^6.2.1", "immer": "^9.0.21", "is-electron": "^2.2.2", @@ -303,7 +303,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", - "react-i18next": "^11.16.7", + "react-i18next": "^11.18.6", "react-icons": "^4.10.1", "react-player": "^2.11.0", "react-router": "^6.16.0", diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js deleted file mode 100644 index 1683c700b..000000000 --- a/src/i18n/i18n.js +++ /dev/null @@ -1,32 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -const en = require('./locales/en.json'); - -const resources = { - en: { translation: en }, -}; - -export const Languages = [ - { - label: 'English', - value: 'en', - }, -]; - -i18n - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - fallbackLng: 'en', - // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources - // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage - // if you're using a language detector, do not define the lng option - interpolation: { - escapeValue: false, // react already safes from xss - }, - - lng: 'en', - - resources, - }); - -export default i18n; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 000000000..95c13e5a8 --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,73 @@ +import { PostProcessorModule } from 'i18next'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +const en = require('./locales/en.json'); + +const resources = { + en: { translation: en }, +}; + +export const Languages = [ + { + label: 'English', + value: 'en', + }, +]; + +const lowerCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'lowerCase', + process: (value: string) => { + return value.toLocaleLowerCase(); + }, +}; + +const upperCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'upperCase', + process: (value: string) => { + return value.toLocaleUpperCase(); + }, +}; + +const titleCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'titleCase', + process: (value: string) => { + return value.replace(/\w\S*/g, (txt) => { + return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase(); + }); + }, +}; + +const sentenceCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'sentenceCase', + process: (value: string) => { + const sentences = value.split('. '); + + return sentences + .map((sentence) => { + return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLocaleLowerCase(); + }) + .join('. '); + }, +}; +i18n.use(lowerCasePostProcessor) + .use(upperCasePostProcessor) + .use(titleCasePostProcessor) + .use(sentenceCasePostProcessor) + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + fallbackLng: 'en', + // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + interpolation: { + escapeValue: false, // react already safes from xss + }, + lng: 'en', + resources, + }); + +export default i18n; diff --git a/src/i18n/i18next-parser.config.js b/src/i18n/i18next-parser.config.js index d3d12e0ed..22071636c 100644 --- a/src/i18n/i18next-parser.config.js +++ b/src/i18n/i18next-parser.config.js @@ -1,117 +1,44 @@ -// i18next-parser.config.js +// Reference: https://github.com/i18next/i18next-parser#options module.exports = { - contextSeparator: '_', - // Key separator used in your translation keys - - createOldCatalogs: true, - - // Exit with an exit code of 1 when translations are updated (for CI purpose) - customValueTemplate: null, - - // Save the \_old files - defaultNamespace: 'translation', - - // Default namespace used in your i18next config - defaultValue: '', - - // Exit with an exit code of 1 on warnings - failOnUpdate: false, - - // Display info about the parsing including some stats - failOnWarnings: false, - - // The locale to compare with default values to determine whether a default value has been changed. - // If this is set and a default value differs from a translation in the specified locale, all entries - // for that key across locales are reset to the default value, and existing translations are moved to - // the `_old` file. - i18nextOptions: null, - - // Default value to give to empty keys - // You may also specify a function accepting the locale, namespace, and key as arguments - indentation: 2, - - // Plural separator used in your translation keys - // If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys. - input: [ - '../components/**/*.{js,jsx,ts,tsx}', - '../features/**/*.{js,jsx,ts,tsx}', - '../layouts/**/*.{js,jsx,ts,tsx}', - '!../../src/node_modules/**', - '!../../src/**/*.prod.js', - ], - - // Indentation of the catalog files - keepRemoved: false, - - // Keep keys from the catalog that are no longer in code - keySeparator: '.', - - // Key separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. - // see below for more details - lexers: { - default: ['JavascriptLexer'], - handlebars: ['HandlebarsLexer'], - - hbs: ['HandlebarsLexer'], - htm: ['HTMLLexer'], - - html: ['HTMLLexer'], - js: ['JavascriptLexer'], - jsx: ['JsxLexer'], - - mjs: ['JavascriptLexer'], - // if you're writing jsx inside .js files, change this to JsxLexer - ts: ['JavascriptLexer'], - - tsx: ['JsxLexer'], - }, - - lineEnding: 'auto', - - // Control the line ending. See options at https://github.com/ryanve/eol - locales: ['en'], - - // An array of the locales in your applications - namespaceSeparator: false, - - // Namespace separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. - output: 'src/renderer/i18n/locales/$LOCALE.json', - - // Supports $LOCALE and $NAMESPACE injection - // Supports JSON (.json) and YAML (.yml) file formats - // Where to write the locale files relative to process.cwd() - pluralSeparator: '_', - - // If you wish to customize the value output the value as an object, you can set your own format. - // ${defaultValue} is the default value you set in your translation function. - // Any other custom property will be automatically extracted. - // - // Example: - // { - // message: "${defaultValue}", - // description: "${maxLength}", // - // } - resetDefaultValueLocale: 'en', - - // Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters) - skipDefaultValues: false, - - // An array of globs that describe where to look for source files - // relative to the location of the configuration file - sort: true, - - // Whether to ignore default values - // You may also specify a function accepting the locale and namespace as arguments - useKeysAsDefaultValue: true, - - // Whether to use the keys as the default value; ex. "Hello": "Hello", "World": "World" - // This option takes precedence over the `defaultValue` and `skipDefaultValues` options - // You may also specify a function accepting the locale and namespace as arguments - verbose: false, - // If you wish to customize options in internally used i18next instance, you can define an object with any - // configuration property supported by i18next (https://www.i18next.com/overview/configuration-options). - // { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals. + contextSeparator: '_', + createOldCatalogs: true, + customValueTemplate: null, + defaultNamespace: 'translation', + defaultValue: '', + failOnUpdate: false, + failOnWarnings: false, + i18nextOptions: null, + indentation: 4, + input: [ + '../renderer/components/**/*.{js,jsx,ts,tsx}', + '../renderer/features/**/*.{js,jsx,ts,tsx}', + '../renderer/layouts/**/*.{js,jsx,ts,tsx}', + '!../src/node_modules/**', + '!../src/**/*.prod.js', + ], + keepRemoved: false, + keySeparator: '.', + lexers: { + default: ['JavascriptLexer'], + handlebars: ['HandlebarsLexer'], + hbs: ['HandlebarsLexer'], + htm: ['HTMLLexer'], + html: ['HTMLLexer'], + js: ['JavascriptLexer'], + jsx: ['JsxLexer'], + mjs: ['JavascriptLexer'], + ts: ['JavascriptLexer'], + tsx: ['JsxLexer'], + }, + lineEnding: 'auto', + locales: ['en'], + namespaceSeparator: false, + output: 'src/renderer/i18n/locales/$LOCALE.json', + pluralSeparator: '_', + resetDefaultValueLocale: 'en', + skipDefaultValues: false, + sort: true, + useKeysAsDefaultValue: true, + verbose: false, }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 147e95db7..360854a70 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1,9 +1,593 @@ { - "player": { - "next": "player.next", - "play": "player.play", - "prev": "player.prev", - "seekBack": "player.seekBack", - "seekForward": "player.seekForward" - } + "action": { + "addToFavorites": "add to $t(entity.favorite_other)", + "addToPlaylist": "add to $t(entity.playlist_one)", + "createPlaylist": "create $t(entity.playlist_one)", + "deletePlaylist": "delete $t(entity.playlist_one)", + "deselectAll": "deselect all", + "editPlaylist": "edit $t(entity.playlist_one)", + "moveToBottom": "move to bottom", + "moveToTop": "move to top", + "refresh": "$t(glossary.refresh)", + "removeFromFavorites": "remove from $t(entity.favorite_other)", + "removeFromPlaylist": "remove from $t(entity.playlist_one)", + "removeFromQueue": "remove from queue", + "setRating": "set rating", + "toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor", + "viewPlaylists": "view $t(entity.playlist_other)" + }, + "common": { + "backward": "backward", + "forward": "forward", + "modified": "modified", + "minimize": "minimize", + "increase": "increase", + "decrease": "decrease", + "maximize": "maximize", + "areYouSure": "are you sure?", + "resetToDefault": "reset to default", + "manage": "manage", + "add": "add", + "cancel": "cancel", + "confirm": "confirm", + "create": "create", + "delete": "delete", + "disable": "disable", + "dismiss": "dismiss", + "edit": "edit", + "enable": "enable", + "no": "no", + "none": "none", + "ok": "ok", + "playerMustBePaused": "player must be paused", + "quit": "quit", + "restartRequired": "restart required", + "forceRestartRequired": "restart to apply changes... close the notification to restart", + "save": "save", + "saveAs": "save as", + "saveAndReplace": "save and replace", + "yes": "yes", + "left": "left", + "center": "center", + "right": "right", + "search": "search", + "noResultsFromQuery": "the query returned no results" + }, + "entity": { + "album_one": "album", + "album_other": "albums", + "albumArtist_one": "album artist", + "albumArtist_other": "album artists", + "albumArtistCount_one": "{{count}} album artist", + "albumArtistCount_other": "{{count}} album artists", + "albumWithCount_one": "{{count}} album", + "albumWithCount_other": "{{count}} albums", + "artist_one": "artist", + "artist_other": "artists", + "artistWithCount_one": "{{count}} artist", + "artistWithCount_other": "{{count}} artists", + "favorite_one": "favorite", + "favorite_other": "favorites", + "folder_one": "folder", + "folder_other": "folders", + "folderWithCount_one": "{{count}} folder", + "folderWithCount_other": "{{count}} folders", + "genre_one": "genre", + "genre_other": "genres", + "genreWithCount_one": "{{count}} genre", + "genreWithCount_other": "{{count}} genres", + "playlist_one": "playlist", + "playlist_other": "playlists", + "playlistWithCount_one": "{{count}} playlist", + "playlistWithCount_other": "{{count}} playlists", + "setting_one": "setting", + "setting_other": "settings", + "smartPlaylist": "smart $t(entity.playlist_one)", + "track_one": "track", + "track_other": "tracks", + "trackWithCount_one": "{{count}} track", + "trackWithCount_other": "{{count}} tracks" + }, + "error": { + "playbackError": "an error occurred when trying to play the media", + "genericError": "an error occurred", + "apiRouteError": "unable to route request", + "serverNotSelectedError": "no server selected", + "endpointNotImplementedError": "endpoint {{endpoint} is not implemented for {{serverType}}", + "audioDeviceFetchError": "an error occurred when trying to get audio devices", + "localFontAccessDenied": "access denied to local fonts", + "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", + "remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server", + "remotePortError": "an error occurred when trying to set the remote server port", + "remotePortWarning": "restart the server to apply the new port", + "systemFontError": "an error occurred when trying to get system fonts", + "invalidServer": "invalid server", + "authenticationFailed": "authentication failed", + "sessionExpiredError": "your session has expired", + "loginRateError": "too many login attempts, please try again in a few seconds", + "mpvRequired": "MPV required", + "credentialsRequired": "credentials required", + "serverRequired": "server required" + }, + "form": { + "addServer": { + "title": "add server", + "input_name": "server name", + "input_url": "url", + "input_username": "username", + "input_password": "password", + "input_savePassword": "save password", + "input_legacyAuthentication": "enable legacy authentication", + "ignoreCors": "ignore cors ($t(common.restartRequired))", + "ignoreSsl": "ignore ssl ($t(common.restartRequired))", + "success": "server added successfully", + "error_savePassword": "an error occurred when trying to save the password" + }, + "createPlaylist": { + "title": "create $t(entity.playlist_one)", + "input_name": "$t(glossary.name)", + "input_description": "$t(glossary.description)", + "input_public": "public", + "input_owner": "$t(glossary.owner)", + "success": "$t(entity.playlist_one) created successfully" + }, + "editPlaylist": { + "title": "edit $t(entity.playlist_one)" + }, + "deletePlaylist": { + "title": "delete $t(entity.playlist_one)", + "input_confirm": "type the name of the $t(entity.playlist_one) to confirm", + "success": "$t(entity.playlist_one) deleted successfully" + }, + "addToPlaylist": { + "title": "add to $t(entity.playlist_one)", + "input_playlists": "$t(entity.playlist_other)", + "input_skipDuplicates": "skip duplicates", + "success": "added {{message}} $t(entity.song_other) to {{numOfPlaylists}} $t(entity.playlist_other)" + }, + "updateServer": { + "title": "update server", + "success": "server updated successfully" + }, + "lyricSearch": { + "title": "lyric search", + "input_name": "$t(glossary.name)", + "input_artist": "$t(entity.artist_one)" + } + }, + "filter": { + "isRated": "is rated", + "isFavorited": "is favorited", + "isCompilation": "is compilation", + "isRecentlyPlayed": "is recently played", + "fromYear": "from year", + "toYear": "to year", + "albumArtist": "$t(entity.albumArtist_one)", + "artist": "$t(entity.artist_one)", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "communityRating": "community rating", + "criticRating": "critic rating", + "dateAdded": "date added", + "disc": "disc", + "duration": "duration", + "favorited": "favorited", + "lastPlayed": "last played", + "mostPlayed": "most played", + "name": "name", + "note": "note", + "path": "path", + "playCount": "play count", + "random": "random", + "rating": "rating", + "recentlyAdded": "recently added", + "recentlyPlayed": "recently played", + "releaseDate": "release date", + "releaseYear": "release year", + "search": "search", + "songCount": "song count", + "title": "title", + "trackNumber": "track" + }, + "glossary": { + "sortOrder": "order", + "limit": "limit", + "reset": "reset", + "clear": "clear", + "action": "action", + "expand": "expand", + "collapse": "collapse", + "action_other": "actions", + "ascending": "ascending", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "channel": "channel", + "channel_other": "channels", + "configure": "configure", + "descending": "descending", + "disc": "disc", + "duration": "duration", + "favorite": "favorite", + "filter_one": "filter", + "filter_other": "filters", + "description": "description", + "gap": "gap", + "home": "home", + "menu": "menu", + "name": "name", + "note": "note", + "owner": "owner", + "path": "path", + "random": "random", + "rating": "rating", + "refresh": "refresh", + "search": "search", + "setting": "setting", + "setting_other": "settings", + "size": "size", + "title": "title", + "trackNumber": "track", + "version": "version", + "year": "year" + }, + "page": { + "albumDetail": { + "moreFromArtist": "more from this $t(entity.genre_one)", + "moreFromGeneric": "more from {{item}}" + }, + "setting": { + "generalTab": "general", + "playbackTab": "playback", + "hotkeysTab": "hotkeys", + "windowTab": "window" + }, + "albumArtistList": { + "title": "$t(entity.albumArtist_other)" + }, + "albumList": { + "title": "$t(entity.album_other)" + }, + "appMenu": { + "collapseSidebar": "collapse sidebar", + "expandSidebar": "expand sidebar", + "goBack": "go back", + "goForward": "go forward", + "manageServers": "manage servers", + "openBrowserDevtools": "open browser devtools", + "quit": "$t(common.quit)", + "selectServer": "select server", + "settings": "$t(glossary.setting_other)", + "version": "version {{version}}" + }, + "contextMenu": { + "addFavorite": "$t(action.addToFavorites)", + "addLast": "$t(player.addLast)", + "addNext": "$t(player.addNext)", + "addToFavorites": "$t(action.addToFavorites)", + "addToPlaylist": "$t(action.addToPlaylist)", + "createPlaylist": "$t(action.createPlaylist)", + "deletePlaylist": "$t(action.deletePlaylist)", + "deselectAll": "$t(action.deselectAll)", + "moveToBottom": "$t(action.moveToBottom)", + "moveToTop": "$t(action.moveToTop)", + "numberSelected": "{{count}} selected", + "play": "$t(player.play)", + "removeFromFavorites": "$t(action.removeFromFavorites)", + "removeFromPlaylist": "$t(action.removeFromPlaylist)", + "removeFromQueue": "$t(action.removeFromQueue)", + "setRating": "$t(action.setRating)" + }, + "genreList": { + "title": "$t(entity.genre_other)" + }, + "home": { + "explore": "explore from your library", + "mostPlayed": "most played", + "newlyAdded": "newly added releases", + "recentlyPlayed": "recently played", + "title": "$t(glossary.home)" + }, + "playlistList": { + "title": "$t(entity.playlist_other)" + }, + "sidebar": { + "albums": "$t(entity.album_other)", + "artists": "$t(entity.artist_other)", + "albumArtists": "$t(entity.albumArtist_other)", + "folders": "$t(entity.folder_other)", + "genres": "$t(entity.genre_other)", + "home": "$t(glossary.home)", + "nowPlaying": "now playing", + "playlists": "$t(entity.playlist_other)", + "search": "$t(glossary.search)", + "settings": "$t(entity.setting_other)", + "tracks": "$t(entity.track_other)" + }, + "trackList": { + "title": "$t(entity.track_other)" + }, + "globalSearch": { + "title": "commands", + "commands": { + "searchFor": "search for {{query}}", + "goToPage": "go to page", + "serverCommands": "server commands" + } + }, + "fullscreenPlayer": { + "config": { + "dynamicBackground": "dynamic background", + "useImageAspectRatio": "use image aspect ratio", + "opacity": "opacity", + "followCurrentLyric": "follow current lyric", + "showLyricProvider": "show lyric provider", + "showLyricMatch": "show lyric match", + "lyricSize": "lyric size", + "synchronized": "synchronized", + "unsynchronized": "unsynchronized", + "lyricGap": "lyric gap", + "lyricAlignment": "lyric alignment" + } + } + }, + "player": { + "favorite": "favorite", + "unfavorite": "unfavorite", + "addLast": "add last", + "addNext": "add next", + "mute": "mute", + "muted": "muted", + "next": "next", + "play": "play", + "previous": "previous", + "queue_clear": "clear queue", + "queue_moveToBottom": "move selected to top", + "queue_moveToTop": "move selected to bottom", + "queue_remove": "remove selected", + "repeat": "repeat", + "repeat_all": "repeat all", + "repeat_off": "repeat disabled", + "repeat_one": "repeat one", + "skip": "skip", + "skip_back": "skip backwards", + "skip_forward": "skip forwards", + "shuffle": "shuffle", + "playRandom": "play random", + "shuffle_off": "shuffle disabled", + "stop": "stop", + "toggleFullscreenPlayer": "toggle fullscreen player", + "playbackSpeed": "playback speed", + "playbackFetchNoResults": "no songs found", + "playbackFetchInProgress": "loading songs...", + "playbackFetchCancel": "this is taking a while... close the notification to cancel" + }, + "setting": { + "exitToTray": "exit to tray", + "exitToTray_description": "exit the application to the system tray", + "minimizeToTray": "minimize to tray", + "minimizeToTray_description": "minimize the application to the system tray", + "windowBarStyle": "window bar style", + "windowBarStyle_description": "select the style of the window bar", + "disableAutomaticUpdates": "disable automatic updates", + "disableLibraryUpdateOnStartup": "disable checking for new versions on startup", + "discordRichPresence": "{{discord}} rich presence", + "discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ", + "discordApplicationId": "{{discord}} application id", + "discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}}", + "discordUpdateInterval": "{{discord}} rich presence update interval", + "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", + "discordIdleStatus": "show rich presence idle status", + "discordIdleStatus_description": "when enabled, update status while player is idle", + "accentColor": "accent color", + "accentColor_description": "sets the accent color for the application", + "applicationHotkeys": "application hotkeys", + "applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)", + "audioDevice": "audio device", + "audioDevice_description": "select the audio device to use for playback (web player only)", + "audioExclusiveMode": "audio exclusive mode", + "audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio", + "audioPlayer": "audio player", + "audioPlayer_description": "select the audio player to use for playback", + "crossfadeDuration": "crossfade duration", + "crossfadeDuration_description": "sets the duration of the crossfade effect", + "crossfadeStyle": "crossfade style", + "crossfadeStyle_description": "select the crossfade style to use for the audio player", + "customFontPath": "custom font path", + "customFontPath_description": "sets the path to the custom font to use for the application", + "enableRemote": "enable remote control server", + "enableRemote_description": "enables the remote control server to allow other devices to control the application", + "floatingQueueArea": "show floating queue hover area", + "floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue", + "followLyric": "follow current lyric", + "followLyric_description": "scroll the lyric to the current playing position", + "font": "font", + "font_description": "sets the font to use for the application", + "fontType": "font type", + "fontType_description": "built-in font selects one of the fonts provided by Feishin. system font allows you to select any font provided by your operating system. custom allows you to provide your own font", + "fontType_optionBuiltIn": "built-in font", + "fontType_optionCustom": "custom font", + "fontType_optionSystem": "system font", + "gaplessAudio": "gapless audio", + "gaplessAudio_description": "sets the gapless audio setting for mpv", + "gaplessAudio_optionWeak": "weak (recommended)", + "globalMediaHotkeys": "global media hotkeys", + "globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback", + "hotkey_browserBack": "browser back", + "hotkey_browserForward": "browser forward", + "hotkey_globalSearch": "global search", + "hotkey_localSearch": "in-page search", + "hotkey_playbackNext": "next track", + "hotkey_playbackPause": "pause", + "hotkey_playbackPlay": "play", + "hotkey_playbackPlayPause": "play / pause", + "hotkey_playbackPrevious": "previous track", + "hotkey_playbackStop": "stop", + "hotkey_rate0": "rating clear", + "hotkey_rate1": "rating 1 star", + "hotkey_rate2": "rating 2 stars", + "hotkey_rate3": "rating 3 stars", + "hotkey_rate4": "rating 4 stars", + "hotkey_rate5": "rating 5 stars", + "hotkey_skipBackward": "skip backward", + "hotkey_skipForward": "skip forward", + "hotkey_toggleFullScreenPlayer": "toggle full screen player", + "hotkey_toggleQueue": "toggle queue", + "hotkey_toggleRepeat": "toggle repeat", + "hotkey_toggleShuffle": "toggle shuffle", + "hotkey_volumeDown": "volume down", + "hotkey_volumeMute": "volume mute", + "hotkey_volumeUp": "volume up", + "hotkey_zoomIn": "zoom in", + "hotkey_zoomOut": "zoom out", + "language": "language", + "language_description": "sets the language for the application", + "lyricFetch": "fetch lyrics from the internet", + "lyricFetch_description": "fetch lyrics from various internet sources", + "lyricFetchProvider": "providers to fetch lyrics from", + "lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried", + "lyricOffset": "lyric offset (ms)", + "lyricOffset_description": "offset the lyric by the specified amount of milliseconds", + "minimumScrobblePercentage": "minimum scrobble duration (percentage)", + "minimumScrobblePercentage_description": "the minimum percentage of the song that must be played before it is scrobbled", + "minimumScrobbleSeconds": "minimum scrobble (seconds)", + "minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled", + "mpvExecutablePath": "mpv executable path", + "mpvExecutablePath_description": "sets the path to the mpv executable", + "mpvExecutablePath_help": "one per line", + "mpvExtraParameters": "mpv parameters", + "playbackStyle": "playback style", + "playbackStyle_description": "select the playback style to use for the audio player", + "playbackStyle_optionCrossFade": "crossfade", + "playbackStyle_optionNormal": "normal", + "playButtonBehavior": "play button behavior", + "playButtonBehavior_description": "sets the default behavior of the play button when adding songs to the queue", + "playButtonBehavior_optionAddLast": "$t(player.addLast)", + "playButtonBehavior_optionAddNext": "$t(player.addNext)", + "playButtonBehavior_optionPlay": "$t(player.play)", + "remotePassword": "remote control server password", + "remotePassword_description": "sets the password for the remote control server. These credentials are by default transferred insecurely, so you should use a unique password that you do not care about", + "remotePort": "remote control server port", + "remotePort_description": "sets the port for the remote control server", + "remoteUsername": "remote control server username", + "remoteUsername_description": "sets the username for the remote control server. if both username and password are empty, authentication will be disabled", + "replayGainClipping": "{{ReplayGain}} clipping", + "replayGainClipping_description": "Prevent clipping caused by {{ReplayGain}} by automatically lowering the gain", + "replayGainFallback": "{{ReplayGain}} fallback", + "replayGainFallback_description": "gain in db to apply if the file has no {{ReplayGain}} tags", + "replayGainMode": "{{ReplayGain}} mode", + "replayGainMode_description": "adjust volume gain according to {{ReplayGain}} values stored in the file metadata", + "replayGainMode_optionAlbum": "$t(entity.album_one)", + "replayGainMode_optionNone": "$t(common.none)", + "replayGainMode_optionTrack": "$t(entity.track_one)", + "replayGainPreamp": "{{ReplayGain}} preamp (dB)", + "replayGainPreamp_description": "adjust the preamp gain applied to the {{ReplayGain}} values", + "sampleRate": "sample rate", + "sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media", + "savePlayQueue": "save play queue", + "savePlayQueue_description": "save the play queue when the application is closed and restore it when the application is opened", + "scrobble": "scrobble", + "scrobble_description": "scrobble plays to your media server", + "showSkipButton": "show skip buttons", + "showSkipButton_description": "show or hide the skip buttons on the player bar", + "showSkipButtons": "show skip buttons", + "showSkipButtons_description": "show or hide the skip buttons on the player bar", + "sidebarCollapsedNavigation": "sidebar (collapsed) navigation", + "sidebarCollapsedNavigation_description": "show or hide the navigation in the collapsed sidebar", + "sidebarConfiguration": "sidebar configuration", + "sidebarConfiguration_description": "select the items and order in which they appear in the sidebar", + "sidebarPlaylistList": "sidebar playlist list", + "sidebarPlaylistList_description": "show or hide the playlist list in the sidebar", + "sidePlayQueueStyle": "side play queue style", + "sidePlayQueueStyle_description": "sets the style of the side play queue", + "sidePlayQueueStyle_optionAttached": "attached", + "sidePlayQueueStyle_optionDetached": "detached", + "skipDuration": "skip duration", + "skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar", + "skipPlaylistPage": "skip playlist page", + "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", + "theme": "theme", + "theme_description": "sets the theme to use for the application", + "themeDark": "theme (dark)", + "themeDark_description": "sets the dark theme to use for the application", + "themeLight": "theme (light)", + "themeLight_description": "sets the light theme to use for the application", + "useSystemTheme": "use system theme", + "useSystemTheme_description": "follow the system-defined light or dark preference", + "volumeWheelStep": "volume wheel step", + "volumeWheelStep_description": "the amount of volume to change when scrolling the mouse wheel on the volume slider", + "zoom": "zoom percentage", + "zoom_description": "sets the zoom percentage for the application", + "zoomPercentage": "zoom percentage", + "zoomPercentage_description": "sets the zoom percentage for the application" + }, + "table": { + "column": { + "album": "album", + "albumArtist": "album artist", + "albumCount": "$t(entity.album_other)", + "artist": "$t(entity.artist_one)", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "channels": "channels", + "comment": "comment", + "dateAdded": "date added", + "discNumber": "disc", + "favorite": "favorite", + "genre": "$t(entity.genre_one)", + "lastPlayed": "last played", + "path": "path", + "playCount": "plays", + "rating": "rating", + "releaseDate": "release date", + "releaseYear": "year", + "songCount": "$t(entity.track_other)", + "title": "title", + "trackNumber": "track" + }, + "config": { + "general": { + "autoFitColumns": "auto fit columns", + "displayType": "display type", + "gap": "$t(glossary.gap)", + "size": "$t(glossary.size)", + "tableColumns": "table columns" + }, + "label": { + "actions": "$t(glossary.action_other)", + "album": "$t(entity.album_one)", + "albumArtist": "$t(entity.albumArtist_one)", + "artist": "$t(entity.artist_one)", + "biography": "$t(glossary.biography)", + "bitrate": "$t(glossary.bitrate)", + "bpm": "$t(glossary.bpm)", + "channels": "$t(glossary.channel_other)", + "dateAdded": "date added", + "discNumber": "disc number", + "duration": "$t(glossary.duration)", + "favorite": "$t(glossary.favorite)", + "genre": "$t(entity.genre_one)", + "lastPlayed": "last played", + "note": "$t(glossary.note)", + "owner": "$t(glossary.owner)", + "path": "$t(glossary.path)", + "playCount": "play count", + "rating": "$t(glossary.rating)", + "releaseDate": "release date", + "rowIndex": "row index", + "size": "$t(glossary.size)", + "title": "$t(glossary.title)", + "titleCombined": "$t(glossary.title) (combined)", + "trackNumber": "track number", + "year": "$t(glossary.year)" + }, + "view": { + "card": "card", + "poster": "poster", + "table": "table" + } + } + } }