Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions src/My.Extensions.Localization.Json/Caching/IResourceNamesCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@

namespace My.Extensions.Localization.Json.Caching;

/// <summary>
/// Defines a cache for storing and retrieving collections of resource names by key.
/// </summary>
public interface IResourceNamesCache
{
/// <summary>
/// Gets the list of strings associated with the specified name, or adds a new list using the provided factory if
/// none exists.
/// </summary>
/// <param name="name">The key used to locate the associated list of strings. Cannot be null.</param>
/// <param name="valueFactory">A function that generates a new list of strings if the specified name does not exist. Cannot be null.</param>
/// <returns>The list of strings associated with the specified name. If the name was not present, returns the newly created
/// list from the factory.</returns>
IList<string> GetOrAdd(string name, Func<string, IList<string>> valueFactory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

namespace My.Extensions.Localization.Json.Caching;

/// <summary>
/// Provides a thread-safe cache for storing and retrieving lists of resource names by key.
/// </summary>
public class ResourceNamesCache : IResourceNamesCache
{
private readonly ConcurrentDictionary<string, IList<string>> _cache = new();

/// <inheritdoc />
public IList<string> GetOrAdd(string name, Func<string, IList<string>> valueFactory)
=> _cache.GetOrAdd(name, valueFactory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Defines a provider for retrieving all resource strings for a specified culture.
/// </summary>
public interface IResourceStringProvider
{
/// <summary>
/// Retrieves all resource strings for the specified culture.
/// </summary>
/// <param name="culture">The culture for which to retrieve resource strings. Cannot be null.</param>
/// <param name="throwOnMissing">Specifies whether to throw an exception if resource strings for the specified culture are missing. If <see
/// langword="true"/>, an exception is thrown when resources are not found; otherwise, an empty list is returned.</param>
/// <returns>A list of resource strings associated with the specified culture. The list is empty if no resources are found
/// and <paramref name="throwOnMissing"/> is <see langword="false"/>.</returns>
IList<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing);
}
20 changes: 20 additions & 0 deletions src/My.Extensions.Localization.Json/Internal/JsonFileWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Provides a mechanism for monitoring changes to JSON files within a specified directory.
/// </summary>
public class JsonFileWatcher : IDisposable
{
private const string JsonExtension = "*.json";
Expand All @@ -11,8 +14,16 @@ public class JsonFileWatcher : IDisposable

private readonly FileSystemWatcher _filesWatcher;

/// <summary>
/// Occurs when a file or directory in the specified path is changed.
/// </summary>
public event FileSystemEventHandler Changed;

/// <summary>
/// Initializes a new instance of the JsonFileWatcher class to monitor changes to JSON files in the specified
/// directory.
/// </summary>
/// <param name="rootDirectory">The path to the directory to monitor for changes to JSON files. Must be a valid directory path.</param>
public JsonFileWatcher(string rootDirectory)
{
_filesWatcher = new(rootDirectory)
Expand All @@ -24,17 +35,26 @@ public JsonFileWatcher(string rootDirectory)
_filesWatcher.Changed += (s, e) => Changed?.Invoke(s, e);
}

/// <summary>
/// Finalizes the JsonFileWatcher instance and releases unmanaged resources before the object is reclaimed by
/// garbage collection.
/// </summary>
~JsonFileWatcher()
{
Dispose(false);
}

/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(true);
}

/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
public virtual void Dispose(bool disposing)
{
if (_disposed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Provides functionality to load string resources from a JSON file into a dictionary, using dot notation for nested
/// keys.
/// </summary>
public static class JsonResourceLoader
{
private static readonly JsonDocumentOptions _jsonDocumentOptions = new()
Expand All @@ -12,7 +16,12 @@ public static class JsonResourceLoader
AllowTrailingCommas = true,
};


/// <summary>
/// Loads key-value pairs from a JSON resource file at the specified path.
/// </summary>
/// <param name="filePath">The path to the JSON file containing resource definitions. Must refer to an existing file.</param>
/// <returns>A dictionary containing resource keys and their corresponding values parsed from the file. Returns an empty
/// dictionary if the file does not exist or contains no resources.</returns>
public static IDictionary<string, string> Load(string filePath)
{
var resources = new Dictionary<string, string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,35 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Provides access to localized string resources loaded from JSON files, supporting culture-specific lookups and
/// optional fallback to parent UI cultures.
/// </summary>
public class JsonResourceManager
{
private readonly List<JsonFileWatcher> _jsonFileWatchers = [];
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _resourcesCache = new();
private readonly ConcurrentDictionary<string, HashSet<string>> _loadedFilesCache = new();

/// <summary>
/// Initializes a new instance of the JsonResourceManager class using the specified resource directory and optional
/// resource name.
/// </summary>
/// <param name="resourcesPath">The path to the directory containing the JSON resource files. Cannot be null or empty.</param>
/// <param name="resourceName">The name of the resource to load. If null, the default resource name is used.</param>
public JsonResourceManager(string resourcesPath, string resourceName = null)
: this([resourcesPath], fallBackToParentUICultures: true, resourceName)
{
}

/// <summary>
/// Initializes a new instance of the JsonResourceManager class using the specified resource file paths and
/// configuration options.
/// </summary>
/// <param name="resourcesPaths">An array of file system paths to JSON resource files to be managed. If null, an empty array is used.</param>
/// <param name="fallBackToParentUICultures">Indicates whether resource lookups should fall back to parent UI cultures when a resource is not found for the
/// requested culture.</param>
/// <param name="resourceName">The name of the resource to be managed. If null, the manager will use the default resource name resolution.</param>
public JsonResourceManager(string[] resourcesPaths, bool fallBackToParentUICultures, string resourceName = null)
{
ResourcesPaths = resourcesPaths ?? Array.Empty<string>();
Expand All @@ -30,15 +48,31 @@ public JsonResourceManager(string[] resourcesPaths, bool fallBackToParentUICultu
}
}

/// <summary>
/// Initializes a new instance of the JsonResourceManager class using the specified resource file paths and an
/// optional resource name.
/// </summary>
/// <param name="resourcesPaths">An array of file system paths to JSON resource files to be managed. Each path should point to a valid resource
/// file. Cannot be null.</param>
/// <param name="resourceName">The name of the resource to be used for lookups. If null, the default resource name will be used.</param>
public JsonResourceManager(string[] resourcesPaths, string resourceName = null)
: this(resourcesPaths, fallBackToParentUICultures: true, resourceName)
{
}

/// <summary>
/// Gets the name of the resource associated with this instance.
/// </summary>
public string ResourceName { get; }

/// <summary>
/// Gets the collection of file system paths to resource files associated with the current instance.
/// </summary>
public string[] ResourcesPaths { get; }

/// <summary>
/// Gets the file path to the resources file used by the application.
/// </summary>
public string ResourcesFilePath { get; private set; }

/// <summary>
Expand All @@ -47,6 +81,15 @@ public JsonResourceManager(string[] resourcesPaths, string resourceName = null)
/// </summary>
public bool FallBackToParentUICultures { get; }

/// <summary>
/// Retrieves the set of localized resources for the specified culture, optionally including resources from parent
/// cultures.
/// </summary>
/// <param name="culture">The culture for which to retrieve the resource set. This determines which localized resources are returned.</param>
/// <param name="tryParents">If <see langword="true"/>, resources from parent cultures are included in the result; otherwise, only resources
/// for the specified culture are returned.</param>
/// <returns>A <see cref="ConcurrentDictionary{string, string}"/> containing the resources for the specified culture, or <see
/// langword="null"/> if no resources are available for that culture.</returns>
public virtual ConcurrentDictionary<string, string> GetResourceSet(CultureInfo culture, bool tryParents)
{
TryLoadResourceSet(culture);
Expand Down Expand Up @@ -84,6 +127,11 @@ public virtual ConcurrentDictionary<string, string> GetResourceSet(CultureInfo c
}
}

/// <summary>
/// Retrieves the localized string resource associated with the specified name for the current UI culture.
/// </summary>
/// <param name="name">The name of the resource to retrieve. This value is case-sensitive and must not be null.</param>
/// <returns>The localized string value if found; otherwise, null.</returns>
public virtual string GetString(string name)
{
var culture = CultureInfo.CurrentUICulture;
Expand Down Expand Up @@ -116,6 +164,13 @@ public virtual string GetString(string name)
return null;
}

/// <summary>
/// Retrieves the localized string resource associated with the specified name and culture.
/// </summary>
/// <param name="name">The name of the resource to retrieve. This value is case-sensitive and must not be null.</param>
/// <param name="culture">The culture for which the resource should be retrieved. If the resource is not found for this culture and parent
/// culture fallback is enabled, parent cultures will be searched.</param>
/// <returns>The localized string value for the specified resource name and culture, or null if the resource is not found.</returns>
public virtual string GetString(string name, CultureInfo culture)
{
GetResourceSet(culture, tryParents: FallBackToParentUICultures);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Provides resource string retrieval for JSON-based resource sets, supporting culture-specific access and caching of
/// resource names.
/// </summary>
/// <param name="resourceNamesCache">A cache used to store and retrieve lists of resource names for specific cultures, improving performance by avoiding
/// repeated resource enumeration.</param>
/// <param name="jsonResourceManager">The resource manager responsible for accessing JSON resource sets and their associated strings for a given culture.</param>
public class JsonStringProvider(IResourceNamesCache resourceNamesCache, JsonResourceManager jsonResourceManager) : IResourceStringProvider
{
private string GetResourceCacheKey(CultureInfo culture)
Expand All @@ -14,6 +21,7 @@ private string GetResourceCacheKey(CultureInfo culture)
return $"Culture={culture.Name};resourceName={resourceName}";
}

/// <inheritdoc />
public IList<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing)
{
var cacheKey = GetResourceCacheKey(culture);
Expand Down
8 changes: 8 additions & 0 deletions src/My.Extensions.Localization.Json/Internal/PathHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@

namespace My.Extensions.Localization.Json.Internal;

/// <summary>
/// Provides helper methods for working with file system paths related to the application's location.
/// </summary>
public static class PathHelpers
{
/// <summary>
/// Gets the root directory of the currently executing application.
/// </summary>
/// <returns>A string containing the full path to the application's root directory. Returns null if the directory cannot be
/// determined.</returns>
public static string GetApplicationRoot() => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

namespace My.Extensions.Localization.Json;

/// <summary>
/// Provides configuration options for JSON-based localization, including resource type and resource path settings.
/// </summary>
public class JsonLocalizationOptions : LocalizationOptions
{
/// <summary>
/// Gets or sets the strategy used to determine how resources are categorized or accessed.
/// </summary>
public ResourcesType ResourcesType { get; set; } = ResourcesType.TypeBased;

/// <summary>
/// Gets or sets the collection of file system paths to resource directories used by the application.
/// </summary>
public new string[] ResourcesPath { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Provides extension methods for registering JSON-based localization services with an <see
/// cref="IServiceCollection"/>.
/// </summary>
public static class JsonLocalizationServiceCollectionExtensions
{
/// <summary>
/// Adds JSON-based localization services to the specified service collection.
/// </summary>
/// <param name="services">The service collection to which the JSON localization services will be added. Cannot be null.</param>
/// <returns>The same service collection instance, with JSON localization services registered.</returns>
public static IServiceCollection AddJsonLocalization(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
Expand All @@ -19,6 +28,12 @@ public static IServiceCollection AddJsonLocalization(this IServiceCollection ser
return services;
}

/// <summary>
/// Adds JSON-based localization services to the specified service collection.
/// </summary>
/// <param name="services">The service collection to which the JSON localization services will be added. Cannot be null.</param>
/// <param name="setupAction">An action to configure the JSON localization options. Cannot be null.</param>
/// <returns>The same instance of <see cref="IServiceCollection"/> with JSON localization services registered.</returns>
public static IServiceCollection AddJsonLocalization(this IServiceCollection services, Action<JsonLocalizationOptions> setupAction)
{
ArgumentNullException.ThrowIfNull(services);
Expand Down
Loading
Loading