| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="Translator.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System.Globalization; |
| | | 8 | | using System.Linq; |
| | | 9 | | using MyNet.Globalization.Inflection; |
| | | 10 | | using MyNet.Globalization.Localization.Policies; |
| | | 11 | | using MyNet.Globalization.Localization.Translation.Catalog; |
| | | 12 | | using MyNet.Globalization.Localization.Translation.KeyResolving; |
| | | 13 | | using MyNet.Text.Templating; |
| | | 14 | | |
| | | 15 | | namespace MyNet.Globalization.Localization.Translation; |
| | | 16 | | |
| | | 17 | | /// <summary> |
| | | 18 | | /// Pure, stateless implementation of <see cref="ITranslator"/> backed by an <see cref="ITranslationCatalog"/>. |
| | | 19 | | /// This class has no dependency on any "current culture" context — the caller always provides the culture explicitly. |
| | | 20 | | /// Culture fallback is applied according to the configured <see cref="ICultureFallbackPolicy"/>: |
| | | 21 | | /// by default, walks up the hierarchy: requested culture → parent culture → invariant culture. |
| | | 22 | | /// </summary> |
| | | 23 | | public sealed class Translator( |
| | | 24 | | ITranslationCatalog catalog, |
| | | 25 | | ITranslationKeyResolver keyResolver, |
| | | 26 | | IPluralizationService pluralizationService, |
| | | 27 | | ICultureFallbackPolicy? fallbackPolicy = null) : ITranslator |
| | | 28 | | { |
| | 328 | 29 | | private readonly ICultureFallbackPolicy _fallbackPolicy = fallbackPolicy ?? CultureFallbackPolicies.ParentCulture; |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Initializes a new instance of the <see cref="Translator"/> class with default parent-culture fallback. |
| | | 33 | | /// </summary> |
| | | 34 | | public Translator(ITranslationCatalog catalog, ITranslationKeyResolver keyResolver, IPluralizationService pluralizat |
| | 0 | 35 | | : this(catalog, keyResolver, pluralizationService, CultureFallbackPolicies.ParentCulture) |
| | | 36 | | { |
| | 0 | 37 | | } |
| | | 38 | | |
| | | 39 | | /// <inheritdoc /> |
| | 5358 | 40 | | public string Translate(string key, TranslationOptions options, CultureInfo culture) => TranslateInternal(key, optio |
| | | 41 | | |
| | | 42 | | /// <inheritdoc /> |
| | 324 | 43 | | public string Translate(string key, TranslationOptions options, CultureInfo culture, string resourceKey) => Translat |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Internal translation method that performs the actual lookup and fallback logic. |
| | | 47 | | /// </summary> |
| | | 48 | | /// <param name="key">The translation key.</param> |
| | | 49 | | /// <param name="options">The translation options.</param> |
| | | 50 | | /// <param name="culture">The target culture.</param> |
| | | 51 | | /// <param name="resourceKey">The resource key to search in. If null or empty, all registered resources are searched |
| | | 52 | | /// <returns>The translated string if found; otherwise, an empty string.</returns> |
| | | 53 | | private string TranslateInternal(string key, TranslationOptions options, CultureInfo culture, string? resourceKey = |
| | | 54 | | { |
| | 5682 | 55 | | if (string.IsNullOrEmpty(key)) |
| | 3 | 56 | | return string.Empty; |
| | | 57 | | |
| | 5679 | 58 | | var effectiveCulture = culture; |
| | | 59 | | |
| | 1272 | 60 | | while (true) |
| | | 61 | | { |
| | 6951 | 62 | | var candidateKeys = keyResolver.Resolve(key, options, effectiveCulture); |
| | 22998 | 63 | | foreach (var candidateKey in candidateKeys) |
| | | 64 | | { |
| | 6963 | 65 | | var translation = TryTranslate(candidateKey.Key, effectiveCulture, resourceKey); |
| | | 66 | | |
| | 6963 | 67 | | if (!string.IsNullOrWhiteSpace(translation)) |
| | | 68 | | { |
| | 4830 | 69 | | return PostProcessing(translation, candidateKey.Policy, options, effectiveCulture); |
| | | 70 | | } |
| | | 71 | | } |
| | | 72 | | |
| | 2121 | 73 | | var fallback = _fallbackPolicy.GetFallback(effectiveCulture); |
| | 2121 | 74 | | if (fallback is null) |
| | | 75 | | break; |
| | | 76 | | |
| | 1272 | 77 | | effectiveCulture = fallback; |
| | | 78 | | } |
| | | 79 | | |
| | 849 | 80 | | return options.UseKeyAsFallback ? key : string.Empty; |
| | 4830 | 81 | | } |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Performs post-processing on the translated value, such as formatting the count if applicable. If the translation |
| | | 85 | | /// </summary> |
| | | 86 | | /// <param name="value">The translation value.</param> |
| | | 87 | | /// <param name="policy">The translation fallback policy.</param> |
| | | 88 | | /// <param name="options">The translation options.</param> |
| | | 89 | | /// <param name="culture">The culture to use for formatting.</param> |
| | | 90 | | /// <returns>The translation value with post-processing applied.</returns> |
| | | 91 | | private string PostProcessing(string value, TranslationFallbackPolicy policy, TranslationOptions options, CultureInf |
| | | 92 | | { |
| | 4830 | 93 | | var translation = options.UseInflectionFallback && policy.AllowInflectionFallback && options.Quantity.HasValue |
| | 4830 | 94 | | ? ApplyInflectionFallback(value, options.Quantity.Value, culture) |
| | 4830 | 95 | | : value; |
| | | 96 | | |
| | 4830 | 97 | | return new TemplateTransform(new() |
| | 4830 | 98 | | { |
| | 4830 | 99 | | Quantity = options.Quantity, |
| | 4830 | 100 | | Arguments = options.Arguments, |
| | 4830 | 101 | | QuantityFormat = options.QuantityFormat, |
| | 4830 | 102 | | QuantityRenderingMode = options.QuantityRenderingMode, |
| | 4830 | 103 | | QuantitySeparator = options.QuantitySeparator |
| | 4830 | 104 | | }).Apply(translation, culture); |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | /// <summary> |
| | | 108 | | /// Attempts to look up the translation for the given key and culture, optionally restricted to a specific resource. |
| | | 109 | | /// </summary> |
| | | 110 | | /// <param name="key">The translation key.</param> |
| | | 111 | | /// <param name="culture">The target culture.</param> |
| | | 112 | | /// <param name="resourceKey">The resource key to search in. If null or empty, all registered resources are searched |
| | | 113 | | /// <returns>The translated string if found; otherwise, null.</returns> |
| | | 114 | | private string? TryTranslate(string key, CultureInfo culture, string? resourceKey) |
| | 6963 | 115 | | => string.IsNullOrWhiteSpace(resourceKey) |
| | 6963 | 116 | | ? catalog.Resources.Values.Select(resource => resource.GetString(key, culture)).FirstOrDefault(value => !str |
| | 6963 | 117 | | : catalog.Resources.TryGetValue(resourceKey, out var rm) |
| | 6963 | 118 | | ? rm.GetString(key, culture) |
| | 6963 | 119 | | : null; |
| | | 120 | | |
| | | 121 | | /// <summary> |
| | | 122 | | /// Applies inflection fallback to the given value based on the count and culture using the provided inflector. |
| | | 123 | | /// </summary> |
| | | 124 | | /// <param name="value">The value to apply inflection fallback to.</param> |
| | | 125 | | /// <param name="count">The count used to determine pluralization.</param> |
| | | 126 | | /// <param name="culture">The culture to use for inflection.</param> |
| | | 127 | | /// <returns>The value with inflection fallback applied.</returns> |
| | | 128 | | private string ApplyInflectionFallback(string value, decimal count, CultureInfo culture) |
| | | 129 | | { |
| | 528 | 130 | | var isPlural = pluralizationService.IsPlural(count, culture); |
| | | 131 | | |
| | 528 | 132 | | return isPlural ? pluralizationService.Pluralize(value, culture) : pluralizationService.Singularize(value, cultu |
| | | 133 | | } |
| | | 134 | | } |
| | | 135 | | |