Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
97 changes: 79 additions & 18 deletions src/My.Extensions.Localization.Json/Internal/JsonResourceManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand All @@ -8,23 +9,36 @@ namespace My.Extensions.Localization.Json.Internal;

public class JsonResourceManager
{
private readonly JsonFileWatcher _jsonFileWatcher;
private readonly List<JsonFileWatcher> _jsonFileWatchers = new();
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _resourcesCache = new();
private readonly ConcurrentDictionary<string, HashSet<string>> _loadedFilesCache = new();


public JsonResourceManager(string resourcesPath, string resourceName = null)
public JsonResourceManager(string resourcesPath, string resourceName = null, params string[] additionalResourcesPaths)
{
ResourcesPath = resourcesPath;
ResourceName = resourceName;
AdditionalResourcesPaths = additionalResourcesPaths ?? Array.Empty<string>();

_jsonFileWatcher = new(resourcesPath);
_jsonFileWatcher.Changed += RefreshResourcesCache;
SetupFileWatcher(resourcesPath);

foreach (var additionalPath in AdditionalResourcesPaths)
{
SetupFileWatcher(additionalPath);
}
}

public JsonResourceManager(string resourcesPath, params string[] additionalResourcesPaths)
: this(resourcesPath, null, additionalResourcesPaths)
{
}

public string ResourceName { get; }

public string ResourcesPath { get; }

public string[] AdditionalResourcesPaths { get; }
Copy link
Owner

Choose a reason for hiding this comment

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

@copilot we could change ResourcesPath instead of introducing a new property

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean changing ResourcesPath from a single string to a collection (like IList<string>), so users can specify multiple paths directly? That would be a breaking change for existing users since it changes the type. Could you clarify the preferred approach?

Copy link
Owner

Choose a reason for hiding this comment

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

Use string[] instead as the return type. Don't worry about the breaking change, this will be included as part of the 4.0.0 release

Copy link
Owner

Choose a reason for hiding this comment

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

@copilot, apply the changes based on my above comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed ResourcesPath from string to string[] in commit ec0bb44. Removed the separate AdditionalResourcesPaths property.


public string ResourcesFilePath { get; private set; }

public virtual ConcurrentDictionary<string, string> GetResourceSet(CultureInfo culture, bool tryParents)
Expand Down Expand Up @@ -111,68 +125,115 @@ public virtual string GetString(string name, CultureInfo culture)
}

private void TryLoadResourceSet(CultureInfo culture)
{
// Load from primary resources path
TryLoadResourceSetFromPath(ResourcesPath, culture);

// Load from additional resources paths (merging resources)
foreach (var additionalPath in AdditionalResourcesPaths)
{
TryLoadResourceSetFromPath(additionalPath, culture);
}
}

private void TryLoadResourceSetFromPath(string basePath, CultureInfo culture)
{
if (string.IsNullOrEmpty(ResourceName))
{
var file = Path.Combine(ResourcesPath, $"{culture.Name}.json");
var file = Path.Combine(basePath, $"{culture.Name}.json");

TryAddResources(file);
TryAddResources(file, culture);
}
else
{
var resourceFiles = Enumerable.Empty<string>();
var rootCulture = culture.Name[..2];
if (ResourceName.Contains('.'))
{
resourceFiles = Directory.EnumerateFiles(ResourcesPath, $"{ResourceName}.{rootCulture}*.json");
if (Directory.Exists(basePath))
{
resourceFiles = Directory.EnumerateFiles(basePath, $"{ResourceName}.{rootCulture}*.json");
}

if (!resourceFiles.Any())
{
resourceFiles = GetResourceFiles(rootCulture);
resourceFiles = GetResourceFiles(basePath, rootCulture);
}
}
else
{
resourceFiles = GetResourceFiles(rootCulture);
resourceFiles = GetResourceFiles(basePath, rootCulture);
}

foreach (var file in resourceFiles)
{
var fileName = Path.GetFileNameWithoutExtension(file);
var cultureName = fileName[(fileName.LastIndexOf('.') + 1)..];

culture = CultureInfo.GetCultureInfo(cultureName);
var fileCulture = CultureInfo.GetCultureInfo(cultureName);

TryAddResources(file);
TryAddResources(file, fileCulture);
}
}

IEnumerable<string> GetResourceFiles(string culture)
IEnumerable<string> GetResourceFiles(string baseResourcesPath, string cultureName)
{
var resourcePath = ResourceName.Replace('.', Path.AltDirectorySeparatorChar);
var resourcePathLastDirectorySeparatorIndex = resourcePath.LastIndexOf(Path.AltDirectorySeparatorChar);
var resourceName = resourcePath[(resourcePathLastDirectorySeparatorIndex + 1)..];
var resourcesPath = resourcePathLastDirectorySeparatorIndex == -1
? ResourcesPath
: Path.Combine(ResourcesPath, resourcePath[..resourcePathLastDirectorySeparatorIndex]);
? baseResourcesPath
: Path.Combine(baseResourcesPath, resourcePath[..resourcePathLastDirectorySeparatorIndex]);

return Directory.Exists(resourcesPath)
? Directory.EnumerateFiles(resourcesPath, $"{resourceName}.{culture}*.json")
? Directory.EnumerateFiles(resourcesPath, $"{resourceName}.{cultureName}*.json")
: [];
}

void TryAddResources(string resourceFile)
void TryAddResources(string resourceFile, CultureInfo resourceCulture)
{
var key = $"{ResourceName}.{culture.Name}";
var key = $"{ResourceName}.{resourceCulture.Name}";

// Track loaded files to avoid re-loading
var loadedFiles = _loadedFilesCache.GetOrAdd(key, _ => new HashSet<string>());
if (!loadedFiles.Add(resourceFile))
{
// File already loaded for this key, skip
return;
}

if (!_resourcesCache.ContainsKey(key))
{
var resources = JsonResourceLoader.Load(resourceFile);

_resourcesCache.TryAdd(key, new ConcurrentDictionary<string, string>(resources));
}
else
{
// Merge resources from additional paths (don't override existing keys)
var existingResources = _resourcesCache[key];
var additionalResources = JsonResourceLoader.Load(resourceFile);

foreach (var item in additionalResources)
{
existingResources.TryAdd(item.Key, item.Value);
}
}
}
}

private void SetupFileWatcher(string path)
{
if (!Directory.Exists(path))
{
return;
}

var watcher = new JsonFileWatcher(path);
watcher.Changed += RefreshResourcesCache;
_jsonFileWatchers.Add(watcher);
}

private void RefreshResourcesCache(object sender, FileSystemEventArgs e)
{
var key = Path.GetFileNameWithoutExtension(e.FullPath);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;

namespace My.Extensions.Localization.Json;

public class JsonLocalizationOptions : LocalizationOptions
{
public ResourcesType ResourcesType { get; set; } = ResourcesType.TypeBased;

public IList<string> AdditionalResourcesPaths { get; set; } = new List<string>();
}
12 changes: 10 additions & 2 deletions src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
Expand All @@ -17,6 +19,7 @@ public class JsonStringLocalizerFactory : IStringLocalizerFactory
private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
private readonly ConcurrentDictionary<string, JsonStringLocalizer> _localizerCache = new();
private readonly string _resourcesRelativePath;
private readonly IList<string> _additionalResourcesPaths;
private readonly ResourcesType _resourcesType = ResourcesType.TypeBased;
private readonly ILoggerFactory _loggerFactory;

Expand All @@ -27,6 +30,7 @@ public JsonStringLocalizerFactory(
ArgumentNullException.ThrowIfNull(localizationOptions);

_resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
_additionalResourcesPaths = localizationOptions.Value.AdditionalResourcesPaths ?? new List<string>();
_resourcesType = localizationOptions.Value.ResourcesType;
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
Expand Down Expand Up @@ -90,9 +94,13 @@ protected virtual JsonStringLocalizer CreateJsonStringLocalizer(
string resourcesPath,
string resourceName)
{
var additionalResourcesPaths = _additionalResourcesPaths
.Select(p => Path.Combine(PathHelpers.GetApplicationRoot(), p))
.ToArray();

var resourceManager = _resourcesType == ResourcesType.TypeBased
? new JsonResourceManager(resourcesPath, resourceName)
: new JsonResourceManager(resourcesPath);
? new JsonResourceManager(resourcesPath, resourceName, additionalResourcesPaths)
: new JsonResourceManager(resourcesPath, additionalResourcesPaths);
var logger = _loggerFactory.CreateLogger<JsonStringLocalizer>();

return new JsonStringLocalizer(resourceManager, _resourceNamesCache, logger);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"AdditionalKey": "Clé supplémentaire",
"Welcome": "Bienvenue"
}
Loading
Loading