.netcultureinvariantculture

Convert month name from one language to another without knowing the Culture?


How can I convert a string with a month name like "agosto" to the english translation "August" without knowing if "agosto" is spanish or italian?

I know that I get the month name for the month number 8 in Spanish with

Dim SpanishMonthName as String = Globalization.CultureInfo.GetCultureInfo("ES").DateTimeFormat.GetMonthName(8)

But how can I get the string "August" (8th month name in english) as translation for the spanish or italian month name "agosto"?


Solution

  • Inside the various CultureInfo there are the month names (see someCulture.DateTimeFormat.MonthNames), so you could:

    var italian = CultureInfo.GetCultureInfo("it-IT");
    var spanish = CultureInfo.GetCultureInfo("es-ES");
    var english = CultureInfo.GetCultureInfo("en-US");
    
    string month = "agosto";
    
    var italianMonthNames = italian.DateTimeFormat.MonthNames;
    var spanishMonthNames = spanish.DateTimeFormat.MonthNames;
    
    int ix = Array.FindIndex(italianMonthNames, x => StringComparer.OrdinalIgnoreCase.Equals(x, month));
    if (ix == -1)
    {
        ix = Array.FindIndex(spanishMonthNames, x => StringComparer.OrdinalIgnoreCase.Equals(x, month));
    }
    
    // ix is 0 based, while months are 1 based
    string englishMonth = ix != -1 ? english.DateTimeFormat.GetMonthName(ix + 1) : null;
    

    You could even try to delegate a little to the .NET DateTime.ParseExact:

    var italian = CultureInfo.GetCultureInfo("it-IT");
    var spanish = CultureInfo.GetCultureInfo("es-ES");
    var english = CultureInfo.GetCultureInfo("en-US");
    
    string month = "agosto";
    string englishMonth = null;
    DateTime dt;
    
    if (DateTime.TryParseExact(month, "MMMM", italian, 0, out dt) || DateTime.TryParseExact(month, "MMMM", spanish, 0, out dt))
    {
        englishMonth = dt.ToString("MMMM", english);
    }
    

    In general there is at least one month that has a different meaning in two languages: listopad (October or November, see here). The full list is Hlakola, listopad, Mopitlo, Nhlangula, Nyakanga, Phupu

    A first version that uses a Dictionary<> to collect month names:

    public class MonthNameFinder
    {
        private readonly IReadOnlyDictionary<string, int> MonthToNumber;
    
        public MonthNameFinder(params string[] cultures)
        {
            MonthToNumber = BuildDictionary(cultures.Select(x => CultureInfo.GetCultureInfo(x)));
        }
    
        public MonthNameFinder(params CultureInfo[] cultureInfos)
        {
            MonthToNumber = BuildDictionary(cultureInfos);
        }
    
        public MonthNameFinder(CultureTypes cultureTypes = CultureTypes.AllCultures)
        {
            MonthToNumber = BuildDictionary(CultureInfo.GetCultures(cultureTypes));
        }
    
        private static IReadOnlyDictionary<string, int> BuildDictionary(IEnumerable<CultureInfo> cultureInfos)
        {
            // Note that the comparer will always be wrong, sadly. Each culture has its comparer
            var dict = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
    
            foreach (var culture in cultureInfos)
            {
                var monthNames = culture.DateTimeFormat.MonthNames;
    
                for (int i = 0; i < monthNames.Length; i++)
                {
                    string monthName = monthNames[i];
    
                    int other;
    
                    if (!dict.TryGetValue(monthName, out other))
                    {
                        dict[monthName] = i + 1;
                    }
                    else if (other != i + 1)
                    {
                        Debug.WriteLine($"Repeated month {monthName}: {i + 1} in {culture.Name} ({culture.DisplayName})");
                    }
                }
            }
    
            return dict;
        }
    
        public int? GetMonthNumber(string monthName)
        {
            int monthNumber;
    
            if (MonthToNumber.TryGetValue(monthName, out monthNumber))
            {
                return monthNumber;
            }
    
            return null;
        }
    }
    

    use it like:

    var mnf = new MonthNameFinder();
    int? n = mnf.GetMonthNumber("agosto");
    
    if (n != null)
    {
        string name = new DateTime(1, n.Value, 1).ToString("MMMM", CultureInfo.GetCultureInfo("en-US"));
    }
    

    (note that you should cache mnf... it is probably quite expensive to build)

    Mmmh... I don't like it... I'm a little OC... and the simple fact that I know that there are some collisions in the name of the months is bothering me.

    Here is a second version, using a ILookup<> and saving even the CultureName, so that it is possible to discover the language(s) of the month name. The GetMonthNumbers(monthName) now returns a (int MonthNumber, string CultureName)[], an array of anonymous valuet types. You can clearly take the first one and live happily, or you can check it to see if there are multiple different MonthNumbers.

    public class MonthNameFinder
    {
        private readonly ILookup<string, (int MonthNumber, string CultureName)> MonthToNumber;
    
        public MonthNameFinder(params string[] cultures)
        {
            MonthToNumber = BuildLookup(cultures.Select(x => CultureInfo.GetCultureInfo(x)));
        }
    
        public MonthNameFinder(params CultureInfo[] cultureInfos)
        {
            MonthToNumber = BuildLookup(cultureInfos);
        }
    
        public MonthNameFinder(CultureTypes cultureTypes = CultureTypes.AllCultures)
        {
            MonthToNumber = BuildLookup(CultureInfo.GetCultures(cultureTypes));
        }
    
        private static ILookup<string, (int MonthNumber, string CultureName)> BuildLookup(IEnumerable<CultureInfo> cultureInfos)
        {
            // Note that the comparer will always be wrong, sadly. Each culture has its comparer
            var lst = new List<(string Name, int MonthNumber, string CultureName)>();
    
            foreach (var culture in cultureInfos)
            {
                var monthNames = culture.DateTimeFormat.MonthNames;
    
                for (int i = 0; i < monthNames.Length; i++)
                {
                    string monthName = monthNames[i];
                    lst.Add((monthName, i + 1, culture.Name));
                }
            }
    
            return lst.OrderBy(x => x.Name)
                .ThenBy(x => x.MonthNumber)
                .ToLookup(x => x.Name, x => (x.MonthNumber, x.CultureName), StringComparer.InvariantCultureIgnoreCase);
        }
    
        public (int MonthNumber, string CultureName)[] GetMonthNumbers(string monthName)
        {
            return MonthToNumber[monthName].ToArray();
        }
    }
    

    Use it like:

    // This is an array of (MonthNumber, CultureName)
    var mnf = new MonthNameFinder();
    
    var numbers = mnf.GetMonthNumbers("agosto");
    
    if (numbers.Length != 0)
    {
        string monthName = new DateTime(1, numbers[0].MonthNumber, 1).ToString("MMMM", CultureInfo.GetCultureInfo("en-US"));
    }
    

    (even here you should cache mnf... it is probably quite expensive to build)

    Note that there are many similar cultures, so numbers will be quite big (for example just for Italian there are 5 cultures, and a search for agosto returned 52 different cultures with the month agosto.