diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index cfb956a09..a8abbd501 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -71,7 +71,6 @@ public class AccountController : BaseApiController /// /// /// - [AllowAnonymous] [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { @@ -119,7 +118,7 @@ public class AccountController : BaseApiController public async Task> RegisterFirstUser(RegisterDto registerDto) { var admins = await _userManager.GetUsersInRoleAsync("Admin"); - if (admins.Count > 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "denied")); + if (admins.Count > 0) return BadRequest(await _localizationService.Get("en", "denied")); try { @@ -137,8 +136,8 @@ public class AccountController : BaseApiController if (!result.Succeeded) return BadRequest(result.Errors); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "confirm-token-gen")); - if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "validate-email", token)); + if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); + if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token)); var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); @@ -165,7 +164,7 @@ public class AccountController : BaseApiController await _unitOfWork.CommitAsync(); } - return BadRequest(await _localizationService.Translate(User.GetUserId(), "register-user")); + return BadRequest(await _localizationService.Get("en", "register-user")); } @@ -182,9 +181,9 @@ public class AccountController : BaseApiController .Include(u => u.UserPreferences) .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); - if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + if (user == null) return Unauthorized(await _localizationService.Get("en", "bad-credentials")); var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "disabled-account")); + if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); var result = await _signInManager .CheckPasswordSignInAsync(user, loginDto.Password, true); @@ -192,12 +191,12 @@ public class AccountController : BaseApiController if (result.IsLockedOut) { await _userManager.UpdateSecurityStampAsync(user); - return Unauthorized(await _localizationService.Translate(User.GetUserId(), "locked-out")); + return Unauthorized(await _localizationService.Translate(user.Id, "locked-out")); } if (!result.Succeeded) { - return Unauthorized(await _localizationService.Translate(User.GetUserId(), result.IsNotAllowed ? "confirm-email" : "bad-credentials")); + return Unauthorized(await _localizationService.Translate(user.Id, result.IsNotAllowed ? "confirm-email" : "bad-credentials")); } // Update LastActive on account @@ -258,7 +257,7 @@ public class AccountController : BaseApiController var token = await _tokenService.ValidateRefreshToken(tokenRequestDto); if (token == null) { - return Unauthorized(new { message = await _localizationService.Translate(User.GetUserId(), "invalid-token") }); + return Unauthorized(new { message = await _localizationService.Get("en", "invalid-token") }); } return Ok(token); @@ -670,7 +669,7 @@ public class AccountController : BaseApiController if (user == null) { _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation")); + return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); } // Validate Password and Username @@ -691,7 +690,7 @@ public class AccountController : BaseApiController if (!await ConfirmEmailToken(dto.Token, user)) { _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation")); + return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); } user.UserName = dto.Username; @@ -734,13 +733,13 @@ public class AccountController : BaseApiController if (user == null) { _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation")); + return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); } if (!await ConfirmEmailToken(dto.Token, user)) { _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation")); + return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); } _logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email); @@ -748,7 +747,7 @@ public class AccountController : BaseApiController if (!result.Succeeded) { _logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description)); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-email-update")); + return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update")); } user.ConfirmationToken = null; await _unitOfWork.CommitAsync(); @@ -766,12 +765,12 @@ public class AccountController : BaseApiController [HttpPost("confirm-password-reset")] public async Task> ConfirmForgotPassword(ConfirmPasswordResetDto dto) { + var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); try { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + return BadRequest(await _localizationService.Get("en", "bad-credentials")); } var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, @@ -779,16 +778,16 @@ public class AccountController : BaseApiController if (!result) { _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials")); } var errors = await _accountService.ChangeUserPassword(user, dto.Password); - return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(User.GetUserId(), "password-updated")); + return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(user.Id, "password-updated")); } catch (Exception ex) { _logger.LogError(ex, "There was an unexpected error when confirming new password"); - return BadRequest("generic-password-update"); + return BadRequest(await _localizationService.Translate(user.Id, "generic-password-update")); } } @@ -807,15 +806,15 @@ public class AccountController : BaseApiController if (user == null) { _logger.LogError("There are no users with email: {Email} but user is requesting password reset", email); - return Ok(await _localizationService.Translate(User.GetUserId(), "forgot-password-generic")); + return Ok(await _localizationService.Get("en", "forgot-password-generic")); } var roles = await _userManager.GetRolesAsync(user); if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole)) - return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied")); if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "confirm-email")); + return BadRequest(await _localizationService.Translate(user.Id, "confirm-email")); var token = await _userManager.GeneratePasswordResetTokenAsync(user); var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); @@ -828,10 +827,10 @@ public class AccountController : BaseApiController ServerConfirmationLink = emailLink, InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value }); - return Ok(await _localizationService.Translate(User.GetUserId(), "email-sent")); + return Ok(await _localizationService.Translate(user.Id, "email-sent")); } - return Ok(await _localizationService.Translate(User.GetUserId(), "not-accessible-password")); + return Ok(await _localizationService.Translate(user.Id, "not-accessible-password")); } [HttpGet("email-confirmed")] @@ -848,12 +847,12 @@ public class AccountController : BaseApiController public async Task> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + if (user == null) return BadRequest(await _localizationService.Get("en", "bad-credentials")); if (!await ConfirmEmailToken(dto.Token, user)) { _logger.LogInformation("confirm-migration-email email token is invalid"); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials")); } await _unitOfWork.CommitAsync(); @@ -884,12 +883,12 @@ public class AccountController : BaseApiController public async Task> ResendConfirmationSendEmail([FromQuery] int userId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user")); + if (user == null) return BadRequest(await _localizationService.Get("en", "no-user")); if (string.IsNullOrEmpty(user.Email)) return BadRequest( - await _localizationService.Translate(User.GetUserId(), "user-migration-needed")); - if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-confirmed")); + await _localizationService.Translate(user.Id, "user-migration-needed")); + if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed")); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email); @@ -910,12 +909,12 @@ public class AccountController : BaseApiController catch (Exception ex) { _logger.LogError(ex, "There was an issue resending invite email"); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-email")); + return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email")); } return Ok(emailLink); } - return Ok(await _localizationService.Translate(User.GetUserId(), "not-accessible")); + return Ok(await _localizationService.Translate(user.Id, "not-accessible")); } /// @@ -929,7 +928,7 @@ public class AccountController : BaseApiController { // If there is an admin account already, return var users = await _unitOfWork.UserRepository.GetAdminUsersAsync(); - if (users.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "admin-already-exists")); + if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists")); // Check if there is an existing invite var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); @@ -937,27 +936,27 @@ public class AccountController : BaseApiController { var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName)); + return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName)); _logger.LogInformation("A user is attempting to login, but hasn't accepted email invite"); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited")); + return BadRequest(await _localizationService.Get("en", "user-already-invited")); } var user = await _userManager.Users .Include(u => u.UserPreferences) .SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper()); - if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-username")); + if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username")); var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password); - if (!validPassword) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials")); + if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials")); try { var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); user.Email = dto.Email; - if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "critical-email-migration")); + if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "critical-email-migration")); _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); @@ -971,7 +970,7 @@ public class AccountController : BaseApiController await _unitOfWork.CommitAsync(); } - return BadRequest(await _localizationService.Translate(User.GetUserId(), "critical-email-migration")); + return BadRequest(await _localizationService.Get("en", "critical-email-migration")); } @@ -981,8 +980,6 @@ public class AccountController : BaseApiController var result = await _userManager.ConfirmEmailAsync(user, token); if (result.Succeeded) return true; - - _logger.LogCritical("[Account] Email validation failed"); if (!result.Errors.Any()) return false; diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 81065960a..f2b351a65 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -96,14 +96,14 @@ public class BookController : BaseApiController [AllowAnonymous] public async Task GetBookPageResources(int chapterId, [FromQuery] string file) { - if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); var key = BookService.CoalesceKeyForAnyFile(book, file); - if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "file-missing")); + if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing")); var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key); var content = await bookFile.ReadContentAsBytesAsync(); diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index a8c9ebd88..bbb32e042 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -43,9 +43,10 @@ public class ImageController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})] public async Task GetChapterCoverImage(int chapterId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); + if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); var format = _directoryService.FileSystem.Path.GetExtension(path); return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); @@ -60,9 +61,10 @@ public class ImageController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})] public async Task GetLibraryCoverImage(int libraryId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); + if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); var format = _directoryService.FileSystem.Path.GetExtension(path); return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); @@ -77,9 +79,10 @@ public class ImageController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})] public async Task GetVolumeCoverImage(int volumeId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); + if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); var format = _directoryService.FileSystem.Path.GetExtension(path); return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); @@ -94,9 +97,10 @@ public class ImageController : BaseApiController [HttpGet("series-cover")] public async Task GetSeriesCoverImage(int seriesId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); + if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); var format = _directoryService.FileSystem.Path.GetExtension(path); Response.AddCacheHeader(path); @@ -113,13 +117,15 @@ public class ImageController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})] public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { var destFile = await GenerateCollectionCoverImage(collectionTagId); - if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); - return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile)); + if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); + return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), + _directoryService.FileSystem.Path.GetFileName(destFile)); } var format = _directoryService.FileSystem.Path.GetExtension(path); @@ -135,12 +141,13 @@ public class ImageController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})] public async Task GetReadingListCoverImage(int readingListId, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { var destFile = await GenerateReadingListCoverImage(readingListId); - if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image")); + if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile)); } @@ -202,7 +209,7 @@ public class ImageController : BaseApiController var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId); - if (bookmark == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-doesnt-exist")); + if (bookmark == null) return BadRequest(await _localizationService.Translate(userId, "bookmark-doesnt-exist")); var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; @@ -223,7 +230,7 @@ public class ImageController : BaseApiController { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); - if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "must-be-defined", "Url")); + if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url")); var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; // Check if the domain exists @@ -238,7 +245,7 @@ public class ImageController : BaseApiController } catch (Exception) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-favicon")); + return BadRequest(await _localizationService.Translate(userId, "generic-favicon")); } } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 3c49de62b..55890c13e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -279,7 +279,7 @@ public class LibraryController : BaseApiController var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); if (!isAdmin) return BadRequest("API key must belong to an admin"); - if (dto.FolderPath.Contains("..")) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-path")); + if (dto.FolderPath.Contains("..")) return BadRequest(await _localizationService.Translate(user.Id, "invalid-path")); dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index f14602444..57a0e00a4 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -107,7 +107,8 @@ public class ReaderController : BaseApiController public async Task GetImage(int chapterId, int page, string apiKey, bool extractPdf = false) { if (page < 0) page = 0; - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); @@ -115,7 +116,7 @@ public class ReaderController : BaseApiController { var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-image-for-page", page)); + return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); @@ -139,7 +140,8 @@ public class ReaderController : BaseApiController [AllowAnonymous] public async Task GetThumbnail(int chapterId, int pageNum, string apiKey) { - if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + if (userId == 0) return BadRequest(); var chapter = await _cacheService.Ensure(chapterId, true); if (chapter == null) return NoContent(); var images = _cacheService.GetCachedPages(chapterId); @@ -175,7 +177,7 @@ public class ReaderController : BaseApiController try { var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); - if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-image-for-page", page)); + if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path)); diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index 15eea11ea..2b9284f27 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -73,7 +73,7 @@ public class ThemeController : BaseApiController } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); + return BadRequest(await _localizationService.Get("en", ex.Message)); } } } diff --git a/API/I18N/nl.json b/API/I18N/nl.json index 0967ef424..bdd280343 100644 --- a/API/I18N/nl.json +++ b/API/I18N/nl.json @@ -1 +1,103 @@ -{} +{ + "password-updated": "Wachtwoord bijgewerkt", + "user-already-registered": "Gebruiker is al geregistreerd als {0}", + "generic-invite-user": "Er is een probleem opgetreden bij het uitnodigen van de gebruiker. Controleer de logboeken.", + "generate-token": "Er is een probleem opgetreden bij het genereren van een bevestigings- e-mailtoken. Zie logboek", + "generic-user-email-update": "E-mail voor gebruiker kan niet worden bijgewerkt. Controleer de logboeken.", + "generic-password-update": "Er is een onverwachte fout opgetreden bij het bevestigen van het nieuwe wachtwoord", + "locked-out": "U bent uitgesloten door te veel autorisatiepogingen. Wacht alsjeblieft 10 minuten.", + "register-user": "Er is iets misgegaan bij het registreren van de gebruiker", + "bad-credentials": "Uw inloggegevens zijn niet correct", + "disabled-account": "Uw account is uitgeschakeld. Neem contact op met de serverbeheerder.", + "validate-email": "Er is een probleem opgetreden bij het valideren van uw e-mailadres: {0}", + "confirm-token-gen": "Er is een probleem opgetreden bij het genereren van een bevestigingstoken", + "denied": "Niet toegestaan", + "invalid-password": "Ongeldig wachtwoord", + "invalid-token": "Ongeldige Token", + "unable-to-reset-key": "Er is iets misgegaan, kan de sleutel niet resetten", + "invalid-payload": "Ongeldige lading", + "nothing-to-do": "Niets te doen", + "share-multiple-emails": "U kunt geen e-mailadressen delen met meerdere accounts", + "age-restriction-update": "Er is een fout opgetreden bij het updaten van de leeftijdsbeperking", + "no-user": "Gebruiker bestaat niet", + "username-taken": "Gebruikersnaam al in gebruik", + "user-already-confirmed": "Gebruiker is al bevestigd", + "manual-setup-fail": "Handmatige aanmaak kan niet worden voltooid. Annuleer en maak de uitnodiging opnieuw", + "user-already-invited": "Gebruiker is al uitgenodigd onder dit e-mailadres en moet de uitnodiging nog accepteren.", + "invalid-email-confirmation": "Ongeldige e-mailbevestiging", + "forgot-password-generic": "Er wordt een e-mail verzonden naar het e-mailadres als deze in onze database voorkomt", + "not-accessible-password": "Uw server is niet toegankelijk. De link om je wachtwoord te resetten staat in het logboek", + "not-accessible": "Uw server is niet extern toegankelijk", + "email-sent": "Email verzonden", + "confirm-email": "U moet eerst uw e-mail bevestigen", + "permission-denied": "U heeft geen toestemming voor deze operatie", + "password-required": "U moet uw bestaande wachtwoord invoeren om uw account te wijzigen, tenzij u een beheerder bent", + "generic-reading-list-delete": "Er is een probleem opgetreden bij het verwijderen van de leeslijst", + "reading-list-deleted": "Leeslijst is verwijderd", + "reading-list-doesnt-exist": "Leeslijst bestaat niet", + "generic-relationship": "Er is een probleem opgetreden bij het updaten van relaties", + "no-series-collection": "Kan series niet ophalen van collectie", + "generic-series-delete": "Er is een probleem opgetreden bij het verwijderen van de serie", + "series-updated": "Succesvol geüpdatet", + "update-metadata-fail": "Kan metadata niet updaten", + "generic-series-update": "Er is een fout opgetreden bij het updaten van de serie", + "age-restriction-not-applicable": "Geen beperkingen", + "job-already-running": "Taak loopt al", + "greater-0": "{0} moet groter zijn dan 0", + "send-to-kavita-email": "Verzenden naar apparaat kan niet worden gebruikt met de e-mailservice van Kavita. Configureer uw eigen.", + "send-to-device-status": "Bestanden overzetten naar uw apparaat", + "generic-send-to": "Er is een fout opgetreden bij het verzenden van de bestanden naar het apparaat", + "volume-doesnt-exist": "Volume bestaat niet", + "series-doesnt-exist": "Serie bestaat niet", + "bookmarks-empty": "Bladwijzers kunnen niet leeg zijn", + "reading-list-updated": "Bijgewerkt", + "user-migration-needed": "Deze gebruiker moet migreren. Laat ze uitloggen en inloggen om de migratie op gang te brengen", + "generic-invite-email": "Er is een probleem opgetreden bij het opnieuw verzenden van de uitnodigingsmail", + "admin-already-exists": "Beheerder bestaat al", + "invalid-username": "Ongeldige gebruikersnaam", + "critical-email-migration": "Er is een probleem opgetreden tijdens de e-mailmigratie. Neem contact op met de ondersteuning", + "chapter-doesnt-exist": "Hoofdstuk bestaat niet", + "file-missing": "Bestand is niet gevonden in boek", + "collection-updated": "Verzameling succesvol bijgewerkt", + "generic-error": "Er is iets mis gegaan, probeer het alstublieft nogmaals", + "collection-doesnt-exist": "Collectie bestaat niet", + "device-doesnt-exist": "Apparaat bestaat niet", + "generic-device-create": "Er is een fout opgetreden bij het maken van het apparaat", + "generic-device-update": "Er is een fout opgetreden bij het updaten van het apparaat", + "generic-device-delete": "Er is een fout opgetreden bij het verwijderen van het apparaat", + "no-cover-image": "Geen omslagafbeelding", + "bookmark-doesnt-exist": "Bladwijzer bestaat niet", + "must-be-defined": "{0} moet gedefinieerd zijn", + "generic-favicon": "Er is een probleem opgetreden bij het ophalen van de favicon voor het domein", + "invalid-filename": "Ongeldige bestandsnaam", + "file-doesnt-exist": "Bestand bestaat niet", + "library-name-exists": "Bibliotheeknaam bestaat al. Kies een unieke naam voor de server.", + "generic-library": "Er was een kritiek probleem. Probeer het opnieuw.", + "no-library-access": "Gebruiker heeft geen toegang tot deze bibliotheek", + "user-doesnt-exist": "Gebruiker bestaat niet", + "library-doesnt-exist": "Bibliotheek bestaat niet", + "invalid-path": "Ongeldig pad", + "delete-library-while-scan": "U kunt een bibliotheek niet verwijderen terwijl er een scan wordt uitgevoerd. Wacht tot de scan is voltooid of herstart Kavita en probeer het vervolgens te verwijderen", + "generic-library-update": "Er is een kritiek probleem opgetreden bij het updaten van de bibliotheek.", + "pdf-doesnt-exist": "PDF bestaat niet wanneer het zou moeten", + "invalid-access": "Ongeldige toegang", + "no-image-for-page": "Zo'n afbeelding ontbreekt voor pagina {0}. Probeer te vernieuwen om opnieuw cachen mogelijk te maken.", + "perform-scan": "Voer een scan uit op deze serie of bibliotheek en probeer het opnieuw", + "generic-read-progress": "Er is een probleem opgetreden bij het opslaan van de voortgang", + "generic-clear-bookmarks": "Kan bladwijzers niet wissen", + "bookmark-permission": "U heeft geen toestemming om een bladwijzer te maken/de bladwijzer ongedaan te maken", + "bookmark-save": "Kan bladwijzer niet opslaan", + "cache-file-find": "Kan afbeelding in cache niet vinden. Laad opnieuw en probeer het opnieuw.", + "name-required": "Naam mag niet leeg zijn", + "valid-number": "Moet een geldig paginanummer zijn", + "duplicate-bookmark": "Dubbele bladwijzervermelding bestaat al", + "reading-list-permission": "U heeft geen rechten voor deze leeslijst of de lijst bestaat niet", + "reading-list-position": "Kan positie niet updaten", + "reading-list-item-delete": "Kan item(s) niet verwijderen", + "generic-reading-list-update": "Er is een probleem opgetreden bij het updaten van de leeslijst", + "generic-reading-list-create": "Er is een probleem opgetreden bij het maken van de leeslijst", + "series-restricted": "Gebruiker heeft geen toegang tot deze serie", + "libraries-restricted": "Gebruiker heeft geen toegang tot de bibliotheken", + "no-series": "Kan series van bibliotheek niet ophalen", + "generic-user-update": "Er was een uitzondering bij het updaten van de gebruiker" +} \ No newline at end of file diff --git a/API/Services/LocalizationService.cs b/API/Services/LocalizationService.cs index 273bf8141..ab3ad3d89 100644 --- a/API/Services/LocalizationService.cs +++ b/API/Services/LocalizationService.cs @@ -42,7 +42,7 @@ public class LocalizationService : ILocalizationService { _localizationDirectoryUi = directoryService.FileSystem.Path.Join( directoryService.FileSystem.Directory.GetCurrentDirectory(), - "UI/Web/src/assets/langs"); + "../UI/Web/src/assets/langs"); } else if (environment.EnvironmentName.Equals("Testing", StringComparison.OrdinalIgnoreCase)) { _localizationDirectoryUi = directoryService.FileSystem.Path.Join( @@ -136,11 +136,12 @@ public class LocalizationService : ILocalizationService /// public IEnumerable GetLocales() { - return - _directoryService.GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json") - .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)) - .Union(_directoryService.GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json") - .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty))) - .Distinct(); + var uiLanguages = _directoryService + .GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json") + .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)); + var backendLanguages = _directoryService + .GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json") + .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)); + return uiLanguages.Intersect(backendLanguages).Distinct(); } } diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index 7dc1ce01b..92b084e23 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -36,7 +36,6 @@ public class ThemeService : IThemeService /// /// /// - [AllowAnonymous] public async Task GetContent(int themeId) { var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId); diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 22817fd36..83b7907a8 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -45,11 +45,11 @@ export interface Preferences { locale: string; } -export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}]; -export const bookWritingStyles = [{text: 'Horizontal', value: WritingStyle.Horizontal}, {text: 'Vertical', value: WritingStyle.Vertical}]; -export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}]; -export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}]; -export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}]; -export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} -export const bookLayoutModes = [{text: 'Scroll', value: BookPageLayoutMode.Default}, {text: '1 Column', value: BookPageLayoutMode.Column1}, {text: '2 Column', value: BookPageLayoutMode.Column2}]; -export const pageLayoutModes = [{text: 'Cards', value: PageLayoutMode.Cards}, {text: 'List', value: PageLayoutMode.List}]; +export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}]; +export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}]; +export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; +export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; +export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; +export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} +export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; +export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 309bf43b8..f6660482e 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -192,27 +192,27 @@ export class ActionFactoryService { this.libraryActions = [ { action: Action.Scan, - title: 'Scan Library', + title: 'scan-library', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Submenu, - title: 'Others', + title: 'others', callback: this.dummyCallback, requiresAdmin: true, children: [ { action: Action.RefreshMetadata, - title: 'Refresh Covers', + title: 'refresh-covers', callback: this.dummyCallback, requiresAdmin: true, children: [], }, { action: Action.AnalyzeFiles, - title: 'Analyze Files', + title: 'analyze-files', callback: this.dummyCallback, requiresAdmin: true, children: [], @@ -221,7 +221,7 @@ export class ActionFactoryService { }, { action: Action.Edit, - title: 'Settings', + title: 'settings', callback: this.dummyCallback, requiresAdmin: true, children: [], @@ -231,7 +231,7 @@ export class ActionFactoryService { this.collectionTagActions = [ { action: Action.Edit, - title: 'Edit', + title: 'edit', callback: this.dummyCallback, requiresAdmin: true, children: [], @@ -241,55 +241,55 @@ export class ActionFactoryService { this.seriesActions = [ { action: Action.MarkAsRead, - title: 'Mark as Read', + title: 'mark-as-read', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.MarkAsUnread, - title: 'Mark as Unread', + title: 'mark-as-unread', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Scan, - title: 'Scan Series', + title: 'scan-series', callback: this.dummyCallback, requiresAdmin: true, children: [], }, { action: Action.Submenu, - title: 'Add to', + title: 'add-to', callback: this.dummyCallback, requiresAdmin: false, children: [ { action: Action.AddToWantToReadList, - title: 'Add to Want to Read', + title: 'add-to-want-to-read', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.RemoveFromWantToReadList, - title: 'Remove from Want to Read', + title: 'remove-from-want-to-read', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.AddToReadingList, - title: 'Add to Reading List', + title: 'add-to-reading-list', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.AddToCollection, - title: 'Add to Collection', + title: 'add-to-collection', callback: this.dummyCallback, requiresAdmin: true, children: [], @@ -298,7 +298,7 @@ export class ActionFactoryService { }, { action: Action.Submenu, - title: 'Send To', + title: 'send-to', callback: this.dummyCallback, requiresAdmin: false, children: [ @@ -316,27 +316,27 @@ export class ActionFactoryService { }, { action: Action.Submenu, - title: 'Others', + title: 'others', callback: this.dummyCallback, requiresAdmin: true, children: [ { action: Action.RefreshMetadata, - title: 'Refresh Covers', + title: 'refresh-covers', callback: this.dummyCallback, requiresAdmin: true, children: [], }, { action: Action.AnalyzeFiles, - title: 'Analyze Files', + title: 'analyze-files', callback: this.dummyCallback, requiresAdmin: true, children: [], }, { action: Action.Delete, - title: 'Delete', + title: 'delete', callback: this.dummyCallback, requiresAdmin: true, class: 'danger', @@ -346,14 +346,14 @@ export class ActionFactoryService { }, { action: Action.Download, - title: 'Download', + title: 'download', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Edit, - title: 'Edit', + title: 'edit', callback: this.dummyCallback, requiresAdmin: true, children: [], @@ -363,34 +363,34 @@ export class ActionFactoryService { this.volumeActions = [ { action: Action.IncognitoRead, - title: 'Read Incognito', + title: 'read-incognito', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.MarkAsRead, - title: 'Mark as Read', + title: 'mark-as-read', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.MarkAsUnread, - title: 'Mark as Unread', + title: 'mark-as-unread', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Submenu, - title: 'Add to', + title: 'add-to', callback: this.dummyCallback, requiresAdmin: false, children: [ { action: Action.AddToReadingList, - title: 'Add to Reading List', + title: 'add-to-reading-list', callback: this.dummyCallback, requiresAdmin: false, children: [], @@ -399,7 +399,7 @@ export class ActionFactoryService { }, { action: Action.Submenu, - title: 'Send To', + title: 'send-to', callback: this.dummyCallback, requiresAdmin: false, children: [ @@ -417,14 +417,14 @@ export class ActionFactoryService { }, { action: Action.Download, - title: 'Download', + title: 'download', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Edit, - title: 'Details', + title: 'details', callback: this.dummyCallback, requiresAdmin: false, children: [], @@ -434,34 +434,34 @@ export class ActionFactoryService { this.chapterActions = [ { action: Action.IncognitoRead, - title: 'Read Incognito', + title: 'read-incognito', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.MarkAsRead, - title: 'Mark as Read', + title: 'mark-as-read', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.MarkAsUnread, - title: 'Mark as Unread', + title: 'mark-as-unread', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Submenu, - title: 'Add to', + title: 'add-to', callback: this.dummyCallback, requiresAdmin: false, children: [ { action: Action.AddToReadingList, - title: 'Add to Reading List', + title: 'add-to-reading-list', callback: this.dummyCallback, requiresAdmin: false, children: [], @@ -470,7 +470,7 @@ export class ActionFactoryService { }, { action: Action.Submenu, - title: 'Send To', + title: 'send-to', callback: this.dummyCallback, requiresAdmin: false, children: [ @@ -489,14 +489,14 @@ export class ActionFactoryService { // RBS will handle rendering this, so non-admins with download are appicable { action: Action.Download, - title: 'Download', + title: 'download', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Edit, - title: 'Details', + title: 'details', callback: this.dummyCallback, requiresAdmin: false, children: [], @@ -506,14 +506,14 @@ export class ActionFactoryService { this.readingListActions = [ { action: Action.Edit, - title: 'Edit', + title: 'edit', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Delete, - title: 'Delete', + title: 'delete', callback: this.dummyCallback, requiresAdmin: false, class: 'danger', @@ -524,21 +524,21 @@ export class ActionFactoryService { this.bookmarkActions = [ { action: Action.ViewSeries, - title: 'View Series', + title: 'view-series', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.DownloadBookmark, - title: 'Download', + title: 'download', callback: this.dummyCallback, requiresAdmin: false, children: [], }, { action: Action.Delete, - title: 'Clear', + title: 'clear', callback: this.dummyCallback, class: 'danger', requiresAdmin: false, diff --git a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html index dc23615a6..5e2602807 100644 --- a/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html +++ b/UI/Web/src/app/book-reader/_components/reader-settings/reader-settings.component.html @@ -158,7 +158,7 @@ diff --git a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html index e8143d400..f2452ea19 100644 --- a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html @@ -1,37 +1,40 @@ - + +
+ (click)="preventEvent($event)">
- +
- - - - + + + + - - - + + + + - - - + + + + + + + +
+ +
+ +
+
+
- - - -
- -
- -
-
-
-
-
-
+
+
+
diff --git a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.ts b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.ts index ac87503e9..429559505 100644 --- a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.ts +++ b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.ts @@ -5,11 +5,12 @@ import { AccountService } from 'src/app/_services/account.service'; import { Action, ActionItem } from 'src/app/_services/action-factory.service'; import {CommonModule} from "@angular/common"; import {DynamicListPipe} from "../../dynamic-list.pipe"; +import {TranslocoModule} from "@ngneat/transloco"; @Component({ selector: 'app-card-actionables', standalone: true, - imports: [CommonModule, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, DynamicListPipe], + imports: [CommonModule, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, DynamicListPipe, TranslocoModule], templateUrl: './card-actionables.component.html', styleUrls: ['./card-actionables.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index 1cb40c9e0..e04bacd1a 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -87,8 +87,7 @@ export class SeriesCardComponent implements OnInit, OnChanges { if (this.data) { this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem, series: Series) => this.handleSeriesActionCallback(action, series)); if (this.isOnDeck) { - const otherStr = this.translocoService.translate('actionable.others'); - const othersIndex = this.actions.findIndex(obj => obj.title === otherStr); + const othersIndex = this.actions.findIndex(obj => obj.title === 'others'); if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) { this.actions[othersIndex].children.push({ action: Action.RemoveFromOnDeck, diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index c002447e8..6418b1e45 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -171,7 +171,7 @@
@@ -223,7 +223,7 @@
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index f4fe44437..7f2901e2d 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -186,8 +186,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { pagingDirection$: Observable = this.pagingDirectionSubject.asObservable(); - pageSplitOptions = pageSplitOptions; - layoutModes = layoutModes; + pageSplitOptionsTranslated = pageSplitOptions.map(this.translatePrefOptions); + layoutModesTranslated = layoutModes.map(this.translatePrefOptions); isLoading = true; hasBookmarkRights: boolean = false; // TODO: This can be an observable @@ -458,9 +458,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService) { - this.navService.hideNavBar(); - this.navService.hideSideNav(); - this.cdRef.markForCheck(); + this.navService.hideNavBar(); + this.navService.hideSideNav(); + this.cdRef.markForCheck(); } ngOnInit(): void { @@ -1652,4 +1652,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }) }); } + + translatePrefOptions(o: {text: string, value: any}) { + const d = {...o}; + d.text = translate('preferences.' + o.text); + return d; + } } diff --git a/UI/Web/src/app/manga-reader/_service/managa-reader.service.ts b/UI/Web/src/app/manga-reader/_service/managa-reader.service.ts index 9bdf4c03c..d746eebe1 100644 --- a/UI/Web/src/app/manga-reader/_service/managa-reader.service.ts +++ b/UI/Web/src/app/manga-reader/_service/managa-reader.service.ts @@ -30,7 +30,7 @@ export class ManagaReaderService { }); this.pairs = chapterInfo.doublePairs!; } - + adjustForDoubleReader(page: number) { if (!this.pairs.hasOwnProperty(page)) return page; return this.pairs[page]; @@ -61,10 +61,10 @@ export class ManagaReaderService { /** - * If pagenumber is 0 aka first page, which on double page rendering should always render as a single. - * + * If pageNumber is 0 aka first page, which on double page rendering should always render as a single. + * * @param pageNumber current page number - * @returns + * @returns */ isCoverImage(pageNumber: number) { return pageNumber === 0; @@ -104,9 +104,9 @@ export class ManagaReaderService { /** * Should Canvas Renderer be used - * @param img - * @param pageSplitOption - * @returns + * @param img + * @param pageSplitOption + * @returns */ shouldSplit(img: HTMLImageElement, pageSplitOption: PageSplitOption) { const needsSplitting = this.isWidePage(this.readerService.imageUrlToPageNum(img?.src)); diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index 2a43d24aa..0ae5d5832 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -42,7 +42,7 @@ export class SideNavComponent implements OnInit { libraries: Library[] = []; actions: ActionItem[] = []; - readingListActions = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]; + readingListActions = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]; filterQuery: string = ''; filterLibrary = (library: Library) => { return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0; diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index b6fcf6b84..5ef2ff930 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -40,7 +40,7 @@
@@ -158,7 +158,7 @@ @@ -169,7 +169,7 @@ @@ -182,13 +182,13 @@
@@ -201,7 +201,7 @@
@@ -309,7 +309,7 @@
@@ -334,7 +334,7 @@ @@ -345,7 +345,7 @@ @@ -359,7 +359,7 @@ diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 0dcd4c951..bd25d0dbc 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -48,7 +48,7 @@ import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbAccor import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import {LocalizationService} from "../../_services/localization.service"; import {Language} from "../../_models/metadata/language"; -import {TranslocoModule, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco"; enum AccordionPanelID { ImageReader = 'image-reader', @@ -80,15 +80,20 @@ enum FragmentID { }) export class UserPreferencesComponent implements OnInit, OnDestroy { - readingDirections = readingDirections; - scalingOptions = scalingOptions; - pageSplitOptions = pageSplitOptions; - readingModes = readingModes; - layoutModes = layoutModes; - bookLayoutModes = bookLayoutModes; - bookColorThemes = bookColorThemes; - pageLayoutModes = pageLayoutModes; - bookWritingStyles = bookWritingStyles; + readingDirectionsTranslated = readingDirections.map(this.translatePrefOptions); + scalingOptionsTranslated = scalingOptions.map(this.translatePrefOptions); + pageSplitOptionsTranslated = pageSplitOptions.map(this.translatePrefOptions); + readingModesTranslated = readingModes.map(this.translatePrefOptions); + layoutModesTranslated = layoutModes.map(this.translatePrefOptions); + bookLayoutModesTranslated = bookLayoutModes.map(this.translatePrefOptions); + bookColorThemesTranslated = bookColorThemes.map(o => { + const d = {...o}; + d.name = translate('theme.' + d.translationKey); + return d; + }); + + pageLayoutModesTranslated = pageLayoutModes.map(this.translatePrefOptions); + bookWritingStylesTranslated = bookWritingStyles.map(this.translatePrefOptions); settingsForm: FormGroup = new FormGroup({}); user: User | undefined = undefined; @@ -314,4 +319,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.settingsForm.markAsTouched(); this.cdRef.markForCheck(); } + + translatePrefOptions(o: {text: string, value: any}) { + const d = {...o}; + d.text = translate('preferences.' + o.text); + return d; + } } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 43fbadf89..da9894680 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -180,6 +180,13 @@ "scan-queued": "A site theme scan has been queued" }, + "theme": { + "theme-dark": "Dark", + "theme-black": "Black", + "theme-paper": "Paper", + "theme-white": "White" + }, + "restriction-selector": { "title": "Age Rating Restriction", "description": "When selected, all series and reading lists that have at least one item that is greater than the selected restriction will be pruned from results.", @@ -1750,10 +1757,53 @@ }, "actionable": { + "scan-library": "Scan Library", + "refresh-covers": "Refresh Covers", + "analyze-files": "Analyze Files", + "settings": "Settings", + "edit": "Edit", + "mark-as-read": "Mark as Read", + "mark-as-unread": "Mark as Unread", + "scan-series": "Scan Series", + "add-to": "Add to", + "add-to-want-to-read": "Add to Want to Read", + "remove-from-want-to-read": "Remove from Want to Read", "remove-from-on-deck": "Remove From On Deck", - "others": "Others" + "others": "Others", + "add-to-reading-list": "Add to Reading List", + "add-to-collection": "Add to Collection", + "send-to": "Send To", + "delete": "Delete", + "download": "Download", + "read-incognito": "Read Incognito", + "details": "Details", + "view-series": "View Series", + "clear": "Clear", + "import-cbl": "Import CBL" }, + "preferences": { + "left-to-right": "Left to Right", + "right-to-left": "Right to Left", + "horizontal": "Horizontal", + "vertical": "Vertical", + "automatic": "Automatic", + "fit-to-height": "Fit to Height", + "fit-to-width": "Fit to Width", + "original": "Original", + "fit-to-screen": "Fit to Screen", + "no-split": "No Split", + "webtoon": "Webtoon", + "single": "Single", + "double": "Double", + "double-manga": "Double (Manga)", + "scroll": "Scroll", + "1-column": "1 Column", + "2-column": "2 Column", + "cards": "Cards", + "list": "List", + "up-to-down": "Up to Down" + }, "validation": { diff --git a/UI/Web/src/assets/langs/es.json b/UI/Web/src/assets/langs/es.json index 6ab6a241f..24391904f 100644 --- a/UI/Web/src/assets/langs/es.json +++ b/UI/Web/src/assets/langs/es.json @@ -21,7 +21,10 @@ "not-valid-email": "El correo electronico tiene que ser válido", "saving": "Guardando …", "update": "Actualizar", - "required": "Este campo es obligatorio" + "required": "Este campo es obligatorio", + "close": "Cerrar", + "email": "dirección de correo", + "cancel": "Cancelar" }, "user-scrobble-history": { "data-header": "Datos", @@ -29,6 +32,7 @@ "filter-label": "Filtro", "type-header": "Tipo", "rating": "Puntuación {{r}}", - "no-data": "No hay datos" + "no-data": "No hay datos", + "last-modified-header": "Última Modificación" } } diff --git a/UI/Web/src/assets/langs/nl.json b/UI/Web/src/assets/langs/nl.json new file mode 100644 index 000000000..feb67b3e5 --- /dev/null +++ b/UI/Web/src/assets/langs/nl.json @@ -0,0 +1,1631 @@ +{ + "login": { + "title": "", + "username": "", + "password": "", + "password-validation": "", + "forgot-password": "", + "submit": "" + }, + "dashboard": { + "no-libraries": "", + "server-settings-link": "", + "not-granted": "", + "on-deck-title": "", + "recently-updated-title": "", + "recently-added-title": "" + }, + "edit-user": { + "edit": "", + "close": "", + "username": "", + "required": "", + "email": "", + "not-valid-email": "", + "cancel": "", + "saving": "", + "update": "" + }, + "user-scrobble-history": { + "title": "", + "description": "", + "filter-label": "", + "created-header": "", + "last-modified-header": "", + "type-header": "", + "series-header": "", + "data-header": "", + "is-processed-header": "", + "no-data": "", + "volume-and-chapter-num": "", + "rating": "", + "not-applicable": "", + "processed": "", + "not-processed": "" + }, + "scrobble-event-type-pipe": { + "chapter-read": "", + "score-updated": "", + "want-to-read-add": "", + "want-to-read-remove": "", + "review": "" + }, + "spoiler": { + "click-to-show": "" + }, + "review-series-modal": { + "title": "", + "tagline-label": "", + "review-label": "", + "close": "", + "save": "" + }, + "review-card-modal": { + "close": "", + "user-review": "", + "external-mod": "", + "go-to-review": "" + }, + "review-card": { + "your-review": "", + "external-review": "", + "local-review": "", + "rating-percentage": "" + }, + "want-to-read": { + "title": "", + "series-count": "", + "no-items": "", + "no-items-filtered": "" + }, + "user-preferences": { + "title": "", + "pref-description": "", + "account-tab": "", + "preferences-tab": "", + "3rd-party-clients-tab": "", + "theme-tab": "", + "devices-tab": "", + "stats-tab": "", + "scrobbling-tab": "", + "success-toast": "", + "global-settings-title": "", + "page-layout-mode-label": "", + "page-layout-mode-tooltip": "", + "locale-label": "", + "locale-tooltip": "", + "blur-unread-summaries-label": "", + "blur-unread-summaries-tooltip": "", + "prompt-on-download-label": "", + "prompt-on-download-tooltip": "", + "disable-animations-label": "", + "disable-animations-tooltip": "", + "collapse-series-relationships-label": "", + "collapse-series-relationships-tooltip": "", + "share-series-reviews-label": "", + "share-series-reviews-tooltip": "", + "image-reader-settings-title": "", + "reading-direction-label": "", + "reading-direction-tooltip": "", + "scaling-option-label": "", + "scaling-option-tooltip": "", + "page-splitting-label": "", + "page-splitting-tooltip": "", + "reading-mode-label": "", + "layout-mode-label": "", + "layout-mode-tooltip": "", + "background-color-label": "", + "auto-close-menu-label": "", + "show-screen-hints-label": "", + "emulate-comic-book-label": "", + "swipe-to-paginate-label": "", + "book-reader-settings-title": "", + "tap-to-paginate-label": "", + "tap-to-paginate-tooltip": "", + "immersive-mode-label": "", + "immersive-mode-tooltip": "", + "reading-direction-book-label": "", + "reading-direction-book-tooltip": "", + "font-family-label": "", + "font-family-tooltip": "", + "writing-style-label": "", + "writing-style-tooltip": "", + "layout-mode-book-label": "", + "layout-mode-book-tooltip": "", + "color-theme-book-label": "", + "color-theme-book-tooltip": "", + "font-size-book-label": "", + "line-height-book-label": "", + "line-height-book-tooltip": "", + "margin-book-label": "", + "margin-book-tooltip": "", + "clients-opds-alert": "", + "clients-opds-description": "", + "clients-api-key-tooltip": "", + "clients-opds-url-tooltip": "", + "reset": "", + "save": "" + }, + "user-holds": { + "title": "", + "description": "" + }, + "theme-manager": { + "title": "", + "looking-for-theme": "", + "looking-for-theme-continued": "", + "scan": "", + "site-themes": "", + "set-default": "", + "apply": "", + "applied": "", + "updated-toastr": "", + "scan-queued": "" + }, + "restriction-selector": { + "title": "", + "description": "", + "not-applicable-for-admins": "", + "age-rating-label": "", + "no-restriction": "", + "include-unknowns-label": "", + "include-unknowns-tooltip": "" + }, + "site-theme-provider-pipe": { + "system": "", + "user": "" + }, + "manage-devices": { + "title": "", + "description": "", + "devices-title": "", + "no-devices": "", + "platform-label": "", + "email-label": "", + "add": "", + "delete": "", + "edit": "" + }, + "edit-device": { + "device-name-label": "", + "email-label": "", + "email-tooltip": "", + "device-platform-label": "", + "save": "", + "required-field": "", + "valid-email": "" + }, + "change-password": { + "password-label": "", + "current-password-label": "", + "new-password-label": "", + "confirm-password-label": "", + "reset": "", + "edit": "", + "cancel": "", + "save": "", + "required-field": "", + "passwords-must-match": "", + "permission-error": "" + }, + "change-email": { + "email-label": "", + "current-password-label": "", + "email-not-confirmed": "", + "email-updated-title": "", + "email-updated-description": "", + "setup-user-account": "", + "invite-url-label": "", + "invite-url-tooltip": "", + "permission-error": "", + "required-field": "", + "reset": "", + "edit": "", + "cancel": "", + "save": "" + }, + "change-age-restriction": { + "age-restriction-label": "", + "unknowns": "", + "reset": "", + "edit": "", + "cancel": "", + "save": "" + }, + "api-key": { + "copy": "", + "regen-warning": "", + "no-key": "", + "confirm-reset": "", + "key-reset": "" + }, + "scrobbling-providers": { + "title": "", + "requires": "", + "token-expired": "", + "no-token-set": "", + "token-set": "", + "generate": "", + "instructions": "", + "token-input-label": "", + "edit": "", + "cancel": "", + "save": "" + }, + "typeahead": { + "locked-field": "", + "close": "", + "loading": "", + "add-item": "", + "no-data": "", + "add-custom-item": "" + }, + "generic-list-modal": { + "close": "", + "clear": "", + "filter": "", + "open-filtered-search": "" + }, + "user-stats-info-cards": { + "total-pages-read-label": "", + "total-pages-read-tooltip": "", + "total-words-read-label": "", + "total-words-read-tooltip": "", + "time-spent-reading-label": "", + "time-spent-reading-tooltip": "", + "chapters-read-label": "", + "chapters-read-tooltip": "", + "avg-reading-per-week-label": "", + "last-active-label": "", + "chapters": "" + }, + "user-stats": { + "library-read-progress-title": "", + "read-percentage": "" + }, + "top-readers": { + "title": "", + "time-selection-label": "", + "comics-label": "", + "manga-label": "", + "books-label": "", + "this-week": "", + "last-7-days": "", + "last-30-days": "", + "last-90-days": "", + "last-year": "", + "all-time": "" + }, + "role-selector": { + "title": "" + }, + "directory-picker": { + "title": "", + "close": "", + "path-label": "", + "path-placeholder": "", + "instructions": "", + "type-header": "", + "name-header": "", + "cancel": "", + "share": "", + "help": "" + }, + "library-access-modal": { + "select-all": "", + "deselect-all": "", + "title": "", + "close": "", + "reset": "", + "cancel": "", + "save": "", + "no-data": "" + }, + "time-periods": { + "this-week": "", + "last-7-days": "", + "last-30-days": "", + "last-90-days": "", + "last-year": "", + "all-time": "" + }, + "device-platform-pipe": { + "custom": "" + }, + "day-of-week-pipe": { + "monday": "", + "tuesday": "", + "wednesday": "", + "thursday": "", + "friday": "", + "saturday": "", + "sunday": "" + }, + "cbl-import-result-pipe": { + "success": "", + "partial": "", + "failure": "" + }, + "cbl-conflict-reason-pipe": { + "all-series-missing": "", + "chapter-missing": "", + "empty-file": "", + "name-conflict": "", + "series-collision": "", + "series-missing": "", + "volume-missing": "", + "all-chapter-missing": "", + "invalid-file": "", + "success": "" + }, + "time-duration-pipe": { + "hours": "", + "minutes": "", + "days": "", + "months": "", + "years": "" + }, + "time-ago-pipe": { + "just-now": "", + "min-ago": "", + "mins-ago": "", + "hour-ago": "", + "hours-ago": "", + "day-ago": "", + "days-ago": "", + "month-ago": "", + "months-ago": "", + "year-ago": "", + "years-ago": "" + }, + "relationship-pipe": { + "adaptation": "", + "alternative-setting": "", + "alternative-version": "", + "character": "", + "contains": "", + "doujinshi": "", + "other": "", + "prequel": "", + "sequel": "", + "side-story": "", + "spin-off": "", + "parent": "", + "edition": "" + }, + "publication-status-pipe": { + "ongoing": "", + "hiatus": "", + "completed": "", + "cancelled": "", + "ended": "" + }, + "person-role-pipe": { + "artist": "", + "character": "", + "colorist": "", + "cover-artist": "", + "editor": "", + "inker": "", + "letterer": "", + "penciller": "", + "publisher": "", + "writer": "", + "other": "" + }, + "manga-format-pipe": { + "epub": "", + "archive": "", + "image": "", + "pdf": "", + "unknown": "" + }, + "library-type-pipe": { + "book": "", + "comic": "", + "manga": "" + }, + "age-rating-pipe": { + "unknown": "", + "early-childhood": "", + "adults-only": "", + "everyone": "", + "everyone-10-plus": "", + "g": "", + "kids-to-adults": "", + "mature": "", + "ma15-plus": "", + "mature-17-plus": "", + "rating-pending": "", + "teen": "", + "x18-plus": "", + "not-applicable": "", + "pg": "", + "r18-plus": "" + }, + "reset-password": { + "title": "", + "description": "", + "email-label": "", + "required-field": "", + "valid-email": "", + "submit": "" + }, + "reset-password-modal": { + "title": "", + "new-password-label": "", + "error-label": "", + "close": "", + "cancel": "", + "save": "" + }, + "all-series": { + "series-count": "" + }, + "announcements": { + "title": "" + }, + "changelog": { + "installed": "", + "download": "", + "published-label": "", + "available": "", + "description": "", + "description-continued": "" + }, + "invite-user": { + "title": "", + "close": "", + "description": "", + "email": "", + "required-field": "", + "setup-user-title": "", + "setup-user-description": "", + "setup-user-account": "", + "setup-user-account-tooltip": "", + "invite-url-label": "", + "invite": "", + "inviting": "", + "cancel": "" + }, + "library-selector": { + "title": "", + "select-all": "", + "deselect-all": "", + "no-data": "" + }, + "license": { + "title": "", + "manage": "", + "invalid-license-tooltip": "", + "check": "", + "cancel": "", + "edit": "", + "buy": "", + "activate": "", + "renew": "", + "no-license-key": "", + "license-valid": "", + "license-not-valid": "", + "loading": "", + "activate-description": "", + "activate-license-label": "", + "activate-email-label": "", + "activate-delete": "", + "activate-save": "" + }, + "book-line-overlay": { + "copy": "", + "bookmark": "", + "close": "", + "required-field": "", + "bookmark-label": "", + "save": "" + }, + "book-reader": { + "title": "", + "page-label": "", + "pagination-header": "", + "go-to-page": "", + "go-to-last-page": "", + "prev-page": "", + "next-page": "", + "prev-chapter": "", + "next-chapter": "", + "skip-header": "", + "virtual-pages": "", + "settings-header": "", + "table-of-contents-header": "", + "bookmarks-header": "", + "toc-header": "", + "loading-book": "", + "go-back": "", + "incognito-mode-alt": "", + "incognito-mode-label": "", + "next": "", + "previous": "" + }, + "personal-table-of-contents": { + "no-data": "", + "page": "", + "delete": "" + }, + "confirm-email": { + "title": "", + "description": "", + "error-label": "", + "username-label": "", + "password-label": "", + "email-label": "", + "required-field": "", + "valid-email": "", + "password-validation": "", + "register": "" + }, + "confirm-email-change": { + "title": "", + "non-confirm-description": "", + "confirm-description": "", + "success": "" + }, + "confirm-reset-password": { + "title": "", + "description": "", + "password-label": "", + "required-field": "", + "submit": "", + "password-validation": "" + }, + "register": { + "title": "", + "description": "", + "username-label": "", + "email-label": "", + "email-tooltip": "", + "password-label": "", + "required-field": "", + "valid-email": "", + "password-validation": "", + "register": "" + }, + "series-detail": { + "page-settings-title": "", + "close": "", + "layout-mode-label": "", + "layout-mode-option-card": "", + "layout-mode-option-list": "", + "continue-from": "", + "read": "", + "continue": "", + "read-options-alt": "", + "incognito": "", + "remove-from-want-to-read": "", + "add-to-want-to-read": "", + "edit-series-alt": "", + "download-series--tooltip": "", + "downloading-status": "", + "user-reviews-alt": "", + "storyline-tab": "", + "books-tab": "", + "volumes-tab": "", + "specials-tab": "", + "related-tab": "", + "recommendations-tab": "", + "send-to": "", + "no-pages": "", + "no-chapters": "", + "cover-change": "" + }, + "series-metadata-detail": { + "links-title": "", + "genres-title": "", + "tags-title": "", + "collections-title": "", + "reading-lists-title": "", + "writers-title": "", + "cover-artists-title": "", + "characters-title": "", + "colorists-title": "", + "editors-title": "", + "inkers-title": "", + "letterers-title": "", + "translators-title": "", + "pencillers-title": "", + "publishers-title": "", + "promoted": "", + "see-more": "", + "see-less": "" + }, + "badge-expander": { + "more-items": "" + }, + "read-more": { + "read-more": "", + "read-less": "" + }, + "update-notification-modal": { + "title": "", + "close": "", + "help": "", + "download": "" + }, + "side-nav-companion-bar": { + "page-settings-title": "", + "open-filter-and-sort": "", + "close-filter-and-sort": "", + "filter-and-sort-alt": "" + }, + "side-nav": { + "home": "", + "want-to-read": "", + "collections": "", + "reading-lists": "", + "bookmarks": "", + "filter-label": "", + "all-series": "", + "clear": "", + "donate": "" + }, + "library-settings-modal": { + "close": "", + "edit-title": "", + "add-title": "", + "general-tab": "", + "folder-tab": "", + "cover-tab": "", + "advanced-tab": "", + "name-label": "", + "library-name-unique": "", + "last-scanned-label": "", + "type-label": "", + "type-tooltip": "", + "folder-description": "", + "browse": "", + "help-us-part-1": "", + "help-us-part-2": "", + "help-us-part-3": "", + "naming-conventions-part-1": "", + "naming-conventions-part-2": "", + "naming-conventions-part-3": "", + "cover-description": "", + "cover-description-extra": "", + "manage-collection-label": "", + "manage-collection-tooltip": "", + "manage-reading-list-label": "", + "manage-reading-list-tooltip": "", + "allow-scrobbling-label": "", + "allow-scrobbling-tooltip": "", + "folder-watching-label": "", + "folder-watching-tooltip": "", + "include-in-dashboard-label": "", + "include-in-dashboard-tooltip": "", + "include-in-recommendation-label": "", + "include-in-recommendation-tooltip": "", + "include-in-search-label": "", + "include-in-search-tooltip": "", + "force-scan": "", + "force-scan-tooltip": "", + "reset": "", + "cancel": "", + "next": "", + "save": "", + "required-field": "" + }, + "reader-settings": { + "general-settings-title": "", + "font-family-label": "", + "font-size-label": "", + "line-spacing-label": "", + "margin-label": "", + "reset-to-defaults": "", + "reader-settings-title": "", + "reading-direction-label": "", + "right-to-left": "", + "left-to-right": "", + "horizontal": "", + "vertical": "", + "writing-style-label": "", + "writing-style-tooltip": "", + "tap-to-paginate-label": "", + "tap-to-paginate-tooltip": "", + "on": "", + "off": "", + "immersive-mode-label": "", + "immersive-mode-tooltip": "", + "fullscreen-label": "", + "fullscreen-tooltip": "", + "exit": "", + "enter": "", + "layout-mode-label": "", + "layout-mode-tooltip": "", + "layout-mode-option-scroll": "", + "layout-mode-option-1col": "", + "layout-mode-option-2col": "", + "color-theme-title": "", + "theme-dark": "", + "theme-black": "", + "theme-white": "", + "theme-paper": "" + }, + "table-of-contents": { + "no-data": "" + }, + "bookmarks": { + "title": "", + "series-count": "", + "no-data": "", + "no-data-2": "", + "confirm-delete": "", + "confirm-single-delete": "", + "delete-success": "", + "delete-single-success": "" + }, + "bulk-operations": { + "title": "", + "items-selected": "", + "mark-as-unread": "", + "mark-as-read": "", + "deselect-all": "" + }, + "card-detail-drawer": { + "general-tab": "", + "metadata-tab": "", + "cover-tab": "", + "info-tab": "", + "no-summary": "", + "writers-title": "", + "genres-title": "", + "publishers-title": "", + "tags-title": "", + "not-defined": "", + "read": "", + "unread": "", + "files": "", + "pages": "", + "added": "", + "size": "" + }, + "card-detail-layout": { + "total-items": "" + }, + "card-item": { + "cannot-read": "" + }, + "chapter-metadata-detail": { + "no-data": "", + "writers-title": "", + "publishers-title": "", + "characters-title": "", + "translators-title": "", + "letterers-title": "", + "colorists-title": "", + "inkers-title": "", + "pencillers-title": "", + "cover-artists-title": "", + "editors-title": "" + }, + "cover-image-chooser": { + "drag-n-drop": "", + "upload": "", + "upload-continued": "", + "url-label": "", + "load": "", + "back": "", + "reset-cover-tooltip": "", + "reset": "", + "image-num": "", + "apply": "", + "applied": "" + }, + "download-indicator": { + "progress": "" + }, + "edit-series-relation": { + "description-part-1": "", + "description-part-2": "", + "target-series": "", + "relationship": "", + "remove": "", + "add-relationship": "", + "parent": "" + }, + "entity-info-cards": { + "tags-title": "", + "characters-title": "", + "release-date-title": "", + "release-date-tooltip": "", + "age-rating-title": "", + "length-title": "", + "pages-count": "", + "words-count": "", + "reading-time-title": "", + "date-added-title": "", + "size-title": "", + "id-title": "", + "links-title": "", + "isbn-title": "", + "last-read-title": "", + "less-than-hour": "", + "range-hours": "", + "hour": "", + "hours": "" + }, + "series-info-cards": { + "release-date-title": "", + "release-year-tooltip": "", + "age-rating-title": "", + "language-title": "", + "publication-status-title": "", + "publication-status-tooltip": "", + "scrobbling-title": "", + "scrobbling-tooltip": "", + "on": "", + "off": "", + "disabled": "", + "format-title": "", + "last-read-title": "", + "length-title": "", + "read-time-title": "", + "less-than-hour": "", + "hour": "", + "hours": "", + "time-left-title": "", + "ongoing": "", + "pages-count": "", + "words-count": "" + }, + "bulk-add-to-collection": { + "title": "", + "promoted": "", + "close": "", + "filter-label": "", + "clear": "", + "no-data": "", + "loading": "", + "collection-label": "", + "create": "" + }, + "entity-title": { + "special": "", + "issue-num": "", + "chapter": "" + }, + "external-series-card": { + "open-external": "" + }, + "list-item": { + "read": "" + }, + "manage-alerts": { + "description-part-1": "", + "description-part-2": "", + "filter-label": "", + "clear-alerts": "", + "extension-header": "", + "file-header": "", + "comment-header": "", + "details-header": "" + }, + "manage-email-settings": { + "title": "", + "description": "", + "send-to-warning": "", + "email-url-label": "", + "email-url-tooltip": "", + "reset": "", + "test": "", + "host-name-label": "", + "host-name-tooltip": "", + "host-name-validation": "", + "reset-to-default": "", + "save": "" + }, + "manage-library": { + "title": "", + "add-library": "", + "no-data": "", + "loading": "", + "last-scanned-title": "", + "shared-folders-title": "", + "type-title": "", + "scan-library": "", + "delete-library": "", + "delete-library-by-name": "", + "edit-library": "", + "edit-library-by-name": "" + }, + "manage-media-settings": { + "encode-as-description-part-1": "", + "encode-as-description-part-2": "", + "encode-as-description-part-3": "", + "encode-as-warning": "", + "media-warning": "", + "encode-as-label": "", + "encode-as-tooltip": "", + "bookmark-dir-label": "", + "bookmark-dir-tooltip": "", + "change": "", + "reset-to-default": "", + "reset": "", + "save": "", + "media-issue-title": "", + "scrobble-issue-title": "" + }, + "manage-scrobble-errors": { + "description": "", + "filter-label": "", + "clear-errors": "", + "series-header": "", + "created-header": "", + "comment-header": "", + "edit-header": "", + "edit-item-alt": "" + }, + "default-date-pipe": { + "never": "" + }, + "manage-settings": { + "notice": "", + "restart-required": "", + "base-url-label": "", + "base-url-tooltip": "", + "ip-address-label": "", + "ip-address-tooltip": "", + "port-label": "", + "port-tooltip": "", + "backup-label": "", + "backup-tooltip": "", + "log-label": "", + "log-tooltip": "", + "logging-level-label": "", + "logging-level-tooltip": "", + "cache-size-label": "", + "cache-size-tooltip": "", + "on-deck-last-progress-label": "", + "on-deck-last-progress-tooltip": "", + "on-deck-last-chapter-add-label": "", + "on-deck-last-chapter-add-tooltip": "", + "allow-stats-label": "", + "allow-stats-tooltip-part-1": "", + "allow-stats-tooltip-part-2": "", + "send-data": "", + "opds-label": "", + "opds-tooltip": "", + "enable-opds": "", + "folder-watching-label": "", + "folder-watching-tooltip": "", + "enable-folder-watching": "", + "reset-to-default": "", + "reset": "", + "save": "", + "cache-size-validation": "", + "field-required": "", + "max-logs-validation": "", + "min-logs-validation": "", + "min-days-validation": "", + "min-cache-validation": "", + "max-backup-validation": "", + "min-backup-validation": "", + "ip-address-validation": "", + "base-url-validation": "" + }, + "manage-system": { + "title": "", + "version-title": "", + "installId-title": "", + "more-info-title": "", + "home-page-title": "", + "wiki-title": "", + "discord-title": "", + "donations-title": "", + "source-title": "", + "feature-request-title": "" + }, + "manage-tasks-settings": { + "title": "", + "library-scan-label": "", + "library-scan-tooltip": "", + "library-database-backup-label": "", + "library-database-backup-tooltip": "", + "adhoc-tasks-title": "", + "job-title-header": "", + "description-header": "", + "action-header": "", + "reset-to-default": "", + "reset": "", + "save": "", + "recurring-tasks-title": "", + "last-executed-header": "", + "cron-header": "", + "convert-media-task": "", + "convert-media-task-desc": "", + "convert-media-success": "", + "bust-cache-task": "", + "bust-cache-task-desc": "", + "bust-cache-task-success": "", + "clear-reading-cache-task": "", + "clear-reading-cache-task-desc": "", + "clear-reading-cache-task-success": "", + "clean-up-want-to-read-task": "", + "clean-up-want-to-read-task-desc": "", + "clean-up-want-to-read-task-success": "", + "backup-database-task": "", + "backup-database-task-desc": "", + "backup-database-task-success": "", + "download-logs-task": "", + "download-logs-task-desc": "", + "analyze-files-task": "", + "analyze-files-task-desc": "", + "analyze-files-task-success": "", + "check-for-updates-task": "", + "check-for-updates-task-desc": "" + }, + "manage-users": { + "title": "", + "invite": "", + "you-alt": "", + "pending-title": "", + "delete-user-tooltip": "", + "delete-user-alt": "", + "edit-user-tooltip": "", + "edit-user-alt": "", + "resend-invite-tooltip": "", + "resend-invite-alt": "", + "setup-user-tooltip": "", + "setup-user-alt": "", + "change-password-tooltip": "", + "change-password-alt": "", + "resend": "", + "setup": "", + "last-active-title": "", + "roles-title": "", + "none": "", + "never": "", + "online-now-tooltip": "", + "sharing-title": "", + "no-data": "", + "loading": "" + }, + "edit-collection-tags": { + "title": "", + "required-field": "", + "save": "", + "close": "", + "cancel": "", + "general-tab": "", + "cover-image-tab": "", + "series-tab": "", + "name-label": "", + "name-validation": "", + "promote-label": "", + "promote-tooltip": "", + "summary-label": "", + "series-title": "", + "deselect-all": "", + "select-all": "" + }, + "library-detail": { + "library-tab": "", + "recommended-tab": "" + }, + "library-recommended": { + "no-data": "", + "more-in-genre": "", + "rediscover": "", + "highly-rated": "", + "quick-catchups": "", + "quick-reads": "", + "on-deck": "" + }, + "admin-dashboard": { + "title": "", + "general-tab": "", + "users-tab": "", + "libraries-tab": "", + "media-tab": "", + "logs-tab": "", + "email-tab": "", + "tasks-tab": "", + "statistics-tab": "", + "system-tab": "", + "kavita+-tab": "", + "kavita+-desc-part-1": "", + "kavita+-desc-part-2": "", + "kavita+-desc-part-3": "" + }, + "collection-detail": { + "no-data": "", + "no-data-filtered": "", + "title-alt": "" + }, + "all-collections": { + "title": "", + "item-count": "", + "no-data": "", + "create-one-part-1": "", + "create-one-part-2": "" + }, + "carousel-reel": { + "prev-items": "", + "next-items": "" + }, + "draggable-ordered-list": { + "instructions-alt": "", + "reorder-label": "", + "remove-item-alt": "" + }, + "reading-lists": { + "title": "", + "item-count": "", + "no-data": "", + "create-one-part-1": "", + "create-one-part-2": "" + }, + "reading-list-item": { + "remove": "", + "read": "" + }, + "reading-list-detail": { + "item-count": "", + "page-settings-title": "", + "remove-read": "", + "order-numbers-label": "", + "continue": "", + "read": "", + "read-options-alt": "", + "incognito-alt": "", + "no-data": "" + }, + "events-widget": { + "title-alt": "", + "dismiss-all": "", + "update-available": "", + "downloading-item": "", + "more-info": "", + "close": "", + "users-online-count": "", + "active-events-title": "", + "no-data": "" + }, + "shortcuts-modal": { + "title": "", + "close": "", + "prev-page": "", + "next-page": "", + "go-to": "", + "bookmark": "", + "double-click": "", + "close-reader": "", + "toggle-menu": "" + }, + "grouped-typeahead": { + "files": "", + "chapters": "", + "people": "", + "tags": "", + "genres": "", + "libraries": "", + "reading-lists": "", + "collections": "", + "close": "", + "loading": "" + }, + "nav-header": { + "skip-alt": "", + "search-series-alt": "", + "search-alt": "", + "promoted": "", + "no-data": "", + "scroll-to-top-alt": "", + "server-settings": "", + "settings": "", + "help": "", + "announcements": "", + "logout": "" + }, + "add-to-list-modal": { + "title": "", + "close": "", + "filter-label": "", + "promoted-alt": "", + "no-data": "", + "loading": "", + "reading-list-label": "", + "create": "" + }, + "edit-reading-list-modal": { + "title": "", + "general-tab": "", + "cover-image-tab": "", + "close": "", + "save": "", + "year-validation": "", + "month-validation": "", + "name-unique-validation": "", + "required-field": "", + "summary-label": "", + "year-label": "", + "month-label": "", + "ending-title": "", + "starting-title": "", + "promote-label": "", + "promote-tooltip": "" + }, + "import-cbl-modal": { + "close": "", + "title": "", + "import-description": "", + "validate-description": "", + "validate-warning": "", + "validate-no-issue": "", + "validate-no-issue-description": "", + "dry-run-description": "", + "prev": "", + "import": "", + "restart": "", + "next": "", + "import-step": "", + "validate-cbl-step": "", + "dry-run-step": "", + "final-import-step": "" + }, + "pdf-reader": { + "loading-message": "", + "incognito-mode": "", + "light-theme-alt": "", + "dark-theme-alt": "", + "close-reader-alt": "" + }, + "infinite-reader": { + "continuous-reading-prev-chapter-alt": "", + "continuous-reading-prev-chapter": "", + "continuous-reading-next-chapter-alt": "", + "continuous-reading-next-chapter": "" + }, + "manga-reader": { + "back": "", + "save-globally": "", + "incognito-alt": "", + "incognito-title": "", + "shortcuts-menu-alt": "", + "prev-page-tooltip": "", + "next-page-tooltip": "", + "prev-chapter-tooltip": "", + "next-chapter-tooltip": "", + "first-page-tooltip": "", + "last-page-tooltip": "", + "left-to-right-alt": "", + "right-to-left-alt": "", + "reading-direction-tooltip": "", + "reading-mode-tooltip": "", + "collapse": "", + "fullscreen": "", + "settings-tooltip": "", + "image-splitting-label": "", + "image-scaling-label": "", + "height": "", + "width": "", + "original": "", + "auto-close-menu-label": "", + "swipe-enabled-label": "", + "enable-comic-book-label": "", + "brightness-label": "", + "first-time-reading-manga": "", + "layout-mode-switched": "", + "no-next-chapter": "", + "no-prev-chapter": "", + "user-preferences-updated": "" + }, + "metadata-filter": { + "filter-title": "", + "format-label": "", + "format-tooltip": "", + "libraries-label": "", + "collections-label": "", + "genres-label": "", + "tags-label": "", + "cover-artist-label": "", + "writer-label": "", + "publisher-label": "", + "penciller-label": "", + "letterer-label": "", + "inker-label": "", + "editor-label": "", + "colorist-label": "", + "character-label": "", + "translator-label": "", + "read-progress-label": "", + "unread": "", + "read": "", + "in-progress": "", + "rating-label": "", + "age-rating-label": "", + "language-label": "", + "publication-status-label": "", + "series-name-label": "", + "series-name-tooltip": "", + "release-label": "", + "min": "", + "max": "", + "sort-by-label": "", + "ascending-alt": "", + "descending-alt": "", + "reset": "", + "apply": "" + }, + "sort-field-pipe": { + "sort-name": "", + "created": "", + "last-modified": "", + "last-chapter-added": "", + "time-to-read": "", + "release-year": "" + }, + "edit-series-modal": { + "title": "", + "general-tab": "", + "metadata-tab": "", + "people-tab": "", + "web-links-tab": "", + "cover-image-tab": "", + "related-tab": "", + "info-tab": "", + "collections-label": "", + "genres-label": "", + "tags-label": "", + "cover-artist-label": "", + "writer-label": "", + "publisher-label": "", + "penciller-label": "", + "letterer-label": "", + "inker-label": "", + "editor-label": "", + "colorist-label": "", + "character-label": "", + "translator-label": "", + "language-label": "", + "age-rating-label": "", + "publication-status-label": "", + "required-field": "", + "close": "", + "name-label": "", + "sort-name-label": "", + "localized-name-label": "", + "summary-label": "", + "release-year-label": "", + "web-link-description": "", + "web-link-label": "", + "add-link-alt": "", + "remove-link-alt": "", + "cover-image-description": "", + "save": "", + "field-locked-alt": "", + "info-title": "", + "library-title": "", + "format-title": "", + "created-title": "", + "last-read-title": "", + "last-added-title": "", + "last-scanned-title": "", + "folder-path-title": "", + "publication-status-title": "", + "total-pages-title": "", + "total-items-title": "", + "max-items-title": "", + "size-title": "", + "loading": "", + "added-title": "", + "last-modified-title": "", + "view-files": "", + "pages-title": "", + "chapter-title": "", + "volume-num": "", + "highest-count-tooltip": "", + "max-issue-tooltip": "" + }, + "day-breakdown": { + "title": "", + "x-axis-label": "", + "y-axis-label": "" + }, + "file-breakdown-stats": { + "format-title": "", + "format-tooltip": "", + "visualisation-label": "", + "data-table-label": "", + "extension-header": "", + "format-header": "", + "total-size-header": "", + "total-files-header": "", + "not-classified": "", + "total-file-size-title": "" + }, + "reading-activity": { + "title": "", + "legend-label": "", + "x-axis-label": "", + "y-axis-label": "", + "no-data": "", + "time-frame-label": "" + }, + "manga-format-stats": { + "title": "", + "visualisation-label": "", + "data-table-label": "", + "format-header": "", + "count-header": "" + }, + "publication-status-stats": { + "title": "", + "visualisation-label": "", + "data-table-label": "", + "year-header": "", + "count-header": "" + }, + "server-stats": { + "total-series-label": "", + "total-series-tooltip": "", + "total-volumes-label": "", + "total-volumes-tooltip": "", + "total-files-label": "", + "total-files-tooltip": "", + "total-size-label": "", + "total-genres-label": "", + "total-genres-tooltip": "", + "total-tags-label": "", + "total-tags-tooltip": "", + "total-people-label": "", + "total-people-tooltip": "", + "total-read-time-label": "", + "total-read-time-tooltip": "", + "series": "", + "reads": "", + "release-years-title": "", + "most-active-users-title": "", + "popular-libraries-title": "", + "popular-series-title": "", + "recently-read-title": "", + "genre-count": "", + "tag-count": "", + "people-count": "", + "tags": "", + "people": "", + "genres": "" + }, + "errors": { + "series-doesnt-exist": "", + "collection-invalid-access": "", + "unknown-crit": "", + "user-not-auth": "", + "error-code": "", + "download": "", + "not-found": "", + "generic": "", + "rejected-cover-upload": "", + "invalid-confirmation-url": "", + "invalid-confirmation-email": "", + "invalid-password-reset-url": "" + }, + "toasts": { + "regen-cover": "", + "no-pages": "", + "download-in-progress": "", + "scan-queued": "", + "server-settings-updated": "", + "reset-ip-address": "", + "reset-base-url": "", + "unauthorized-1": "", + "unauthorized-2": "", + "no-updates": "", + "confirm-delete-user": "", + "user-deleted": "", + "email-sent-to-user": "", + "click-email-link": "", + "series-added-to-collection": "", + "no-series-collection-warning": "", + "collection-updated": "", + "reading-list-deleted": "", + "reading-list-updated": "", + "confirm-delete-reading-list": "", + "item-removed": "", + "nothing-to-remove": "", + "series-added-to-reading-list": "", + "volumes-added-to-reading-list": "", + "chapter-added-to-reading-list": "", + "multiple-added-to-reading-list": "", + "select-files-warning": "", + "reading-list-imported": "", + "incognito-off": "", + "email-service-reset": "", + "email-service-reachable": "", + "email-service-unresponsive": "", + "refresh-covers-queued": "", + "library-file-analysis-queued": "", + "entity-read": "", + "entity-unread": "", + "mark-read": "", + "mark-unread": "", + "series-removed-want-to-read": "", + "series-deleted": "", + "file-send-to": "", + "theme-missing": "", + "email-sent": "", + "k+-license-saved": "", + "k+-unlocked": "", + "k+-error": "", + "k+-delete-key": "", + "library-deleted": "", + "copied-to-clipboard": "", + "book-settings-info": "", + "no-next-chapter": "", + "no-prev-chapter": "", + "load-next-chapter": "", + "load-prev-chapter": "", + "account-registration-complete": "", + "account-migration-complete": "", + "password-reset": "", + "password-updated": "", + "forced-scan-queued": "", + "library-created": "", + "anilist-token-updated": "", + "age-restriction-updated": "", + "email-sent-to-no-existing": "", + "email-sent-to": "", + "change-email-private": "", + "device-updated": "", + "device-created": "", + "confirm-regen-covers": "", + "alert-long-running": "", + "confirm-delete-multiple-series": "", + "confirm-delete-series": "", + "alert-bad-theme": "", + "confirm-library-delete": "", + "confirm-library-type-change": "", + "confirm-download-size": "" + }, + "actionable": { + "remove-from-on-deck": "", + "others": "" + }, + "validation": { + "required-field": "", + "valid-email": "", + "password-validation": "" + }, + "entity-type": { + "volume": "", + "chapter": "", + "series": "", + "bookmark": "", + "logs": "" + }, + "common": { + "reset-to-default": "", + "close": "", + "cancel": "", + "create": "", + "save": "", + "reset": "", + "add": "", + "apply": "", + "delete": "", + "edit": "", + "help": "", + "submit": "", + "email": "", + "read": "", + "loading": "", + "username": "", + "password": "", + "promoted": "", + "select-all": "", + "deselect-all": "", + "series-count": "", + "item-count": "", + "book-num": "", + "issue-hash-num": "", + "issue-num": "", + "chapter-num": "", + "volume-num": "" + } +} diff --git a/UI/Web/src/main.ts b/UI/Web/src/main.ts index 84421e410..aa520734f 100644 --- a/UI/Web/src/main.ts +++ b/UI/Web/src/main.ts @@ -47,6 +47,33 @@ export const preLoad = { deps: [AccountService, TranslocoService] }; +// All Languages Kavita will support: http://www.lingoes.net/en/translator/langcode.htm +const languageCodes = [ + 'af', 'af-ZA', 'ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', + 'ar-LB', 'ar-LY', 'ar-MA', 'ar-OM', 'ar-QA', 'ar-SA', 'ar-SY', 'ar-TN', 'ar-YE', + 'az', 'az-AZ', 'az-AZ', 'be', 'be-BY', 'bg', 'bg-BG', 'bs-BA', 'ca', 'ca-ES', 'cs', + 'cs-CZ', 'cy', 'cy-GB', 'da', 'da-DK', 'de', 'de-AT', 'de-CH', 'de-DE', 'de-LI', 'de-LU', + 'dv', 'dv-MV', 'el', 'el-GR', 'en', 'en-AU', 'en-BZ', 'en-CA', 'en-CB', 'en-GB', 'en-IE', + 'en-JM', 'en-NZ', 'en-PH', 'en-TT', 'en-US', 'en-ZA', 'en-ZW', 'eo', 'es', 'es-AR', 'es-BO', + 'es-CL', 'es-CO', 'es-CR', 'es-DO', 'es-EC', 'es-ES', 'es-ES', 'es-GT', 'es-HN', 'es-MX', + 'es-NI', 'es-PA', 'es-PE', 'es-PR', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et', 'et-EE', + 'eu', 'eu-ES', 'fa', 'fa-IR', 'fi', 'fi-FI', 'fo', 'fo-FO', 'fr', 'fr-BE', 'fr-CA', + 'fr-CH', 'fr-FR', 'fr-LU', 'fr-MC', 'gl', 'gl-ES', 'gu', 'gu-IN', 'he', 'he-IL', 'hi', + 'hi-IN', 'hr', 'hr-BA', 'hr-HR', 'hu', 'hu-HU', 'hy', 'hy-AM', 'id', 'id-ID', 'is', + 'is-IS', 'it', 'it-CH', 'it-IT', 'ja', 'ja-JP', 'ka', 'ka-GE', 'kk', 'kk-KZ', 'kn', + 'kn-IN', 'ko', 'ko-KR', 'kok', 'kok-IN', 'ky', 'ky-KG', 'lt', 'lt-LT', 'lv', 'lv-LV', + 'mi', 'mi-NZ', 'mk', 'mk-MK', 'mn', 'mn-MN', 'mr', 'mr-IN', 'ms', 'ms-BN', 'ms-MY', + 'mt', 'mt-MT', 'nb', 'nb-NO', 'nl', 'nl-BE', 'nl-NL', 'nn-NO', 'ns', 'ns-ZA', 'pa', + 'pa-IN', 'pl', 'pl-PL', 'ps', 'ps-AR', 'pt', 'pt-BR', 'pt-PT', 'qu', 'qu-BO', 'qu-EC', + 'qu-PE', 'ro', 'ro-RO', 'ru', 'ru-RU', 'sa', 'sa-IN', 'se', 'se-FI', 'se-FI', 'se-FI', + 'se-NO', 'se-NO', 'se-NO', 'se-SE', 'se-SE', 'se-SE', 'sk', 'sk-SK', 'sl', 'sl-SI', + 'sq', 'sq-AL', 'sr-BA', 'sr-BA', 'sr-SP', 'sr-SP', 'sv', 'sv-FI', 'sv-SE', 'sw', 'sw-KE', + 'syr', 'syr-SY', 'ta', 'ta-IN', 'te', 'te-IN', 'th', 'th-TH', 'tl', 'tl-PH', 'tn', + 'tn-ZA', 'tr', 'tr-TR', 'tt', 'tt-RU', 'ts', 'uk', 'uk-UA', 'ur', 'ur-PK', 'uz', + 'uz-UZ', 'uz-UZ', 'vi', 'vi-VN', 'xh', 'xh-ZA', 'zh', 'zh-CN', 'zh-HK', 'zh-MO', + 'zh-SG', 'zh-TW', 'zu', 'zu-ZA' +]; + bootstrapApplication(AppComponent, { providers: [ importProvidersFrom(BrowserModule, @@ -82,7 +109,7 @@ bootstrapApplication(AppComponent, { provide: TRANSLOCO_CONFIG, useValue: { reRenderOnLangChange: true, - availableLangs: ['en', 'es'], // TODO: Derive this from the directory + availableLangs: languageCodes, // TODO: Derive this from the directory prodMode: environment.production, defaultLang: 'en', fallbackLang: 'en', diff --git a/openapi.json b/openapi.json index e70d7d442..18abbc66e 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.6.1" + "version": "0.7.6.2" }, "servers": [ {