Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add localization #12

Merged
merged 14 commits into from
Jul 14, 2024
42 changes: 42 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
- [Description](#description)
- [Documentation](#documentation)
- [Branches](#branches)
- [Workflows](#workflows)
- [Variables and secrets](#variables-and-secrets)
- [.env file](#env-file)
- [Languages](#languages)
- [Resource file](#resource-file)
- [Integration Tests](#integration-tests)
- [Add language to the supported cultures](#add-language-to-the-supported-cultures)
- [Version](#version)
- [Database fields](#database-fields)
- [Primary key](#primary-key)
- [All fields](#all-fields)
- [Foreign Keys](#foreign-keys)
- [Working locally](#working-locally)
- [Local build](#local-build)
- [Running the container](#running-the-container)

# Description
You are welcome to participate in the development of this tool, in this file some information and rules for the development are described.

Expand Down Expand Up @@ -32,6 +51,29 @@ In order for the workflows to run on your GitHub account, the following variable
# .env file
In order that parameter values are not directly in the docker-compose.yml, the .env file is used here. This allows the parameters and values to be specified as a key-value pair in this file. The [CreateEnvFile.ps1](./src/CreateEnvFile.ps1) script was created to avoid having to create the file manually. If new parameters are defined for the docker-compose.yml file, the script should be extended accordingly.

# Languages
A new language can be added very easily, you need Visual Studio, you can download it [here](https://visualstudio.microsoft.com/downloads/). In this example, we will add `Spanish` as a new language.

## Resource file
Create a new resource file in the folder [Resources](./src/BulkRename/Resources) and provide your language code between the file `SharedResource` name and the extension `resx`, for example `SharedResource.es.resx`. Copy all the keys from the [default language file](./src/BulkRename/Resources/SharedResource.resx) which is English, and add the translations. Check out this [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/provide-resources?view=aspnetcore-8.0) to learn more about resource files.

### Integration Tests
There are a few integration tests, that will ensure that all the language keys, that exist in the English version, have also been translated to the new language. Please run [these tests](./src/BulkRename.IntegrationTests/Resources/LanguageResourcesTests.cs) before creating a pull request.

## Add language to the supported cultures
Go to the class [Program.cs](./src/BulkRename/Program.cs) and add your language to the `supportedCultures` with the corresponding culture.

```c#
var supportedCultures = new[]
{
new CultureInfo(defaultCulture),
new CultureInfo("hu"),
new CultureInfo("de"),
// Add here your new CultureInfo
new CultureInfo("es")
};
```

# Version
The version is set in the following files:
- VERSION in [Dockerfile](./src/Dockerfile)
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- [Renamed series overview](#renamed-series-overview)
- [History](#history)
- [Development](#development)
- [Adding new languages](#adding-new-languages)
- [Contributors ✨](#contributors-)


Expand Down Expand Up @@ -178,6 +179,9 @@ Call up the history page by pressing **History (8)**. The first time, the page w
# Development
Please read the [development documentation](./DEVELOPMENT.md) if you would like to participate in the development.

## Adding new languages
This app can provide multiple languages and includes already the languages `English`, `German`, and `Hungarian`. A new language can be added within a few steps, to do this, please check out the [documentation](./DEVELOPMENT.md#languages).

## Contributors ✨

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
Expand Down
15 changes: 15 additions & 0 deletions src/BulkRename.IntegrationTests/Resources/LanguageResourceEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace BulkRename.IntegrationTests.Resources
{
internal class LanguageResourceEntry
{
public LanguageResourceEntry(string name, string value)
{
Name = name;
Value = value;
}

public string Name { get; }

public string Value { get; }
}
}
173 changes: 173 additions & 0 deletions src/BulkRename.IntegrationTests/Resources/LanguageResourcesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
namespace BulkRename.IntegrationTests.Resources
{
using System.Collections.Generic;
using System.Linq;
using System.Xml;

[TestFixture]
internal class LanguageResourcesTests
{
private const string APP_RESOURCES = "SharedResource";

private const string APP_RESOURCES_FILE_ENDING = "resx";

private const string DEFAULT_LANGUAGE_CODE = "en";

private const string DEFAULT_LANGUAGE_RESOURCE_FILENAME = "SharedResource.resx";

private const int LANGUAGE_CODE_LENGTH = 2;

private List<string> GetLanguageResourceFiles()
{
var files = Directory.GetFiles(@"../../../../BulkRename/Resources/", "*.resx").ToList();
return files;
}

private List<LanguageResourceEntry> ReadLanguageResourceEntries(string filePath)
{
var list = new List<LanguageResourceEntry>();

var document = new XmlDocument();
document.Load(filePath);
var node = document.GetElementsByTagName("data");
foreach (XmlElement element in node)
{
var name = element.GetAttribute("name");
var value = element.ChildNodes[1]!.InnerText;

list.Add(new LanguageResourceEntry(name, value));
}

return list;
}

private Dictionary<string, List<LanguageResourceEntry>> GetLanguageDictionary()
{
var languageDictionary = new Dictionary<string, List<LanguageResourceEntry>>();
var files = GetLanguageResourceFiles();

var defaultLanguageFile = files.Single(f => Path.GetFileName(f).Equals(DEFAULT_LANGUAGE_RESOURCE_FILENAME));
var defaultLanguageResourceEntries = ReadLanguageResourceEntries(defaultLanguageFile);
languageDictionary.Add(DEFAULT_LANGUAGE_CODE, defaultLanguageResourceEntries);

foreach (var filePath in files)
{
var fileName = Path.GetFileName(filePath);
if (fileName == DEFAULT_LANGUAGE_RESOURCE_FILENAME)
{
continue;
}

var parts = fileName.Split('.');
var language = parts[1];
var languageResourceEntries = ReadLanguageResourceEntries(filePath);

languageDictionary.Add(language, languageResourceEntries);
}

return languageDictionary;
}

[Test]
public void GetAllLanguageFiles_CheckNamingConvention_NamingIsCorrect()
{
var files = GetLanguageResourceFiles();

var defaultFileErrorMessage = $"There should be a file called '{DEFAULT_LANGUAGE_RESOURCE_FILENAME}', it actually doesn't exist";
Assert.That(files.Any(f => Path.GetFileName(f).Equals(DEFAULT_LANGUAGE_RESOURCE_FILENAME)), defaultFileErrorMessage);

foreach (var file in files)
{
var fileName = Path.GetFileName(file);
if (fileName == DEFAULT_LANGUAGE_RESOURCE_FILENAME)
{
continue;
}

var parts = fileName.Split('.');
var appResourcesPart = parts[0];
var appResourcesPartErrorMessage = $"Language resource file name has to start with '{APP_RESOURCES}', actual one is '{appResourcesPart}'";
Assert.That(appResourcesPart, Is.EqualTo(APP_RESOURCES), appResourcesPartErrorMessage);

var languageCodePart = parts[1];
var languageCodeErrorMessage = $"Language code should be {LANGUAGE_CODE_LENGTH} characters string, actual one is named '{languageCodePart}'";
Assert.That(languageCodePart, Has.Length.EqualTo(LANGUAGE_CODE_LENGTH), languageCodeErrorMessage);

var fileEndingPart = parts[2];
var fileEndingPartErrorMessage = $"The language resource file name has to end with '{APP_RESOURCES_FILE_ENDING}', actual is '{fileEndingPart}'";
Assert.That(fileEndingPart, Is.EqualTo(APP_RESOURCES_FILE_ENDING), fileEndingPartErrorMessage);
}
}

[Test]
public void ReadAllLanguageResources_CheckKeysCount_AllKeysCountAreSame()
{
// arrange
var languageDictionary = GetLanguageDictionary();

// act
languageDictionary.TryGetValue(DEFAULT_LANGUAGE_CODE, out var defaultLanguageList);
var defaultCount = defaultLanguageList!.Count;

foreach (var dictionary in languageDictionary)
{
if (dictionary.Key.Equals(DEFAULT_LANGUAGE_CODE))
{
continue;
}

var currentCount = dictionary.Value.Count;
var dictionaryCountErrorMessage = $"The language '{dictionary.Key}' should have {defaultCount} entries but actually has {currentCount} entries";

// assert
Assert.That(currentCount, Is.EqualTo(defaultCount), dictionaryCountErrorMessage);
}
}

[Test]
public void ReadAllLanguageResources_CheckAllValues_NoneOfThemIsEmpty()
{
// arrange
var languageDictionary = GetLanguageDictionary();

foreach (var dictionary in languageDictionary)
{
foreach (var entry in dictionary.Value)
{
// act
var entryHasEmptyValueErrorMessage = $"Entry with the name '{entry.Name}' has an empty value in '{dictionary.Key}' language";

// assert
Assert.That(entry.Value, Is.Not.Empty, entryHasEmptyValueErrorMessage);
}
}
}

[Test]
public void ReadAllLanguageResources_CheckOtherLanguages_AllEntriesExistInDefaultLanguage()
{
// arrange
var languageDictionary = GetLanguageDictionary();

// act
languageDictionary.TryGetValue(DEFAULT_LANGUAGE_CODE, out var defaultLanguageList);

foreach (var dictionary in languageDictionary)
{
if (dictionary.Key.Equals(DEFAULT_LANGUAGE_CODE))
{
continue;
}

foreach (var entry in dictionary.Value)
{
var exists = defaultLanguageList!.Any(l => l.Name.Equals(entry.Name));
var entryDoesntExistInDefaultLanguageMessage = $"Entry with the name '{entry.Name}' does not exist in default language but in language '{dictionary.Key}'";

// assert
Assert.That(exists, entryDoesntExistInDefaultLanguageMessage);
}
}
}
}
}
39 changes: 39 additions & 0 deletions src/BulkRename/Constants/LocalizationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace BulkRename.Constants
{
public static class LocalizationConstants
{
public const string AN_ERROR_OCCURED_HEADER = "AnErrorOccuredHeader";

public const string DETAILED_ERROR_MESSAGE = "DetailedErrorMessage";

public const string DEVELOPMENT_MODE = "DevelopmentMode";

public const string ERROR = "Error";

public const string HISTORY = "History";

public const string HOME = "Home";

public const string LOAD_HISTORY = "LoadHistory";

public const string NEW_NAME = "NewName";

public const string OLD_NAME = "OldName";

public const string PREVIEW_RENAMING_OF_TV_SHOWS = "PreviewRenamingOfTvShows";

public const string RENAMED_ON = "RenamedOn";

public const string REQUEST = "Request";

public const string SERIES = "Series";

public const string SUBMIT_RENAMING = "Submit Renaming";

public const string SUCCESSFULLY_RENAMED_FILES = "SuccessfullyRenamedFiles";

public const string SWAPPING_TO_DEVELOPMENT_MODE_DISPLAY = "SwappingToDevelopmentModeDisplay";

public const string WELCOME_TO_BULK_RENAME = "WelcomeToBulkRename";
}
}
10 changes: 8 additions & 2 deletions src/BulkRename/Controllers/HistoryController.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
namespace BulkRename.Controllers
{
using BulkRename.Constants;
using BulkRename.Interfaces;
using BulkRename.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

public class HistoryController : Controller
{
private readonly IPersistanceService _persistanceService;

private static readonly Dictionary<string, List<Series>> _dictionary = [];

public HistoryController(IPersistanceService persistanceService)
private readonly string _renamedOn;

public HistoryController(IPersistanceService persistanceService, IStringLocalizer<SharedResource> sharedLocalizer)
{
_persistanceService = persistanceService;

_renamedOn = sharedLocalizer[LocalizationConstants.RENAMED_ON];
}

public IActionResult Index()
Expand Down Expand Up @@ -47,7 +53,7 @@ public async Task<IActionResult> LoadHistory()
});
}

var key = $"{renamingSessionToEpisode.RenamingSession.RenName}, Renamed on: {renamingSessionToEpisode.RenamingSession.RenExecutingDateTime:yyyy-MM-dd HH:mm:ss}";
var key = $"{renamingSessionToEpisode.RenamingSession.RenName}, {_renamedOn}: {renamingSessionToEpisode.RenamingSession.RenExecutingDateTime:yyyy-MM-dd HH:mm:ss}";
_dictionary.Add(key, series);
}

Expand Down
38 changes: 37 additions & 1 deletion src/BulkRename/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using BulkRename;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Serilog;
using System.Globalization;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
Expand All @@ -8,10 +12,42 @@
startup.ConfigureServices(builder.Services);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddControllersWithViews()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
const string defaultCulture = "en";

var supportedCultures = new[]
{
new CultureInfo(defaultCulture),
new CultureInfo("hu"),
new CultureInfo("de")
};

builder.Services.Configure<RequestLocalizationOptions>(options => {
options.DefaultRequestCulture = new RequestCulture(defaultCulture);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});

builder.Services.AddRazorPages().AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
var assemblyName = new AssemblyName(typeof(SharedResource)!.GetTypeInfo()!.Assembly!.FullName!);

return factory.Create("SharedResource", assemblyName!.Name!);
};
});

var app = builder.Build();

app.UseRequestLocalization();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
Expand Down
Loading