| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="ServiceCollectionExtensions.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Globalization; |
| | | 9 | | using System.Linq; |
| | | 10 | | using System.Resources; |
| | | 11 | | using Microsoft.Extensions.DependencyInjection; |
| | | 12 | | using Microsoft.Extensions.DependencyInjection.Extensions; |
| | | 13 | | using MyNet.Globalization.Culture; |
| | | 14 | | using MyNet.Globalization.DateTime; |
| | | 15 | | using MyNet.Globalization.Events; |
| | | 16 | | using MyNet.Globalization.Facade; |
| | | 17 | | using MyNet.Globalization.Inflection; |
| | | 18 | | using MyNet.Globalization.Inflection.Cultures; |
| | | 19 | | using MyNet.Globalization.Localization.Providers; |
| | | 20 | | using MyNet.Globalization.Localization.Providers.Factories; |
| | | 21 | | using MyNet.Globalization.Localization.Providers.Registration; |
| | | 22 | | using MyNet.Globalization.Localization.Translation; |
| | | 23 | | using MyNet.Globalization.Localization.Translation.Catalog; |
| | | 24 | | using MyNet.Globalization.Localization.Translation.KeyGeneration; |
| | | 25 | | using MyNet.Globalization.Localization.Translation.KeyResolving; |
| | | 26 | | |
| | | 27 | | #pragma warning disable IDE0130 // Namespace does not match folder structure |
| | | 28 | | namespace MyNet.Globalization; |
| | | 29 | | #pragma warning restore IDE0130 // Namespace does not match folder structure |
| | | 30 | | |
| | | 31 | | public static class ServiceCollectionExtensions |
| | | 32 | | { |
| | | 33 | | extension(IServiceCollection services) |
| | | 34 | | { |
| | | 35 | | /// <summary> |
| | | 36 | | /// Adds globalization services (culture, time zone, events) to the service collection. |
| | | 37 | | /// Registers <see cref="ICultureService"/> as the application-level <see cref="ICultureContext"/>. |
| | | 38 | | /// </summary> |
| | | 39 | | /// <param name="configure">An optional action to configure the <see cref="GlobalizationOptions"/>.</param> |
| | | 40 | | /// <returns>The updated service collection.</returns> |
| | | 41 | | public IServiceCollection AddGlobalization(Action<GlobalizationOptions>? configure = null) |
| | | 42 | | { |
| | 9 | 43 | | var options = new GlobalizationOptions(); |
| | 9 | 44 | | configure?.Invoke(options); |
| | | 45 | | |
| | 9 | 46 | | services.TryAddSingleton(options); |
| | | 47 | | |
| | | 48 | | // Core services |
| | 9 | 49 | | services.TryAddSingleton<ICultureService>(static sp => new CultureService(sp.GetRequiredService<Globalizatio |
| | 9 | 50 | | services.TryAddSingleton<ITimeZoneService>(static sp => new TimeZoneService(sp.GetRequiredService<Globalizat |
| | 9 | 51 | | services.TryAddSingleton<IGlobalizationService, GlobalizationService>(); |
| | | 52 | | |
| | | 53 | | // Events |
| | 9 | 54 | | services.TryAddSingleton<IGlobalizationEvents, GlobalizationEvents>(); |
| | | 55 | | |
| | 9 | 56 | | return services; |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// Adds localization services (translation pipeline, provider resolver) to the service collection. |
| | | 61 | | /// Requires that an <see cref="ICultureContext"/> has already been registered — |
| | | 62 | | /// either via <c>AddGlobalization()</c> (which registers the application-level <see cref="ICultureService"/>) |
| | | 63 | | /// or by registering a custom <see cref="ICultureContext"/> implementation. |
| | | 64 | | /// When neither is present, falls back to reading <see cref="System.Globalization.CultureInfo.CurrentCulture"/> |
| | | 65 | | /// of the current thread via <c>ThreadCultureContext</c>. |
| | | 66 | | /// </summary> |
| | | 67 | | /// <returns>The updated service collection.</returns> |
| | | 68 | | public IServiceCollection AddLocalization(Action<LocalizationServiceFactoryBuilder<IInflector>>? configure = nul |
| | | 69 | | { |
| | | 70 | | // ------------------------------------------------------------------ |
| | | 71 | | // Culture context |
| | | 72 | | // Prefer the application-level ICultureService (registered by AddGlobalization). |
| | | 73 | | // Fall back to ThreadCultureContext only when neither is available. |
| | | 74 | | // ------------------------------------------------------------------ |
| | 93 | 75 | | services.TryAddSingleton<ICultureContext>(sp => sp.GetService<ICultureService>() ?? (ICultureContext)new Thr |
| | | 76 | | |
| | | 77 | | // ------------------------------------------------------------------ |
| | | 78 | | // Translation catalog |
| | | 79 | | // ------------------------------------------------------------------ |
| | 93 | 80 | | services.TryAddSingleton<ITranslationCatalog>(sp => |
| | 93 | 81 | | { |
| | 93 | 82 | | var builder = new TranslationCatalogBuilder(); |
| | 93 | 83 | | |
| | 93 | 84 | | foreach (var contribution in sp.GetServices<ITranslationCatalogContribution>().OrderBy(x => x.Priority)) |
| | 93 | 85 | | { |
| | 93 | 86 | | contribution.Apply(builder); |
| | 93 | 87 | | } |
| | 93 | 88 | | |
| | 93 | 89 | | return builder.Build(); |
| | 93 | 90 | | }); |
| | | 91 | | |
| | | 92 | | // ------------------------------------------------------------------ |
| | | 93 | | // Localization provider registry (uses IServiceProvider to resolve factories on demand) |
| | | 94 | | // ------------------------------------------------------------------ |
| | 93 | 95 | | services.TryAddSingleton<ILocalizationFactoryRegistry>(sp => |
| | 93 | 96 | | { |
| | 93 | 97 | | var factories = sp.GetServices<ILocalizationServiceFactory>().ToDictionary(x => x.TargetType, x => x); |
| | 93 | 98 | | |
| | 93 | 99 | | return new LocalizationFactoryRegistry(factories); |
| | 93 | 100 | | }); |
| | | 101 | | |
| | | 102 | | // ------------------------------------------------------------------ |
| | | 103 | | // Localization provider resolver |
| | | 104 | | // ------------------------------------------------------------------ |
| | 93 | 105 | | services.TryAddSingleton<ILocalizationServiceResolver, LocalizationServiceResolver>(); |
| | 93 | 106 | | services.TryAddSingleton(typeof(ICultureScopedServiceSource<>), typeof(CultureScopedServiceSource<>)); |
| | | 107 | | |
| | | 108 | | // ------------------------------------------------------------------ |
| | | 109 | | // Translation pipeline |
| | | 110 | | // ------------------------------------------------------------------ |
| | 93 | 111 | | services.TryAddSingleton<ITranslationKeyProvider, TranslationKeyProvider>(); |
| | 93 | 112 | | services.TryAddSingleton<ITranslationKeyResolver, TranslationKeyResolver>(); |
| | | 113 | | |
| | | 114 | | // --- Pure, stateless translator --- |
| | 93 | 115 | | services.TryAddSingleton<ITranslator>(sp => |
| | 93 | 116 | | new Translator(sp.GetRequiredService<ITranslationCatalog>(), |
| | 93 | 117 | | sp.GetRequiredService<ITranslationKeyResolver>(), |
| | 93 | 118 | | sp.GetRequiredService<IPluralizationService>(), |
| | 93 | 119 | | sp.GetService<GlobalizationOptions>()?.CultureFallbackPolicy)); |
| | | 120 | | |
| | | 121 | | // --- Contextual translation service --- |
| | 93 | 122 | | services.TryAddSingleton<ITranslationService, TranslationService>(); |
| | | 123 | | |
| | | 124 | | // Static facade initialization |
| | 93 | 125 | | services.TryAddSingleton<ILocalizationRuntime, LocalizationRuntime>(); |
| | | 126 | | |
| | | 127 | | // ------------------------------------------------------------------ |
| | | 128 | | // Inflection providers |
| | | 129 | | // ------------------------------------------------------------------ |
| | 93 | 130 | | services.AddInflection(); |
| | | 131 | | |
| | 93 | 132 | | if (configure is not null) |
| | 0 | 133 | | services.ConfigureLocalizationService(configure); |
| | | 134 | | |
| | 93 | 135 | | return services; |
| | | 136 | | } |
| | | 137 | | |
| | | 138 | | /// <summary> |
| | | 139 | | /// Adds the default inflection provider to the service collection with built-in support for English and French. |
| | | 140 | | /// Additional cultures can be registered via <c>ConfigureLocalizationService</c>. |
| | | 141 | | /// </summary> |
| | | 142 | | /// <returns>The updated service collection.</returns> |
| | | 143 | | public IServiceCollection AddInflection() |
| | | 144 | | { |
| | 99 | 145 | | services.TryAddSingleton<IPluralizationService, PluralizationService>(); |
| | 99 | 146 | | services.AddLocalizationService<IInflector>((_, _) => Inflectors.Invariant); |
| | | 147 | | |
| | 99 | 148 | | services.ConfigureLocalizationService<IInflector>(builder => builder |
| | 99 | 149 | | .RegisterCulture(SupportedCultures.English, () => Inflectors.English) |
| | 99 | 150 | | .RegisterCulture(SupportedCultures.French, () => Inflectors.French)); |
| | | 151 | | |
| | 99 | 152 | | return services; |
| | | 153 | | } |
| | | 154 | | |
| | | 155 | | /// <summary> |
| | | 156 | | /// Registers a culture-aware localization provider of type <typeparamref name="TService"/>. |
| | | 157 | | /// The <paramref name="defaultFactory"/> is invoked for cultures that have no explicit registration. |
| | | 158 | | /// Use <c>ConfigureLocalizationService</c> to register culture-specific overrides. |
| | | 159 | | /// </summary> |
| | | 160 | | /// <param name="defaultFactory">Factory invoked with (IServiceProvider, CultureInfo) to create the default prov |
| | | 161 | | /// <typeparam name="TService">The culture-aware provider type to register.</typeparam> |
| | | 162 | | /// <returns>The updated service collection.</returns> |
| | | 163 | | public IServiceCollection AddLocalizationService<TService>(Func<IServiceProvider, CultureInfo, TService> default |
| | | 164 | | where TService : class, ICultureScoped |
| | | 165 | | { |
| | | 166 | | // Guard against duplicate registrations (TryAddEnumerable with factory delegates |
| | | 167 | | // cannot distinguish entries by implementation type and would throw ArgumentException). |
| | 171 | 168 | | if (services.Any(d => d.ServiceType == typeof(ILocalizationServiceFactory<TService>))) |
| | 59 | 169 | | return services; |
| | | 170 | | |
| | 112 | 171 | | services.AddSingleton<ILocalizationServiceFactory<TService>>(sp => |
| | 112 | 172 | | { |
| | 112 | 173 | | var builder = new LocalizationServiceFactoryBuilder<TService>(x => defaultFactory(sp, x)); |
| | 112 | 174 | | |
| | 112 | 175 | | var services = sp.GetServices<ILocalizationFactoryRegistration<TService>>() |
| | 112 | 176 | | .OrderBy(x => x.Priority).ToList(); |
| | 112 | 177 | | |
| | 112 | 178 | | services.ForEach(cfg => cfg.Configure(builder)); |
| | 112 | 179 | | |
| | 112 | 180 | | return builder.Build(); |
| | 112 | 181 | | }); |
| | | 182 | | |
| | | 183 | | // Register the non-generic marker so the registry can discover all factories. |
| | | 184 | | // We use AddSingleton (not TryAddEnumerable) because TryAddEnumerable requires |
| | | 185 | | // a concrete implementation type for deduplication, which factory-delegate descriptors lack. |
| | 112 | 186 | | services.AddSingleton<ILocalizationServiceFactory>(sp => sp.GetRequiredService<ILocalizationServiceFactory<T |
| | | 187 | | |
| | 112 | 188 | | return services; |
| | | 189 | | } |
| | | 190 | | |
| | | 191 | | /// <summary> |
| | | 192 | | /// Registers culture-specific overrides for a previously added <typeparamref name="TService"/>. |
| | | 193 | | /// Multiple calls are allowed; registrations are applied in ascending priority order. |
| | | 194 | | /// </summary> |
| | | 195 | | /// <param name="configure">Action that registers culture-specific factories on the builder.</param> |
| | | 196 | | /// <param name="priority">Priority of this registration. Higher values are applied later (override earlier ones |
| | | 197 | | /// <typeparam name="TService">The culture-aware provider type to configure.</typeparam> |
| | | 198 | | /// <returns>The updated service collection.</returns> |
| | | 199 | | public IServiceCollection ConfigureLocalizationService<TService>(Action<LocalizationServiceFactoryBuilder<TServi |
| | | 200 | | where TService : class, ICultureScoped |
| | | 201 | | { |
| | 127 | 202 | | services.TryAddEnumerable(ServiceDescriptor.Singleton<ILocalizationFactoryRegistration<TService>>(new Locali |
| | | 203 | | |
| | 127 | 204 | | return services; |
| | | 205 | | } |
| | | 206 | | |
| | | 207 | | /// <summary> |
| | | 208 | | /// Contributes a translation resource manager to the catalog at startup. |
| | | 209 | | /// </summary> |
| | | 210 | | /// <param name="resourceKey">Unique key identifying the resource (e.g., "DateTimeResources").</param> |
| | | 211 | | /// <param name="resourceManager">The resource manager instance.</param> |
| | | 212 | | /// <param name="priority">Contribution priority. Higher values are applied later.</param> |
| | | 213 | | /// <returns>The updated service collection.</returns> |
| | | 214 | | public IServiceCollection AddTranslationResource(string resourceKey, ResourceManager resourceManager, int priori |
| | | 215 | | { |
| | 63 | 216 | | ArgumentException.ThrowIfNullOrWhiteSpace(resourceKey); |
| | 63 | 217 | | ArgumentNullException.ThrowIfNull(resourceManager); |
| | | 218 | | |
| | | 219 | | // Multiple resources share the same contribution implementation type, |
| | | 220 | | // so we must not use TryAddEnumerable here (it deduplicates by implementation type). |
| | 63 | 221 | | services.AddSingleton<ITranslationCatalogContribution>( |
| | 63 | 222 | | new TranslationCatalogContribution(registry => registry.Register(resourceKey, resourceManager), priority |
| | | 223 | | |
| | 63 | 224 | | return services; |
| | | 225 | | } |
| | | 226 | | } |
| | | 227 | | |
| | | 228 | | extension(IServiceProvider serviceProvider) |
| | | 229 | | { |
| | | 230 | | /// <summary> |
| | | 231 | | /// Initializes the globalization static facade by configuring it with the registered <see cref="IGlobalizationS |
| | | 232 | | /// </summary> |
| | | 233 | | /// <returns>The updated service provider.</returns> |
| | | 234 | | public IServiceProvider UseGlobalization() |
| | | 235 | | { |
| | 3 | 236 | | GlobalizationServices.Configure(serviceProvider.GetRequiredService<IGlobalizationService>()); |
| | | 237 | | |
| | 3 | 238 | | return serviceProvider; |
| | | 239 | | } |
| | | 240 | | |
| | | 241 | | /// <summary> |
| | | 242 | | /// Initializes the localization static facade by configuring it with the registered <see cref="ILocalizationRun |
| | | 243 | | /// </summary> |
| | | 244 | | /// <returns>The updated service provider.</returns> |
| | | 245 | | public IServiceProvider UseLocalization() |
| | | 246 | | { |
| | 11 | 247 | | Localizer.Configure(serviceProvider.GetRequiredService<ILocalizationRuntime>()); |
| | | 248 | | |
| | 11 | 249 | | return serviceProvider; |
| | | 250 | | } |
| | | 251 | | } |
| | | 252 | | } |
| | | 253 | | |