Localization - Part 2 (#2178)

* Changed language codes in the UI to be a list of all codes we will ever support.

* Converted actionables

* Fixed the GetLocales not using Intersect, but Union.

* Fixed some localization strings in backend when user doesn't exist.

Removed AllowAnonymous from reset-password, since it is a protected API

* Fixed all instances of anonymous APIs where Claim wouldn't work

* Keyed preference options and mixed misc localization issues

* Translations update from Hosted Weblate (#2177)

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Dutch)

* Bump versions by dotnet-bump-version.

* Translated using Weblate (Dutch)

Currently translated at 20.8% (33 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Spanish)

Currently translated at 1.4% (20 of 1371 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Added translation using Weblate (Dutch)

---------

Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Javier Barbero <javier.agustin.barbero@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Javier Barbero <javier.agustin.barbero@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
This commit is contained in:
Joe Milazzo 2023-08-03 17:51:32 -05:00 committed by GitHub
parent c7701bc729
commit 69532d45ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2035 additions and 195 deletions

View File

@ -71,7 +71,6 @@ public class AccountController : BaseApiController
/// </summary>
/// <param name="resetPasswordDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("reset-password")]
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
{
@ -119,7 +118,7 @@ public class AccountController : BaseApiController
public async Task<ActionResult<UserDto>> 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<ActionResult<string>> 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<ActionResult<UserDto>> 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<ActionResult<string>> 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"));
}
/// <summary>
@ -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;

View File

@ -96,14 +96,14 @@ public class BookController : BaseApiController
[AllowAnonymous]
public async Task<ActionResult> 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();

View File

@ -43,9 +43,10 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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"));
}
}

View File

@ -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);

View File

@ -107,7 +107,8 @@ public class ReaderController : BaseApiController
public async Task<ActionResult> 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<ActionResult> 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));

View File

@ -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));
}
}
}

View File

@ -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"
}

View File

@ -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
/// <returns></returns>
public IEnumerable<string> 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();
}
}

View File

@ -36,7 +36,6 @@ public class ThemeService : IThemeService
/// </summary>
/// <param name="themeId"></param>
/// <returns></returns>
[AllowAnonymous]
public async Task<string> GetContent(int themeId)
{
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);

View File

@ -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}];

View File

@ -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,

View File

@ -158,7 +158,7 @@
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{t('theme.translationKey')}}
{{t(theme.translationKey)}}
</button>
</ng-container>
</div>

View File

@ -1,37 +1,40 @@
<ng-container *ngIf="actions.length > 0">
<ng-container *transloco="let t; read: 'actionable'">
<ng-container *ngIf="actions.length > 0">
<div ngbDropdown container="body" class="d-inline-block">
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle
(click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
(click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
</div>
</div>
<ng-template #submenu let-list="list">
<ng-container *ngFor="let action of list">
<!-- Non Submenu items -->
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined ; else submenuDropdown">
<ng-template #submenu let-list="list">
<ng-container *ngFor="let action of list">
<!-- Non Submenu items -->
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined ; else submenuDropdown">
<ng-container *ngIf="action.dynamicList !== undefined && (action.dynamicList | async | dynamicList) as dList; else justItem">
<ng-container *ngFor="let dynamicItem of dList">
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
<ng-container *ngIf="action.dynamicList !== undefined && (action.dynamicList | async | dynamicList) as dList; else justItem">
<ng-container *ngFor="let dynamicItem of dList">
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
</ng-container>
</ng-container>
</ng-container>
<ng-template #justItem>
<button ngbDropdownItem *ngIf="willRenderAction(action)" (click)="performAction($event, action)" (mouseover)="closeAllSubmenus()">{{action.title}}</button>
<ng-template #justItem>
<button ngbDropdownItem *ngIf="willRenderAction(action)" (click)="performAction($event, action)" (mouseover)="closeAllSubmenus()">{{t(action.title)}}</button>
</ng-template>
</ng-container>
<ng-template #submenuDropdown>
<!-- Submenu items -->
<ng-container *ngIf="shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left" (click)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<button *ngIf="willRenderAction(action)" id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{t(action.title)}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>
</div>
</div>
</ng-container>
</ng-template>
</ng-container>
<ng-template #submenuDropdown>
<!-- Submenu items -->
<ng-container *ngIf="shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left" (click)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<button *ngIf="willRenderAction(action)" id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{action.title}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>
</div>
</div>
</ng-container>
</ng-template>
</ng-container>
</ng-template>
</ng-template>
</ng-container>
</ng-container>

View File

@ -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

View File

@ -87,8 +87,7 @@ export class SeriesCardComponent implements OnInit, OnChanges {
if (this.data) {
this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, 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,

View File

@ -171,7 +171,7 @@
<div class="{{SplitIconClass}}"></div>
</div>
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text}}</option>
</select>
</div>
@ -223,7 +223,7 @@
</ng-container>
</ng-container>
<select class="form-control" id="layout-mode" formControlName="layoutMode">
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
<option [value]="opt.value" *ngFor="let opt of layoutModesTranslated">{{opt.text}}</option>
</select>
</div>
<div class="col-md-3 col-sm-12">

View File

@ -186,8 +186,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
pagingDirection$: Observable<PAGING_DIRECTION> = 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;
}
}

View File

@ -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));

View File

@ -42,7 +42,7 @@ export class SideNavComponent implements OnInit {
libraries: Library[] = [];
actions: ActionItem<Library>[] = [];
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;

View File

@ -40,7 +40,7 @@
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
<option *ngFor="let opt of pageLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
@ -158,7 +158,7 @@
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
@ -169,7 +169,7 @@
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
@ -182,13 +182,13 @@
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
@ -201,7 +201,7 @@
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
@ -309,7 +309,7 @@
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
@ -334,7 +334,7 @@
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStyles" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
@ -345,7 +345,7 @@
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
@ -359,7 +359,7 @@
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
</select>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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": {

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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',

View File

@ -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": [
{