Migrate to lingui

Switch to "lingui" for better DX.
Tried to persist existing languages as much as possible.

Removed "vite-plugin-node-polyfills" because it's incompatible with "lingui"
This commit is contained in:
schroda
2026-01-10 19:45:02 +01:00
parent c1ac58f4d6
commit 65a9a905be
287 changed files with 120766 additions and 37748 deletions

View File

@@ -1,5 +1,5 @@
module.exports = {
extends: ['airbnb', 'airbnb-typescript', 'prettier'],
extends: ['airbnb', 'airbnb-typescript', 'prettier', 'plugin:lingui/recommended'],
plugins: [
'unused-imports',
'eslint-plugin-import',
@@ -15,6 +15,8 @@ module.exports = {
{
files: ['*'],
rules: {
'lingui/no-expression-in-message': 'off',
'no-param-reassign': ['error', { props: true, ignorePropertyModificationsForRegex: ["^draft"] }],
'unused-imports/no-unused-imports': 'error',

View File

@@ -1 +1,3 @@
yarn lint-staged
yarn i18n:extract
git add src/i18n/locales/*.po

26
lingui.config.ts Normal file
View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { defineConfig } from '@lingui/cli';
// eslint-disable-next-line import/no-extraneous-dependencies
import { formatter } from '@lingui/format-po';
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
import localesConfig from './src/i18n/locales.json';
// eslint-disable-next-line import/no-default-export
export default defineConfig({
sourceLocale: localesConfig.defaultLocale,
locales: localesConfig.locales,
catalogs: [
{
path: '<rootDir>/src/i18n/locales/{locale}',
include: ['src'],
},
],
format: formatter({ lineNumbers: false }),
});

View File

@@ -21,6 +21,7 @@
"gql:codegen-base": "graphql-codegen --config gql_codegen.ts",
"gql:codegen-formatter": "tsx tools/scripts/codegenFormatter.ts",
"gql:codegen": "yarn gql:codegen-base && yarn gql:codegen-formatter",
"i18n:extract": "lingui extract --locale en --clean",
"i18n:gen-resources": "tsx tools/scripts/weblate/generatei18nResources.ts",
"dayjs:gen-locales-array": "tsx tools/scripts/dayjs/generateDayJsLocales.ts",
"dayjs:gen-locales-import": "tsx tools/scripts/dayjs/generateDayJsLocalesImport.ts",
@@ -33,6 +34,9 @@
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint --fix"
},
"resolutions": {
"@swc/core": "1.13.5"
},
"dependencies": {
"@apollo/client": "3.13.9",
"@dnd-kit/core": "6.3.1",
@@ -42,6 +46,8 @@
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@juggle/resize-observer": "3.4.0",
"@lingui/core": "5.7.0",
"@lingui/react": "5.7.0",
"@loadable/component": "5.16.7",
"@mantine/hooks": "8.3.8",
"@mui/icons-material": "7.3.5",
@@ -61,9 +67,6 @@
"graphql-sock": "1.0.1",
"graphql-tag": "2.12.6",
"graphql-ws": "6.0.6",
"i18next": "25.6.2",
"i18next-browser-languagedetector": "8.2.0",
"i18next-http-backend": "3.0.2",
"immer": "10.2.0",
"jsonrepair": "3.13.1",
"koration": "1.0.0",
@@ -76,7 +79,6 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hotkeys-hook": "5.2.1",
"react-i18next": "16.3.3",
"react-lazily": "0.9.2",
"react-router-dom": "6.26.1",
"react-virtuoso": "4.14.1",
@@ -92,6 +94,10 @@
"@graphql-codegen/client-preset": "4.8.3",
"@graphql-codegen/typescript-apollo-client-helpers": "3.0.1",
"@graphql-codegen/typescript-operations": "4.6.1",
"@lingui/cli": "5.7.0",
"@lingui/format-po": "5.7.0",
"@lingui/swc-plugin": "5.7.0",
"@lingui/vite-plugin": "5.7.0",
"@types/apollo-upload-client": "18.0.0",
"@types/node": "24.10.1",
"@types/react": "19.2.5",
@@ -104,7 +110,7 @@
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"@vitejs/plugin-legacy": "7.2.1",
"@vitejs/plugin-react-swc": "4.2.2",
"@vitejs/plugin-react-swc": "4.2.1",
"dotenv": "17.2.3",
"eslint": "8.57.0",
"eslint-config-airbnb": "19.0.4",
@@ -113,6 +119,7 @@
"eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-lingui": "0.11.0",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-react": "7.37.5",

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
{
"category": {
"error": {
"label": {
"request_failure": "এই ক্যাটাগরিটি লোড হচ্ছে না",
"empty": "এই ক্যাটাগরিটি খালি"
},
"no_categories_found": {
"label": {
"info": "আপনার এখন পর্যন্ত কোন ক্যাটাগরি নেই।"
}
}
},
"label": {
"category_name": "ক্যাটাগরির নাম",
"use_as_default_category": "ডিফল্ট ক্যাটাগরি যখন নতুন মাঙ্গা লাইব্রেরীতে এড হবে"
},
"title": {
"category_one": "ক্যাটাগরি",
"category_other": "ক্যাটাগরি গুলো",
"set_categories": "ক্যাটাগরি গুলো সেট করুন"
},
"dialog": {
"title": {
"edit_category_one": "ক্যাটাগরি সংশোধন",
"edit_category_other": ""
}
},
"settings": {
"inclusion": {
"label": {
"exclude": "বাদ{{excludedCategoriesText}}",
"include": "সংযুক্ত: {{includedCategoriesText}}"
}
}
}
},
"chapter": {
"action": {
"bookmark": {
"add": {
"button": {
"selected": "বুকমার্ক সিলেক্টেড"
},
"label": {
"action": "নতুন বুকমার্ক সংযুক্ত করুন",
"error_one": "এই চ্যাপ্টারটি বুকমার্ক করা যাচ্ছে না",
"error_other": "এই চ্যাপ্টারগুলো বুকমার্ক করা যাচ্ছে না",
"success_one": "চ্যাপ্টারটি বুক মার্ক করা হয়েছে",
"success_other": "গুলো চ্যাপ্টার বুক মার্ক করা হয়েছে"
}
}
}
}
}
}

View File

@@ -1,102 +0,0 @@
{
"category": {
"dialog": {
"title": {
"edit_category_one": "Editar Categoria",
"edit_category_many": "Editar Categories",
"edit_category_other": "Editar Categories",
"new_category": "Nova categoria"
}
},
"error": {
"label": {
"create_failure": "No s'ha pogut crear la categoria",
"empty": "La categoria està buida",
"request_failure": "No s'han pogut carregar les categories"
},
"no_categories_found": {
"label": {
"info": "Encara no teniu cap categoria."
}
}
},
"label": {
"category_name": "Nom de la categoria",
"use_as_default_category": "Categoria predeterminada en afegir nous manga a la biblioteca"
},
"settings": {
"inclusion": {
"label": {
"exclude": "Exclou: {{excludedCategoriesText}}",
"include": "Inclou: {{includedCategoriesText}}"
}
}
},
"title": {
"category_one": "Categoria",
"category_many": "Categories",
"category_other": "Categories"
},
"value": "{{count}} $t(category.title.category, lowercase)"
},
"chapter": {
"action": {
"download": {
"delete": {
"button": {
"selected": "Elimina els seleccionats"
},
"label": {
"action": "Elimina",
"confirmation_one": "Esteu a punt d'eliminar una descàrrega",
"confirmation_many": "Esteu a punt d'eliminar {{count}} descàrregues",
"confirmation_other": "Esteu a punt d'eliminar {{count}} descàrregues",
"success_one": "Capítol eliminat",
"success_many": "{{count}} capítols eliminats",
"success_other": "{{count}} capítols eliminats"
}
}
},
"filter_and_sort": {
"label": "Filtra i ordena"
},
"label": {
"select": "Selecciona"
},
"mark_as_read": {
"add": {
"button": {
"selected": "Marca els seleccionats com a llegits"
},
"label": {
"action": {
"all": "Marca'ls tots com a llegits",
"current": "Marca com a llegit",
"previous": "Marca els anteriors com a llegits"
},
"confirmation_one": "Esteu a punt de marcar un capítol com a llegit",
"confirmation_many": "Esteu a punt de marcar {{count}} capítols com a llegits",
"confirmation_other": "Esteu a punt de marcar {{count}} capítols com a llegits",
"error_one": "No s'ha pogut marcar el capítol com a llegit",
"error_many": "No s'han pogut marcar els capítols com a llegits",
"error_other": "No s'han pogut marcar els capítols com a llegits",
"success_one": "Capítol marcat com a llegit",
"success_many": "{{count}} capítols marcats com a llegits",
"success_other": "{{count}} capítols marcats com a llegits"
}
},
"remove": {
"button": {
"selected": "Marca els seleccionats com a no llegits"
},
"label": {
"action": "Marca com a no llegit",
"confirmation_one": "Esteu a punt de marcar un capítol com a no llegits",
"confirmation_many": "Esteu a punt de marcar {{count}} capítols com a no llegits",
"confirmation_other": "Esteu a punt de marcar {{count}} capítols com a no llegits"
}
}
}
}
}
}

View File

@@ -1,281 +0,0 @@
{
"category": {
"dialog": {
"title": {
"new_category": "Ny kategori",
"edit_category_one": "Rediger kategori",
"edit_category_other": ""
}
},
"error": {
"label": {
"request_failure": "Kunne ikke indlæse kategorier",
"empty": "Denne kategori er tom"
},
"no_categories_found": {
"label": {
"info": "Du har ikke nogen kategorier endnu."
}
}
},
"label": {
"category_name": "Kategorinavn",
"use_as_default_category": "Standardkategori ved tilføjelse af ny manga til biblioteket"
},
"settings": {
"inclusion": {
"label": {
"exclude": "Ekskluder: {{excludedCategoriesText}}",
"include": "Inkluder: {{includedCategoriesText}}"
}
}
},
"title": {
"category_one": "Kategori",
"category_other": "Kategorier",
"set_categories": "Vælg kategorier"
}
},
"chapter": {
"action": {
"bookmark": {
"add": {
"button": {
"selected": "Bogmærke valgt"
},
"label": {
"action": "Tilføj bogmærke",
"error_one": "Kunne ikke bogmærke kapitlet",
"error_other": "Kunne ikke bogmærke kapitlerne",
"success_one": "Kapitel bogmærket",
"success_other": "{{count}} kapitler bogmærket"
}
},
"remove": {
"button": {
"selected": "Fjern bogmærker fra de valgte"
},
"label": {
"action": "Fjern bogmærke",
"error_one": "Kunne ikke fjerne bogmærket",
"error_other": "Kunne ikke fjerne bogmærkerne",
"success_one": "Kapitelbogmærke fjernet",
"success_other": "{{count}} kapitelbogmærker fjernet"
}
}
},
"download": {
"add": {
"button": {
"selected": "Download de valgte"
},
"label": {
"ahead": "$t(download.settings.download_ahead.title) ({{count}})",
"error_one": "Kunne ikke tilføje dit download",
"error_other": "Kunne ikke tilføje dine downloads",
"action": "Download",
"success_one": "Download tilføjet",
"success_other": "{{count}} downloads tilføjet"
}
},
"delete": {
"label": {
"action": "Slet",
"error_one": "Kunne ikke slette kapitlet",
"error_other": "Kunne ikke slette kapitlerne",
"success_one": "Kapitel slettet",
"success_other": "{{count}} kapitler slettet"
},
"button": {
"selected": "Slet valgte"
}
}
},
"label": {
"select": "Vælg"
},
"mark_as_read": {
"add": {
"label": {
"action": {
"previous": "Marker forrige som læst",
"current": "Marker som læst"
},
"error_one": "Kunne ikke markere kapitlet som læst",
"error_other": "Kunne ikke markere kapitlerne som læst",
"success_one": "Kapitel markeret som læst",
"success_other": "{{count}} kapitler markeret som læst"
},
"button": {
"selected": "Marker valgte som læst"
}
},
"remove": {
"button": {
"selected": "Marker valgte som ulæst"
},
"label": {
"action": "Marker som ulæst",
"error_one": "Kunne ikke markere kapitlet som ulæst",
"error_other": "Kunne ikke markere kapitlerne som ulæst",
"success_one": "Kapitel markeret som ulæst",
"success_other": "{{count}} kapitler markeret som ulæst"
}
}
}
},
"error": {
"label": {
"no_matches": "Ingen kapitler matcher filteret",
"no_chapter_found": "Ingen kapitler fundet"
}
},
"option": {
"display": {
"label": {
"chapter_number": "Kapitelnummer",
"source_title": "Kildenavn"
}
}
},
"title_one": "Kapitel",
"title_other": "Kapitler",
"status": {
"label": {
"downloaded": "Downloaded"
}
}
},
"download": {
"queue": {
"error": {
"label": {
"failed_delete_all": "Kunne ikke fjerne alle downloads fra køen",
"failed_to_retry": "Kunne ikke genstarte mislykket download.",
"failed_to_remove": "Kunne ikke fjerne download fra køen."
}
},
"label": {
"no_downloads": "Ingen downloads",
"delete_all": "Slet alle"
}
},
"settings": {
"auto_download": {
"download_limit": {
"label": {
"title": "Kapiteldownloadgrænse",
"description": "Begræns antallet af nye kapitler der bliver downloadet."
}
},
"label": {
"ignore_with_unread_chapters": "Ignorer automatiske kapiteldownloads for emner med ulæste kapitler",
"ignore_re_uploads": "Ignorer re-uploaded kapitler",
"new_chapters": "Download nye kapitler"
},
"categories": {
"label": {
"include_in_download": "Emner i ekskluderede kategorier vil ikke blive downloadet, selvom de også er i de inkluderede kategorier"
}
}
},
"delete_chapters": {
"label": {
"manually_marked_as_read": "Slet kapitel efter det er blevet manuelt markeret som læst",
"allow_deletion_of_bookmarked": "Tillad sletning af bogmærket kapitler"
},
"while_reading": {
"label": {
"title": "Slet færdiggjorte kapitler mens du læser"
},
"option": {
"label": {
"fifth": "Femtesidste læste kapitel",
"first": "Sidste læste kapitel",
"fourth": "Fjerdesidste læste kapitel",
"second": "Andensidste læste kapitel",
"third": "Tredjesidste læste kapitel"
}
}
},
"title": "Slet kapitler"
},
"download_ahead": {
"label": {
"description": "Hvor mange kapitler der skal downloades mens der læses.",
"while_reading": "Download automatisk mens du læser",
"unread_chapters_to_download": "Antal af ulæste kapitler der skal downloades",
"disclaimer": "Virker kun hvis nuværende kapitel og det næste kapitel allerede er downloadet."
},
"title": "Download fremad"
},
"download_path": {
"label": {
"description": "Stien til mappen på serveren, hvor de downloadede filer skal gemmes",
"title": "Downloadlokation"
}
},
"file_type": {
"label": {
"cbz": "Gem som CBZ arkiv"
}
}
},
"state": {
"label": {
"error": "Fejl",
"finished": "Gennemført",
"queued": "I kø",
"downloading": "Downloader"
}
}
},
"extension": {
"action": {
"label": {
"install": "Installer",
"uninstall": "Afinstaller",
"update": "Opdater",
"update_all": "Opdater alle",
"install_external": "Installer eksternt plugin"
}
},
"label": {
"add_repository_info": "Du skal tilføje et repository for at kunne installere plugins",
"installation_failed_one": "Kunne ikke installere plugin",
"installation_failed_other": "Kunne ikke installere plugins",
"installed_successfully_one": "Plugin installeret",
"installed_successfully_other": "Plugins installeret",
"uninstallation_failed_one": "Kunne ikke afinstallere pluginnet",
"uninstallation_failed_other": "Kunne ikke afinstallere plugins",
"update_failed_one": "Kunne ikke opdatere pluginet",
"update_failed_other": "Kunne ikke opdatere plugins",
"installing_file": "Installerer pluginfil…"
},
"language": {
"all": "Alle",
"other": "Andre"
},
"settings": {
"repositories": {
"custom": {
"dialog": {
"action": {
"button": {
"add": "Tilføj repository"
}
}
},
"error": {
"label": {
"invalid_url": "Ugyldigt repositorylink"
}
},
"label": {
"description": "Tilføj repositories hvor plugins kan blive installeret fra"
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,438 +0,0 @@
{
"category": {
"dialog": {
"title": {
"edit_category_one": "I-edit ang kategorya",
"edit_category_other": "I-edit ang mga kategorya",
"new_category": "Bagong kategorya"
}
},
"error": {
"label": {
"empty": "Walang laman ang kategorya",
"request_failure": "Hindi ma-load ang mga kategorya",
"create_failure": "Hindi makalikha ng kategorya"
},
"no_categories_found": {
"label": {
"info": "Wala ka pang mga kategorya."
}
}
},
"label": {
"category_name": "Pangalan ng Kategorya",
"use_as_default_category": "Default na kategorya kung magdagdag ng bagong manga sa aklatan"
},
"settings": {
"inclusion": {
"label": {
"exclude": "Ibukod: {{excludedCategoriesText}}",
"include": "Isama: {{includedCategoriesText}}"
}
}
},
"title": {
"category_one": "Kategorya",
"category_other": "Mga Kategorya",
"set_categories": "I-set ang mga kategorya"
}
},
"chapter": {
"action": {
"bookmark": {
"add": {
"label": {
"success_one": "Na-bookmark ang kabanata",
"success_other": "{{count}} na kabanata ay na-bookmark",
"action": "Magdagdag ng bookmark",
"error_one": "Hindi ma-bookmark ang kabanata",
"error_other": "Hindi ma-bookmark ang mga kabanata"
},
"button": {
"selected": "Napili ang bookmark"
}
},
"remove": {
"button": {
"selected": "Alisin ang mga bookmark sa napili"
},
"label": {
"action": "Alisin ang bookmark",
"error_one": "Hindi maalis ang bookmark",
"error_other": "Hindi maalis ang mga bookmark",
"success_one": "Natanggal ang bookmark ng kabanata",
"success_other": "{{count}} na kabanata ay natanggal",
"confirmation_one": "Aalisin mo na ang isang bookmark",
"confirmation_other": "Aalisin mo na ang {{count}} na mga bookmark"
}
}
},
"download": {
"add": {
"label": {
"error_one": "Hindi maidagdag ang pag-download",
"error_other": "Hindi maidagdag ang mga pag-download",
"action": "I-download",
"success_one": "Idinagdag ang pag-download",
"success_other": "{{count}} na download ang nadagdag",
"confirmation_one": "Magda-download ka na ng isang kabanata",
"confirmation_other": "Magda-download ka na ng {{count}} na mga kabanata.\nAng Suwayomi ay hindi isang mass downloader at masyadong maraming pag-download ang maaaring ma-ban ka mula sa mga source at/o magdulot ng mga isyu sa performance.",
"next_one": "Susunod na kabanata",
"next_other": "Susunod na {{count}} mga kabanata"
},
"button": {
"selected": "Napili ang pag-download"
}
},
"delete": {
"label": {
"action": "Tanggalin",
"error_one": "Hindi matanggal ang kabanata",
"error_other": "Hindi matanggal ang mga kabanata",
"success_one": "Natanggal ang kabanata",
"success_other": "{{count}} na kabanata ang natanggal",
"confirmation_one": "Magde-delete ka na ng isang download",
"confirmation_other": "Ide-delete mo na ang {{count}} mga download"
},
"button": {
"selected": "Tanggalin ang napili"
}
}
},
"label": {
"select": "Pumili"
},
"mark_as_read": {
"add": {
"button": {
"selected": "Markahan ang napili bilang nabasa na"
},
"label": {
"action": {
"current": "Markahan bilang nabasa na",
"previous": "Markahan ang nakaraan bilang nabasa na",
"all": "Markahan ang lahat bilang nabasa na"
},
"error_one": "Hindi namarkahan ang kabanata bilang nabasa na",
"error_other": "Hindi namarkahan ang mga kabanata bilang nabasa na",
"success_one": "Ang kabanata ay minarkahan bilang nabasa na",
"success_other": "{{count}} na mga kabanata ay minarkahan bilang nabasa na",
"confirmation_one": "Mamarkahan mo na ang isang kabanata bilang nabasa na",
"confirmation_other": "Mamarkahan mo na ang {{count}} na mga kabanata bilang nabasa na"
}
},
"remove": {
"button": {
"selected": "Markahan ang napili bilang hindi pa nababasa"
},
"label": {
"action": "Markahan bilang hindi pa nababasa",
"error_one": "Hindi mamarkahan ang kabanata bilang hindi pa nababasa",
"error_other": "Hindi mamarkahan ang mga kabanata bilang hindi pa nababasa",
"success_one": "Ang kabanata ay minarkahan bilang hindi pa nababasa",
"success_other": "{{count}} na mga kabanata ang minarkahan bilang hindi pa nababasa",
"confirmation_one": "Mamarkahan mo na ang isang kabanata bilang hindi pa nababasa",
"confirmation_other": "Mamarkahan mo na ang {{count}} na mga kabanata bilang hindi pa nababasa"
}
}
},
"filter_and_sort": {
"label": "I-filter at I-sort"
},
"read": {
"resume": "Magpatuloy sa pagbabasa\n#{{chapterNumber}} — {{title}}\n{{scanlator}}",
"start": "Simulang magbasa\n#{{chapterNumber}} — {{title}}\n{{scanlator}}"
}
},
"title_one": "Kabanata",
"title_other": "Mga kabanata",
"error": {
"label": {
"no_chapter_found": "Walang nahanap na mga kabanata",
"no_matches": "Walang mga kabanata na tumutugma sa filter"
}
},
"option": {
"display": {
"label": {
"chapter_number": "Numero ng kabanata",
"source_title": "Pamagat ng source"
}
},
"exclude_scanlators": "Ibukod ang mga scanlator"
},
"status": {
"label": {
"downloaded": "Na-download"
}
},
"missing_one": "Nawawalang {{count}} na kabanata",
"missing_other": "Nawawalang {{count}} na mga kabanata",
"value_one": "{{count}} na kabanata",
"value_other": "{{count}} na mga kabanata"
},
"download": {
"settings": {
"auto_download": {
"download_limit": {
"label": {
"description": "Limitahan ang dami ng mga bagong kabanata na mada-download.",
"title": "Limitasyon sa pag-download ng kabanata"
}
},
"label": {
"ignore_re_uploads": "Huwag pansinin ang mga muling na-upload na kabanata",
"ignore_with_unread_chapters": "Huwag pansinin ang mga awtomatikong pag-download ng kabanata para sa mga entry na may mga hindi pa nababasang kabanata",
"new_chapters": "Mag-download ng mga bagong kabanata"
},
"categories": {
"label": {
"include_in_download": "Ang mga entry sa mga ibinukod na kategorya ay hindi mada-download kahit na sila ay kasama rin sa mga kategoryang kasama"
}
}
},
"download_ahead": {
"label": {
"description": "Ilang kabanata ang dapat ma-download habang nagbabasa.",
"disclaimer": "Gumagana lamang kung ang kasalukuyang kabanata at ang susunod na kabanata ay na-download na.",
"while_reading": "Auto download habang nagbabasa",
"unread_chapters_to_download": "Bilang ng mga hindi pa nababasang kabanata na ida-download"
},
"title": "Mag-download nang maaga"
},
"delete_chapters": {
"while_reading": {
"option": {
"label": {
"fourth": "Ikaapat hanggang huling binasa na kabanata",
"fifth": "Ikalima hanggang huling binasa ang kabanata",
"first": "Huling nabasa na kabanata",
"second": "Pangalawa hanggang sa huling binasa na kabanata",
"third": "Ikatlo hanggang huling binasa ang kabanata"
}
},
"label": {
"title": "Tanggalin ang mga natapos na kabanata habang nagbabasa"
}
},
"label": {
"allow_deletion_of_bookmarked": "Payagan ang pagtanggal ng mga naka-bookmark na kabanata",
"manually_marked_as_read": "Tanggalin ang kabanata pagkatapos ng mano-manong markahan ito bilang nabasa na"
},
"title": "Tanggalin ang mga kabanata"
},
"download_path": {
"label": {
"description": "Ang path sa directory sa server kung saan dapat ma-save ang mga na-download na file",
"title": "Lokasyon ng download"
}
},
"file_type": {
"label": {
"cbz": "I-save bilang CBZ archive"
}
},
"conversion": {
"compression_level": "Antas ng compression",
"description": "Kung sakaling walang tinukoy na MIME-Type, ang \"default\" ang gagamitin para sa pagproseso.\nItakda ang target na mode sa hindi pinagana upang maiwasan ang pagproseso para sa isang MIME-Type",
"headers": {
"button": "Mga header ({{count}})",
"name": "Pangalan",
"title": "Mga header",
"value": "Halaga"
},
"target_modes": {
"image": {
"description": "I-convert ang mga larawan sa iba't ibang mga format",
"title": "Larawan"
},
"url": {
"description": "Hahawakan ng isang external na serbisyo ang pag-convert ng larawan"
}
},
"title": "Pagproseso ng pag-download ng larawan"
}
},
"title": {
"queue": "Pila ng download",
"download": "Mga Downoad"
},
"queue": {
"error": {
"label": {
"failed_to_remove": "Hindi maalis ang pag-download mula sa pila.",
"failed_to_retry": "Hindi masubukang muli ang nabigong pag-download.",
"failed_delete_all": "Hindi maalis ang lahat ng pag-download mula sa pila"
}
},
"label": {
"delete_all": "Tanggalin lahat",
"no_downloads": "Walang mga download"
},
"info": {
"paused": "Naka-pause — $t(download.queue.info.remaining)",
"remaining": "{{count}} ang natitira"
}
},
"state": {
"label": {
"downloading": "Nagda-download",
"queued": "Nakapila"
}
}
},
"extension": {
"settings": {
"repositories": {
"custom": {
"label": {
"title": "Mga Extension repository",
"description": "Magdagdag ng mga repository kung saan maaaring mai-install ang mga extension",
"disclaimer": "<strong>Ang Suwayomi ay hindi nagbibigay ng anumang suporta para sa mga 3rd party na repository o extension!</strong><br/>Gumamit nang may pag-iingat dahil maaaring may mga malisyosong aktor na gumagawa ng mga repository na iyon.<br/>Ikaw bilang user ay kailangang i-verify ang seguridad at na pinagkakatiwalaan mo ang anumang repository o extension."
},
"dialog": {
"action": {
"button": {
"add": "Magdagdag ng repository"
}
}
},
"error": {
"label": {
"invalid_url": "Di-wastong url ng repository"
}
}
}
},
"dialog": {
"title": "Mga setting ng extension sa bawat wika"
}
},
"state": {
"label": {
"installed": "Na-install na",
"installing": "Nag-iinstall",
"obsolete": "Di-suportado",
"uninstalling": "Nag-uninstall",
"updating": "Nag-a-update",
"update_pending": "Nakabinbin ang pag-update"
}
},
"action": {
"label": {
"install_external": "Mag-install ng external na extension",
"install": "I-install",
"uninstall": "I-uninstall",
"update": "I-update",
"update_all": "I-update lahat"
}
},
"label": {
"installation_failed_one": "Hindi ma-install ang extension",
"installation_failed_other": "Hindi ma-install ang mga extension",
"update_failed_one": "Hindi ma-update ang extension",
"update_failed_other": "Hindi ma-update ang mga extension",
"installed_successfully_one": "Naka-install ang extension",
"installed_successfully_other": "Naka-install na mga extension",
"installing_file": "Ini-install ang extension file…",
"uninstallation_failed_one": "Hindi ma-uninstall ang extension",
"uninstallation_failed_other": "Hindi ma-uninstall ang mga extension",
"add_repository_info": "Kailangan mong magdagdag ng extension repository upang makapag-install ng mga extension",
"available_updates_one": "{{count}} update na available",
"available_updates_other": "{{count}} na mga update ang available"
},
"language": {
"all": "Lahat",
"other": "Iba pa"
},
"title_one": "Extension",
"title_other": "Mga Extension"
},
"global": {
"button": {
"add": "Magdagdag",
"browse": "Maghanap",
"cancel": "Kanselahin",
"copy": "Kopyahin",
"clear_selection": "I-clear",
"create": "Lumikha",
"delete": "I-Delete",
"deselect": "Alisin sa pagkakapili",
"dont_show_dialog_again": "Huwag ipakita muli ang dialog na ito",
"download": "I-download",
"edit": "I-edit",
"filter": "I-filter",
"ignore": "Huwag pansinin",
"latest": "Pinakabago",
"log_in": "Mag-log in",
"log_out": "Mag-log out",
"migrate": "Mag-migrate",
"open": "Bukasan",
"open_browser": "Buksan sa browser",
"open_webview": "Buksan sa WebView",
"options": "Mga Opsyon",
"popular": "Sikat",
"refresh": "I-refresh",
"remind_later": "Paalalahanan mamaya",
"remove": "Alisin",
"reset": "I-reset",
"reset_to_default": "I-reset sa Default",
"restore": "Ibalik",
"resume": "Ipagpatuloy",
"retry": "Subukan muli",
"save": "I-save",
"select": "Pumili",
"select_all": "Piliin lahat",
"set": "I-set",
"show_less": "Magpakita ng mas kaunti",
"show_more": "Magpakita ng higit pa",
"start": "Magsimula",
"stop": "Hinto",
"submit": "Isumite"
},
"colors": {
"black": "Itim",
"white": "Puti"
},
"date": {
"label": {
"day_one": "Araw",
"day_other": "Mga araw",
"second_one": "Segundo",
"second_other": "Mga sigundo",
"today": "Ngayon",
"today_at": "Ngayon sa {{timeString}}",
"yesterday": "Kahapon",
"yesterday_at": "Kahapon sa {{timeString}}"
}
},
"error": {
"label": {
"failed_to_load_data": "Hindi ma-load ang data",
"failed_to_save_changes": "Nabigong i-save ang mga pagbabago",
"invalid_action": "Ito ay hindi isang wastong aksyon",
"invalid_file_type": "Di-wastong uri ng file",
"invalid_input": "Di-wastong input",
"unrecoverable_error": "Nagkaproblema\nMay naganap na error na hindi namin mababawi",
"update_failed": "Hindi maka-check ng mga update"
}
},
"filter": {
"label": {
"bookmarked": "Naka-bookmark",
"downloaded": "Nai-download",
"duplicate_chapters": "Mga nadobleng kabanata",
"started": "Nasimulan",
"tracked": "Nai-track",
"unread": "Di pa nabasa"
}
},
"grid_layout": {
"label": {
"comfortable_grid": "Kumportableng grid",
"compact_grid": "Compact na grid",
"list": "Listahan"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,910 +0,0 @@
{
"category": {
"dialog": {
"title": {
"new_category": "Kategori baru",
"edit_category_other": "Ubah kategori"
}
},
"error": {
"label": {
"empty": "Kategori ini kosong",
"request_failure": "Tidak dapat memuat kategori",
"create_failure": "Tidak dapat membuat kategori"
},
"no_categories_found": {
"label": {
"info": "Kategori tak ditemukan."
}
}
},
"label": {
"category_name": "Nama kategori",
"use_as_default_category": "Kategori saat menambahkan manga ke perpustakaan"
},
"title": {
"set_categories": "Atur kategori",
"category_other": "Kategori"
},
"settings": {
"inclusion": {
"label": {
"exclude": "Pengecualian: {{excludedCategoriesText}}",
"include": "Disertakan: {{includedCategoriesText}}"
}
}
},
"value": "{{count}} $t(category.title.category, lowercase)"
},
"chapter": {
"action": {
"bookmark": {
"remove": {
"button": {
"selected": "Hapus yang terpilih dari favorit"
},
"label": {
"action": "Hapus favorit",
"success_other": "Telah menghapus {{count}} dari favorit",
"error_other": "Tidak dapat menghapus favorit",
"confirmation_other": "Kamu akan menghapus {{count}} bookmark"
}
},
"add": {
"label": {
"action": "Tambahkan ke favorit",
"error_other": "Tidak dapat mengatur favorit pada bab",
"success_other": "{{count}} bab telah difavorit"
},
"button": {
"selected": "Telah difavorit"
}
}
},
"download": {
"delete": {
"label": {
"success_other": "{{count}} bab dihapus",
"action": "Hapus",
"error_other": "Tidak dapat menghapus bab",
"confirmation_other": "Kamu akan menghapus {{count}} unduhan"
},
"button": {
"selected": "Hapus Pilihan"
}
},
"add": {
"label": {
"success_other": "{{count}} unduhan ditambahkan",
"error_other": "Tidak dapat menambah unduhan",
"action": "Unduh",
"unread": "$t(global.filter.label.unread)",
"ahead": "$t(download.settings.download_ahead.title) ({{count}})",
"all": "$t(extension.language.all)",
"next_other": "{{count}} bab selanjutnya",
"confirmation_other": "Kamu akan mengunduh {{count}} chapter.\nSuwayomi bukanlah pengunduh massal, dan terlalu banyak unduhan dapat menyebabkan Anda diblokir dari sumber dan/atau menyebabkan masalah kinerja."
},
"button": {
"selected": "Memilih Unduhan"
}
}
},
"label": {
"select": "Pilih"
},
"mark_as_read": {
"add": {
"label": {
"action": {
"current": "Tandai telah dibaca",
"previous": "Tandai sebelumnya telah dibaca",
"all": "Tandai semua telah dibaca"
},
"error_other": "Tidak dapat menandai bab telah dibaca",
"success_other": "{{count}} chapter ditandai telah dibaca",
"confirmation_other": "Kamu akan menandai {{count}} bab sebagai telah dibaca"
},
"button": {
"selected": "Tandai terpilih telah dibaca"
}
},
"remove": {
"button": {
"selected": "Tandai pilihan belum dibaca"
},
"label": {
"action": "Tandai belum dibaca",
"error_other": "Tidak dapat menandai bab belum dibaca",
"success_other": "{{count}} bab ditandai belum dibaca",
"confirmation_other": "Kamu akan menandai {{count}} bab sebagai belum dibaca"
}
}
},
"filter_and_sort": {
"label": "Saring dan Urutkan"
},
"read": {
"resume": "Lanjutkan membaca\n#{{chapterNumber}} — {{title}}\n{{scanlator}}",
"start": "Mulai membaca\n#{{chapterNumber}} — {{title}}\n{{scanlator}}"
}
},
"error": {
"label": {
"no_matches": "Tidak menemukan bab berdasarkan filter",
"no_chapter_found": "Bab tidak ditemukan"
}
},
"status": {
"label": {
"downloaded": "Telah diunduh"
}
},
"title_other": "Bab",
"option": {
"display": {
"label": {
"chapter_number": "Nomor Bab",
"source_title": "Judul sumber"
}
},
"exclude_scanlators": "Kecualikan penerjemah"
},
"missing_other": "Kehilangan {{count}} bab",
"value_other": "{{count}} bab"
},
"download": {
"queue": {
"label": {
"no_downloads": "Tidak ada unduhan",
"delete_all": "Hapus semua"
},
"error": {
"label": {
"failed_delete_all": "Tidak dapat menghapus seluruh unduhan dari barisan",
"failed_to_remove": "Tidak dapat menghapus unduhan dari antrian.",
"failed_to_retry": "Tidak dapat mengulang unduhan yang gagal."
}
},
"info": {
"paused": "Dijeda — $t(download.queue.info.remaining)",
"remaining": "Tersisa {{count}}"
}
},
"settings": {
"auto_download": {
"download_limit": {
"label": {
"description": "Batasi jumlah bab baru yang akan diunduh.",
"title": "Batas pengunduhan bab"
}
},
"label": {
"new_chapters": "Unduh bab baru",
"ignore_re_uploads": "Abaikan bab yang diunggah ulang",
"ignore_with_unread_chapters": "Abaikan pengunduhan bab otomatis untuk entri dengan bab yang belum dibaca"
},
"title": "Unduh otomatis",
"categories": {
"label": {
"include_in_download": "Entri dalam kategori yang dikecualikan tidak akan diunduh meskipun entri tersebut juga termasuk dalam kategori yang disertakan"
}
}
},
"download_path": {
"label": {
"title": "Lokasi pengunduhan",
"description": "Jalur ke direktori di server tempat file yang diunduh harus disimpan"
}
},
"download_ahead": {
"title": "Unduh terlebih dahulu",
"label": {
"value": "{{chapters}} $t(chapter.title)",
"unread_chapters_to_download": "Jumlah bab yang belum dibaca untuk diunduh",
"while_reading": "Unduh otomatis saat membaca",
"description": "Seberapa banyak bab yang dapat diunduh sembari membaca.",
"disclaimer": "Hanya berjalan apabila bab sekarang dan bab berikutnya telah diunduh."
}
},
"file_type": {
"label": {
"cbz": "Simpan sebagai arsip CBZ"
}
},
"delete_chapters": {
"title": "Hapus bab",
"label": {
"allow_deletion_of_bookmarked": "Izinkan penghapusan bab yang ditandai",
"manually_marked_as_read": "Hapus bab setelah menandainya secara manual sebagai telah dibaca"
},
"while_reading": {
"label": {
"title": "Hapus bab yang sudah selesai saat membaca"
},
"option": {
"label": {
"fifth": "Lima bab terakhir yang sudah dibaca",
"first": "Bab terakhir yang sudah dibaca",
"fourth": "Empat bab terakhir yang sudah dibaca",
"third": "Tiga bab terakhir yang sudah dibaca",
"second": "Dua bab terakhir yang sudah dibaca"
}
}
}
},
"conversion": {
"compression_level": "Tingkat kompresi"
}
},
"state": {
"label": {
"finished": "$t(global.label.finished)",
"queued": "Diantrikan",
"downloading": "Mengunduh",
"error": "Terjadi kesalahan"
}
},
"title": {
"queue": "Unduh antrian",
"download": "Unduh"
}
},
"global": {
"button": {
"create": "Buat",
"deselect": "Batalkan Pilihan",
"reset": "Reset",
"retry": "Ulangi",
"select": "Pilih",
"options": "Opsi",
"reset_to_default": "Kembalikan ke Setelan Awal",
"ok": "Ok",
"save": "Simpan",
"add": "Tambah",
"start": "Mulai",
"submit": "Kirim",
"copy": "Salin",
"select_all": "Pilih Semua",
"show_more": "Tampilkan lebih banyak",
"stop": "Stop",
"cancel": "Batal",
"log_in": "Masuk",
"refresh": "Muat Ulang",
"remind_later": "Ingatkan Nanti",
"restore": "Pulihkan",
"resume": "Lanjutkan",
"delete": "Hapus",
"latest": "Terakhir",
"log_out": "Keluar",
"popular": "Populer",
"dont_show_dialog_again": "Jangan tampilkan dialog ini lagi",
"remove": "Copot",
"show_less": "Tampilkan lebih sedikit",
"edit": "Sunting",
"filter": "Saring",
"ignore": "Abaikan",
"browse": "Browser",
"migrate": "Bermigasi",
"changelog": "Catatan Perubahan",
"set": "Atur",
"open": "Buka",
"open_browser": "Buka di peramban"
},
"date": {
"label": {
"day_other": "Hari",
"today": "Hari ini",
"today_at": "Hari ini pada waktu {{timeString}}",
"yesterday": "Kemarin",
"yesterday_at": "Kemarin pada waktu {{timeString}}"
},
"value": {
"label": {
"day": "{{days}} $t(global.date.label.day, lowercase)"
}
}
},
"label": {
"close": "Tutup",
"are_you_sure": "Anda yakin?",
"auto": "Otomatis",
"copied_clipboard": "Disalin ke papan klip",
"desktop": "Desktop",
"both": "Keduanya",
"bottom": "Bawah",
"default": "Bawaan",
"dark": "Gelap",
"advanced": "Lanjutan",
"client": "Klien",
"browse": "Jelajahi",
"asterisk": "*",
"left": "Kiri",
"small": "Kecil",
"sort": "Pendek",
"success": "Berhasil",
"right": "Kanan",
"share": "Bagikan",
"last_used": "Terakhir digunakan",
"loading": "Memuat…",
"system": "Sistem",
"warning": "Peringatan",
"discord": "Discord",
"display": "Tampilan",
"other": "Lain",
"finished": "Selesai",
"general": "Umum",
"never": "Tidak pernah",
"next": "Selanjutnya",
"open_in_browser": "Buka di browser",
"disabled": "Dinonaktifkan",
"error": "Kesalahan",
"filter": "Saring",
"footnote": "{{value}}<0>$t(global.label.asterisk)</0>",
"github": "GitHub",
"horizontal": "Mendatar",
"light": "Terang",
"links": "Tautan",
"logged_in": "Masuk",
"medium": "Sedang",
"menu": "Menu",
"mobile": "Seluler",
"none": "Tidak ada",
"pinned": "Disematkan",
"placeholder": "Tempat sementara",
"scanlator": "Penerjemah",
"stopped": "Berhenti",
"tiny": "Kecil",
"unknown": "Tidak diketahui",
"username": "Nama pengguna",
"version": "Versi",
"vertical": "Vertikal",
"hidden": "Tersembunyi",
"info": "Informasi",
"large": "Besar",
"load_in_progress": "Masih memuat data yang diperlukan…",
"more": "Lebih",
"password": "Sandi",
"previous": "Sebelumnya",
"standard": "Standar",
"started": "Dimulai",
"type": "Tipe"
},
"filter": {
"label": {
"unread": "Belum dibaca",
"started": "Dimulai",
"duplicate_chapters": "Bab duplikat",
"bookmarked": "Ditandai",
"downloaded": "Telah diunduh",
"tracked": "Dilacak"
}
},
"error": {
"label": {
"failed_to_load_data": "Tidak dapat memuat data",
"invalid_action": "Ini bukan aksi yang valid",
"failed_to_save_changes": "Tidak dapat menyimpan perubahan",
"unrecoverable_error": "Terjadi kesalahan\nTerjadi kesalahan yang tidak dapat kami pulihkan",
"invalid_input": "Masukan tidak valid",
"update_failed": "Tidak dapat memeriksa pembaruan",
"invalid_file_type": "Tipe berkas tidak valid"
}
},
"grid_layout": {
"label": {
"comfortable_grid": "Grid nyaman",
"list": "Daftar",
"compact_grid": "Grid padat"
},
"title": "Mode tampilan"
},
"colors": {
"gray": "Abu - abu",
"white": "Putih",
"black": "Hitam"
},
"language": {
"label": {
"language": "Bahasa",
"language_with_code": "Bahasa dengan kode: {{code}}"
},
"error": {
"load": "Tidak dapat memuat bahasa"
},
"title": {
"weblate": "Weblate",
"enabled_languages": "Bahasa yang diijinkan"
}
},
"time": {
"minutes": {
"minute_other": "Menit",
"value": "{{count}} $t(global.time.minutes.minute, lowercase)"
},
"seconds": {
"second_other": "Detik",
"value": "{{count}} $t(global.time.seconds.second, lowercase)"
},
"hour_short": "h"
},
"sort": {
"label": {
"by_chapter_number": "Berdasarkan nomor bab",
"asc": "Urut naik",
"by_fetch_date": "Berdasarkan tanggal diperoleh",
"by_source": "Berdasarkan sumber",
"by_upload_date": "Berdasarkan tanggal diunggah",
"desc": "Urut turun"
}
},
"update": {
"label": {
"info": "Versi server {{version}} ({{channel}}) tersedia untuk diunduh",
"check_failure": "Tidak dapat memeriksa pembaruan",
"checking": "Memeriksa pembaruan",
"update_failure": "Pembaruan gagal",
"update_success": "{{name}} telah diperbarui ke versi {{version}}{{channel}}",
"updating": "{{progress}}% | Memperbarui…",
"available": "Pembaharuan tersedia",
"up_to_date": "Ini adalah versi terbaru"
},
"settings": {
"inform": {
"label": {
"description": "Menampilkan dialog jika versi baru tersedia",
"title": "Memberi tahu tentang pembaruan yang tersedia"
}
}
}
},
"unit": {
"px": "px"
},
"value": "{{value}}{{unit}}"
},
"extension": {
"label": {
"update_failed_other": "Tidak dapat memperbaharui file ekstensi",
"uninstallation_failed_other": "Tidak dapat memasang file ekstensi",
"installation_failed_other": "Tidak dapat memasang file ekstensi",
"installing_file": "Memasang file ekstensi…",
"installed_successfully_other": "File ekstensi terpasang",
"add_repository_info": "Anda harus menambahkan repositori ekstensi untuk dapat menginstal ekstensi",
"age_rating": "Peringkat usia"
},
"language": {
"other": "Lainnya",
"all": "Semua"
},
"action": {
"label": {
"install": "Pasang",
"update": "Perbaharui",
"uninstall": "Hapus pemasangan",
"install_external": "Pasang ekstensi eksternal",
"update_all": "Perbaharui semua"
}
},
"settings": {
"repositories": {
"custom": {
"error": {
"label": {
"invalid_url": "Alamat url repositori salah"
}
},
"dialog": {
"action": {
"button": {
"add": "Tambahkan repositori"
}
}
},
"label": {
"disclaimer": "<strong>Suwayomi tidak mendukung repositori atau file tambahan pihak ketiga!</strong><br/>Gunakan dengan hati-hati karena ada kemungkinan adanya aktor jahat yang membuat repositori tersebut .<br/>Anda sebagai pengguna harus memverifikasi keamanan dan mempercayai repositori dan ekstensi apapun.",
"title": "Repositori ekstensi",
"description": "Tambahkan repositori tempat ekstensi dapat diinstal"
}
}
},
"dialog": {
"title": "Pengaturan ekstensi per bahasa"
}
},
"state": {
"label": {
"uninstalling": "Menghapus pemasangan",
"installing": "Memasang",
"installed": "Terpasang",
"obsolete": "Usang",
"update_pending": "Pembaruan tertunda",
"updating": "Memperbarui"
}
},
"title_other": "Ekstensi"
},
"history": {
"error": {
"label": {
"no_history_available": "Kamu belum membaca seri apa pun sampai sekarang."
}
},
"settings": {
"hide": "Sembunyikan riwayat"
},
"title": "Riwayat"
},
"hotkeys": {
"create": {
"dialog": {
"label": "Kunci yang tercatat: <Keys/>",
"placeholder": "Tekan tombol",
"title": "Rekam tombol pintas"
},
"error": {
"exists": "Tombol pintas sudah ada"
}
},
"info": {
"delete": "Klik tombol untuk menghapus pengikatan"
},
"title_other": "Tombol pintas"
},
"library": {
"action": {
"label": {
"search_globally": "Cari \"{{query}}\" secara global",
"stop_update": "Hentikan pembaruan global",
"update_category": "Perbarui kategori",
"update_library": "Perbarui pustaka"
}
},
"error": {
"label": {
"add_to_library": "Gagal menambahkan manga ke pustaka!",
"empty": "Pustaka anda kosong",
"no_matches": "Tidak ada manga yang sesuai dengan filter ini",
"remove_from_library": "Tidak dapat menghapus manga dari pustaka!",
"stop_global_update": "Gagal menghentikan pembaruan global"
}
},
"info": {
"label": {
"added_to_library": "Manga berhasil ditambahkan ke pustaka!",
"removed_from_library": "Manga berhasil di hapus dari pustaka!"
}
},
"option": {
"display": {
"badge": {
"label": {
"download_badges": "Lencana unduhan",
"unread_badges": "Lencana belum dibaca"
},
"title": "Lencana"
},
"other": {
"label": {
"show_continue_reading_button": "Tampilkan tombol lanjutkan membaca"
}
},
"tab": {
"label": {
"show_number_of_items": "Tampilkan jumlah item"
},
"title": "Tab"
}
},
"sort": {
"label": {
"alphabetically": "A-Z",
"by_date_added": "Baru ditambahkan",
"by_last_read": "Baru dibaca",
"by_latest_fetched_chapter": "Bab terbaru yang diambil",
"by_latest_uploaded_chapter": "Bab terbaru yang diunggah",
"by_total_chapters": "Total bab",
"by_unread_chapters": "Bab yang belum dibaca"
}
}
},
"settings": {
"advanced": {
"database": {
"cleanup": {
"label": {
"description": "Hapus manga yang tidak ada di pustaka dari kategori",
"error": "Gagal menghapus manga yang tidak ada di pustaka dari kategori",
"success": "Manga yang tidak ada di pustaka berhasil dihapus dari kategori",
"title": "Bersihkan basis data"
}
}
},
"duplicates": {
"label": {
"description": "Tampilkan semua entri duplikat di pustaka Anda",
"title": "Entri duplikat"
},
"settings": {
"label": {
"check_description": "Cek deskripsi"
}
}
}
},
"general": {
"add_to_library": {
"category_selection": {
"label": {
"description": "Tampilkan dialog pemilihan kategori saat menambahkan manga ke pustaka",
"title": "Dialog pemilihan kategori"
}
}
},
"remove_from_library": {
"remove_from_categories": {
"label": {
"description": "Hapus manga dari kategori saat menghapusnya dari pustaka",
"title": "Lupakan kategori manga"
}
}
},
"search": {
"ignore_filters": {
"label": {
"description": "Hasil pencarian akan mencakup manga yang tidak cocok dengan filter saat ini",
"title": "Abaikan filter saat mencari"
}
}
}
},
"global_update": {
"auto_update": {
"interval": {
"label": {
"title": "Interval pembaruan otomatis",
"value": "{{hours}}$t(global.time.hour_short)"
}
},
"label": {
"title": "Pembaruan otomatis"
}
},
"categories": {
"label": {
"info": "Entri dalam kategori yang dikecualikan tidak akan diperbarui meskipun entri tersebut juga ada dalam kategori yang disertakan"
}
},
"entries": {
"label": {
"completed": "Dengan status \"Selesai\"",
"not_started": "Yang belum dimulai",
"unread_chapters": "Dengan bab yang belum dibaca"
},
"title": "Lewati pembaruan entri"
},
"label": {
"last_update": "Pembaruan terakhir: {{date}}",
"last_update_tooltip": "$t(library.settings.global_update.title) ($t(library.settings.global_update.label.last_update, lowercase))"
},
"metadata": {
"label": {
"description": "Periksa sampul dan detail baru saat memperbarui pustaka",
"title": "Otomatis segarkan metadata"
}
},
"title": "Pembaruan global"
}
},
"title": "Pustaka"
},
"manga": {
"action": {
"category": {
"button": {
"selected": "Ubah kategori yang dipilih"
},
"label": {
"action": "Ubah kategori",
"confirmation_other": "Anda akan mengubah kategori {{count}} entri",
"error_other": "Tidak dapat mengubah kategori manga",
"success_other": "Mengubah kategori {{count}} manga"
}
},
"library": {
"add": {
"dialog": {
"duplicate": {
"label": {
"failure": "Tidak dapat memeriksa manga duplikat di pustaka Anda.\n\nKesalahan: {{error}}",
"info": "Anda memiliki entri di pustaka Anda dengan nama yang sama."
}
}
}
},
"remove": {
"button": {
"selected": "Hapus yang terpilih dari pustaka"
},
"dialog": {
"label": {
"message": "Anda akan menghapus \"**{{title}}**\" dari pustaka Anda"
}
},
"label": {
"action": "Hapus dari pustaka",
"confirmation_other": "Anda akan menghapus **{{count}}** entri dari pustaka Anda",
"error_other": "Tidak dapat menghapus manga dari pustaka",
"success_other": "Menghapus **{{count}}** manga dari pustaka"
}
}
},
"migrate": {
"label": {
"error": "Tidak dapat memindahkan manga",
"success": "Berhasil memindahkan manga"
}
},
"track": {
"add": {
"label": {
"action": "Lacak",
"error": "Tidak dapat melacak manga",
"success": "Manga terlacak"
}
},
"remove": {
"dialog": {
"label": {
"delete_remote_track": "Juga hapus dari {{tracker}}",
"description": "Ini akan menghapus pelacakan secara lokal.",
"title": "Hapus pelacakan {{tracker}}?"
}
},
"label": {
"error": "Tidak dapat berhenti melacak manga",
"success": "Manga tidak terlacak"
}
}
}
},
"button": {
"add_to_library": "Tambahkan Ke Pustaka",
"in_library": "Di Pustaka",
"track": {
"active_other": "{{count}} Pelacak",
"start": "Pelacakan"
}
},
"error": {
"label": {
"no_mangas_found": "Tidak ada manga ditemukan",
"no_matches": "Tidak ada manga yang cocok dengan filter ini",
"request_failure": "Tidak dapat memuat manga"
}
},
"label": {
"artist": "Artis",
"author": "Penulis",
"edit_categories": "Edit kategori manga",
"reload_from_source": "Muat ulang data dari sumber",
"status": "Status"
},
"status": {
"cancelled": "Dibatalkan",
"completed": "Selesai",
"hiatus": "Hiatus",
"licensed": "Berlisensi",
"ongoing": "Sedang Berlangsung",
"publishing_finished": "Penerbitan selesai",
"unknown": "$t(global.label.unknown)"
},
"title_other": "Manga",
"type": {
"comic": "Komik",
"long_strip": "Strip panjang",
"manga": "$t(manga.title_one)",
"manhua": "Manhua",
"manhwa": "Manhwa",
"webtoon": "$t(reader.settings.reader_type.label.webtoon)"
}
},
"migrate": {
"dialog": {
"action": {
"button": {
"show_entry": "Tampilkan entri"
}
},
"label": {
"delete_downloaded": "Hapus yang diunduh"
},
"title": "Pilih data untuk disertakan"
},
"label": {
"info": "Memindahkan manga…"
},
"search": {
"title": "$t(migrate.title) \"{{title}}\""
},
"sort": {
"by_manga_count": "Berdasarkan jumlah manga",
"by_source_name": "Berdasarkan nama sumber"
},
"title": "Migrasi"
},
"reader": {
"button": {
"chapter_list": "Daftar bab",
"next_chapter": "Bab selanjutnya",
"next_page": "Halaman selanjutnya",
"open_menu": "Buka menu",
"previous_chapter": "Bab sebelumnya",
"previous_page": "Halaman sebelumnya",
"retry_load_pages": "Mengulang halaman yang galat"
},
"chapter_transition": {
"warning": {
"next": {
"chapter_number_other": "Terdapat {{count}} bab yang hilang.\nSaat ini: {{currentChapter}}\nSelanjutnya: {{nextChapter}}",
"scanlator": "Bab selanjutnya berasal dari scanlator yang berbeda dari saat ini.\nSaat ini: {{currentScanlator}}\nSelanjutnya: {{nextScanlator}}"
},
"previous": {
"chapter_number_other": "Terdapat {{count}} bab yang hilang.\nSebelumnya: {{nextChapter}}\nSaat ini: {{currentChapter}}",
"scanlator": "Bab sebelumnya berasal dari scanlator yang berbeda dari saat ini.\nSebelumnya: {{nextScanlator}}\nSaat ini: {{currentScanlator}}"
},
"title": "Peringatan transisi bab"
}
},
"error": {
"label": {
"chapter_not_found": "Bab tidak ditemukan",
"next_chapter_does_not_exist": "Tidak ada bab selanjutnya",
"no_pages_found": "Halaman tidak ditemukan",
"prev_chapter_does_not_exist": "Tidak ada bab sebelumnya"
}
},
"page_info": {
"label": {
"currently_on_page": "Di halaman saat ini",
"of_max_pages": "dari {{maxPages}}",
"page": "Halaman"
}
},
"settings": {
"active_setting": "Pengaturan aktif",
"auto_scroll": {
"smooth": "Gulir halus otomatis",
"speed": "Kecepatan gulir otomatis",
"title": "Gulir otomatis"
},
"auto_webtoon_mode": {
"title": "Mode webtoon otomatis"
},
"background_color": "Warna latar",
"chapter_transition": {
"warning": {
"missing_chapter": "Beri tahu tentang bab yang hilang saat berganti bab",
"scanlator_change": "Beri tahu tentang pergantian scanlator saat berganti bab"
}
},
"custom_filter": {
"brightness": "Kecerahan kustom",
"contrast": "Kontras kustom",
"grayscale": "Skala abu-abu",
"hue": "Rona kustom",
"invert": "Balikkan warna",
"rgba": {
"alpha": "Alpha",
"blend_mode": {
"darken": "Gelapkan",
"default": "$t(global.label.default)",
"lighten": "Cerahkan",
"multiply": "Multiply",
"overlay": "Overlay",
"screen": "Screen"
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
{
"category": {
"settings": {
"inclusion": {
"label": {
"exclude": "Utelukk: {{excludedCategoriesText}}",
"include": "Inkluder:{{includedCategoriesText}}"
}
}
},
"title": {
"category_one": "Kategori",
"category_other": "Kategorier"
},
"dialog": {
"title": {
"edit_category_one": "Rediger kategori",
"edit_category_other": "",
"new_category": "Ny kategori"
}
},
"error": {
"label": {
"empty": "Kategorien er tom",
"request_failure": "Kunne ikke laste kategorier"
}
},
"label": {
"category_name": "Kategori navn"
}
},
"chapter": {
"action": {
"bookmark": {
"add": {
"button": {
"selected": "Bokmerke valgt"
},
"label": {
"action": "Legg til bokmerke"
}
}
}
}
}
}

View File

@@ -1,860 +0,0 @@
{
"category": {
"dialog": {
"title": {
"edit_category_one": "Categorie bewerken",
"edit_category_other": "Categorieën bewerken",
"new_category": "Nieuw categorie"
}
},
"error": {
"no_categories_found": {
"label": {
"info": "Je hebt nog geen categorieën."
}
},
"label": {
"empty": "De categorie is leeg",
"request_failure": "Kan de categorieën niet laden",
"create_failure": "Kan de categorie niet aanmaken"
}
},
"label": {
"category_name": "Categorie Naam",
"use_as_default_category": "Standaard categorie bij het toevoegen van een nieuwe manga in de bibliotheek"
},
"settings": {
"inclusion": {
"label": {
"include": "Insluiten:{{includedCategoriesText}}",
"exclude": "Uitsluiten:{{excludedCategoriesText}}"
}
}
},
"title": {
"category_one": "Categorie",
"category_other": "Categorieën",
"set_categories": "Zet categorieën"
},
"value": "{{count}} $t(category.title.category, lowercase)"
},
"download": {
"queue": {
"error": {
"label": {
"failed_delete_all": "Kan niet alle downloads uit de wachtrij verwijderen",
"failed_to_remove": "Kan niet alle downloads uit de wachtrij verwijderen.",
"failed_to_retry": "Kon de mislukte download niet opnieuw proberen."
}
},
"label": {
"delete_all": "Alle verwijderen",
"no_downloads": "Geen downloads"
},
"info": {
"paused": "Gepauzeerd — $t(download.queue.info.remaining)",
"remaining": "{{count}} resterend"
}
},
"settings": {
"auto_download": {
"download_limit": {
"label": {
"description": "Beperk het aantal nieuwe hoofdstukken dat wordt gedownload.",
"title": "Hoofdstuk downloadlimiet"
}
},
"label": {
"ignore_re_uploads": "Negeer opnieuw geüploade hoofdstukken",
"new_chapters": "Download nieuwe hoofdstukken",
"ignore_with_unread_chapters": "Negeer automatische hoofdstukdownloads voor items met ongelezen hoofdstukken"
},
"title": "Automatisch downloaden",
"categories": {
"label": {
"include_in_download": "Geselecteerde in uitgesloten categorieën worden niet gedownload, zelfs als ze zich ook in de opgenomen categorieën bevinden"
}
}
},
"delete_chapters": {
"label": {
"allow_deletion_of_bookmarked": "Sta het verwijderen van hoofdstukken met bladwijzers toe",
"manually_marked_as_read": "Verwijder het hoofdstuk nadat hij handmatig als gelezen is gemarkeerd"
},
"title": "Verwijder hoofdstukken",
"while_reading": {
"label": {
"title": "Verwijder voltooide hoofdstukken tijdens het lezen"
},
"option": {
"label": {
"first": "Laatst gelezen hoofdstuk",
"second": "Tweede tot laatst gelezen hoofdstuk",
"third": "Derde tot laatst gelezen hoofdstuk",
"fifth": "Vijfde tot laatst gelezen hoofdstuk",
"fourth": "Vierde tot laatst gelezen hoofdstuk"
}
}
}
},
"download_ahead": {
"label": {
"description": "Hoeveel hoofdstukken moeten worden gedownload tijdens het lezen.",
"disclaimer": "Werkt alleen als het huidige hoofdstuk plus het volgende hoofdstuk al zijn gedownload.",
"unread_chapters_to_download": "Aantal ongelezen hoofdstukken om te downloaden",
"while_reading": "Automatisch downloaden bij het lezen",
"value": "{{chapters}} $t(chapter.title)"
},
"title": "Vooraf downloaden"
},
"download_path": {
"label": {
"description": "Het pad naar de map op de server waar gedownloade bestanden moeten worden opgeslagen",
"title": "Download locatie"
}
},
"file_type": {
"label": {
"cbz": "Opslaan als CBZ archief"
}
}
},
"state": {
"label": {
"downloading": "Downloaden",
"error": "Fout",
"finished": "$t(global.label.finished)",
"queued": "Wachtrij"
}
},
"title": {
"download": "Downloads",
"queue": "Download wachtrij"
}
},
"chapter": {
"title_one": "Hoofdstuk",
"title_other": "Hoofdstukken",
"action": {
"download": {
"add": {
"label": {
"action": "Download",
"error_one": "Kan de download niet toevoegen",
"error_other": "Kan de downloads niet toevoegen",
"success_one": "Download toegevoegd",
"success_other": "{{count}} downloads toegevoegd",
"next_one": "Volgend hoofdstuk",
"next_other": "Volgende {{count}} hoofdstukken",
"ahead": "$t(download.settings.download_ahead.title) ({{count}})",
"all": "$t(extension.language.all)",
"unread": "$t(global.filter.label.unread)",
"confirmation_one": "U staat op het punt een hoofdstuk te downloaden",
"confirmation_other": "Je staat op het punt {{count}} hoofdstukken te downloaden.\nSuwayomi downloadt niet massaal en te veel downloads kunnen leiden tot een blokkade van de bronnen en/of prestatieproblemen."
},
"button": {
"selected": "Download geselecteerde"
}
},
"delete": {
"label": {
"error_one": "Kan het hoofdstuk niet verwijderen",
"error_other": "Kan de hoofdstukken niet verwijderen",
"action": "Verwijder",
"success_one": "Hoofstuk verwijderd",
"success_other": "{{count}} hoofstukken verwijderd",
"confirmation_one": "U staat op het punt één download te verwijderen",
"confirmation_other": "U staat op het punt {{count}} downloads te verwijderen"
},
"button": {
"selected": "Verwijder geselecteerde"
}
}
},
"bookmark": {
"add": {
"button": {
"selected": "Bladwijzer geselecteerd"
},
"label": {
"action": "Bladwijzer toevoegen",
"error_one": "Kan geen bladwijzer maken voor het hoofdstuk",
"error_other": "Kan geen bladwijzer maken voor de hoofdstukken",
"success_one": "Hoofdstuk gemarkeerd",
"success_other": "{{count}} hoofdstukken gemarkeerd"
}
},
"remove": {
"button": {
"selected": "Verwijder bladwijzers uit geselecteerde"
},
"label": {
"action": "Verwijder bladwijzer",
"error_one": "Kan de bladwijzer niet verwijderen",
"error_other": "Kan de bladwijzers niet verwijderen",
"success_one": "Hoofstuk bladwijzer verwijderd",
"success_other": "{{count}} hoofstuk bladwijzers verwijderd",
"confirmation_one": "Je staat op het punt om een badwijzer te verwijderen",
"confirmation_other": "Je staat op het punt om {{count}} badwijzers te verwijderen"
}
}
},
"label": {
"select": "Selecteer"
},
"mark_as_read": {
"add": {
"button": {
"selected": "Markeer geselecteerd als gelezen"
},
"label": {
"action": {
"current": "Markeer als gelezen",
"previous": "Markeer vorige als gelezen"
},
"error_one": "Kan het hoofdstuk niet markeren als gelezen",
"error_other": "Kan de hoofdstukken niet markeren als gelezen",
"success_one": "Hoofdstuk gemarkeerd als gelezen",
"success_other": "{{count}} hoofdstukken gemarkeerd als gelezen",
"confirmation_one": "U staat op het punt om een hoofdstuk als gelezen te markeren",
"confirmation_other": "Je staat op het punt om {{count}} hoofdstukken als gelezen te markeren"
}
},
"remove": {
"button": {
"selected": "Markeer geselecteerde als ongelezen"
},
"label": {
"action": "Markeer als ongelezen",
"error_one": "Kan het hoofdstuk niet markeren als ongelezen",
"error_other": "Kan de hoofstukken niet markeren als ongelezen",
"success_one": "Hoofdstuk gemarkeerd als ongelezen",
"success_other": "{{count}} hoofdstukken gemerkeerd als ongelezen",
"confirmation_one": "U staat op het punt een hoofdstuk als ongelezen te markeren",
"confirmation_other": "U staat op het punt om {{count}} hoofdstukken als ongelezen te markeren"
}
}
}
},
"error": {
"label": {
"no_chapter_found": "Geen hoofdstukken gevonden",
"no_matches": "Er zijn geen hoofdstukken die overeenkomen met het filter"
}
},
"option": {
"display": {
"label": {
"chapter_number": "Hoofdstuk nummer",
"source_title": "Bron titel"
}
},
"exclude_scanlators": "Scanlators uitsluiten"
},
"status": {
"label": {
"downloaded": "Gedownload"
}
},
"missing_one": "{{count}} missend hoofdstuk",
"missing_other": "{{count}} missende hoofdstukken",
"value_one": "{{count}} hoofdstuk",
"value_other": "{{count}} hoofdstukken"
},
"extension": {
"action": {
"label": {
"install": "Installatie",
"install_external": "Installeer externe extensies",
"update": "Update",
"uninstall": "De-installeer",
"update_all": "Alles updaten"
}
},
"label": {
"add_repository_info": "U moet een extensierepository toevoegen om extensies te kunnen installeren",
"installation_failed_one": "Kan de extensie niet installeren",
"installation_failed_other": "Kan de extensies niet installeren",
"installed_successfully_one": "Extensie geïnstalleerd",
"installed_successfully_other": "Extensies geïnstalleerd",
"installing_file": "Extensie bestand aan het installeren…",
"uninstallation_failed_one": "Kan de extensie niet de-installeren",
"uninstallation_failed_other": "Kan de extensies niet de-installeren",
"update_failed_one": "Kan de extensie niet updaten",
"update_failed_other": "Kan de extensies niet updaten",
"age_rating": "Leeftijdsclassificatie"
},
"settings": {
"repositories": {
"custom": {
"dialog": {
"action": {
"button": {
"add": "Voeg repository toe"
}
}
},
"label": {
"description": "Voeg repositories toe, waaruit extensies kunnen worden geïnstalleerd",
"title": "Extensie-repository's",
"disclaimer": "<strong>Suwayomi biedt geen ondersteuning voor repository's of extensies van derden!</strong><br/>Wees voorzichtig, want er kunnen kwaadwillende mensen zijn die deze repository's maken.<br/>U als gebruiker moet de beveiliging verifiëren, dat u elke repository of extensie vertrouwt."
},
"error": {
"label": {
"invalid_url": "Ongeldige repository-URL"
}
}
}
},
"dialog": {
"title": "Instellingen voor extensies per taal"
}
},
"state": {
"label": {
"installed": "Geïnstalleerd",
"installing": "Installeren",
"update_pending": "Update in behandeling",
"updating": "Aan het updaten",
"uninstalling": "De-installeren",
"obsolete": "Verouderd"
}
},
"language": {
"other": "Ander",
"all": "Alles"
},
"title_one": "Extensie",
"title_other": "Extensies"
},
"global": {
"button": {
"add": "Toevoegen",
"browse": "Zoeken",
"cancel": "Annuleer",
"changelog": "Wijzigingslog",
"deselect": "De-selecteer",
"dont_show_dialog_again": "Laat dit dialoogvenster niet meer zien",
"edit": "Aanpassen",
"filter": "Filter",
"ignore": "Negeren",
"latest": "Laatste",
"log_in": "Inloggen",
"log_out": "Uitloggen",
"migrate": "Migreren",
"ok": "Oké",
"options": "Opties",
"popular": "Populair",
"refresh": "Vernieuwen",
"remind_later": "Herinner mij later",
"remove": "Verwijder",
"reset": "Reset",
"reset_to_default": "Reset naar standaard",
"set": "instellen",
"restore": "Terugzetten",
"resume": "Hervat",
"retry": "Opnieuw proberen",
"select_all": "Alles selecteren",
"copy": "Kopiëren",
"create": "Aanmaken",
"delete": "Verwijder",
"select": "Selecteer",
"show_less": "Laat minder zien",
"show_more": "Meer laten zien",
"start": "Begin",
"stop": "Stop",
"submit": "Indienen",
"open": "Open",
"save": "Opslaan"
},
"label": {
"disabled": "Uitgeschakeld",
"discord": "Discord",
"links": "Links",
"load_in_progress": "De vereiste gegevens worden nog steeds geladen…",
"loading": "Laden…",
"logged_in": "Ingelogd",
"never": "Nooit",
"none": "Geen",
"open_in_browser": "Openen in browser",
"other": "Ander",
"sort": "Sorteer",
"started": "Gestart",
"type": "Type",
"unknown": "Onbekend",
"browse": "Zoeken",
"advanced": "Geavanceerd",
"close": "Afsluiten",
"general": "Algemeen",
"are_you_sure": "Weet u het zeker?",
"client": "Cliënt",
"filter": "Filter",
"password": "Wachtwoord",
"display": "Weergaven",
"github": "GitHub",
"placeholder": "Plaatshouder",
"username": "Gebruikersnaam",
"asterisk": "*",
"auto": "Automatisch",
"both": "Beiden",
"bottom": "Bodum",
"copied_clipboard": "Gekopieerd naar klembord",
"dark": "Donker",
"default": "Standaard",
"desktop": "Bureablad",
"error": "Fout",
"finished": "Afgerond",
"footnote": "{{value}}<0>$t(global.label.asterisk)</0>",
"hidden": "Verborgen",
"horizontal": "Horizontaal",
"info": "Informatie",
"large": "Groot",
"last_used": "Laatst gebruikt",
"left": "Links",
"light": "Licht",
"medium": "Gemiddeld",
"menu": "Menu",
"mobile": "Mobiel",
"more": "Meer",
"next": "Volgende",
"pinned": "Vastgemaakt",
"previous": "Vorige",
"right": "Rechts",
"scanlator": "Scanlator",
"share": "Delen",
"small": "Klein",
"standard": "Standaard",
"stopped": "Gestopt",
"success": "Succes",
"system": "Systeem",
"tiny": "Heel klein",
"version": "Versie",
"vertical": "Verticaal",
"warning": "Waarschuwing"
},
"language": {
"label": {
"language_with_code": "Taal met code: {{code}}",
"language": "Taal"
},
"title": {
"enabled_languages": "Toegestane talen",
"weblate": "Weblate"
},
"error": {
"load": "Kan de taal niet laden"
}
},
"sort": {
"label": {
"by_chapter_number": "Bij hoofdstuk nummer",
"by_fetch_date": "Per opgehaalde datum",
"by_source": "Per bron",
"by_upload_date": "Op uploaddatum",
"asc": "Oplopend",
"desc": "Aflopend"
}
},
"time": {
"hour_short": "u",
"minutes": {
"minute_one": "Minuut",
"minute_other": "minuten",
"value": "{{count}} $t(global.time.minutes.minute, lowercase)"
},
"seconds": {
"second_one": "Seconde",
"second_other": "Seconden",
"value": "{{count}} $t(global.time.seconds.second, lowercase)"
}
},
"date": {
"label": {
"day_one": "Dag",
"day_other": "Dagen",
"today": "Vandaag",
"yesterday_at": "Gister om {{timeString}}",
"today_at": "Vandaag om {{timeString}}",
"yesterday": "Gister"
},
"value": {
"label": {
"day": "{{days}} $t(global.date.label.day, lowercase)"
}
}
},
"error": {
"label": {
"invalid_file_type": "Ongeldig bestandstype",
"failed_to_load_data": "Kan gegevens niet laden",
"invalid_action": "Dit is geen geldige actie",
"failed_to_save_changes": "Kan wijzigingen niet opslaan",
"update_failed": "Kan niet controleren op updates",
"invalid_input": "Ongeldig invoer",
"unrecoverable_error": "Er is iets misgegaan\nEr is een fout opgetreden die we niet kunnen herstellen"
}
},
"filter": {
"label": {
"bookmarked": "Gemarkeerd",
"downloaded": "Gedownload",
"tracked": "Bijgehouden",
"unread": "Ongelezen",
"duplicate_chapters": "Dubbele hoofdstukken",
"started": "Gestart"
}
},
"grid_layout": {
"label": {
"comfortable_grid": "Comfortabel rooster",
"compact_grid": "Compakt rooster",
"list": "Lijst"
},
"title": "Weergaven modus"
},
"update": {
"label": {
"available": "Update beschikbaar",
"check_failure": "Kan niet controleren op updates",
"checking": "Controlleren op updates",
"update_failure": "Update mislukt",
"update_success": "{{name}} is bijgewerkt naar versie {{version}} {{channel}}",
"updating": "{{progress}}% | Bijgewerkt…",
"info": "Serverversie {{version}} ({{channel}}) beschikbaar om te downloaden",
"up_to_date": "Dit is de laatste versie"
},
"settings": {
"inform": {
"label": {
"title": "Informeer over beschikbare updates",
"description": "Toont een dialoogvenster voor het geval er een nieuwe versie beschikbaar is"
}
}
}
},
"colors": {
"black": "Zwart",
"gray": "Grijs",
"white": "Wit"
},
"unit": {
"px": "px"
},
"value": "{{value}}{{unit}}"
},
"library": {
"error": {
"label": {
"stop_global_update": "Kan de globale update niet stoppen",
"add_to_library": "Kan manga niet toevoegen aan bibliotheek!",
"empty": "Je bibliotheek is leeg",
"remove_from_library": "Kan manga niet uit de bibliotheek verwijderen!",
"no_matches": "Geen enkele manga komt overeen met dit filter"
}
},
"info": {
"label": {
"added_to_library": "Manga toegevoegd aan bibliotheek!",
"removed_from_library": "Manga uit bibliotheek verwijderd!"
}
},
"settings": {
"global_update": {
"entries": {
"title": "Sla het bijwerken van manga's over",
"label": {
"completed": "Met de status \"Voltooid\"",
"not_started": "Die zijn nog niet begonnen",
"unread_chapters": "Met ongelezen hoofdstukken"
}
},
"label": {
"last_update": "Laatste update: {{date}}",
"last_update_tooltip": "$t(library.settings.global_update.title) ($t(library.settings.global_update.label.last_update, lowercase))"
},
"metadata": {
"label": {
"description": "Controleer op nieuwe voorblad en details, wanneer de bibliotheek wordt bijwerkt",
"title": "Metagegevens automatisch vernieuwen"
}
},
"title": "Globale update",
"auto_update": {
"interval": {
"label": {
"value": "{{hours}}$t(global.time.hour_short)",
"title": "Automatisch update-interval"
}
},
"label": {
"title": "Automatische updates"
}
},
"categories": {
"label": {
"info": "Manga's in uitgesloten categorieën worden niet bijgewerkt, zelfs als ze zich ook in opgenomen categorieën bevinden"
}
}
},
"advanced": {
"database": {
"cleanup": {
"label": {
"description": "Verwijder niet-bibliotheek-manga uit categorieën",
"title": "Database opschonen",
"error": "Kan manga die niet in de bibliotheek staan, niet uit de categorieën verwijderen",
"success": "Niet-bibliotheek manga uit categorieën verwijderd"
}
}
},
"duplicates": {
"label": {
"description": "Alle dubbele manga's in bibliotheek weergeven",
"title": "Dubbele manga's"
},
"settings": {
"label": {
"check_description": "Controleer beschrijving"
}
}
}
},
"general": {
"add_to_library": {
"category_selection": {
"label": {
"description": "Toon het categorieselectiedialoogvenster wanneer u een manga aan de bibliotheek toevoegt",
"title": "Dialoogvenster voor categorieselectie"
}
}
},
"search": {
"ignore_filters": {
"label": {
"description": "Zoekresultaten bevatten manga die niet overeenkomen met de huidige filters",
"title": "Negeer filters tijdens het zoeken"
}
}
},
"remove_from_library": {
"remove_from_categories": {
"label": {
"description": "Verwijder manga uit categorieën wanneer u ze uit de bibliotheek verwijdert",
"title": "Vergeet mangacategorieën"
}
}
}
}
},
"action": {
"label": {
"stop_update": "Stop de globale update",
"update_category": "Categorie bijwerken",
"update_library": "Bibliotheek bijwerken"
}
},
"option": {
"display": {
"tab": {
"title": "Tabbladen",
"label": {
"show_number_of_items": "Toon aantal hoofdstukken"
}
},
"badge": {
"title": "Badges",
"label": {
"download_badges": "Badges downloaden",
"unread_badges": "Ongelezen badges"
}
},
"other": {
"label": {
"show_continue_reading_button": "Knop doorgaan met lezen tonen"
}
}
},
"sort": {
"label": {
"alphabetically": "A-Z",
"by_date_added": "Recent toegevoegd",
"by_last_read": "Recent gelezen",
"by_latest_fetched_chapter": "Laatst opgehaalde hoofdstuk",
"by_unread_chapters": "Ongelezen hoofdstukken",
"by_latest_uploaded_chapter": "Laatst geüpload hoofdstuk"
}
}
},
"title": "Bibliotheek"
},
"manga": {
"button": {
"track": {
"active_one": "{{count}} bijgehouden",
"active_other": "{{count}} bijgehouden",
"start": "Bijhouden"
},
"add_to_library": "Toevoegen aan bibliotheek",
"in_library": "In bibliotheek"
},
"action": {
"category": {
"button": {
"selected": "Wijzig de categorieën van geselecteerde"
},
"label": {
"action": "Wijzig de categorieën",
"error_one": "Kan de categorie van de manga niet wijzigen",
"error_other": "Kan de categorieën van de manga niet wijzigen",
"success_one": "Gewijzigde mangacategorieën",
"success_other": "Gewijzigde categorieën van {{count}} manga"
}
},
"library": {
"add": {
"dialog": {
"duplicate": {
"label": {
"failure": "Kan niet controleren op dubbele manga in je bibliotheek.\n\nFout: {{error}}",
"info": "U heeft een manga in uw bibliotheek met dezelfde naam."
}
}
}
},
"remove": {
"button": {
"selected": "Verwijder geselecteerde uit de bibliotheek"
},
"label": {
"action": "Verwijderen uit de bibliotheek",
"error_one": "Kan manga niet uit de bibliotheek verwijderen",
"error_other": "Kan manga's niet uit de bibliotheek verwijderen",
"success_one": "Manga uit de bibliotheek verwijderd",
"success_other": "{{count}} manga uit de bibliotheek verwijderd"
},
"dialog": {
"label": {
"message": "U staat op het punt \"{{title}}\" uit uw bibliotheek te verwijderen"
}
}
}
},
"migrate": {
"label": {
"success": "Manga met succes gemigreerd",
"error": "Kan manga niet migreren"
}
},
"track": {
"add": {
"label": {
"action": "Bijhouden",
"error": "Kan manga niet bijhouden",
"success": "Bijgehouden manga"
}
},
"remove": {
"dialog": {
"label": {
"delete_remote_track": "Ook verwijderen uit {{tracker}}",
"description": "Hiermee wordt het bijhouden ook lokaal verwijderd.",
"title": "Bijhouden {{tracker}} weghalen?"
}
},
"label": {
"error": "Kan bijhouden van manga niet stopzetten",
"success": "Niet bijgehouden manga"
}
}
}
},
"error": {
"label": {
"no_mangas_found": "Geen manga gevonden",
"no_matches": "Geen enkele manga komt overeen met dit filter",
"request_failure": "Kan manga niet laden"
}
},
"label": {
"artist": "Artiest",
"author": "Schrijver",
"edit_categories": "Manga-categorieën bewerken",
"reload_from_source": "Gegevens opnieuw laden vanuit de bron",
"status": "Status"
},
"title_one": "Manga",
"title_other": "Manga's"
},
"migrate": {
"dialog": {
"action": {
"button": {
"show_entry": "Invoer tonen"
}
},
"label": {
"delete_downloaded": "Verwijder downloads"
},
"title": "Selecteer gegevens die u wilt opnemen"
},
"label": {
"info": "Manga migreren…"
},
"title": "Migreren",
"search": {
"title": "$t(migrate.title) \"{{title}}\""
}
},
"reader": {
"button": {
"close_menu": "Menu sluiten",
"exit": "Verlaat lezer",
"next_chapter": "Volgend hoofdstuk",
"open_menu": "Menu openen",
"previous_chapter": "Vorig hoofdstuk"
},
"error": {
"label": {
"no_pages_found": "Geen pagina's gevonden",
"prev_chapter_does_not_exist": "Er is geen vorig hoofdstuk om te openen",
"next_chapter_does_not_exist": "Er is geen volgend hoofdstuk om te openen"
}
},
"page_info": {
"label": {
"currently_on_page": "Momenteel op pagina",
"of_max_pages": "van {{maxPages}}"
}
},
"settings": {
"error": {
"label": {
"failed_to_save_settings": "Kon de lezerinstellingen niet opslaan op de server"
}
},
"label": {
"fit_page_to_window": "Pagina passend maken voor venster",
"load_next_chapter": "Laad het volgende hoofdstuk aan het einde"
}
}
},
"history": {
"error": {
"label": {
"no_history_available": "Je hebt nog geen enkele serie gelezen."
}
},
"settings": {
"hide": "Geschiedenis verbergen"
},
"title": "Geschiedenis"
},
"hotkeys": {
"create": {
"dialog": {
"label": "Opgenomen toetsen: <Keys/>",
"placeholder": "Druk op toetsen"
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
{}

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +0,0 @@
{
"category": {
"dialog": {
"title": {
"new_category": "Categorie nouă",
"edit_category_one": "Editare categorie",
"edit_category_few": "Editare categorii",
"edit_category_other": "Editare categorii"
}
},
"error": {
"label": {
"create_failure": "Nu s-a putut crea categoria",
"empty": "Această categorie este goală",
"request_failure": "Nu s-au putut încărca categoriile"
},
"no_categories_found": {
"label": {
"info": "Nu ai nicio categorie încă."
}
}
},
"label": {
"category_name": "Numele Categoriei",
"use_as_default_category": "Categoria principală când adaugi manga nouă în librărie"
},
"title": {
"category_one": "Categorie",
"category_few": "Categorii",
"category_other": "Categorii",
"set_categories": "Setare categorii"
}
},
"chapter": {
"action": {
"download": {
"add": {
"button": {
"selected": "Descărcare selectată"
},
"label": {
"action": "Descărcare",
"confirmation_one": "Urmează să descarci un capitol",
"confirmation_few": "Urmează să descarci {{count}} capitole.",
"confirmation_other": "Urmează să descarci {{count}} capitole."
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +0,0 @@
{
"category": {
"dialog": {
"title": {
"edit_category_one": "Redigera kategori",
"edit_category_other": "Redigera kategorier",
"new_category": "Ny kategori"
}
},
"error": {
"label": {
"empty": "Kategorin är tom",
"request_failure": "Kunde inte ladda kategorier",
"create_failure": "Kunde inte skapa kategori"
},
"no_categories_found": {
"label": {
"info": "Inga kategorier hittades."
}
}
},
"settings": {
"inclusion": {
"label": {
"exclude": "Utesluta: {{excludedCategoriesText}}",
"include": "Inkludera: {{includedCategoriesText}}"
}
}
},
"title": {
"category_one": "Kategori",
"category_other": "Kategorier",
"set_categories": "Ställ in kategorier"
},
"label": {
"category_name": "Kategori namn",
"use_as_default_category": "Standard kategori när man lägger till ny manga i biblioteket"
}
},
"chapter": {
"action": {
"bookmark": {
"add": {
"button": {
"selected": "Bokmärke valt"
},
"label": {
"action": "Lägg till bokmärke",
"error_one": "Det gick inte att bokmärka kapitalet",
"error_other": "Det gick inte att bokmärka kapitlet",
"success_one": "Kapitel bokmärktes",
"success_other": "{{count}} kapitel bokmärktes"
}
},
"remove": {
"label": {
"action": "Ta bort bokmärke",
"error_one": "Kunde inte ta bort bokmärke",
"error_other": "Kunde inte ta bort bokmärkena"
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,402 +0,0 @@
{
"category": {
"dialog": {
"title": {
"new_category": "เพิ่มหมวดหมู่",
"edit_category_other": "แก้ไขหมวดหมู่"
}
},
"label": {
"category_name": "ชื่อหมวดหมู่",
"use_as_default_category": "หมวดหมู่เริ่มต้นเมื่อเพิ่มมังงะใหม่เข้าสู่ห้องสมุด"
},
"error": {
"label": {
"request_failure": "ไม่สามารถโหลดหมวดหมู่ได้",
"empty": "หมวดหมู่ว่างเปล่า",
"create_failure": "ไม่สามารถสร้างหมวดหมู่ได้"
},
"no_categories_found": {
"label": {
"info": "คุณยังไม่มีหมวดหมู่ใด ๆ"
}
}
},
"settings": {
"inclusion": {
"label": {
"exclude": "ยกเว้น: {{excludedCategoriesText}}",
"include": "รวม: {{includedCategoriesText}}"
}
}
},
"title": {
"category_other": "หมวดหมู่",
"set_categories": "กำหนดหมวดหมู่"
},
"value": "{{count}} $t(category.title.category, lowercase)"
},
"chapter": {
"action": {
"download": {
"add": {
"label": {
"action": "ดาวน์โหลด",
"error_other": "ไม่สามารถเพิ่มเพื่อดาวน์โหลดได้",
"success_other": "{{count}} ถูกเพิ่มการดาวน์โหลดแล้ว",
"ahead": "$t(download.settings.download_ahead.title) ({{count}})",
"all": "$t(extension.language.all)",
"next_other": "$t(reader.button.next_chapter)",
"unread": "$t(global.filter.label.unread)"
},
"button": {
"selected": "ดาวน์โหลดที่เลือก"
}
},
"delete": {
"button": {
"selected": "ลบที่เลือก"
},
"label": {
"action": "ลบ",
"error_other": "ไม่สามารถลบตอนได้",
"success_other": "{{count}} ตอนถูกลบ"
}
}
},
"label": {
"select": "เลือก"
},
"mark_as_read": {
"remove": {
"button": {
"selected": "ทำเครื่องหมายที่เลือกเป็นยังไม่อ่าน"
},
"label": {
"action": "ทำเครื่องหมายว่ายังไม่อ่าน",
"success_other": "{{count}} ตอนนี้ถูกทำเครื่องหมายว่ายังไม่ได้อ่าน",
"error_other": "ไม่สามารถทำเครื่องหมายตอนนี้ว่ายังไม่อ่าน"
}
},
"add": {
"button": {
"selected": "ทำเครื่องหมายที่เลือกเป็นอ่านแล้ว"
},
"label": {
"action": {
"current": "ทำเครื่องหมายว่าอ่านแล้ว",
"previous": "ทำเครื่องหมายก่อนหน้านี้ว่าอ่านแล้ว"
},
"error_other": "ไม่สามารถทำเครื่องหมายว่าตอนนี้อ่านแล้วได้",
"success_other": "{{count}} ตอนนี้ถูกทำเครื่องหมายว่าอ่านแล้ว"
}
}
},
"bookmark": {
"add": {
"button": {
"selected": "บุ๊คมาร์กที่เลือก"
},
"label": {
"action": "เพิ่มบุ๊คมาร์ก",
"error_other": "ไม่สามารถบุ๊คมาร์กตอนนี้ได้",
"success_other": "{{count}} บุ๊คตอนนี้แล้ว"
}
},
"remove": {
"button": {
"selected": "ลบบุ๊คมาร์กจากที่เลือก"
},
"label": {
"action": "ลบบุ๊คมาร์ก",
"error_other": "ไม่สามารถลบบุ๊คมาร์กได้",
"success_other": "{{count}} ตอนที่บุ๊กมาร์กถูกนำออกแล้ว"
}
}
}
},
"error": {
"label": {
"no_chapter_found": "ไม่พบตอน",
"no_matches": "ไม่มีตอนที่ตรงกับตัวกรอง"
}
},
"option": {
"display": {
"label": {
"chapter_number": "ตอนที่",
"source_title": "ที่มาชื่อเรื่อง"
}
},
"exclude_scanlators": "นักสแกนที่ไม่รวม"
},
"status": {
"label": {
"downloaded": "ดาวน์โหลดแล้ว"
}
},
"title_other": "ตอน",
"missing_other": "หายไป {{count}} ตอน",
"value_other": "{{count}} ตอน"
},
"global": {
"button": {
"save": "บันทึก",
"open": "เปิด",
"remove": "ลบ",
"reset": "รีเซ็ต",
"reset_to_default": "รีเซ็ตเป็นค่าเริ่มต้น",
"restore": "คืนค่า",
"resume": "ดำเนินต่อ",
"retry": "ลองใหม่อีกครั้ง",
"select": "เลือก",
"browse": "ค้นหา",
"cancel": "ยกเลิก",
"changelog": "บันทึกการเปลี่ยนแปลง",
"add": "เพิ่ม",
"edit": "แก้ไข",
"filter": "กรอง",
"ignore": "ไม่สนใจ",
"select_all": "เลือกทั้งหมด",
"set": "ตั้ง",
"show_less": "แสดงน้อยลง",
"show_more": "แสดงเพิ่มเติม",
"start": "เริ่ม",
"stop": "หยุด",
"submit": "ส่ง",
"copy": "คัดลอก",
"create": "สร้าง",
"delete": "ลบ",
"deselect": "ยกเลิกการเลือก",
"dont_show_dialog_again": "อย่าแสดงหน้านี้อีกครั้ง",
"latest": "ล่าสุด",
"log_in": "เข้าสู่ระบบ",
"log_out": "ออกจากระบบ",
"migrate": "ย้าย",
"ok": "โอเค",
"options": "ตัวเลือก",
"refresh": "รีเฟรช",
"popular": "เป็นที่นิยม",
"remind_later": "เตือนทีหลัง"
},
"date": {
"label": {
"day_other": "วัน",
"today": "วันนี้",
"yesterday_at": "เมื่อวานนี้เวลา {{timeString}}",
"today_at": "วันนี้ที่ {{timeString}}",
"yesterday": "เมื่อวาน"
}
},
"label": {
"disabled": "ปิดใช้งาน",
"discord": "ดิสคอร์ด",
"default": "ค่าเริ่มต้น",
"display": "แสดงผล",
"never": "ไม่เคย",
"menu": "เมนู",
"mobile": "มือถือ",
"copied_clipboard": "คัดลอกไปยังคลิปบอร์ดแล้ว",
"desktop": "เดสก์ท็อป",
"finished": "เสร็จเรียบร้อย",
"left": "ซ้าย",
"info": "ข้อมูล",
"both": "ทั้งคู่",
"filter": "ตัวกรอง",
"browse": "ค้นหา",
"are_you_sure": "คุณแน่ใจมั้ย?",
"client": "ผู้ใช้",
"close": "ปิด",
"load_in_progress": "กำลังโหลดข้อมูลที่จำเป็นอยู่…",
"loading": "กำลังโหลด…",
"logged_in": "เข้าสู่ระบบแล้ว",
"general": "ทั่วไป",
"advanced": "ขั้นสูง",
"github": "กิตฮับ",
"links": "ลิงค์",
"error": "ข้อผิดพลาด",
"hidden": "ซ่อน",
"horizontal": "แนวนอน",
"bottom": "ด้านล่าง",
"auto": "ออโต้"
},
"error": {
"label": {
"invalid_input": "ข้อมูลอินพุตไม่ถูกต้อง",
"update_failed": "ไม่สามารถตรวจสอบการอัปเดตได้",
"failed_to_load_data": "ไม่สามารถโหลดข้อมูลได้",
"failed_to_save_changes": "ไม่สามารถบันทึกการเปลี่ยนแปลงได้",
"invalid_action": "นี่ไม่ใช่การกระทำที่ถูกต้อง",
"invalid_file_type": "ประเภทไฟล์ไม่ถูกต้อง"
}
},
"filter": {
"label": {
"bookmarked": "บุ๊คมาร์กแล้ว",
"downloaded": "ดาวน์โหลด",
"unread": "ยังไม่ได้อ่าน",
"duplicate_chapters": "คัดลอกตอน",
"tracked": "ติดตาม",
"started": "เริ่มแล้ว"
}
},
"grid_layout": {
"label": {
"comfortable_grid": "กริดที่สะดวกสบาย",
"list": "รายการ",
"compact_grid": "กริดแบบกะทัดรัด"
},
"title": "โหมดการแสดงผล"
},
"colors": {
"black": "สีดำ",
"gray": "สีเทา",
"white": "สีขาว"
}
},
"extension": {
"settings": {
"repositories": {
"custom": {
"label": {
"description": "เพิ่มที่เก็บข้อมูลซึ่งสามารถติดตั้งส่วนขยายได้",
"disclaimer": "<strong>Suwayomi ไม่ได้ให้การสนับสนุนใด ๆ สำหรับที่เก็บข้อมูลหรือส่วนขยายของบุคคลที่สาม!</strong><br/>ใช้ด้วยความระมัดระวัง เนื่องจากอาจมีผู้ไม่ประสงค์ดีสร้างที่เก็บข้อมูลเหล่านี้<br/>คุณในฐานะผู้ใช้จำเป็นต้องตรวจสอบความปลอดภัยและต้องเชื่อถือที่เก็บข้อมูลหรือส่วนขยายใดๆ",
"title": "ที่เก็บข้อมูลส่วนขยาย"
},
"dialog": {
"action": {
"button": {
"add": "เพิ่มที่เก็บข้อมูล"
}
}
},
"error": {
"label": {
"invalid_url": "URL ของที่เก็บข้อมูลไม่ถูกต้อง"
}
}
}
}
},
"label": {
"installed_successfully_other": "ติดตั้งส่วนขยายแล้ว",
"installing_file": "กำลังติดตั้งไฟล์ส่วนขยาย…",
"add_repository_info": "คุณต้องเพิ่มที่เก็บส่วนขยายจึงจะสามารถติดตั้งส่วนขยายได้",
"installation_failed_other": "ไม่สามารถติดตั้งส่วนขยายได้",
"uninstallation_failed_other": "ไม่สามารถถอนการติดตั้งส่วนขยายได้",
"update_failed_other": "ไม่สามารถอัปเดตส่วนขยายได้"
},
"action": {
"label": {
"install": "ติดตั้ง",
"install_external": "ติดตั้งส่วนขยายภายนอก",
"uninstall": "ถอนการติดตั้ง",
"update": "อัปเดต",
"update_all": "อัพเดตทั้งหมด"
}
},
"language": {
"all": "ทั้งหมด",
"other": "อื่น"
},
"state": {
"label": {
"installed": "ติดตั้งแล้ว",
"installing": "กำลังติดตั้ง",
"obsolete": "ล้าสมัย",
"updating": "กำลังอัพเดต",
"uninstalling": "กำลังถอนการติดตั้ง",
"update_pending": "การอัปเดตอยู่ระหว่างดำเนินการ"
}
},
"title_other": "ส่วนขยาย"
},
"download": {
"queue": {
"error": {
"label": {
"failed_to_remove": "ไม่สามารถลบการดาวน์โหลดออกจากคิวได้",
"failed_to_retry": "ไม่สามารถดาวน์โหลดที่ล้มเหลวอีกครั้งได้",
"failed_delete_all": "ไม่สามารถลบการดาวน์โหลดทั้งหมดออกจากคิวได้"
}
},
"label": {
"no_downloads": "ไม่มีการดาวน์โหลด",
"delete_all": "ลบทั้งหมด"
},
"info": {
"remaining": "{{count}} ที่เหลืออยู่",
"paused": "ถูกพัก — $t(download.queue.info.remaining)"
}
},
"settings": {
"auto_download": {
"categories": {
"label": {
"include_in_download": "รายการในหมวดหมู่ที่ไม่รวมจะไม่ถูกดาวน์โหลดแม้ว่าจะอยู่ในหมวดหมู่ที่รวมอยู่ด้วยก็ตาม"
}
},
"download_limit": {
"label": {
"description": "จำกัดจำนวนตอนใหม่ที่จะดาวน์โหลด",
"title": "จำกัดการดาวน์โหลดตอน"
}
},
"label": {
"ignore_re_uploads": "ละเว้นตอนที่อัพโหลดซ้ำ",
"ignore_with_unread_chapters": "ละเว้นการดาวน์โหลดตอนอัตโนมัติสำหรับรายการที่มีตอนที่ยังไม่ได้อ่าน",
"new_chapters": "ดาวน์โหลดตอนใหม่"
},
"title": "ดาวน์โหลดอัตโนมัติ"
},
"delete_chapters": {
"label": {
"allow_deletion_of_bookmarked": "อนุญาตให้ลบตอนที่บุ๊คมาร์กไว้",
"manually_marked_as_read": "ลบตอนหลังจากทำเครื่องหมายด้วยตนเองว่าอ่านแล้ว"
},
"title": "ลบตอน",
"while_reading": {
"label": {
"title": "ลบตอนที่อ่านจบขณะอ่าน"
},
"option": {
"label": {
"fifth": "ตอนที่ห้าจากสุดท้ายที่อ่าน",
"first": "ตอนที่อ่านล่าสุด",
"fourth": "ตอนที่สี่จากสุดท้ายที่อ่าน",
"second": "อ่านตอนที่สองจากท้ายสุด",
"third": "อ่านตอนที่สามจากสุดท้าย"
}
}
}
},
"download_ahead": {
"label": {
"description": "ควรดาวน์โหลดกี่ตอนขณะอ่าน",
"disclaimer": "ใช้งานได้เฉพาะในกรณีที่ดาวน์โหลดตอนปัจจุบันและตอนถัดไปแล้วเท่านั้น",
"unread_chapters_to_download": "จำนวนตอนที่ยังไม่ได้อ่านที่จะดาวน์โหลด",
"while_reading": "ดาวน์โหลดอัตโนมัติขณะอ่าน"
},
"title": "ดาวน์โหลดก่อน"
},
"download_path": {
"label": {
"description": "เส้นทางไปยังไดเรกทอรีบนเซิร์ฟเวอร์ที่ไฟล์ที่ดาวน์โหลดมาจะถูกบันทึกไว้",
"title": "ตำแหน่งการดาวน์โหลด"
}
},
"file_type": {
"label": {
"cbz": "บันทึกเป็นไฟล์เก็บถาวร CBZ"
}
}
},
"state": {
"label": {
"downloading": "กำลังดาวน์โหลด",
"error": "ข้อผิดพลาด",
"queued": "อยู่ในคิว"
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { AssertionError } from 'assert';
export function assertIsDefined<T>(value: T | undefined): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new AssertionError({ message: 'Value is undefined or null' });
throw new Error('Value is undefined or null');
}
}

View File

@@ -6,8 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { ReactNode } from 'react';
import { ParseKeys } from 'i18next';
export enum GridLayout {
Compact = 0,
@@ -20,11 +20,9 @@ export enum DirectionOffset {
NEXT = 1,
}
export type TranslationKey = ParseKeys;
interface DisplayDataTranslationKey {
interface DisplayDataTranslation {
isTitleString?: never;
title: TranslationKey;
title: MessageDescriptor;
icon: ReactNode;
}
@@ -34,7 +32,7 @@ interface DisplayDataString {
icon: ReactNode;
}
type DisplayData = DisplayDataTranslationKey | DisplayDataString;
type DisplayData = DisplayDataTranslation | DisplayDataString;
export type ValueToDisplayData<Value extends string | number> = Record<Value, DisplayData>;

View File

@@ -7,7 +7,7 @@
*/
import Checkbox from '@mui/material/Checkbox';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
export const SelectableCollectionSelectAll = ({
@@ -19,10 +19,10 @@ export const SelectableCollectionSelectAll = ({
areNoItemsSelected: boolean;
onChange: (checked: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<CustomTooltip title={t(!areAllItemsSelected ? 'global.button.select_all' : 'global.button.clear_selection')}>
<CustomTooltip title={!areAllItemsSelected ? t`Select all` : t`Clear`}>
<Checkbox
sx={{
padding: '8px',

View File

@@ -7,8 +7,8 @@
*/
import Checkbox from '@mui/material/Checkbox';
import { useTranslation } from 'react-i18next';
import ClearIcon from '@mui/icons-material/Clear';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SelectableCollectionSelectAll } from '@/base/collection/components/SelectableCollectionSelectAll.tsx';
@@ -25,7 +25,7 @@ export const SelectableCollectionSelectMode = ({
onSelectAll: (selectAll: boolean) => void;
onModeChange: (checked: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<>
@@ -36,7 +36,7 @@ export const SelectableCollectionSelectMode = ({
onChange={onSelectAll}
/>
)}
<CustomTooltip title={t(!isActive ? 'global.button.select_all' : 'global.button.cancel')}>
<CustomTooltip title={!isActive ? t`Select all` : t`Cancel`}>
<Checkbox
checkedIcon={<ClearIcon />}
sx={{

View File

@@ -11,17 +11,13 @@ import Fab from '@mui/material/Fab';
import Box from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import React, { type JSX } from 'react';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import { DEFAULT_FAB_STYLE } from '@/base/components/buttons/StyledFab.tsx';
import { Menu } from '@/base/components/menu/Menu.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
interface SelectionFABProps {
children: (handleClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element;
selectedItemsCount: number;
title: TranslationKey;
title: string;
}
const FabContainer = styled(Box)(({ theme }) => ({
@@ -34,32 +30,28 @@ const FabContainer = styled(Box)(({ theme }) => ({
},
}));
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, selectedItemsCount, title }) => {
const { t } = useTranslation();
return (
<PopupState variant="popover" popupId="selection-fab-menu">
{(popupState) => (
<>
<FabContainer {...bindTrigger(popupState)}>
<Fab variant="extended" color="primary" id="selectionMenuButton">
{`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`}
<MoreHoriz sx={{ ml: 1 }} />
</Fab>
</FabContainer>
<Menu
{...bindMenu(popupState)}
id="selectionMenu"
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'selectionMenuButton',
}}
>
{(onClose, setHideMenu) => children(onClose, setHideMenu)}
</Menu>
</>
)}
</PopupState>
);
};
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, title }) => (
<PopupState variant="popover" popupId="selection-fab-menu">
{(popupState) => (
<>
<FabContainer {...bindTrigger(popupState)}>
<Fab variant="extended" color="primary" id="selectionMenuButton">
{title}
<MoreHoriz sx={{ ml: 1 }} />
</Fab>
</FabContainer>
<Menu
{...bindMenu(popupState)}
id="selectionMenu"
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'selectionMenuButton',
}}
>
{(onClose, setHideMenu) => children(onClose, setHideMenu)}
</Menu>
</>
)}
</PopupState>
);

View File

@@ -10,10 +10,10 @@ import React, { useState } from 'react';
import SearchIcon from '@mui/icons-material/Search';
import IconButton from '@mui/material/IconButton';
import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SearchTextField } from '@/base/components/inputs/SearchTextField.tsx';
import { SearchParam } from '@/base/Base.types.ts';
@@ -26,7 +26,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
const { isClosable = true } = props;
const theme = useTheme();
const { t } = useTranslation();
const { t } = useLingui();
const [prevLocationKey, setPrevLocationKey] = useState<string>();
const location = useLocation();
@@ -119,7 +119,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}
return (
<CustomTooltip title={t('search.title.search')}>
<CustomTooltip title={t`Search`}>
<IconButton onClick={() => updateSearchOpenState(true)} color="inherit">
<SearchIcon />
</IconButton>

View File

@@ -13,9 +13,8 @@ import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import React from 'react';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { GridLayout } from '@/base/Base.types.ts';
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
@@ -26,7 +25,7 @@ export function GridLayouts({
gridLayout: GridLayout;
onChange: (gridLayout: GridLayout) => void;
}) {
const { t } = useTranslation();
const { t } = useLingui();
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
@@ -43,7 +42,7 @@ export function GridLayouts({
return (
<>
<CustomTooltip title={t('global.label.display')}>
<CustomTooltip title={t`Display`}>
<IconButton
onClick={handleClick}
size="small"
@@ -64,7 +63,7 @@ export function GridLayouts({
>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.compact_grid')}
label={t`Compact grid`}
value={GridLayout.Compact}
control={
<Radio
@@ -77,7 +76,7 @@ export function GridLayouts({
</MenuItem>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.comfortable_grid')}
label={t`Comfortable grid`}
control={
<Radio
name={GridLayout.Comfortable.toString()}
@@ -89,7 +88,7 @@ export function GridLayouts({
</MenuItem>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.list')}
label={t`List`}
control={
<Radio
name={GridLayout.List.toString()}

View File

@@ -13,9 +13,9 @@ import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';
import ImageIcon from '@mui/icons-material/Image';
import { SxProps, Theme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { ImageRequest, requestManager } from '@/lib/requests/RequestManager.ts';
import { Priority } from '@/lib/Queue.ts';
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
@@ -65,7 +65,7 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
retryKeyPrefix,
} = props;
const { t } = useTranslation();
const { t } = useLingui();
const loadingIndicatorRef = useRef<HTMLDivElement | null>(null);
@@ -174,7 +174,6 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
draggable={false}
/>
)}
{(!!isLoading || (src && !imageSourceUrl) || hasError) && (
<Stack
ref={loadingIndicatorRef}
@@ -205,7 +204,7 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
}}
size={small ? 'small' : 'large'}
>
{small ? <RefreshIcon /> : t('global.button.retry')}
{small ? <RefreshIcon /> : t`Retry`}
</Button>
</>
)}

View File

@@ -7,8 +7,8 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { MultiValueButtonProps } from '@/base/Base.types.ts';
import { Superscript } from '@/base/components/texts/Superscript.tsx';
@@ -22,13 +22,13 @@ export const ButtonSelect = <Value extends string | number>({
isDefaultable,
onDefault,
}: MultiValueButtonProps<Value>) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
{isDefaultable && (
<Button key="default" onClick={onDefault} variant={value === undefined ? 'contained' : 'outlined'}>
{t('global.label.default')}
{t`Default`}
</Button>
)}
{values.map((displayValue) => {
@@ -39,13 +39,13 @@ export const ButtonSelect = <Value extends string | number>({
: t(valueToDisplayData[displayValue].title);
return (
<CustomTooltip key={displayValue} title={isDefault ? t('reader.settings.active_setting') : ''}>
<CustomTooltip key={displayValue} title={isDefault ? t`Active setting` : ''}>
<Button
onClick={() => setValue(displayValue)}
variant={displayValue === value ? 'contained' : 'outlined'}
startIcon={valueToDisplayData[displayValue].icon}
>
{isDefault ? <Superscript i18nKey="global.label.footnote" value={text} /> : text}
{isDefault ? <Superscript superscript="*" text={text} /> : text}
</Button>
</CustomTooltip>
);

View File

@@ -7,9 +7,9 @@
*/
import Button, { ButtonProps } from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
type PropsIconButton = { asIconButton: true } & IconButtonProps;
@@ -17,11 +17,11 @@ type PropsButton = { asIconButton?: false } & ButtonProps;
type Props = PropsIconButton | PropsButton;
export const ResetButton = ({ asIconButton, ...props }: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
if (asIconButton) {
return (
<CustomTooltip title={t('global.button.reset')}>
<CustomTooltip title={t`Reset`}>
<IconButton color="inherit" {...props}>
<RestartAltIcon />
</IconButton>
@@ -31,7 +31,7 @@ export const ResetButton = ({ asIconButton, ...props }: Props) => {
return (
<Button startIcon={<RestartAltIcon />} {...(props as ButtonProps)}>
{t('global.button.reset')}
{t`Reset`}
</Button>
);
};

View File

@@ -6,9 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { ReactNode, useMemo } from 'react';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { MultiValueButtonProps } from '@/base/Base.types.ts';
import { getNextRotationValue } from '@/base/utils/ValueRotationButton.utils.ts';
@@ -25,7 +25,7 @@ export const ValueRotationButton = <Value extends string | number>({
onDefault,
defaultIcon,
}: MultiValueButtonProps<Value> & { defaultIcon?: ReactNode }) => {
const { t } = useTranslation();
const { t } = useLingui();
const isDefault = value === undefined;
const indexOfValue = useMemo(() => {
@@ -47,11 +47,11 @@ export const ValueRotationButton = <Value extends string | number>({
size="large"
>
{defaultValue === undefined ? (
t('global.label.default')
t`Default`
) : (
<Superscript
i18nKey="settings.default_value"
value={
superscript={`(${t`Default`})`}
text={
valueToDisplayData[defaultValue].isTitleString
? valueToDisplayData[defaultValue].title
: t(valueToDisplayData[defaultValue].title)

View File

@@ -6,24 +6,25 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
DOWNLOADING: 'download.state.label.downloading',
ERROR: 'download.state.label.error',
FINISHED: 'download.state.label.finished',
QUEUED: 'download.state.label.queued',
const DOWNLOAD_STATE_TO_TRANSLATION_MAP: { [state in DownloadState]: MessageDescriptor } = {
DOWNLOADING: msg`Downloading`,
ERROR: msg`Error`,
FINISHED: msg`Finished`,
QUEUED: msg`Queued`,
} as const;
export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: ChapterIdInfo['id']; color?: string }) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
@@ -35,6 +36,7 @@ export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: Chapte
const isPartiallyDownloaded = download.progress !== 0;
const progress = `${Math.round(download.progress * 100)}%`;
const stateText = t(DOWNLOAD_STATE_TO_TRANSLATION_MAP[download.state]);
return (
<Box
@@ -61,11 +63,12 @@ export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: Chapte
<Typography variant="caption" component="div" sx={{ color }}>
<>
{isDownloading && progress}
{!isDownloading &&
t('global.value', {
value: t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state]),
unit: isPartiallyDownloaded ? ` (${progress})` : '',
})}
{!isDownloading && (
<>
{stateText}
{isPartiallyDownloaded ? ` (${progress})` : ''}
</>
)}
</>
</Typography>
</Box>

View File

@@ -11,10 +11,10 @@
import { type JSX, useMemo, useState } from 'react';
import Typography from '@mui/material/Typography';
import { SxProps, Theme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Collapse from '@mui/material/Collapse';
import { useLingui } from '@lingui/react/macro';
import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts';
const ERROR_FACES = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
@@ -33,7 +33,7 @@ export interface EmptyViewProps {
}
const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const [showFullError, setShowFullError] = useState(false);
@@ -69,7 +69,7 @@ const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) =>
{graphqlError}
</Typography>
<Button variant="text" onClick={() => setShowFullError(!showFullError)} sx={{ pointerEvents: 'all' }}>
{t(showFullError ? 'global.button.show_less' : 'global.button.show_more')}
{showFullError ? t`Show less` : t`Show more`}
</Button>
</Stack>
<Collapse in={showFullError}>
@@ -86,7 +86,7 @@ const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) =>
};
export function EmptyView({ message, messageExtra, retry, noFaces, sx }: EmptyViewProps) {
const { t } = useTranslation();
const { t } = useLingui();
const errorFace = useMemo(() => getRandomErrorFace(), []);
@@ -115,7 +115,7 @@ export function EmptyView({ message, messageExtra, retry, noFaces, sx }: EmptyVi
<ExtraMessage messageExtra={messageExtra} />
{retry && (
<Button onClick={retry} sx={{ pointerEvents: 'all' }}>
{t('global.button.retry')}
{t`Retry`}
</Button>
)}
</Stack>

View File

@@ -8,7 +8,7 @@
import { Component, ErrorInfo, ReactNode, useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { t } from 'i18next';
import { t } from '@lingui/core/macro';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
@@ -57,7 +57,7 @@ class RealErrorBoundary extends Component<Props, State> {
if (error) {
return (
<EmptyView
message={t('global.error.label.unrecoverable_error')}
message={t`Something went wrong\nAn error occurred that we cannot recover from`}
messageExtra={getErrorMessage(error)}
retry={() => window.location.reload()}
/>

View File

@@ -6,27 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { closeSnackbar, CustomContentProps, SnackbarContent, VariantType } from 'notistack';
import { ForwardedRef, Fragment, memo } from 'react';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts';
import { TranslationKey } from '@/base/Base.types.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
const MAX_DESCRIPTION_LENGTH = 200;
const SNACKBAR_VARIANT_TO_TRANSLATION_KEY: Record<VariantType, TranslationKey> = {
default: 'global.label.info',
info: 'global.label.info',
success: 'global.label.success',
warning: 'global.label.warning',
error: 'global.label.error',
const SNACKBAR_VARIANT_TO_TRANSLATION: Record<VariantType, MessageDescriptor> = {
default: msg`Information`,
info: msg`Information`,
success: msg`Success`,
warning: msg`Warning`,
error: msg`Error`,
};
export const SnackbarWithDescription = memo(
@@ -41,7 +42,7 @@ export const SnackbarWithDescription = memo(
description?: string;
ref?: ForwardedRef<HTMLDivElement>;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const severity = variant === 'default' ? 'info' : variant;
@@ -87,11 +88,13 @@ export const SnackbarWithDescription = memo(
title:
typeof message === 'string'
? message
: t(SNACKBAR_VARIANT_TO_TRANSLATION_KEY[variant]),
: t(SNACKBAR_VARIANT_TO_TRANSLATION[variant]),
message: description ?? '',
actions: {
cancel: { show: false },
confirm: { title: t('global.label.close') },
confirm: {
title: t`Close`,
},
},
}).catch(
defaultPromiseErrorHandler(
@@ -101,7 +104,7 @@ export const SnackbarWithDescription = memo(
}}
size="small"
>
{t('global.button.show_more')}
{t`Show more`}
</Button>
) : (
''

View File

@@ -17,8 +17,8 @@ import IconButton from '@mui/material/IconButton';
import FilterListIcon from '@mui/icons-material/FilterList';
import ListItemText from '@mui/material/ListItemText';
import ListItem from '@mui/material/ListItem';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
import { languageSortComparator } from '@/base/utils/Languages.ts';
@@ -31,7 +31,7 @@ interface IProps {
}
export function LanguageSelect(props: IProps) {
const { t } = useTranslation();
const { t } = useLingui();
const { selectedLanguages, setSelectedLanguages, languages } = props;
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueISOLanguageCodes(selectedLanguages));
@@ -66,13 +66,13 @@ export function LanguageSelect(props: IProps) {
return (
<>
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
<FilterListIcon />
</IconButton>
</CustomTooltip>
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
<DialogContent dividers sx={{ padding: 0 }}>
<Virtuoso
style={{
@@ -97,10 +97,10 @@ export function LanguageSelect(props: IProps) {
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -12,10 +12,10 @@ import { useState } from 'react';
import IconButton from '@mui/material/IconButton';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
export const PasswordTextField = (props: TextFieldProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
@@ -24,7 +24,7 @@ export const PasswordTextField = (props: TextFieldProps) => {
<TextField
id="password"
name="password"
label={t('global.label.password')}
label={t`Password`}
type={showPassword ? 'text' : 'password'}
slotProps={{
input: {

View File

@@ -6,30 +6,27 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { TranslationKey } from '@/base/Base.types.ts';
import { MessageDescriptor } from '@lingui/core';
import { i18n } from '@/i18n';
export const createGetMenuItemTitle =
<Action extends string>(
isSingleMode: boolean,
actionToTranslationKey: Record<
actionToTranslation: Record<
Action,
{
action: {
single: TranslationKey;
selected: TranslationKey;
single: MessageDescriptor;
selected: MessageDescriptor;
};
success: TranslationKey;
error: TranslationKey;
success: MessageDescriptor;
error: MessageDescriptor;
}
>,
) =>
(action: Action, count: number): string => {
const countSuffix = count > 0 ? ` (${count})` : '';
return `${translate(
actionToTranslationKey[action].action[isSingleMode ? 'single' : 'selected'],
)}${countSuffix}`;
return `${i18n._(actionToTranslation[action].action[isSingleMode ? 'single' : 'selected'])}${countSuffix}`;
};
export const createShouldShowMenuItem =

View File

@@ -10,11 +10,11 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { Link as RouterLink } from 'react-router-dom';
import { useLingui } from '@lingui/react/macro';
type Action = {
show?: boolean;
@@ -43,7 +43,7 @@ export const ConfirmDialog = ({
actions?: Actions;
onExtra?: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const actions = {
extra: {
@@ -54,12 +54,12 @@ export const ConfirmDialog = ({
},
cancel: {
show: passedActions?.cancel?.show ?? true,
title: passedActions?.cancel?.title ?? t('global.button.cancel'),
title: passedActions?.cancel?.title ?? t`Cancel`,
contain: passedActions?.cancel?.contain ?? false,
},
confirm: {
show: passedActions?.confirm?.show ?? true,
title: passedActions?.confirm?.title ?? t('global.button.ok'),
title: passedActions?.confirm?.title ?? t`Ok`,
contain:
!passedActions?.extra?.contain &&
!passedActions?.cancel?.contain &&

View File

@@ -9,13 +9,13 @@
import Dialog from '@mui/material/Dialog';
import { AwaitableComponent, AwaitableComponentProps } from 'awaitable-component';
import DialogTitle from '@mui/material/DialogTitle';
import { useTranslation } from 'react-i18next';
import DialogContent from '@mui/material/DialogContent';
import TextField from '@mui/material/TextField';
import { useState } from 'react';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
export const LoginDialog = ({
@@ -42,7 +42,7 @@ export const LoginDialog = ({
serverAddress?: string;
withServerAddress?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [serverAddress, setServerAddress] = useState(initialServerAddress);
const [username, setUsername] = useState(initialUsername);
@@ -60,7 +60,7 @@ export const LoginDialog = ({
margin="dense"
id="serverAddress"
name="serverAddress"
label={t('settings.about.server.label.address')}
label={t`Server address`}
type="text"
fullWidth
variant="standard"
@@ -73,7 +73,7 @@ export const LoginDialog = ({
margin="dense"
id="username"
name="username"
label={t('global.label.username')}
label={t`Username`}
type="text"
fullWidth
variant="standard"
@@ -89,7 +89,7 @@ export const LoginDialog = ({
)}
</DialogContent>
<DialogActions>
<Button onClick={() => onSubmit()}>{t('global.button.cancel')}</Button>
<Button onClick={() => onSubmit()}>{t`Cancel`}</Button>
<Button
variant="contained"
disabled={
@@ -101,7 +101,7 @@ export const LoginDialog = ({
}
onClick={() => loginLogout(username, password, serverAddress)}
>
{t(isLoggedIn ? 'global.button.log_out' : 'global.button.log_in')}
{isLoggedIn ? t`Log out` : t`Log in`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -12,10 +12,10 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import { useCallback, useMemo } from 'react';
import { CheckboxProps } from '@mui/material/Checkbox';
import { useLingui } from '@lingui/react/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
@@ -42,7 +42,7 @@ export function CheckboxListSetting<Item>({
checkbox?: Omit<CheckboxProps, 'checked' | 'onChange' | 'label'>;
};
}) {
const { t } = useTranslation();
const { t } = useLingui();
const itemIds = useMemo(() => items.map(getId), [items]);
const currentSelectedItemIds = useMemo(() => items.filter(isChecked).map(getId), [items]);
@@ -105,15 +105,15 @@ export function CheckboxListSetting<Item>({
}}
>
<Button onClick={() => handleSelectAll(!selectedItemIds.length, items.map(getId))}>
{t(selectedItemIds.length ? 'global.button.reset' : 'global.button.select_all')}
{selectedItemIds.length ? t`Reset` : t`Select all`}
</Button>
<Stack direction="row">
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
{!!items.length && (
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
)}
</Stack>

View File

@@ -15,11 +15,11 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { useLingui } from '@lingui/react/macro';
export const DateSetting = ({
settingName,
@@ -34,7 +34,7 @@ export const DateSetting = ({
handleChange: (path?: string | null) => void;
remove?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value ?? defaultValue);
@@ -85,7 +85,6 @@ export const DateSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -117,7 +116,7 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
)}
{remove && (
@@ -128,13 +127,13 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.remove')}
{t`Remove`}
</Button>
)}
</Stack>
<Stack direction="row">
<Button onClick={closeDialogWithReset} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button
onClick={() => {
@@ -142,7 +141,7 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</Stack>
</Stack>

View File

@@ -17,12 +17,12 @@ import Typography from '@mui/material/Typography';
import { useEffect, useState, type JSX } from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TextSetting, TextSettingProps } from '@/base/components/settings/text/TextSetting.tsx';
import { TextSettingDialog } from '@/base/components/settings/text/TextSettingDialog.tsx';
@@ -38,7 +38,7 @@ const MutableListItem = ({
mutable?: boolean;
deletable?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
@@ -49,7 +49,7 @@ const MutableListItem = ({
<ListItemText secondary={textSettingProps.value} />
</ListItem>
)}
<CustomTooltip title={t('chapter.action.download.delete.label.action')} disabled={!deletable}>
<CustomTooltip title={t`Delete`} disabled={!deletable}>
<IconButton disabled={!deletable} onClick={handleDelete}>
<DeleteIcon />
</IconButton>
@@ -87,7 +87,7 @@ export const MutableListSetting = ({
validateItem = () => true,
invalidItemError,
}: MutableListSettingProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const values = getValues(valueInfos);
@@ -129,7 +129,7 @@ export const MutableListSetting = ({
}
if (!validateItem?.(newValue, dialogValues)) {
makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error');
makeToast(invalidItemError ?? t`Invalid input`, 'error');
return;
}
@@ -156,7 +156,6 @@ export const MutableListSetting = ({
}}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogTitle>{settingName}</DialogTitle>
{(!!description || !!dialogDisclaimer) && (
@@ -219,17 +218,14 @@ export const MutableListSetting = ({
width: '100%',
}}
>
<Button onClick={() => setIsAddItemDialogOpen(true)}>
{addItemButtonTitle ?? t('global.button.add')}
</Button>
<Button onClick={() => setIsAddItemDialogOpen(true)}>{addItemButtonTitle ?? t`Add`}</Button>
<Stack direction="row">
<Button onClick={() => closeDialog()}>{t('global.button.cancel')}</Button>
<Button onClick={() => saveChanges()}>{t('global.button.ok')}</Button>
<Button onClick={() => closeDialog()}>{t`Cancel`}</Button>
<Button onClick={() => saveChanges()}>{t`Ok`}</Button>
</Stack>
</Stack>
</DialogActions>
</Dialog>
{isAddItemDialogOpen && (
<TextSettingDialog
settingName=""

View File

@@ -17,7 +17,6 @@ import Typography from '@mui/material/Typography';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ListItemButton from '@mui/material/ListItemButton';
import * as React from 'react';
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -25,6 +24,7 @@ import Slider from '@mui/material/Slider';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { SxProps, Theme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
type BaseProps = {
settingTitle: string;
@@ -70,7 +70,7 @@ export const NumberSetting = ({
handleLiveUpdate,
listItemTextSx: sx,
}: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -125,7 +125,6 @@ export const NumberSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={cancel}>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogContent>
@@ -172,7 +171,7 @@ export const NumberSetting = ({
value={dialogValue}
type="number"
error={isInvalid}
helperText={isInvalid ? t('global.error.label.invalid_input') : ''}
helperText={isInvalid ? t`Invalid input` : ''}
onChange={(e) => {
const newValue = Number(e.target.value);
updateValue(newValue, false);
@@ -201,14 +200,14 @@ export const NumberSetting = ({
<DialogActions>
{defaultValue !== undefined ? (
<Button onClick={resetToDefault} color="primary">
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
) : null}
<Button onClick={cancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button disabled={isInvalid} onClick={submit} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -18,17 +19,15 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { useLingui } from '@lingui/react/macro';
import { Select } from '@/base/components/inputs/Select.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
export type SelectSettingValueDisplayInfo = {
text: TranslationKey | string;
description?: TranslationKey | string;
disclaimer?: TranslationKey | string;
text: MessageDescriptor | string;
description?: MessageDescriptor | string;
disclaimer?: MessageDescriptor | string;
};
export type SelectSettingValue<Value> = [Value: Value, DisplayInfo: SelectSettingValueDisplayInfo];
@@ -48,7 +47,7 @@ export const SelectSetting = <SettingValue extends string | number>({
handleChange: (value: SettingValue) => void;
disabled?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -82,11 +81,10 @@ export const SelectSetting = <SettingValue extends string | number>({
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={settingName}
secondary={valueDisplayText ? t(valueDisplayText as TranslationKey) : t('global.label.loading')}
secondary={valueDisplayText ? t(valueDisplayText as MessageDescriptor) : t`Loading`}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -102,7 +100,7 @@ export const SelectSetting = <SettingValue extends string | number>({
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.description as TranslationKey)}
{t(dialogValueDisplayInfo.description as MessageDescriptor)}
</Typography>
)}
{dialogValueDisplayInfo.disclaimer && (
@@ -121,7 +119,7 @@ export const SelectSetting = <SettingValue extends string | number>({
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.disclaimer as TranslationKey)}
{t(dialogValueDisplayInfo.disclaimer as MessageDescriptor)}
</Typography>
</Stack>
)}
@@ -135,7 +133,7 @@ export const SelectSetting = <SettingValue extends string | number>({
>
{values.map(([selectValue, { text: selectText }]) => (
<MenuItem key={selectValue} value={selectValue}>
{t(selectText as TranslationKey)}
{t(selectText as MessageDescriptor)}
</MenuItem>
))}
</Select>
@@ -143,10 +141,10 @@ export const SelectSetting = <SettingValue extends string | number>({
</DialogContent>
<DialogActions>
<Button onClick={() => closeDialog()} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => updateSetting()} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -14,11 +14,11 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';
import { useLingui } from '@lingui/react/macro';
export const TimeSetting = ({
settingName,
@@ -31,7 +31,7 @@ export const TimeSetting = ({
defaultValue: string;
handleChange: (path: string) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -82,7 +82,6 @@ export const TimeSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -105,11 +104,11 @@ export const TimeSetting = ({
}}
color="primary"
>
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
) : null}
<Button onClick={closeDialogWithReset} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button
onClick={() => {
@@ -117,7 +116,7 @@ export const TimeSetting = ({
}}
color="primary"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -14,7 +14,7 @@ import DialogContentText from '@mui/material/DialogContentText';
import TextField from '@mui/material/TextField';
import DialogActions from '@mui/material/DialogActions';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
export type TextSettingDialogProps = {
@@ -42,7 +42,7 @@ export const TextSettingDialog = ({
setIsDialogOpen,
validate = () => true,
}: TextSettingDialogProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [dialogValue, setDialogValue] = useState(value ?? '');
const [isValidValue, setIsValidValue] = useState(true);
@@ -89,7 +89,7 @@ export const TextSettingDialog = ({
placeholder={placeholder}
value={dialogValue}
error={error}
helperText={error ? t('global.error.label.invalid_input') : ''}
helperText={error ? t`Invalid input` : ''}
onChange={(e) => {
const newValue = e.target.value;
@@ -100,10 +100,10 @@ export const TextSettingDialog = ({
</DialogContent>
<DialogActions>
<Button onClick={() => closeDialog()} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => updateSetting()} disabled={!isValidValue} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -8,28 +8,20 @@
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Trans, useTranslation } from 'react-i18next';
import { TranslationKey } from '@/base/Base.types.ts';
interface SuperscriptProps {
superscript: string;
text: string;
}
/**
* Expects a translation key of format "{{value}}<0>superscript</0>"
* @param i18nKey
* @param value
* @constructor
* Displays a text with a superscript portion.
*/
export const Superscript = ({ i18nKey, value }: { i18nKey: TranslationKey; value: string }) => {
const { t } = useTranslation();
return (
<Stack sx={{ flexDirection: 'row', gap: 0.25 }}>
<Trans
t={t}
// the type of "key" causes tsc error: "TS2590: Expression produces a union type that is too complex to represent"
i18nKey={i18nKey as any}
values={{ value }}
components={[<Typography variant="caption" sx={{ fontSize: 'x-small', opacity: 0.75 }} />]}
/>
</Stack>
);
};
export const Superscript = ({ superscript, text }: SuperscriptProps) => (
<Stack sx={{ flexDirection: 'row', gap: 0.25 }}>
{text}
<Typography variant="caption" sx={{ fontSize: 'x-small', opacity: 0.75 }}>
{superscript}
</Typography>
</Stack>
);

View File

@@ -12,6 +12,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { SnackbarProvider } from 'notistack';
import { I18nProvider } from '@lingui/react';
import { ActiveDeviceContextProvider } from '@/features/device/DeviceContext.tsx';
import { AppHotkeysProvider } from '@/features/hotkeys/AppHotkeysProvider.tsx';
import { SnackbarWithDescription } from '@/base/components/feedback/SnackbarWithDescription.tsx';
@@ -19,6 +20,7 @@ import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryCon
import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx';
import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx';
import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts';
import { i18n } from '@/i18n';
interface Props {
children: React.ReactNode;
@@ -27,27 +29,29 @@ interface Props {
export const AppContext: React.FC<Props> = ({ children }) => (
<Router basename={SubpathUtil.getRouterBasename()}>
<StyledEngineProvider injectFirst>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
<I18nProvider i18n={i18n}>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
</I18nProvider>
</StyledEngineProvider>
</Router>
);

View File

@@ -7,7 +7,7 @@
*/
import dayjs, { Dayjs } from 'dayjs';
import { t } from 'i18next';
import { t } from '@lingui/core/macro';
export const timeFormatter = new Intl.DateTimeFormat(navigator.language, { hour: '2-digit', minute: '2-digit' });
export const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
@@ -53,18 +53,18 @@ export const getDateString = (date: Dayjs | number, withTime: boolean = false) =
if (actualDate.isToday()) {
if (withTime) {
return t('global.date.label.today_at', { timeString });
return t`Today at ${timeString}`;
}
return t('global.date.label.today');
return t`Today`;
}
if (actualDate.isYesterday()) {
if (withTime) {
return t('global.date.label.yesterday_at', { timeString });
return t`Yesterday at ${timeString}`;
}
return t('global.date.label.yesterday');
return t`Yesterday`;
}
return dateFormatter.format(actualDate.toDate());

View File

@@ -6,11 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t } from 'i18next';
import { TranslationKey } from '@/base/Base.types.ts';
import { MessageDescriptor } from '@lingui/core';
import { msg, t } from '@lingui/core/macro';
import { getISOLanguage, getPreferredISOLanguageCodes, LanguageObject } from '@/lib/ISOLanguageUtil.ts';
import { i18n } from '@/i18n';
export enum DefaultLanguage {
ALL = 'all',
OTHER = 'other',
@@ -19,12 +21,12 @@ export enum DefaultLanguage {
LAST_USED_SOURCE = 'last_used_source',
}
const DEFAULT_LANGUAGE_TO_TRANSLATION: Record<DefaultLanguage, TranslationKey> = {
[DefaultLanguage.ALL]: 'extension.language.all',
[DefaultLanguage.OTHER]: 'extension.language.other',
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
[DefaultLanguage.PINNED]: 'global.label.pinned',
[DefaultLanguage.LAST_USED_SOURCE]: 'global.label.last_used',
const DEFAULT_LANGUAGE_TO_TRANSLATION: Record<DefaultLanguage, MessageDescriptor> = {
[DefaultLanguage.ALL]: msg`All`,
[DefaultLanguage.OTHER]: msg`Other`,
[DefaultLanguage.LOCAL_SOURCE]: msg`Other`,
[DefaultLanguage.PINNED]: msg`Pinned`,
[DefaultLanguage.LAST_USED_SOURCE]: msg`Last used`,
};
export function getLanguage(code: string): LanguageObject {
@@ -37,15 +39,15 @@ export function getLanguage(code: string): LanguageObject {
return {
orgCode: code,
isoCode: code,
name: t('global.language.label.language_with_code', { code }),
nativeName: t('global.language.label.language_with_code', { code }),
name: t`Language with code: ${code}`,
nativeName: t`Language with code: ${code}`,
};
}
export function languageCodeToName(code: string): string {
const isCustomLanguage = Object.keys(DEFAULT_LANGUAGE_TO_TRANSLATION).includes(code);
if (isCustomLanguage) {
return t(DEFAULT_LANGUAGE_TO_TRANSLATION[code as DefaultLanguage]);
return i18n._(DEFAULT_LANGUAGE_TO_TRANSLATION[code as DefaultLanguage]);
}
return getLanguage(code).nativeName;

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
import { VersionUpdateInfoDialog } from '@/features/app-updates/components/VersionUpdateInfoDialog.tsx';
@@ -24,7 +24,7 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
const disabledUpdateCheck = () => Promise.resolve();
export const ServerUpdateChecker = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [serverVersion, setServerVersion] = useLocalStorage<string>('serverVersion');
const [open, setOpen] = useState(false);
@@ -98,11 +98,8 @@ export const ServerUpdateChecker = () => {
return (
<VersionUpdateInfoDialog
info={t('global.update.label.info', {
channel: selectedServerChannelInfo.channel,
version: selectedServerChannelInfo.tag,
})}
actionTitle={t('global.button.download')}
info={t`Server version ${selectedServerChannelInfo.tag} (${selectedServerChannelInfo.channel}) available for download`}
actionTitle={t`Download`}
actionUrl={selectedServerChannelInfo.url}
updateCheckerProps={['server', checkForUpdate, selectedServerChannelInfo?.tag]}
/>
@@ -115,20 +112,14 @@ export const ServerUpdateChecker = () => {
return (
<Dialog open={open}>
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
<DialogTitle>{t`Updated version`}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('global.update.label.update_success', {
name: t('settings.server.title.server'),
version,
channel: aboutServer?.buildType,
})}
</DialogContentText>
<DialogContentText>{t`Server was updated to version ${version} (${aboutServer?.buildType})`}</DialogContentText>
</DialogContent>
<DialogActions>
{changelogUrl && (
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
)}
<Button
@@ -138,7 +129,7 @@ export const ServerUpdateChecker = () => {
}}
variant="contained"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -13,8 +13,8 @@ import Typography from '@mui/material/Typography';
import RefreshIcon from '@mui/icons-material/Refresh';
import DownloadIcon from '@mui/icons-material/Download';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { t as translate } from 'i18next';
import DownloadingIcon from '@mui/icons-material/Downloading';
import { t } from '@lingui/core/macro';
import { UpdateState } from '@/lib/graphql/generated/graphql.ts';
export type BaseVersionInfoProps = {
@@ -69,27 +69,27 @@ const getUpdateCheckButtonText = (
) => {
const isUpdating = updateState === UpdateState.Downloading;
if (isUpdating) {
return translate('global.update.label.updating', { progress });
return t`${progress}% | Updating…`;
}
const didUpdateFail = updateState === UpdateState.Error;
if (didUpdateFail) {
return translate('global.update.label.update_failure');
return t`Update failed`;
}
if (isLoading) {
return translate('global.update.label.checking');
return t`Checking for update`;
}
if (error) {
return translate('global.update.label.check_failure');
return t`Could not check for update`;
}
if (isUpdateAvailable) {
return translate('global.update.label.available');
return t`Update available`;
}
return translate('global.update.label.up_to_date');
return t`This is the latest version`;
};
export const VersionInfo = ({

View File

@@ -15,8 +15,8 @@ import DialogContentText from '@mui/material/DialogContentText';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import { useLingui } from '@lingui/react/macro';
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
interface BaseProps {
@@ -48,7 +48,7 @@ export const VersionUpdateInfoDialog = ({
changelogUrl,
disabled,
}: VersionUpdateInfoDialogProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const updateChecker = useUpdateChecker(...updateCheckerProps);
@@ -58,7 +58,7 @@ export const VersionUpdateInfoDialog = ({
return (
<Dialog open>
<DialogTitle>{t('global.update.label.available')}</DialogTitle>
<DialogTitle>{t`Update available`}</DialogTitle>
<DialogContent>
<DialogContentText>{info}</DialogContentText>
</DialogContent>
@@ -72,7 +72,7 @@ export const VersionUpdateInfoDialog = ({
>
{changelogUrl && (
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
)}
<Stack direction="row">
@@ -80,7 +80,7 @@ export const VersionUpdateInfoDialog = ({
{(popupState) => (
<>
<Button disabled={disabled} {...bindTrigger(popupState)}>
{t('global.label.close')}
{t`Close`}
</Button>
<Menu {...bindMenu(popupState)}>
<MenuItem
@@ -89,7 +89,7 @@ export const VersionUpdateInfoDialog = ({
popupState.close();
}}
>
{t('global.button.remind_later')}
{t`Remind later`}
</MenuItem>
<MenuItem
onClick={() => {
@@ -97,7 +97,7 @@ export const VersionUpdateInfoDialog = ({
popupState.close();
}}
>
{t('global.button.ignore')}
{t`Ignore`}
</MenuItem>
</Menu>
</>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
import { useLocalStorage, useSessionStorage } from '@/base/hooks/useStorage.tsx';
@@ -36,7 +36,7 @@ if (BrowserUtil.isActualPageLoad()) {
}
export const WebUIUpdateChecker = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
const [initialLoadTimestamp] = useSessionStorage<number>(INITIAL_LOAD_TIMESTAMP_KEY, Date.now());
@@ -93,7 +93,7 @@ export const WebUIUpdateChecker = () => {
useEffect(() => {
const isError = webUIUpdateState === UpdateState.Error;
if (isError) {
makeToast(t('settings.about.webui.label.update_failure'), 'error');
makeToast(t`Could not update WebUI`, 'error');
}
const updateFinished = webUIUpdateState === UpdateState.Finished;
@@ -139,24 +139,15 @@ export const WebUIUpdateChecker = () => {
return (
<VersionUpdateInfoDialog
info={t('settings.about.webui.label.info', {
version: webUIUpdateData?.checkForWebUIUpdate.tag,
channel: webUIUpdateData?.checkForWebUIUpdate.channel,
})}
info={t`WebUI version ${webUIUpdateData?.checkForWebUIUpdate.tag} (${webUIUpdateData?.checkForWebUIUpdate.channel}) available for download`}
changelogUrl={changelogUrl}
disabled={isUpdateInProgress}
onAction={() =>
requestManager
.updateWebUI()
.response.catch((e) =>
makeToast(t('settings.about.webui.label.update_failure'), 'error', getErrorMessage(e)),
)
}
actionTitle={
isUpdateInProgress
? t('global.update.label.updating', { progress: updateStatus.progress })
: t('extension.action.label.update')
.response.catch((e) => makeToast(t`Could not update WebUI`, 'error', getErrorMessage(e)))
}
actionTitle={isUpdateInProgress ? t`${updateStatus.progress}% | Updating…` : t`Update`}
updateCheckerProps={[
'webUI',
isAutoUpdateEnabled ? disabledUpdateCheck : checkForUpdate,
@@ -172,19 +163,13 @@ export const WebUIUpdateChecker = () => {
return (
<Dialog open={open} onClose={shouldForceRefresh ? () => setOpen(false) : noOp}>
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
<DialogTitle>{t`Updated version`}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('global.update.label.update_success', {
name: t('settings.webui.title.webui'),
version: newVersion,
channel: aboutWebUI?.channel,
})}
</DialogContentText>
<DialogContentText>{t`{name} was updated to version ${newVersion} (${aboutWebUI?.channel})`}</DialogContentText>
</DialogContent>
<DialogActions>
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
<Button
onClick={() => {
@@ -197,7 +182,7 @@ export const WebUIUpdateChecker = () => {
}}
variant="contained"
>
{t(shouldForceRefresh ? 'global.button.refresh' : 'global.button.ok')}
{shouldForceRefresh ? t`Refresh` : t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
@@ -14,6 +13,7 @@ import Button from '@mui/material/Button';
import { Navigate, useNavigate } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { StringParam, useQueryParam } from 'use-query-params';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -27,7 +27,7 @@ import { ServerAddressSetting } from '@/features/settings/components/ServerAddre
export const LoginPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const { t } = useLingui();
const { setOverride } = useNavBarContext();
const navigate = useNavigate();
const isAuthenticated = AuthManager.useIsAuthenticated();
@@ -48,7 +48,7 @@ export const LoginPage = () => {
navigate(redirect ?? AppRoutes.root.path);
}
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: 'Suwayomi' }), 'error', getErrorMessage(e));
makeToast(t`Could not log in to Suwayomi`, 'error', getErrorMessage(e));
}
};
@@ -113,7 +113,7 @@ export const LoginPage = () => {
margin="dense"
id="username"
name="username"
label={t('global.label.username')}
label={t`Username`}
type="text"
fullWidth
variant="standard"
@@ -127,7 +127,7 @@ export const LoginPage = () => {
/>
</Stack>
<Button disabled={isLoading || (!username && !password)} variant="contained" onClick={doLogin}>
{t('global.button.log_in')}
{t`Log in`}
</Button>
<Stack sx={{ position: 'absolute', left: 0, bottom: 0 }}>
<ServerAddressSetting />

View File

@@ -6,22 +6,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { BackupFlag, BackupFlagGroup } from '@/features/backup/Backup.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, TranslationKey> = {
includeManga: 'settings.backup.flag.manga',
includeCategories: 'settings.backup.flag.categories',
includeChapters: 'settings.backup.flag.chapters',
includeClientData: 'settings.backup.flag.client_data',
includeHistory: 'settings.backup.flag.history',
includeServerSettings: 'settings.backup.flag.server_settings',
includeTracking: 'settings.backup.flag.tracking',
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, MessageDescriptor> = {
includeManga: msg`Library entries`,
includeCategories: msg`Categories`,
includeChapters: msg`Chapters`,
includeClientData: msg`Client data`,
includeHistory: msg`History`,
includeServerSettings: msg`Server settings`,
includeTracking: msg`Tracking`,
};
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, TranslationKey> = {
[BackupFlagGroup.LIBRARY]: 'settings.backup.flag.group.library',
[BackupFlagGroup.SETTINGS]: 'settings.backup.flag.group.settings',
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, MessageDescriptor> = {
[BackupFlagGroup.LIBRARY]: msg`Library`,
[BackupFlagGroup.SETTINGS]: msg`Settings`,
};
export const BACKUP_FLAGS = Object.keys(BACKUP_FLAGS_TO_TRANSLATION) as readonly BackupFlag[];

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { plural, t } from '@lingui/core/macro';
import { AutoBackupFlagInclusionState, BackupFlag, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
import { BACKUP_FLAGS_TO_TRANSLATION } from '@/features/backup/Backup.constants.ts';
@@ -32,11 +32,11 @@ export const convertToBackupFlags = (flags: AutoBackupFlagInclusionState): Backu
const getIncludeExcludeText = (count: number, allCount: number, specificText: string): string => {
if (count === 0) {
return translate('global.label.none');
return t`None`;
}
if (count === allCount) {
return translate('extension.language.all');
return t`All`;
}
return specificText;
@@ -49,9 +49,9 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => value.toString());
const includedFlagsString =
flagsByState.true?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
flagsByState.true?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
const excludedFlagsString =
flagsByState.false?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
flagsByState.false?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
return {
false: getIncludeExcludeText(flagsByState.false?.length ?? 0, totalFlags, excludedFlagsString),
@@ -61,8 +61,11 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
export const getBackupCleanupDisplayValue = (ttl: number): string => {
if (ttl === 0) {
return translate('global.label.never');
return t`Never`;
}
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
return plural(ttl, {
one: `Delete backups that are older than # day`,
other: `Delete backups that are older than # days`,
});
};

View File

@@ -11,12 +11,12 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import {
BACKUP_FLAG_GROUP_TO_TRANSLATION,
@@ -34,7 +34,7 @@ export const BackupFlagInclusionDialog = ({
title,
flags,
}: AwaitableComponentProps<BackupFlagInclusionState> & { title: string; flags?: BackupFlagInclusionState }) => {
const { t } = useTranslation();
const { t } = useLingui();
const [includeStateByFlag, setIncludeStateByFlag] = useState(
Object.fromEntries(BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? true])) as BackupFlagInclusionState,
@@ -67,10 +67,10 @@ export const BackupFlagInclusionDialog = ({
</DialogContent>
<DialogActions>
<Button autoFocus onClick={onDismiss} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => onSubmit(includeStateByFlag)} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -7,7 +7,6 @@
*/
import List from '@mui/material/List';
import { useTranslation } from 'react-i18next';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
@@ -17,6 +16,7 @@ import ListItem from '@mui/material/ListItem';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { BrowseTab } from '@/features/browse/Browse.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { ValidateBackupResult } from '@/lib/graphql/generated/graphql.ts';
@@ -28,16 +28,16 @@ export const BackupValidationDialog = ({
isVisible,
onExitComplete,
}: AwaitableComponentProps & { validationResult: ValidateBackupResult }) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Dialog open={isVisible} onTransitionExited={onExitComplete} onClose={onDismiss}>
<DialogTitle>{t('settings.backup.action.validate.dialog.title')}</DialogTitle>
<DialogTitle>{t`Backup validation`}</DialogTitle>
<DialogContent dividers>
{!!validationResult?.missingSources.length && (
<List
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
subheader={t('settings.backup.action.validate.dialog.content.label.missing_sources')}
subheader={t`The following sources are not installed:`}
>
{validationResult?.missingSources.map(({ id, name }) => (
<ListItem sx={{ display: 'list-item' }} key={id}>
@@ -49,7 +49,7 @@ export const BackupValidationDialog = ({
{!!validationResult?.missingTrackers.length && (
<List
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
subheader={t('settings.backup.action.validate.dialog.content.label.missing_trackers')}
subheader={t`The following trackers are not logged in:`}
>
{validationResult?.missingTrackers.map(({ name }) => (
<ListItem sx={{ display: 'list-item' }} key={name}>
@@ -75,7 +75,7 @@ export const BackupValidationDialog = ({
autoFocus={!!validationResult?.missingSources.length}
variant={validationResult?.missingSources.length ? 'contained' : 'text'}
>
{t('extension.action.label.install')}
{t`Install`}
</Button>
)}
{!!validationResult?.missingTrackers.length && (
@@ -86,11 +86,11 @@ export const BackupValidationDialog = ({
autoFocus={!!validationResult?.missingTrackers.length}
variant={validationResult?.missingTrackers.length ? 'contained' : 'text'}
>
{t('global.button.log_in')}
{t`Log in`}
</Button>
)}
<Stack direction="row">
<Button onClick={onDismiss}>{t('global.button.cancel')}</Button>
<Button onClick={onDismiss}>{t`Cancel`}</Button>
<Button
onClick={onSubmit}
autoFocus={
@@ -102,7 +102,7 @@ export const BackupValidationDialog = ({
: 'text'
}
>
{t('global.button.restore')}
{t`Restore`}
</Button>
</Stack>
</Stack>

View File

@@ -10,12 +10,13 @@ import { useEffect, useRef, useState } from 'react';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import { fromEvent } from 'file-selector';
import { useTranslation } from 'react-i18next';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListSubheader from '@mui/material/ListSubheader';
import { useEventListener, useMergedRef, useWindowEvent } from '@mantine/hooks';
import { AwaitableComponent } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
@@ -41,9 +42,9 @@ import { BackupSettingsType } from '@/features/backup/Backup.types.ts';
let backupRestoreId: string | undefined;
export function Backup() {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.backup.title'));
useAppTitle(t`Backup`);
const {
data: settingsData,
@@ -76,7 +77,7 @@ export function Backup() {
value: BackupSettingsType[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -84,7 +85,7 @@ export function Backup() {
settings: Record<Setting, BackupSettingsType[Setting]>,
) => {
mutateSettings({ variables: { input: { settings } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -99,11 +100,11 @@ export function Backup() {
const isRestoreFinished = isSuccess || isFailure;
if (isRestoreFinished) {
if (isSuccess) {
makeToast(t('settings.backup.action.restore.label.success'), 'success');
makeToast(t`Backup restored.`, 'success');
}
if (isFailure) {
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error');
makeToast(t`Could not restore backup`, 'error');
}
requestManager.reset();
@@ -121,21 +122,17 @@ export function Backup() {
const createBackup = async () => {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.action.create.label.title'),
title: t`Create backup`,
});
makeToast(t('settings.backup.action.create.label.in_progress'), 'info');
makeToast(t`Creating backup`, 'info');
try {
const backupFileResponse = await requestManager.createBackupFile({ flags }).response;
const backupFileUrl = backupFileResponse.data?.createBackup.url;
if (!backupFileUrl) {
makeToast(
t('settings.backup.action.create.error.failure'),
'error',
getErrorMessage(backupFileResponse.errors),
);
makeToast(t`Could not create backup`, 'error', getErrorMessage(backupFileResponse.errors));
return;
}
@@ -146,7 +143,7 @@ export function Backup() {
link.click();
document.body.removeChild(link);
} catch (e) {
makeToast(t('settings.backup.action.create.error.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not create backup`, 'error', getErrorMessage(e));
}
};
@@ -172,7 +169,7 @@ export function Backup() {
return true;
} catch (e) {
makeToast(t('settings.backup.action.validate.error.label.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not validate backup`, 'error', getErrorMessage(e));
} finally {
resetBackupState();
}
@@ -182,17 +179,17 @@ export function Backup() {
const restoreBackup = async (backup: File) => {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.action.restore.label.title'),
title: t`Restore Backup`,
});
try {
makeToast(t('settings.backup.action.restore.label.in_progress'), 'info');
makeToast(t`Restoring backup`, 'info');
const response = await requestManager.restoreBackupFile({ backup, flags }).response;
backupRestoreId = response.data?.restoreBackup.id;
setTriggerReRender(Date.now());
} catch (e) {
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not restore backup`, 'error', getErrorMessage(e));
} finally {
resetBackupState();
}
@@ -200,13 +197,13 @@ export function Backup() {
const submitBackup = async (file: File) => {
if (file.name.toLowerCase().endsWith('json')) {
makeToast(t('settings.backup.action.restore.error.label.legacy_backup_unsupported'), 'error');
makeToast(t`legacy backups are not supported!`, 'error');
return;
}
const isValidFilename = file.name.toLowerCase().match(/proto\.gz$|tachibk$/g);
if (!isValidFilename) {
makeToast(t('global.error.label.invalid_file_type'), 'error');
makeToast(t`Invalid filetype`, 'error');
return;
}
@@ -238,7 +235,7 @@ export function Backup() {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('Backup::refetch'))}
/>
@@ -247,19 +244,20 @@ export function Backup() {
const backupSettings = settingsData!.settings;
const autoBackupFlagsInfo = getAutoBackupFlagsInfo(backupSettings);
const includedCategoriesText = autoBackupFlagsInfo.true;
const excludedCategoriesText = autoBackupFlagsInfo.false;
return (
<>
<List sx={{ padding: 0 }}>
<ListItemButton onClick={createBackup}>
<ListItemText
primary={t('settings.backup.action.create.label.title')}
secondary={t('settings.backup.action.create.label.description')}
/>
<ListItemText primary={t`Create backup`} secondary={t`Back up library as a Tachiyomi backup`} />
</ListItemButton>
<ListItemButton onClick={() => inputRef.current?.click()} disabled={!!backupRestoreId}>
<ListItemText
primary={t('settings.backup.action.restore.label.title')}
secondary={t('settings.backup.action.restore.label.description')}
primary={t`Restore Backup`}
secondary={t`You can also drag and drop the backup file here to restore it`}
/>
{backupRestoreId ? (
<ListItemIcon>
@@ -275,19 +273,17 @@ export function Backup() {
}
>
<TextSetting
settingName={t('settings.backup.automated.location.label.title')}
dialogDescription={t('settings.backup.automated.location.label.description')}
settingName={t`Backup location`}
dialogDescription={t`The path to the directory on the server where automated backups should get saved in`}
value={backupSettings.backupPath}
settingDescription={
backupSettings.backupPath.length ? backupSettings.backupPath : t('global.label.default')
}
settingDescription={backupSettings.backupPath.length ? backupSettings.backupPath : t`Default`}
handleChange={(path) => updateSetting('backupPath', path)}
/>
<ListItemButton
onClick={async () => {
try {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.automated.flags.title'),
title: t`Backup data`,
flags: convertToBackupFlags(backupSettings),
});
@@ -298,54 +294,46 @@ export function Backup() {
}}
>
<ListItemText
primary={t('settings.backup.automated.flags.title')}
primary={t`Backup data`}
secondary={
<>
<span>
{t('category.settings.inclusion.label.include', {
includedCategoriesText: getAutoBackupFlagsInfo(backupSettings).true,
})}
</span>
<span>
{t('category.settings.inclusion.label.exclude', {
excludedCategoriesText: getAutoBackupFlagsInfo(backupSettings).false,
})}
</span>
<span>{t`Include: ${includedCategoriesText}`}</span>
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<TimeSetting
settingName={t('settings.backup.automated.label.time')}
settingName={t`Backup time`}
value={backupSettings.backupTime}
defaultValue="00:00"
handleChange={(time: string) => updateSetting('backupTime', time)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.label.interval')}
settingValue={t('global.date.value.label.day', {
days: backupSettings.backupInterval,
count: backupSettings.backupInterval,
settingTitle={t`Backup interval`}
settingValue={plural(backupSettings.backupInterval, {
one: '# day',
other: '# days',
})}
value={backupSettings.backupInterval}
defaultValue={1}
minValue={1}
maxValue={31}
stepSize={1}
valueUnit={t('global.date.label.day_one')}
valueUnit={t`Day`}
showSlider
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.cleanup.label.title')}
settingTitle={t`Backup cleanup`}
settingValue={getBackupCleanupDisplayValue(backupSettings.backupTTL)}
value={backupSettings.backupTTL}
defaultValue={14}
minValue={0}
maxValue={1000}
stepSize={1}
valueUnit={t('global.date.label.day_one')}
valueUnit={t`Day`}
showSlider
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
/>

View File

@@ -14,9 +14,9 @@ import { StringParam, useQueryParam } from 'use-query-params';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useWindowEvent } from '@mantine/hooks';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
@@ -42,7 +42,7 @@ import {
ExtensionState,
TExtension,
} from '@/features/extension/Extensions.types.ts';
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import {
@@ -52,6 +52,7 @@ import {
import { MetadataBrowseSettings } from '@/features/browse/Browse.types.ts';
import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts';
import { SearchParam } from '@/base/Base.types.ts';
import { i18n } from '@/i18n';
const LANGUAGE = 0;
const EXTENSIONS = 1;
@@ -73,7 +74,7 @@ const GroupHeader = ({
setUpdatingExtensionIds: (ids: TExtension['pkgName'][]) => void;
handleExtensionUpdate: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<StyledGroupHeader
@@ -96,8 +97,10 @@ const GroupHeader = ({
.response.then(() => handleExtensionUpdate())
.catch((e) =>
makeToast(
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[ExtensionAction.UPDATE], {
count: groupExtensionIds.length,
/* lingui-extract-ignore */
i18n.t({
...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[ExtensionAction.UPDATE],
values: { count: groupExtensionIds.length },
}),
'error',
getErrorMessage(e),
@@ -106,7 +109,7 @@ const GroupHeader = ({
.finally(() => setUpdatingExtensionIds([]));
}}
>
{t('extension.action.label.update_all')}
{t`Update all`}
</Button>
)}
</StyledGroupHeader>
@@ -114,7 +117,7 @@ const GroupHeader = ({
};
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useTranslation();
const { t } = useLingui();
const {
data: serverSettingsData,
@@ -130,7 +133,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<
keyof Pick<MetadataBrowseSettings, 'extensionLanguages'>
>((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
>((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
@@ -170,18 +173,18 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const submitExternalExtension = (file: File) => {
if (!file.name.toLowerCase().endsWith('apk')) {
makeToast(t('global.error.label.invalid_file_type'), 'error');
makeToast(t`Invalid filetype`, 'error');
return;
}
makeToast(t('extension.label.installing_file'), 'info');
makeToast(t`Installing extension file…`, 'info');
requestManager
.installExternalExtension(file)
.response.then(() => {
handleExtensionUpdate();
makeToast(t('extension.label.installed_successfully'), 'success');
makeToast(t`Extension installed`, 'success');
})
.catch((e) => makeToast(t('extension.label.installation_failed'), 'error', getErrorMessage(e)));
.catch((e) => makeToast(t`Could not install the extension`, 'error', getErrorMessage(e)));
};
useEffect(() => {
@@ -191,7 +194,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
useAppAction(
<>
<AppbarSearch />
<CustomTooltip title={t('extension.action.label.install_external')}>
<CustomTooltip title={t`Install external extension`}>
<IconButton
onClick={() => {
const input = document.createElement('input');
@@ -241,7 +244,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (serverSettingsError) {
@@ -267,9 +270,9 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
paddingTop: '20px',
}}
>
<Typography>{t('extension.label.add_repository_info')}</Typography>
<Typography>{t`You have to add a extension repository to be able to install extensions`}</Typography>
<Button component={Link} variant="contained" to={AppRoutes.settings.childRoutes.browse.path}>
{t('settings.title')}
{t`Settings`}
</Button>
</Stack>
);

View File

@@ -10,10 +10,10 @@ import { useEffect, useState } from 'react';
import Card from '@mui/material/Card';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import Stack from '@mui/material/Stack';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import {
@@ -26,7 +26,7 @@ import {
import {
EXTENSION_ACTION_TO_NEXT_ACTION_MAP,
EXTENSION_ACTION_TO_STATE_MAP,
INSTALLED_STATE_TO_TRANSLATION_KEY_MAP,
INSTALLED_STATE_TO_TRANSLATION_MAP,
} from '@/features/extension/Extensions.constants.ts';
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
@@ -45,7 +45,7 @@ interface IProps {
}
export function ExtensionCard(props: IProps) {
const { t } = useTranslation();
const { t } = useLingui();
const {
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
@@ -134,7 +134,7 @@ export function ExtensionCard(props: IProps) {
{showSourceRepo && <Typography variant="caption">{repo}</Typography>}
</Stack>
{isInstalled && (
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton color="inherit" {...MUIUtil.preventRippleProp()}>
<SettingsIcon />
</IconButton>
@@ -151,7 +151,7 @@ export function ExtensionCard(props: IProps) {
handleButtonClick();
}}
>
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
</Button>
</ListCardContent>
</OptionalCardActionAreaLink>

View File

@@ -8,8 +8,8 @@
import { useCallback, useRef, useState } from 'react';
import Tab from '@mui/material/Tab';
import { useTranslation } from 'react-i18next';
import { StringParam, useQueryParam } from 'use-query-params';
import { useLingui } from '@lingui/react/macro';
import { Sources } from '@/features/browse/sources/Sources.tsx';
import { Extensions } from '@/features/browse/extensions/Extensions.tsx';
import { TabPanel } from '@/base/components/tabs/TabPanel.tsx';
@@ -23,8 +23,8 @@ import { GROUPED_VIRTUOSO_Z_INDEX } from '@/lib/virtuoso/Virtuoso.constants.ts';
import { SearchParam } from '@/base/Base.types.ts';
export function Browse() {
const { t } = useTranslation();
useAppTitle(t('global.label.browse'));
const { t } = useLingui();
useAppTitle(t`Browse`);
const tabsMenuRef = useRef<HTMLDivElement | null>(null);
const [tabsMenuHeight, setTabsMenuHeight] = useState(0);
@@ -49,9 +49,9 @@ export function Browse() {
value={tabName}
onChange={(_, newTab) => setTabSearchParam(newTab, 'replaceIn')}
>
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t('source.title_other')} />
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t('extension.title_other')} />
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t('migrate.title')} />
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t`Source`} />
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t`Extension`} />
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t`Migrate`} />
</TabsMenu>
<TabPanel index={BrowseTab.SOURCE_DEPRECATED} currentIndex={tabName}>
<Sources tabsMenuHeight={tabsMenuHeight} />

View File

@@ -6,11 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Trans, useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { Trans, useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { MutableListSetting } from '@/base/components/settings/MutableListSetting.tsx';
@@ -31,9 +32,9 @@ import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
type ExtensionsSettings = Pick<GqlServerSettings, 'maxSourcesInParallel' | 'localSourcePath' | 'extensionRepos'>;
export const BrowseSettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('global.label.browse'));
useAppTitle(t`Browse`);
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
notifyOnNetworkStatusChange: true,
@@ -45,7 +46,7 @@ export const BrowseSettings = () => {
value: ExtensionsSettings[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -53,7 +54,7 @@ export const BrowseSettings = () => {
settings: { hideLibraryEntries, showNsfw },
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataBrowseSettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
if (loading) {
@@ -63,7 +64,7 @@ export const BrowseSettings = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('BrowseSettings::refetch'))}
/>
@@ -75,7 +76,7 @@ export const BrowseSettings = () => {
return (
<List sx={{ pt: 0 }}>
<ListItem>
<ListItemText primary={t('settings.label.hide_library_entries')} />
<ListItemText primary={t`Hide entries already in library`} />
<Switch
edge="end"
checked={hideLibraryEntries}
@@ -83,10 +84,7 @@ export const BrowseSettings = () => {
/>
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.label.show_nsfw')}
secondary={t('settings.label.show_nsfw_description')}
/>
<ListItemText primary={t`Show NSFW`} secondary={t`Hide NSFW extensions and sources`} />
<Switch
edge="end"
checked={showNsfw}
@@ -94,12 +92,12 @@ export const BrowseSettings = () => {
/>
</ListItem>
<NumberSetting
settingTitle={t('settings.server.requests.sources.parallel.label.title')}
settingValue={t('settings.server.requests.sources.parallel.label.value', {
value: serverSettings.maxSourcesInParallel,
count: serverSettings.maxSourcesInParallel,
settingTitle={t`Parallel source requests`}
settingValue={plural(serverSettings.maxSourcesInParallel, {
one: '# Source',
other: '# Sources',
})}
valueUnit={t('source.title_one')}
valueUnit={t`Source`}
value={serverSettings.maxSourcesInParallel}
defaultValue={6}
minValue={1}
@@ -109,10 +107,10 @@ export const BrowseSettings = () => {
handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)}
/>
<MutableListSetting
settingName={t('extension.settings.repositories.custom.label.title')}
description={t('extension.settings.repositories.custom.label.description')}
settingName={t`Extension repositories`}
description={t`Add repositories from which extensions can be installed`}
dialogDisclaimer={
<Trans i18nKey="extension.settings.repositories.custom.label.disclaimer">
<Trans>
<strong>Suwayomi does not provide any support for 3rd party repositories or extensions!</strong>
<br />
Use with caution as there could be malicious actors making those repositories.
@@ -125,22 +123,20 @@ export const BrowseSettings = () => {
requestManager.clearExtensionCache();
}}
valueInfos={serverSettings.extensionRepos.map((extensionRepo) => [extensionRepo])}
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
addItemButtonTitle={t`Add repository`}
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
validateItem={(repo) =>
!!repo.match(
/https:\/\/(www\.|raw\.)?(github|githubusercontent)\.com\/([^/]+)\/([^/]+)((\/tree|\/blob)?\/([^/\n]*))?(\/([^/\n]*\.json)?)?/g,
)
}
invalidItemError={t('extension.settings.repositories.custom.error.label.invalid_url')}
invalidItemError={t`Invalid repository url`}
/>
<TextSetting
settingName={t('settings.server.local_source.path.label.title')}
dialogDescription={t('settings.server.local_source.path.label.description')}
settingName={t`Local source location`}
dialogDescription={t`The path to the directory on the server where local source files are saved in`}
value={serverSettings.localSourcePath}
settingDescription={
serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t('global.label.default')
}
settingDescription={serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t`Default`}
handleChange={(path) => updateSetting('localSourcePath', path)}
/>
</List>

View File

@@ -10,8 +10,8 @@ import { useCallback, useMemo } from 'react';
import IconButton from '@mui/material/IconButton';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DefaultLanguage } from '@/base/utils/Languages.ts';
@@ -32,7 +32,7 @@ import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupIt
import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx';
export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useTranslation();
const { t } = useLingui();
const { languages: shownLangs, setLanguages: setShownLangs } = SourceService.useLanguages();
const {
@@ -97,7 +97,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
useAppAction(
<>
<CustomTooltip title={t('search.title.global_search')}>
<CustomTooltip title={t`Global Search`}>
<IconButton onClick={() => navigate(AppRoutes.sources.childRoutes.searchAll.path())} color="inherit">
<TravelExploreIcon />
</IconButton>
@@ -117,7 +117,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('Sources::refetch'))}
/>
@@ -125,7 +125,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
}
if (sources?.length === 0) {
return <EmptyViewAbsoluteCentered message={t('source.error.label.no_sources_found')} />;
return <EmptyViewAbsoluteCentered message={t`No sources found. Install Some extensions first.`} />;
}
return (

View File

@@ -11,12 +11,12 @@ import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import Typography from '@mui/material/Typography';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
import IconButton from '@mui/material/IconButton';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SourceContentType } from '@/features/source/browse/screens/SourceMangas.tsx';
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
@@ -38,7 +38,7 @@ interface IProps {
}
export const SourceCard: React.FC<IProps> = (props: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const { source, showSourceRepo, showLanguage } = props;
const {
@@ -53,10 +53,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
const { isPinned } = useGetSourceMetadata(source);
const sourceName = Sources.isLocalSource(source) ? t('source.local_source.title') : name;
const sourceName = Sources.isLocalSource(source) ? t`Local source` : name;
const updateSetting = createUpdateSourceMetadata(source, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
return (
@@ -105,10 +105,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
to={AppRoutes.sources.childRoutes.browse.path(id)}
state={{ contentType: SourceContentType.LATEST, clearCache: true }}
>
{t('global.button.latest')}
{t`Latest`}
</Button>
)}
<CustomTooltip title={t(isPinned ? 'source.pin.remove' : 'source.pin.add')}>
<CustomTooltip title={isPinned ? t`Unpin source` : t`Pin source`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
@@ -16,7 +15,8 @@ import DialogTitle from '@mui/material/DialogTitle';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { t as translate } from 'i18next';
import { useLingui } from '@lingui/react/macro';
import { t as translate } from '@lingui/core/macro';
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
@@ -68,11 +68,11 @@ const getCategoryUpdateInfo = (
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
const includesAllCategories = categories.length === allCategories;
if (noSpecificallyIncludedCategories || includesAllCategories) {
return translate('extension.language.all');
return translate`All`;
}
if (!categories.length) {
return translate('global.label.none');
return translate`None`;
}
return categories.map((category) => category.name).join(', ');
@@ -91,7 +91,7 @@ export const CategoriesInclusionSetting = ({
includeField,
dialogText,
}: CategoriesInclusionSettingProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [dialogCategories, setDialogCategories] = useState(categories);
const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -141,7 +141,7 @@ export const CategoriesInclusionSetting = ({
// TODO - update cache immediately
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
} catch (e) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
// mutate(categoriesEndpoint, [...categories]);
}
};
@@ -155,27 +155,18 @@ export const CategoriesInclusionSetting = ({
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('category.title.category_other')}
primary={t`Category`}
secondary={
<>
<span>
{t('category.settings.inclusion.label.include', {
includedCategoriesText,
})}
</span>
<span>
{t('category.settings.inclusion.label.exclude', {
excludedCategoriesText,
})}
</span>
<span>{t`Include: ${includedCategoriesText}`}</span>
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{t('category.title.category_other')}</DialogTitle>
<DialogTitle>{t`Category`}</DialogTitle>
<DialogContent>
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
<CheckboxContainer>
@@ -207,10 +198,10 @@ export const CategoriesInclusionSetting = ({
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={updateCategories} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -13,10 +13,10 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
@@ -90,7 +90,7 @@ const getCategoryCheckedState = (
};
export function CategorySelect(props: CategorySelectProps) {
const { t } = useTranslation();
const { t } = useLingui();
const {
onDismiss,
@@ -161,7 +161,7 @@ export function CategorySelect(props: CategorySelectProps) {
if (doNotShowAddToLibraryDialogAgain) {
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) =>
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
);
}
@@ -196,10 +196,10 @@ export function CategorySelect(props: CategorySelectProps) {
onTransitionExited={onExitComplete}
onClose={handleCancel}
>
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
<DialogTitle>{t`Set categories`}</DialogTitle>
<DialogContent dividers>
<FormGroup>
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
{allCategories.length === 0 && <span>{t`You don't have any categories yet.`}</span>}
{allCategories.map((category) => (
<ThreeStateCheckboxInput
checked={getCategoryCheckedState(
@@ -232,7 +232,7 @@ export function CategorySelect(props: CategorySelectProps) {
<CheckboxInput
sx={{ margin: 0 }}
size="small"
label={t('global.button.dont_show_dialog_again')}
label={t`Don't show this dialog again`}
onChange={(e) => setDoNotShowAddToLibraryDialogAgain(e.target.checked)}
/>
)}
@@ -249,15 +249,15 @@ export function CategorySelect(props: CategorySelectProps) {
to={AppRoutes.settings.childRoutes.categories.path}
onClick={onDismiss}
>
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
{allCategories.length ? t`Edit` : t`Create`}
</Button>
<Stack direction="row">
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
{!!allCategories.length && (
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
)}
</Stack>

View File

@@ -10,11 +10,11 @@ import IconButton from '@mui/material/IconButton';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CategoryType } from '@/lib/graphql/generated/graphql.ts';
@@ -27,7 +27,7 @@ export const CategorySettingsCard = ({
category: Pick<CategoryType, 'id' | 'name'>;
onEdit: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const deleteCategory = () => {
requestManager.deleteCategory(category.id);
@@ -42,12 +42,12 @@ export const CategorySettingsCard = ({
{category.name}
</Typography>
<Stack sx={{ flexDirection: 'row' }}>
<CustomTooltip title={t('global.button.edit')}>
<CustomTooltip title={t`Edit`}>
<IconButton component={Box} onClick={onEdit}>
<EditIcon />
</IconButton>
</CustomTooltip>
<CustomTooltip title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip title={t`Delete`}>
<IconButton component={Box} onClick={deleteCategory}>
<DeleteIcon />
</IconButton>

View File

@@ -15,7 +15,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CategoryDefaultInfo, CategoryIdInfo, CategoryNameInfo } from '@/features/category/Category.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
@@ -31,7 +31,7 @@ export const CreateOrEditCategoryDialog = ({
}) => {
const isEditMode = !!category;
const { t } = useTranslation();
const { t } = useLingui();
const [dialogName, setDialogName] = useState(category?.name);
const [dialogDefault, setDialogDefault] = useState(!!category?.default);
@@ -47,47 +47,43 @@ export const CreateOrEditCategoryDialog = ({
if (isEditMode) {
requestManager
.updateCategory(category.id, { name: dialogName, default: dialogDefault })
.response.catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
return;
}
requestManager
.createCategory({ name: dialogName, default: dialogDefault })
.response.catch((e) => makeToast(t('category.error.label.create_failure'), 'error', getErrorMessage(e)));
.response.catch((e) => makeToast(t`Could not create category`, 'error', getErrorMessage(e)));
};
return (
<Dialog open onClose={onClose}>
<DialogTitle id="form-dialog-title">
{isEditMode ? t('category.dialog.title.edit_category_one') : t('category.dialog.title.new_category')}
</DialogTitle>
<DialogTitle id="form-dialog-title">{isEditMode ? t`Edit category` : t`New category`}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label={t('category.label.category_name')}
label={t`Category Name`}
type="text"
fullWidth
value={dialogName}
onChange={(e) => setDialogName(e.target.value.trim())}
error={isInvalidName}
helperText={isInvalidName ? t`global.error.label.invalid_input` : undefined}
helperText={isInvalidName ? t`Invalid input` : undefined}
/>
<FormControlLabel
control={<Checkbox checked={dialogDefault} onChange={(e) => setDialogDefault(e.target.checked)} />}
label={t('category.label.use_as_default_category')}
label={t`Default category when adding new manga to the library`}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={handleDialogSubmit} color="primary" disabled={!canSubmit}>
{t('global.button.submit')}
{t`Submit`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -9,10 +9,10 @@
import { ComponentProps, useMemo, useState } from 'react';
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -31,10 +31,10 @@ import { CREATE_NEW_CATEGORY_ID } from '@/features/category/Category.constants.t
import { CreateOrEditCategoryDialog } from '@/features/category/components/CreateOrEditCategoryDialog.tsx';
export function CategorySettings() {
const { t } = useTranslation();
const { t } = useLingui();
const dndSensors = DndKitUtil.useSensorsForDevice();
useAppTitle(t('category.dialog.title.edit_category_other'));
useAppTitle(t`Edit categories`);
const { data, loading, error, refetch } = requestManager.useGetCategories<
GetCategoriesSettingsQuery,
@@ -96,7 +96,7 @@ export function CategorySettings() {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('category.error.label.request_failure')}
message={t`Could not load categories`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('CategorySettings::refetch'))}
/>

View File

@@ -6,8 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { ChapterAction, ChapterListOptions, ChapterSortMode } from '@/features/chapter/Chapter.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isDownloaded: false, isBookmarked: false };
@@ -21,11 +22,11 @@ export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
excludedScanlators: [],
};
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY: Record<ChapterSortMode, TranslationKey> = {
source: 'global.sort.label.by_source',
chapterNumber: 'global.sort.label.by_chapter_number',
uploadedAt: 'global.sort.label.by_upload_date',
fetchedAt: 'global.sort.label.by_fetch_date',
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION: Record<ChapterSortMode, MessageDescriptor> = {
source: msg`By source`,
chapterNumber: msg`By chapter number`,
uploadedAt: msg`By upload date`,
fetchedAt: msg`By date fetched`,
};
export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
@@ -43,65 +44,65 @@ export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
export const CHAPTER_ACTION_TO_TRANSLATION: {
[key in ChapterAction]: {
action: {
single: TranslationKey;
selected: TranslationKey;
single: MessageDescriptor;
selected: MessageDescriptor;
};
confirmation?: TranslationKey;
success: TranslationKey;
error: TranslationKey;
confirmation?: MessageDescriptor;
success: MessageDescriptor;
error: MessageDescriptor;
};
} = {
download: {
action: {
single: 'chapter.action.download.add.label.action',
selected: 'chapter.action.download.add.button.selected',
single: msg`Download`,
selected: msg`Download selected`,
},
confirmation: 'chapter.action.download.add.label.confirmation',
success: 'chapter.action.download.add.label.success',
error: 'chapter.action.download.add.label.error',
confirmation: msg`{count, plural, one {You are about to download one chapter} other {You are about to download # chapters.\nSuwayomi is not a mass downloader and too many downloads can get you banned from sources and/or cause performance issues.}}`,
success: msg`{count, plural, one {Download added} other {# downloads added}}`,
error: msg`{count, plural, one {Could not add the download} other {Could not add downloads}}`,
},
delete: {
action: {
single: 'chapter.action.download.delete.label.action',
selected: 'chapter.action.download.delete.button.selected',
single: msg`Delete`,
selected: msg`Delete selected`,
},
confirmation: 'chapter.action.download.delete.label.confirmation',
success: 'chapter.action.download.delete.label.success',
error: 'chapter.action.download.delete.label.error',
confirmation: msg`{count, plural, one {You are about to delete one download} other {You are about to delete # downloads}}`,
success: msg`{count, plural, one {Chapter deleted} other {# chapters deleted}}`,
error: msg`{count, plural, one {Could not delete the chapter} other {Could not delete chapters}}`,
},
bookmark: {
action: {
single: 'chapter.action.bookmark.add.label.action',
selected: 'chapter.action.bookmark.add.button.selected',
single: msg`Add bookmark`,
selected: msg`Bookmark selected`,
},
success: 'chapter.action.bookmark.add.label.success',
error: 'chapter.action.bookmark.add.label.error',
success: msg`{count, plural, one {Chapter bookmarked} other {# chapters bookmarked}}`,
error: msg`{count, plural, one {Could not bookmark the chapter} other {Could not bookmark chapters}}`,
},
unbookmark: {
action: {
single: 'chapter.action.bookmark.remove.label.action',
selected: 'chapter.action.bookmark.remove.button.selected',
single: msg`Remove bookmark`,
selected: msg`Remove bookmarks from selected`,
},
confirmation: 'chapter.action.bookmark.remove.label.confirmation',
success: 'chapter.action.bookmark.remove.label.success',
error: 'chapter.action.bookmark.remove.label.error',
confirmation: msg`{count, plural, one {You are about to remove one bookmark} other {You are about to remove # bookmarks}}`,
success: msg`{count, plural, one {Chapter bookmark removed} other {# chapter bookmarks removed}}`,
error: msg`{count, plural, one {Could not remove the bookmark} other {Could not remove the bookmarks}}`,
},
mark_as_read: {
action: {
single: 'chapter.action.mark_as_read.add.label.action.current',
selected: 'chapter.action.mark_as_read.add.button.selected',
single: msg`Mark as read`,
selected: msg`Mark selected as read`,
},
confirmation: 'chapter.action.mark_as_read.add.label.confirmation',
success: 'chapter.action.mark_as_read.add.label.success',
error: 'chapter.action.mark_as_read.add.label.error',
confirmation: msg`{count, plural, one {You are about to mark one chapter as read} other {You are about to mark # chapters as read}}`,
success: msg`{count, plural, one {Chapter marked as read} other {# chapters marked as read}}`,
error: msg`{count, plural, one {Could not mark the chapter as read} other {Could not mark chapters as read}}`,
},
mark_as_unread: {
action: {
single: 'chapter.action.mark_as_read.remove.label.action',
selected: 'chapter.action.mark_as_read.remove.button.selected',
single: msg`Mark as unread`,
selected: msg`Mark selected as unread`,
},
confirmation: 'chapter.action.mark_as_read.remove.label.confirmation',
success: 'chapter.action.mark_as_read.remove.label.success',
error: 'chapter.action.mark_as_read.remove.label.error',
confirmation: msg`{count, plural, one {You are about to mark one chapter as unread} other {You are about to mark # chapters as unread}}`,
success: msg`{count, plural, one {Chapter marked as unread} other {# chapters marked as unread}}`,
error: msg`{count, plural, one {Could not mark the chapter as unread} other {Could not mark chapters as unread}}`,
},
};

View File

@@ -7,9 +7,9 @@
*/
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import { useTranslation } from 'react-i18next';
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
import DisabledByDefaultRounded from '@mui/icons-material/DisabledByDefaultRounded';
import { useLingui } from '@lingui/react/macro';
import { CheckboxListSetting } from '@/base/components/settings/CheckboxListSetting.tsx';
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -23,7 +23,7 @@ export const ChapterExcludeSanlatorsFilter = ({
scanlators: string[];
excludedScanlators: string[];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const popupState = usePopupState({ variant: 'dialog', popupId: 'chapter-list-options-scanlator-filter-dialog' });
if (!scanlators.length) {
@@ -34,13 +34,13 @@ export const ChapterExcludeSanlatorsFilter = ({
<>
<CheckboxInput
{...bindTrigger(popupState)}
label={t('global.label.scanlator')}
label={t`Scanlator`}
icon={<PeopleAltOutlinedIcon />}
checkedIcon={<PeopleAltOutlinedIcon color="warning" />}
checked={!!excludedScanlators.length}
/>
<CheckboxListSetting
title={t('chapter.option.exclude_scanlators')}
title={t`Exclude scanlators`}
open={popupState.isOpen}
onClose={(selectedScanlators) => {
if (selectedScanlators) {

View File

@@ -11,7 +11,8 @@ import Stack from '@mui/material/Stack';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { ComponentProps, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ResumeFab } from '@/features/manga/components/ResumeFAB.tsx';
import {
@@ -82,7 +83,7 @@ const ChapterListFAB = ({
}) => {
if (selectedChapters.length) {
return (
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
<SelectionFAB title={plural(selectedChapters.length, { one: '# chapter', other: '# chapters' })}>
{(handleClose) => (
<ChapterActionMenuItems
selectedChapters={selectedChapters}
@@ -110,7 +111,7 @@ export const ChapterList = ({
manga: Pick<MangaScreenFieldsFragment, 'id' | 'firstUnreadChapter' | 'chapters' | 'unreadCount' | 'downloadCount'>;
isRefreshing: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const { appBarHeight } = useNavBarContext();
const isMobileWidth = MediaQuery.useIsBelowWidth('md');
@@ -126,7 +127,7 @@ export const ChapterList = ({
const options = useChapterListOptions(manga);
const updateOption = updateChapterListOptions(manga, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
const {
data: chaptersData,
@@ -173,7 +174,7 @@ export const ChapterList = ({
return (
<Stack sx={{ justifyContent: 'center', position: 'relative', flexGrow: 1 }}>
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('ChapterList::refetch'))}
/>
@@ -193,13 +194,17 @@ export const ChapterList = ({
>
<Stack>
<Typography variant="h5" component="h3">
{t('chapter.value', { count: visibleChapters.length })}
{plural(visibleChapters.length, {
one: '# chapter',
other: '# chapters',
})}
</Typography>
{!!missingChapterCount && (
<Typography variant="body2" color="warning">
{`${t('chapter.missing', {
count: missingChapterCount,
})}`}
{plural(missingChapterCount, {
one: 'Missing # chapter',
other: 'Missing # chapters',
})}
</Typography>
)}
</Stack>
@@ -225,10 +230,8 @@ export const ChapterList = ({
</Stack>
</ChapterListHeader>
{noChaptersFound && <EmptyViewAbsoluteCentered message={t('chapter.error.label.no_chapter_found')} />}
{noChaptersMatchingFilter && (
<EmptyViewAbsoluteCentered message={t('chapter.error.label.no_matches')} />
)}
{noChaptersFound && <EmptyViewAbsoluteCentered message={t`No chapters found`} />}
{noChaptersMatchingFilter && <EmptyViewAbsoluteCentered message={t`No chapters matching filter`} />}
<StyledVirtuoso
persistKey={`manga-${manga.id}-chapter-list`}

View File

@@ -6,18 +6,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import RadioGroup from '@mui/material/RadioGroup';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { RadioInput } from '@/base/components/inputs/RadioInput.tsx';
import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx';
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
import { OptionsTabs } from '@/base/components/modals/OptionsTabs.tsx';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/features/chapter/Chapter.constants.ts';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
import { ChapterListOptions } from '@/features/chapter/Chapter.types.ts';
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
import { ChapterExcludeSanlatorsFilter } from '@/features/chapter/components/ChapterExcludeSanlatorsFilter.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
interface IProps {
open: boolean;
@@ -28,10 +29,10 @@ interface IProps {
excludedScanlators: string[];
}
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
filter: 'global.label.filter',
sort: 'global.label.sort',
display: 'global.label.display',
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
filter: msg`Filter`,
sort: msg`Sort`,
display: msg`Display`,
};
export const ChapterOptions: React.FC<IProps> = ({
@@ -42,7 +43,7 @@ export const ChapterOptions: React.FC<IProps> = ({
scanlators,
excludedScanlators,
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<OptionsTabs<'filter' | 'sort' | 'display'>
@@ -56,17 +57,17 @@ export const ChapterOptions: React.FC<IProps> = ({
return (
<>
<ThreeStateCheckboxInput
label={t('global.filter.label.unread')}
label={t`Unread`}
checked={options.unread}
onChange={(c) => updateOption('unread', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.downloaded')}
label={t`Downloaded`}
checked={options.downloaded}
onChange={(c) => updateOption('downloaded', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.bookmarked')}
label={t`Bookmarked`}
checked={options.bookmarked}
onChange={(c) => updateOption('bookmarked', c)}
/>
@@ -79,7 +80,7 @@ export const ChapterOptions: React.FC<IProps> = ({
);
}
if (key === 'sort') {
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY).map(([mode, label]) => (
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION).map(([mode, label]) => (
<SortRadioInput
key={mode}
label={t(label)}
@@ -87,10 +88,7 @@ export const ChapterOptions: React.FC<IProps> = ({
sortDescending={options.reverse}
onClick={() =>
mode !== options.sortBy
? updateOption(
'sortBy',
mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
)
? updateOption('sortBy', mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION)
: updateOption('reverse', !options.reverse)
}
/>
@@ -102,8 +100,8 @@ export const ChapterOptions: React.FC<IProps> = ({
onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)}
value={options.showChapterNumber}
>
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />
<RadioInput label={t('chapter.option.display.label.chapter_number')} value />
<RadioInput label={t`Source title`} value={false} />
<RadioInput label={t`Chapter number`} value />
</RadioGroup>
);
}

View File

@@ -9,12 +9,12 @@
import FilterList from '@mui/icons-material/FilterList';
import IconButton from '@mui/material/IconButton';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu';
import DownloadIcon from '@mui/icons-material/Download';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ChapterOptions } from '@/features/chapter/components/ChapterOptions.tsx';
import { isFilterActive, updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
@@ -45,7 +45,7 @@ export const ChaptersToolbarMenu = ({
scanlators,
excludeScanlators,
}: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [open, setOpen] = React.useState(false);
const isFiltered = isFilterActive(options);
@@ -55,7 +55,7 @@ export const ChaptersToolbarMenu = ({
return (
<>
<CustomTooltip title={t('chapter.action.mark_as_read.add.label.action.all')} disabled={areAllChaptersRead}>
<CustomTooltip title={t`Mark all as read`} disabled={areAllChaptersRead}>
<IconButton
disabled={areAllChaptersRead}
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, mangaId)}
@@ -67,7 +67,7 @@ export const ChaptersToolbarMenu = ({
<PopupState variant="popover" popupId="chapterlist-download-button">
{(popupState) => (
<>
<CustomTooltip title={t('global.button.download')} disabled={areAllChaptersRead}>
<CustomTooltip title={t`Download`} disabled={areAllChaptersRead}>
<IconButton
disabled={areAllChaptersDownloaded}
{...bindTrigger(popupState)}
@@ -84,7 +84,7 @@ export const ChaptersToolbarMenu = ({
</>
)}
</PopupState>
<CustomTooltip title={t('chapter.action.filter_and_sort.label')}>
<CustomTooltip title={t`Filter and sort`}>
<IconButton onClick={() => setOpen(true)} color="inherit">
<FilterList color={isFiltered ? 'warning' : undefined} />
</IconButton>

View File

@@ -17,40 +17,43 @@
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { plural } from '@lingui/core/macro';
export const MissingChaptersInfoSeparator = ({ missingChaptersGap }: { missingChaptersGap: number }) => {
const { t } = useTranslation();
return (
<Stack
export const MissingChaptersInfoSeparator = ({
missingChaptersGap: missingChapterCount,
}: {
missingChaptersGap: number;
}) => (
<Stack
sx={{
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
p: 2,
pt: 3.5,
pb: 2.5,
}}
>
<Box
sx={{
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
p: 2,
pt: 3.5,
pb: 2.5,
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
{t('chapter.missing', { count: missingChaptersGap })}
</Typography>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
</Stack>
);
};
/>
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
{plural(missingChapterCount, {
one: 'Missing # chapter',
other: 'Missing # chapters',
})}
</Typography>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
</Stack>
);

View File

@@ -11,11 +11,11 @@ import Delete from '@mui/icons-material/Delete';
import Download from '@mui/icons-material/Download';
import RemoveDone from '@mui/icons-material/RemoveDone';
import Done from '@mui/icons-material/Done';
import { useTranslation } from 'react-i18next';
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
import DoneAll from '@mui/icons-material/DoneAll';
import { ComponentProps, useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { MenuItem } from '@/base/components/menu/MenuItem.tsx';
@@ -74,7 +74,7 @@ export const ChapterActionMenuItems = ({
onClose,
selectable = true,
}: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
const isSingleMode = !!chapter;
const { isDownloaded, isRead, isBookmarked } = chapter ?? {};
@@ -173,7 +173,7 @@ export const ChapterActionMenuItems = ({
return (
<>
{isSingleMode && selectable && (
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t`Select`} />
)}
{isSingleMode && (
<>
@@ -184,7 +184,7 @@ export const ChapterActionMenuItems = ({
window.open(chapter!.realUrl!, '_blank', 'noopener,noreferrer');
onClose();
}}
title={t('global.button.open_browser')}
title={t`Open in browser`}
/>
<MenuItem
Icon={IconWebView}
@@ -197,7 +197,7 @@ export const ChapterActionMenuItems = ({
);
onClose();
}}
title={t('global.button.open_webview')}
title={t`Open in WebView`}
/>
</>
)}
@@ -255,7 +255,7 @@ export const ChapterActionMenuItems = ({
<MenuItem
onClick={() => performAction('mark_prev_as_read', [])}
Icon={DoneAll}
title={t('chapter.action.mark_as_read.add.label.action.previous')}
title={t`Mark previous as read`}
/>
)}
</>

View File

@@ -6,9 +6,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import gql from 'graphql-tag';
import { msg } from '@lingui/core/macro';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
@@ -28,26 +29,46 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { TranslationKey } from '@/base/Base.types.ts';
import { i18n } from '@/i18n';
const DOWNLOAD_OPTIONS: {
title: TranslationKey;
title: MessageDescriptor;
getCount: (downloadAheadLimit: number) => number | undefined;
onlyUnread?: boolean;
isDownloadAhead?: boolean;
}[] = [
{ title: 'chapter.action.download.add.label.next', getCount: () => 1 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 5 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 10 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 25 },
{
title: 'chapter.action.download.add.label.ahead',
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 1,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 5,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 10,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 25,
},
{
title: msg`Download ahead ({count})`,
getCount: (downloadAheadLimit) => downloadAheadLimit,
onlyUnread: true,
isDownloadAhead: true,
},
{ title: 'chapter.action.download.add.label.unread', getCount: () => undefined, onlyUnread: true },
{ title: 'chapter.action.download.add.label.all', getCount: () => undefined, onlyUnread: false },
{
title: msg`Unread`,
getCount: () => undefined,
onlyUnread: true,
},
{
title: msg`All`,
getCount: () => undefined,
onlyUnread: false,
},
];
const handleDownload = async (
@@ -131,8 +152,6 @@ export const ChaptersDownloadActionMenuItems = ({
mangaIds: MangaType['id'][];
closeMenu: () => void;
}) => {
const { t } = useTranslation();
const {
settings: { downloadAheadLimit },
} = useMetadataServerSettings();
@@ -140,8 +159,10 @@ export const ChaptersDownloadActionMenuItems = ({
const handleSelect = (size?: number, onlyUnread: boolean = true, downloadAhead: boolean = false) => {
handleDownload(mangaIds, onlyUnread, size, downloadAhead).catch((e) =>
makeToast(
t(CHAPTER_ACTION_TO_TRANSLATION.download.error, {
count: size,
/* lingui-extract-ignore */
i18n.t({
...CHAPTER_ACTION_TO_TRANSLATION.download.error,
values: { count: size },
}),
'error',
getErrorMessage(e),
@@ -155,10 +176,16 @@ export const ChaptersDownloadActionMenuItems = ({
<>
{DOWNLOAD_OPTIONS.map(({ title, getCount, onlyUnread, isDownloadAhead }) => (
<MenuItem
key={t(title, { count: getCount(downloadAheadLimit) })}
key={
/* lingui-extract-ignore */
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
}
onClick={() => handleSelect(getCount(downloadAheadLimit), onlyUnread, isDownloadAhead)}
>
{t(title, { count: getCount(downloadAheadLimit) })}
{
/* lingui-extract-ignore */
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
}
</MenuItem>
))}
</>

View File

@@ -7,8 +7,8 @@
*/
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import DownloadIcon from '@mui/icons-material/Download';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -24,15 +24,13 @@ export const ChapterDownloadButton = ({
chapterId: ChapterIdInfo['id'];
isDownloaded: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
const downloadChapter = () => {
requestManager
.addChapterToDownloadQueue(chapterId)
.response.catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
};
if (download == null && isDownloaded) {
@@ -40,7 +38,7 @@ export const ChapterDownloadButton = ({
}
return (
<CustomTooltip title={t('chapter.action.download.add.label.action')}>
<CustomTooltip title={t`Download`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -8,7 +8,7 @@
import Refresh from '@mui/icons-material/Refresh';
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
@@ -19,14 +19,14 @@ import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
const handleRetry = async () => {
try {
await requestManager.addChapterToDownloadQueue(chapterId).response;
} catch (e) {
makeToast(t('download.queue.error.label.failed_to_retry'), 'error', getErrorMessage(e));
makeToast(t`Could not retry failed download.`, 'error', getErrorMessage(e));
}
};
@@ -35,7 +35,7 @@ export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterId
}
return (
<CustomTooltip title={t('global.button.retry')}>
<CustomTooltip title={t`Retry`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -16,9 +16,9 @@ import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import React, { memo, MouseEvent, TouchEvent, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import { useLongPress } from 'use-long-press';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { getDateString } from '@/base/utils/DateHelper.ts';
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
@@ -61,7 +61,7 @@ interface IProps {
}
export const ChapterCard = memo((props: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const preventMobileContextMenu = MediaQuery.usePreventMobileContextMenu();
@@ -134,13 +134,11 @@ export const ChapterCard = memo((props: IProps) => {
>
<ListCardContent>
<ChapterCardMetadata
title={
showChapterNumber
? `${t('chapter.title_one')} ${chapter.chapterNumber}`
: chapter.name
}
title={showChapterNumber ? `${t`Chapter`} ${chapter.chapterNumber}` : chapter.name}
secondaryText={chapter.scanlator}
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${isDownloaded ? `${t('chapter.status.label.downloaded')}` : ''}`}
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${
isDownloaded ? `${t`Downloaded`}` : ''
}`}
infoIcons={
chapter.isBookmarked && (
<BookmarkIcon
@@ -180,7 +178,7 @@ export const ChapterCard = memo((props: IProps) => {
<Stack sx={{ minHeight: '48px' }}>
{selected === null ? (
<CustomTooltip title={t('global.button.options')}>
<CustomTooltip title={t`Options`}>
<IconButton
ref={menuButtonRef}
{...MUIUtil.preventRippleProp(bindTrigger(popupState), {
@@ -198,9 +196,7 @@ export const ChapterCard = memo((props: IProps) => {
</IconButton>
</CustomTooltip>
) : (
<CustomTooltip
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
>
<CustomTooltip title={selected ? t`Deselect` : t`Select`}>
<Checkbox checked={selected} />
</CustomTooltip>
)}

Some files were not shown because too many files have changed in this diff Show More