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

Data Localization #4466

Open
wants to merge 68 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a126cae
Add data localization infrastructure APIs
hishamco Oct 3, 2019
129296e
Inherit from IStringLocalizer
hishamco Oct 3, 2019
1f5e087
Add unit tests
hishamco Oct 3, 2019
c95b6ea
Add ILocalizationDataProvider and Content implementations
hishamco Oct 3, 2019
0b9b455
First attempt to support import/export deployment step
hishamco Oct 4, 2019
f62c403
Add TranslationsManager unit tests
hishamco Oct 5, 2019
c84f2b4
Introduce DataLocalizedString
hishamco Oct 5, 2019
4589796
Merge branch 'dev' into hishamco/data-localization
Skrypt Dec 20, 2020
6a0431b
Fixing build
Skrypt Dec 20, 2020
94b6599
Cleanup
Skrypt Dec 20, 2020
4a1d923
Fix bug in CreateContentTypeDefinition()
hishamco Dec 21, 2020
9ab13e5
Address feedback
hishamco Dec 22, 2020
b7f74cf
Fix unit tests
hishamco Dec 22, 2020
609cf58
Merge branch 'dev' into skrypt/data-localization
Skrypt Dec 24, 2020
4194e42
Merge branch 'dev' into skrypt/data-localization
Skrypt Jan 1, 2021
25c74b8
Merge branch 'dev' into skrypt/data-localization
Skrypt Jan 4, 2021
8c6a71d
Merge branch 'dev' into skrypt/data-localization
Skrypt Jan 7, 2021
b756c6a
Merge branch 'dev' into skrypt/data-localization
Skrypt Jan 11, 2021
c20de77
Merge branch 'dev' into skrypt/data-localization
Skrypt Jan 23, 2021
0950120
Move to OrchardCore.Localization.Dynamic as a separate feature with a…
Skrypt Jan 23, 2021
2f769ae
Remove unnecessary using
Skrypt Jan 23, 2021
dae05ab
Merge branch 'dev' into skrypt/data-localization
Skrypt Feb 3, 2021
66642dc
Merge branch 'dev' into skrypt/data-localization
Skrypt Feb 5, 2021
5a7e40e
Fix solution after last merge
Skrypt Feb 5, 2021
7ee8f59
Merge branch 'dev' into skrypt/data-localization
Skrypt Feb 17, 2021
b418f5b
Merge branch 'dev' into skrypt/data-localization
Skrypt Feb 26, 2021
1ca4a96
Fix merge and rename to OrchardCore.Localization.Data
Skrypt Feb 26, 2021
3d3d7e3
Merge branch 'dev' into skrypt/data-localization
Skrypt Mar 2, 2021
3a96304
Merge branch 'dev' into skrypt/data-localization
Skrypt Mar 4, 2021
bb8787a
Merge branch 'dev' into skrypt/data-localization
Skrypt May 7, 2021
833eafc
Merge remote-tracking branch 'origin/dev' into hishamco/data-localiza…
hishamco May 7, 2021
01f7d86
DynamicData -> Data
hishamco May 7, 2021
a416e05
Fix extnsion method name
hishamco May 7, 2021
713e089
DataLocalizer should support pluralization
hishamco May 7, 2021
4c7fa54
Call AddDataLocalization() in OC.Localization.Data module
hishamco May 7, 2021
52f1d6d
Fix localizer type in tests
hishamco May 7, 2021
51d4922
Add proper IDataLocalizer registration
hishamco May 7, 2021
a28a2b5
Tweaks
hishamco May 8, 2021
a33c2d8
Uncomment LocalizerReturnsTranslationFromInnerClass() test
hishamco May 8, 2021
1434c94
Add DataLocalizerFactory unit tests
hishamco May 8, 2021
3f1d0a4
OrchardCore.Localization.Data -> OrchardCore.DataLocalization
hishamco May 8, 2021
2ef68b8
Allow mutiple ITranslationProvider registration
hishamco May 8, 2021
3ecac03
Add missing changes in solution file
hishamco May 8, 2021
d4e33f2
Fix OC.DataLocalization folder
hishamco May 8, 2021
c62cd8a
Fix the build
hishamco May 8, 2021
75f9d48
Removing unnecessary views
Skrypt May 10, 2021
2332b87
Merge branch 'dev' into hishamco/data-localization
Skrypt May 10, 2021
b7fa41b
Rename to GetAllStrings to GetDescriptors in ILocalizationDataProvider
Skrypt May 14, 2021
9ca5fdc
Renaming
Skrypt May 14, 2021
734ffa2
Merge branch 'dev' into hishamco/data-localization
Skrypt May 31, 2021
1c88a22
Merge branch 'main' into hishamco/data-localization
hishamco Mar 30, 2023
f981fff
Fix merge conflict
hishamco Mar 30, 2023
5ce581a
New updates from OCC
hishamco Mar 30, 2023
e8dedb2
Fix broken test
hishamco Mar 30, 2023
9fb846a
Fix unit tests namespace
hishamco Mar 30, 2023
6d78ae3
usings cleanup
hishamco Mar 30, 2023
4b029f4
Use file-scoped namespace
hishamco Mar 30, 2023
d6a9bce
Fix namespace
hishamco Mar 30, 2023
dd10ed1
Remove WithCulture method
hishamco Mar 30, 2023
8026d0f
Use IPluralRuleProvider in DataResourceManager
hishamco Mar 30, 2023
4191ad4
Merge branch 'main' into hishamco/data-localization
hishamco Feb 23, 2024
94905b6
Fix the build
hishamco Feb 24, 2024
3c9cc91
Fix unit tests & refactoring
hishamco Feb 24, 2024
8ae7ce3
GetDescriptors() -> GetDescriptorsAsync()
hishamco Feb 24, 2024
594f6ed
Merge branch 'main' into hishamco/data-localization
hishamco Apr 8, 2024
ca0288b
Merge branch 'main' into hishamco/data-localization
hishamco Jun 22, 2024
773756f
Merge branch 'main' into hishamco/data-localization
sebastienros Aug 29, 2024
a172c7e
Merge branch 'main' into hishamco/data-localization
hishamco Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using OrchardCore.Localization;

namespace OrchardCore.ContentTypes.Services
{
public class ContentFieldDataLocalizationProvider : ILocalizationDataProvider
{
private readonly IContentDefinitionService _contentDefinitionService;

private static readonly string ContentFieldsContext = "Content Fields";

public ContentFieldDataLocalizationProvider(IContentDefinitionService contentDefinitionService)
{
_contentDefinitionService = contentDefinitionService;
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
}

// TODO: Check if there's a better way to get the fields
public IEnumerable<DataLocalizedString> GetAllStrings()
=> _contentDefinitionService.GetTypes()
.SelectMany(t => t.TypeDefinition.Parts)
.SelectMany(p => p.PartDefinition.Fields.Select(f => new DataLocalizedString(ContentFieldsContext, f.Name)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using OrchardCore.Localization;

namespace OrchardCore.ContentTypes.Services
{
public class ContentTypeDataLocalizationProvider : ILocalizationDataProvider
{
private readonly IContentDefinitionService _contentDefinitionService;

private static readonly string ContentTypesContext = "Content Types";

public ContentTypeDataLocalizationProvider(IContentDefinitionService contentDefinitionService)
{
_contentDefinitionService = contentDefinitionService;
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
}

public IEnumerable<DataLocalizedString> GetAllStrings()
=> _contentDefinitionService.GetTypes().Select(t => new DataLocalizedString(ContentTypesContext, t.DisplayName));
}
}
11 changes: 11 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.ContentTypes/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using OrchardCore.Navigation;
using OrchardCore.Recipes;
using OrchardCore.Security.Permissions;
using OrchardCore.Localization;

namespace OrchardCore.ContentTypes
{
Expand Down Expand Up @@ -140,4 +141,14 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<IDisplayDriver<DeploymentStep>, ContentDefinitionDeploymentStepDriver>();
}
}

[RequireFeatures("OrchardCore.Localization")]
public class LocalizationStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILocalizationDataProvider, ContentTypeDataLocalizationProvider>();
services.AddTransient<ILocalizationDataProvider, ContentFieldDataLocalizationProvider>();
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OrchardCore.Deployment;
using OrchardCore.Localization.Services;

namespace OrchardCore.Localization.Deployment
{
public class AllDataTranslationsDeploymentSource : IDeploymentSource
{
private readonly TranslationsManager _translationsManager;

public AllDataTranslationsDeploymentSource(TranslationsManager translationsManager)
{
_translationsManager = translationsManager ?? throw new ArgumentNullException(nameof(translationsManager));
}

public async Task ProcessDeploymentStepAsync(DeploymentStep step, DeploymentPlanResult result)
{
var allDataTranslationsState = step as AllDataTranslationsDeploymentStep;

if (allDataTranslationsState == null)
{
return;
}

var translationObjects = new JObject();
var translationsDocument = await _translationsManager.GetTranslationsDocumentAsync();

foreach (var translation in translationsDocument.Translations)
{
translationObjects[translation.Key] = JObject.FromObject(translation.Values);
}

result.Steps.Add(new JObject(
new JProperty("name", "DynamicDataTranslations"),
new JProperty("DynamicDataTranslations", translationObjects)
));

await Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using OrchardCore.Deployment;

namespace OrchardCore.Localization.Deployment
{
public class AllDataTranslationsDeploymentStep : DeploymentStep
{
public AllDataTranslationsDeploymentStep()
{
Name = "AllDataTranslations";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using OrchardCore.Deployment;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;

namespace OrchardCore.Localization.Deployment
{
public class AllDataTranslationsDeploymentStepDriver : DisplayDriver<DeploymentStep, AllDataTranslationsDeploymentStep>
{
public override IDisplayResult Display(AllDataTranslationsDeploymentStep step)
{
return
Combine(
View("AllDataTranslationsDeploymentStep_Summary", step).Location("Summary", "Content"),
View("AllDataTranslationsDeploymentStep_Thumbnail", step).Location("Thumbnail", "Content")
);
}

public override IDisplayResult Edit(AllDataTranslationsDeploymentStep step)
{
return View("AllDataTranslationsDeploymentStep_Edit", step).Location("Content");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;

namespace OrchardCore.Localization.Models
{
public class Translation
{
public string Context { get; set; }
public string Key { get; set; }
public IDictionary<string, string> Values { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;

namespace OrchardCore.Localization.Models
{
public class TranslationsDocument
hishamco marked this conversation as resolved.
Show resolved Hide resolved
{
public int Id { get; set; }
public List<Translation> Translations { get; } = new List<Translation>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OrchardCore.Recipes.Models;
using OrchardCore.Recipes.Services;
using OrchardCore.Localization.Models;
using OrchardCore.Localization.Services;

namespace OrchardCore.Localization.Recipes
{
public class TranslationsStep : IRecipeStepHandler
{
private readonly TranslationsManager _translationsManager;

public TranslationsStep(TranslationsManager translationsManager)
{
_translationsManager = translationsManager;
}

public async Task ExecuteAsync(RecipeExecutionContext context)
{
if (!String.Equals(context.Name, "DynamicDataTranslations", StringComparison.OrdinalIgnoreCase))
{
return;
}

if (context.Step.Property("DynamicDataTranslations").Value is JObject translations)
{
foreach (var property in translations.Properties())
{
var name = property.Name;
var value = property.Value.ToObject<Translation>();

await _translationsManager.UpdateTranslationAsync(name, value);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using OrchardCore.Localization.Models;

namespace OrchardCore.Localization.Services
{
public interface ITranslationsManager
{
Task<TranslationsDocument> GetTranslationsDocumentAsync();

Task UpdateTranslationAsync(string name, Translation template);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using OrchardCore.Localization.Models;

namespace OrchardCore.Localization.Services
{
public class TranslationsManager : ITranslationsManager
{
public async Task<TranslationsDocument> GetTranslationsDocumentAsync()
{
// TODO: Fetch the translations from the database
var document = new TranslationsDocument();
await Task.CompletedTask;

return document;
}

public async Task UpdateTranslationAsync(string name, Translation template)
{
// TODO: update the translations into the database
await Task.CompletedTask;
}
hishamco marked this conversation as resolved.
Show resolved Hide resolved
}
}
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OrchardCore.Deployment;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.Localization.Deployment;
using OrchardCore.Localization.Drivers;
using OrchardCore.Localization.Recipes;
using OrchardCore.Localization.Services;
using OrchardCore.Modules;
using OrchardCore.Navigation;
using OrchardCore.Recipes;
using OrchardCore.Security.Permissions;
using OrchardCore.Settings;

Expand All @@ -31,6 +35,13 @@ public override void ConfigureServices(IServiceCollection services)

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");
services.Replace(ServiceDescriptor.Singleton<ILocalizationFileLocationProvider, ModularPoFileLocationProvider>());

services.AddScoped<TranslationsManager>();
services.AddRecipeExecutionStep<TranslationsStep>();

services.AddTransient<IDeploymentSource, AllDataTranslationsDeploymentSource>();
services.AddSingleton<IDeploymentStepFactory>(new DeploymentStepFactory<AllDataTranslationsDeploymentStep>());
services.AddScoped<IDisplayDriver<DeploymentStep>, AllDataTranslationsDeploymentStepDriver>();
}

/// <inheritdocs />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@model dynamic
Skrypt marked this conversation as resolved.
Show resolved Hide resolved

<h5>@T["Dynamic Data Translations"]</h5>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@model dynamic

<h5>@T["Dynamic Data Translations"]</h5>

<span class="hint">@T["Adds all data translations to the plan."]</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@model dynamic

<h4 class="card-title">@T["Dynamic Data Translations"]</h4>
<p>@T["Exports all data translations."]</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace OrchardCore.Localization
{
public class DataLocalizedString
{
public DataLocalizedString(string context, string name)
{
Context = context;
Name = name;
}

public string Context { get; }
Copy link
Member

@sebastienros sebastienros May 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context: identify the piece of data to localize. Needs to be a list of strings, example: "Customer" (the type), "Address" (the part), "Zip" (the field name)

For a content type, the Context is ["ContentType", "Name"].

public string[] Context { get; } (Key?)

In a view:

  • D[contentType.Name, "ContentType", "Name"]
  • D[contentField.Name, contentType.Name, contentPart.Name, "Name"]

Maybe we don't need "Name" as part of the context, or any property, when this property is the default. We can just add this to the context when it's not the default thing of the context. Example, for content types, we don't need to add "Name" as part of the context, but we'll add "Description" for sure.

ContentTypeDataLocalizer has `LocalizedString LocalizedCategory {get;} => S["Content Types"]

When the view model is created, it will have a list of
{
LocalizedString Category {det;}
DataLocalizedString[] DataLocalizedStrings
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the editor is displayed, the DataLocalizedString are loaded from the database. The providers are also called to get the full list of localization strings. These will be grouped by localized category. Then when displayed we use the values from the database to fill the text fields. The match is done by Context keys.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentTypeDataLocalizer has `LocalizedString LocalizedCategory {get;} => S["Content Types"]

I thought about IDataResourceStringProvider might have LocalizedCategory which is good if we want to list all the providers from DI and display them in a single UI. IMHO it would be nice if each provider uses its own view, it could be turned on or off via the features system, this way whether the above property is a member of IDataResourceStringProvider or not we can display the category in the UI like a normal static localized string `S["Category Name"]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebastienros seems we forgot the last parameters for the DataLocalizer is an argument that could be used for the name/key, so context as array may not be the right option here. I'm still thinking to use something similar to message context in PO

msgctxt "OrchardCore.ArchiveLater.Views.ArchiveLaterPart.Edit"

Customer.Address.Zip as a context


public string Name { get; }

public override string ToString() => $"{Context}-{Name}";
}
}
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace OrchardCore.Localization
{
public interface ILocalizationDataProvider
{
IEnumerable<DataLocalizedString> GetAllStrings();
}
}
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Localization;

namespace OrchardCore.Localization
{
public class DataLocalizer : IDataLocalizer
{
private IDataLocalizer _localizer;

public DataLocalizer(IDataLocalizerFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}

_localizer = factory.Create();
}

public LocalizedString this[string name] => _localizer[name];

public LocalizedString this[string name, params object[] arguments] => _localizer[name, arguments];

public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
=> _localizer.GetAllStrings(includeParentCultures);

public IStringLocalizer WithCulture(CultureInfo culture) => _localizer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Extensions.Localization;

namespace OrchardCore.Localization
{
public interface IDataLocalizer : IStringLocalizer
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OrchardCore.Localization
{
public interface IDataLocalizerFactory
{
IDataLocalizer Create();
}
}
Loading