Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
|
@ -17,7 +18,6 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
@ -49,7 +49,7 @@ public interface IReadingListService
|
|||
/// <summary>
|
||||
/// Methods responsible for management of Reading Lists
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
public class ReadingListService : IReadingListService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
@ -69,13 +69,13 @@ public class ReadingListService : IReadingListService
|
|||
public static string FormatTitle(ReadingListItemDto item)
|
||||
{
|
||||
var title = string.Empty;
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter && item.VolumeNumber != Tasks.Scanner.Parser.Parser.DefaultVolume) {
|
||||
if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.DefaultVolume) {
|
||||
title = $"Volume {item.VolumeNumber}";
|
||||
}
|
||||
|
||||
if (item.SeriesFormat == MangaFormat.Epub) {
|
||||
var specialTitle = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
var specialTitle = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Parser.DefaultChapter)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
else
|
||||
{
|
||||
title = $"Volume {Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
title = $"Volume {Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
}
|
||||
} else {
|
||||
title = $"Volume {specialTitle}";
|
||||
|
@ -92,12 +92,12 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var chapterNum = item.ChapterNumber;
|
||||
if (!string.IsNullOrEmpty(chapterNum) && !JustNumbers.Match(item.ChapterNumber).Success) {
|
||||
chapterNum = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
chapterNum = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
}
|
||||
|
||||
if (title != string.Empty) return title;
|
||||
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter &&
|
||||
if (item.ChapterNumber == Parser.DefaultChapter &&
|
||||
!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
title = item.ChapterTitleName;
|
||||
|
@ -124,13 +124,13 @@ public class ReadingListService : IReadingListService
|
|||
var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title));
|
||||
if (hasExisting)
|
||||
{
|
||||
throw new KavitaException("A list of this name already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
}
|
||||
|
||||
var readingList = new ReadingListBuilder(title).Build();
|
||||
userWithReadingList.ReadingLists.Add(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("There was a problem creating list");
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create");
|
||||
await _unitOfWork.CommitAsync();
|
||||
return readingList;
|
||||
}
|
||||
|
@ -144,10 +144,10 @@ public class ReadingListService : IReadingListService
|
|||
public async Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto)
|
||||
{
|
||||
dto.Title = dto.Title.Trim();
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("Title must be set");
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required");
|
||||
|
||||
if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title))
|
||||
throw new KavitaException("Reading list already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
|
||||
readingList.Summary = dto.Summary;
|
||||
readingList.Title = dto.Title.Trim();
|
||||
|
@ -192,7 +192,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <summary>
|
||||
/// Removes all entries that are fully read from the reading list. This commits
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
/// <param name="readingListId">Reading List Id</param>
|
||||
/// <param name="user">User</param>
|
||||
/// <returns></returns>
|
||||
|
@ -404,7 +404,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
||||
.OrderBy(c => Tasks.Scanner.Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting)
|
||||
.ToList();
|
||||
|
||||
|
@ -529,7 +529,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <param name="cblReading"></param>
|
||||
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading)
|
||||
{
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
@ -542,20 +542,20 @@ public class ReadingListService : IReadingListService
|
|||
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
|
||||
{
|
||||
importSummary.Success = CblImportResult.Fail;
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict,
|
||||
ReadingListName = cblReading.Name
|
||||
});
|
||||
}
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
if (!userSeries.Any())
|
||||
{
|
||||
// Report that no series exist in the reading list
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.AllSeriesMissing
|
||||
});
|
||||
|
@ -569,7 +569,7 @@ public class ReadingListService : IReadingListService
|
|||
importSummary.Success = CblImportResult.Fail;
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.SeriesCollision,
|
||||
Series = conflict.Name,
|
||||
|
@ -593,7 +593,7 @@ public class ReadingListService : IReadingListService
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
@ -601,13 +601,13 @@ public class ReadingListService : IReadingListService
|
|||
SuccessfulInserts = new List<CblBookResult>()
|
||||
};
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.LocalizedName));
|
||||
var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName));
|
||||
|
||||
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
|
||||
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
|
||||
// Get all the user's reading lists
|
||||
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
|
||||
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
|
||||
|
@ -620,7 +620,7 @@ public class ReadingListService : IReadingListService
|
|||
// Reading List exists, check if we own it
|
||||
if (user.ReadingLists.All(l => l.NormalizedTitle != readingListNameNormalized))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict
|
||||
});
|
||||
|
@ -632,7 +632,7 @@ public class ReadingListService : IReadingListService
|
|||
readingList.Items ??= new List<ReadingListItem>();
|
||||
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
||||
{
|
||||
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
|
||||
var normalizedSeries = Parser.Normalize(book.Series);
|
||||
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
@ -644,7 +644,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
|
||||
var bookVolume = string.IsNullOrEmpty(book.Volume)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultVolume
|
||||
? Parser.DefaultVolume
|
||||
: book.Volume;
|
||||
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0);
|
||||
if (matchingVolume == null)
|
||||
|
@ -660,7 +660,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
// We need to handle chapter 0 or empty string when it's just a volume
|
||||
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultChapter
|
||||
? Parser.DefaultChapter
|
||||
: book.Number;
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
|
||||
if (chapter == null)
|
||||
|
@ -720,7 +720,7 @@ public class ReadingListService : IReadingListService
|
|||
private static IList<Series> FindCblImportConflicts(IEnumerable<Series> userSeries)
|
||||
{
|
||||
var dict = new HashSet<string>();
|
||||
return userSeries.Where(series => !dict.Add(Tasks.Scanner.Parser.Parser.Normalize(series.Name))).ToList();
|
||||
return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList();
|
||||
}
|
||||
|
||||
private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary,
|
||||
|
@ -729,7 +729,7 @@ public class ReadingListService : IReadingListService
|
|||
readingListFromCbl = new CblImportSummaryDto();
|
||||
if (cblReading.Books == null || cblReading.Books.Book.Count == 0)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.EmptyFile
|
||||
});
|
||||
|
@ -755,7 +755,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
public static CblReadingList LoadCblFromPath(string path)
|
||||
{
|
||||
var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
|
||||
var reader = new XmlSerializer(typeof(CblReadingList));
|
||||
using var file = new StreamReader(path);
|
||||
var cblReadingList = (CblReadingList) reader.Deserialize(file);
|
||||
file.Close();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue