// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. //////////////////////////////////////////////////////////////////////////// // // // // Purpose: This class represents the software preferences of a particular // culture or community. It includes information such as the // language, writing system, and a calendar used by the culture // as well as methods for common operations such as printing // dates and sorting strings. // // // // !!!! NOTE WHEN CHANGING THIS CLASS !!!! // // If adding or removing members to this class, please update CultureInfoBaseObject // in ndp/clr/src/vm/object.h. Note, the "actual" layout of the class may be // different than the order in which members are declared. For instance, all // reference types will come first in the class before value types (like ints, bools, etc) // regardless of the order in which they are declared. The best way to see the // actual order of the class is to do a !dumpobj on an instance of the managed // object inside of the debugger. // //////////////////////////////////////////////////////////////////////////// using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; namespace System.Globalization { /// <summary> /// This class represents the software preferences of a particular culture /// or community. It includes information such as the language, writing /// system and a calendar used by the culture as well as methods for /// common operations such as printing dates and sorting strings. /// </summary> /// <remarks> /// !!!! NOTE WHEN CHANGING THIS CLASS !!!! /// If adding or removing members to this class, please update /// CultureInfoBaseObject in ndp/clr/src/vm/object.h. Note, the "actual" /// layout of the class may be different than the order in which members /// are declared. For instance, all reference types will come first in the /// class before value types (like ints, bools, etc) regardless of the /// order in which they are declared. The best way to see the actual /// order of the class is to do a !dumpobj on an instance of the managed /// object inside of the debugger. /// </remarks> public partial class CultureInfo : IFormatProvider, ICloneable { // We use an RFC4646 type string to construct CultureInfo. // This string is stored in _name and is authoritative. // We use the _cultureData to get the data for our object private bool _isReadOnly; private CompareInfo? _compareInfo; private TextInfo? _textInfo; internal NumberFormatInfo? _numInfo; internal DateTimeFormatInfo? _dateTimeInfo; private Calendar? _calendar; // // The CultureData instance that we are going to read data from. // For supported culture, this will be the CultureData instance that read data from mscorlib assembly. // For customized culture, this will be the CultureData instance that read data from user customized culture binary file. // internal CultureData _cultureData; internal bool _isInherited; private CultureInfo? _consoleFallbackCulture; // Names are confusing. Here are 3 names we have: // // new CultureInfo() _name _nonSortName _sortName // en-US en-US en-US en-US // de-de_phoneb de-DE_phoneb de-DE de-DE_phoneb // fj-fj (custom) fj-FJ fj-FJ en-US (if specified sort is en-US) // en en // // Note that in Silverlight we ask the OS for the text and sort behavior, so the // textinfo and compareinfo names are the same as the name // This has a de-DE, de-DE_phoneb or fj-FJ style name internal string _name; // This will hold the non sorting name to be returned from CultureInfo.Name property. // This has a de-DE style name even for de-DE_phoneb type cultures private string? _nonSortName; // This will hold the sorting name to be returned from CultureInfo.SortName property. // This might be completely unrelated to the culture name if a custom culture. Ie en-US for fj-FJ. // Otherwise its the sort name, ie: de-DE or de-DE_phoneb private string? _sortName; // Get the current user default culture. This one is almost always used, so we create it by default. private static CultureInfo? s_userDefaultCulture; // The culture used in the user interface. This is mostly used to load correct localized resources. private static CultureInfo? s_userDefaultUICulture; // The Invariant culture; private static readonly CultureInfo s_InvariantCultureInfo = new CultureInfo(CultureData.Invariant, isReadOnly: true); // These are defaults that we use if a thread has not opted into having an explicit culture private static volatile CultureInfo? s_DefaultThreadCurrentUICulture; private static volatile CultureInfo? s_DefaultThreadCurrentCulture; [ThreadStatic] private static CultureInfo? s_currentThreadCulture; [ThreadStatic] private static CultureInfo? s_currentThreadUICulture; private static AsyncLocal<CultureInfo>? s_asyncLocalCurrentCulture; private static AsyncLocal<CultureInfo>? s_asyncLocalCurrentUICulture; private static void AsyncLocalSetCurrentCulture(AsyncLocalValueChangedArgs<CultureInfo> args) { s_currentThreadCulture = args.CurrentValue; } private static void AsyncLocalSetCurrentUICulture(AsyncLocalValueChangedArgs<CultureInfo> args) { s_currentThreadUICulture = args.CurrentValue; } private static Dictionary<string, CultureInfo>? s_cachedCulturesByName; private static Dictionary<int, CultureInfo>? s_cachedCulturesByLcid; // The parent culture. private CultureInfo? _parent; // LOCALE constants of interest to us internally and privately for LCID functions // (ie: avoid using these and use names if possible) internal const int LOCALE_NEUTRAL = 0x0000; private const int LOCALE_USER_DEFAULT = 0x0400; private const int LOCALE_SYSTEM_DEFAULT = 0x0800; internal const int LOCALE_CUSTOM_UNSPECIFIED = 0x1000; internal const int LOCALE_CUSTOM_DEFAULT = 0x0c00; internal const int LOCALE_INVARIANT = 0x007F; private static CultureInfo InitializeUserDefaultCulture() { Interlocked.CompareExchange(ref s_userDefaultCulture, GetUserDefaultCulture(), null); return s_userDefaultCulture; } private static CultureInfo InitializeUserDefaultUICulture() { Interlocked.CompareExchange(ref s_userDefaultUICulture, GetUserDefaultUICulture(), null); return s_userDefaultUICulture; } private static string GetCultureNotSupportedExceptionMessage() => GlobalizationMode.Invariant ? SR.Argument_CultureNotSupportedInInvariantMode : SR.Argument_CultureNotSupported; public CultureInfo(string name) : this(name, true) { } public CultureInfo(string name, bool useUserOverride) { ArgumentNullException.ThrowIfNull(name); // Get our data providing record _cultureData = CultureData.GetCultureData(name, useUserOverride) ?? throw new CultureNotFoundException(nameof(name), name, GetCultureNotSupportedExceptionMessage()); _name = _cultureData.CultureName; _isInherited = GetType() != typeof(CultureInfo); } private CultureInfo(CultureData cultureData, bool isReadOnly = false) { Debug.Assert(cultureData != null); _cultureData = cultureData; _name = cultureData.CultureName; _isReadOnly = isReadOnly; } private static CultureInfo? CreateCultureInfoNoThrow(string name, bool useUserOverride) { Debug.Assert(name != null); CultureData? cultureData = CultureData.GetCultureData(name, useUserOverride); if (cultureData == null) { return null; } return new CultureInfo(cultureData); } public CultureInfo(int culture) : this(culture, true) { } public CultureInfo(int culture, bool useUserOverride) { // We don't check for other invalid LCIDS here... ArgumentOutOfRangeException.ThrowIfNegative(culture); if (culture is LOCALE_CUSTOM_DEFAULT or LOCALE_SYSTEM_DEFAULT or LOCALE_NEUTRAL or LOCALE_USER_DEFAULT or LOCALE_CUSTOM_UNSPECIFIED) { // Can't support unknown custom cultures and we do not support neutral or throw new CultureNotFoundException(nameof(culture), culture, SR.Argument_CultureNotSupported); } // Now see if this LCID is supported in the system default CultureData table. _cultureData = CultureData.GetCultureData(culture, useUserOverride); _isInherited = GetType() != typeof(CultureInfo); _name = _cultureData.CultureName; } /// <summary> /// Constructor called by SQL Server's special munged culture - creates a culture with /// a TextInfo and CompareInfo that come from a supplied alternate source. This object /// is ALWAYS read-only. /// Note that we really cannot use an LCID version of this override as the cached /// name we create for it has to include both names, and the logic for this is in /// the GetCultureInfo override *only*. /// </summary> internal CultureInfo(string cultureName, string textAndCompareCultureName) { ArgumentNullException.ThrowIfNull(textAndCompareCultureName); CultureData? cultureData = CultureData.GetCultureData(cultureName, false) ?? throw new CultureNotFoundException(nameof(cultureName), cultureName, GetCultureNotSupportedExceptionMessage()); _cultureData = cultureData; _name = _cultureData.CultureName; CultureInfo altCulture = GetCultureInfo(textAndCompareCultureName); _compareInfo = altCulture.CompareInfo; _textInfo = altCulture.TextInfo; } /// <summary> /// We do this to try to return the system UI language and the default user languages /// This method will fallback if this fails (like Invariant) /// </summary> private static CultureInfo GetCultureByName(string name) { try { return new CultureInfo(name) { _isReadOnly = true }; } catch (ArgumentException) { return InvariantCulture; } } /// <summary> /// Return a specific culture. A tad irrelevant now since we always /// return valid data for neutral locales. /// /// Note that there's interesting behavior that tries to find a /// smaller name, ala RFC4647, if we can't find a bigger name. /// That doesn't help with things like "zh" though, so the approach /// is of questionable value /// </summary> public static CultureInfo CreateSpecificCulture(string name) { CultureInfo? culture; try { culture = new CultureInfo(name); } catch (ArgumentException) { // When CultureInfo throws this exception, it may be because someone passed the form // like "az-az" because it came out of an http accept lang. We should try a little // parsing to perhaps fall back to "az" here and use *it* to create the neutral. int idx = name.IndexOf('-'); if (idx >= 0) { try { culture = new CultureInfo(name.Substring(0, idx)); } catch (ArgumentException) { // throw the original exception so the name in the string will be right throw; } } else { // nothing to save here; throw the original exception throw; } } // In the most common case, they've given us a specific culture, so we'll just return that. if (!culture.IsNeutralCulture) { return culture; } return new CultureInfo(culture._cultureData.SpecificCultureName); } internal static bool VerifyCultureName(string cultureName, bool throwException) { // This function is used by ResourceManager.GetResourceFileName(). // ResourceManager searches for resource using CultureInfo.Name, // so we should check against CultureInfo.Name. for (int i = 0; i < cultureName.Length; i++) { char c = cultureName[i]; // TODO: Names can only be RFC4646 names (ie: a-zA-Z0-9) while this allows any unicode letter/digit if (char.IsLetterOrDigit(c) || c == '-' || c == '_') { continue; } if (throwException) { throw new ArgumentException(SR.Format(SR.Argument_InvalidResourceCultureName, cultureName)); } return false; } return true; } internal static bool VerifyCultureName(CultureInfo culture, bool throwException) { // If we have an instance of one of our CultureInfos, the user can't have changed the // name and we know that all names are valid in files. if (!culture._isInherited) { return true; } return VerifyCultureName(culture.Name, throwException); } /// <summary> /// This instance provides methods based on the current user settings. /// These settings are volatile and may change over the lifetime of the /// thread. /// </summary> /// <remarks> /// We use the following order to return CurrentCulture and CurrentUICulture /// o Use WinRT to return the current user profile language /// o use current thread culture if the user already set one using CurrentCulture/CurrentUICulture /// o use thread culture if the user already set one using DefaultThreadCurrentCulture /// or DefaultThreadCurrentUICulture /// o Use NLS default user culture /// o Use NLS default system culture /// o Use Invariant culture /// </remarks> public static CultureInfo CurrentCulture { get { return s_currentThreadCulture ?? s_DefaultThreadCurrentCulture ?? s_userDefaultCulture ?? InitializeUserDefaultCulture(); } set { ArgumentNullException.ThrowIfNull(value); if (s_asyncLocalCurrentCulture == null) { Interlocked.CompareExchange(ref s_asyncLocalCurrentCulture, new AsyncLocal<CultureInfo>(AsyncLocalSetCurrentCulture), null); } s_asyncLocalCurrentCulture.Value = value; } } public static CultureInfo CurrentUICulture { get { return s_currentThreadUICulture ?? s_DefaultThreadCurrentUICulture ?? UserDefaultUICulture; } set { ArgumentNullException.ThrowIfNull(value); VerifyCultureName(value, true); if (s_asyncLocalCurrentUICulture == null) { Interlocked.CompareExchange(ref s_asyncLocalCurrentUICulture, new AsyncLocal<CultureInfo>(AsyncLocalSetCurrentUICulture), null); } // this one will set s_currentThreadUICulture too s_asyncLocalCurrentUICulture.Value = value; } } internal static CultureInfo UserDefaultUICulture => s_userDefaultUICulture ?? InitializeUserDefaultUICulture(); public static CultureInfo InstalledUICulture => s_userDefaultCulture ?? InitializeUserDefaultCulture(); public static CultureInfo? DefaultThreadCurrentCulture { get => s_DefaultThreadCurrentCulture; set => // If you add pre-conditions to this method, check to see if you also need to // add them to Thread.CurrentCulture.set. s_DefaultThreadCurrentCulture = value; } public static CultureInfo? DefaultThreadCurrentUICulture { get => s_DefaultThreadCurrentUICulture; set { // If they're trying to use a Culture with a name that we can't use in resource lookup, // don't even let them set it on the thread. // If you add more pre-conditions to this method, check to see if you also need to // add them to Thread.CurrentUICulture.set. if (value != null) { VerifyCultureName(value, true); } s_DefaultThreadCurrentUICulture = value; } } /// <summary> /// This instance provides methods, for example for casing and sorting, /// that are independent of the system and current user settings. It /// should be used only by processes such as some system services that /// require such invariant results (eg. file systems). In general, /// the results are not linguistically correct and do not match any /// culture info. /// </summary> public static CultureInfo InvariantCulture { get { Debug.Assert(s_InvariantCultureInfo != null, "[CultureInfo.InvariantCulture] s_InvariantCultureInfo is null"); return s_InvariantCultureInfo; } } /// <summary> /// Return the parent CultureInfo for the current instance. /// </summary> public virtual CultureInfo Parent { get { if (_parent == null) { CultureInfo culture; string parentName = _cultureData.ParentName; if (parentName == "zh") { if (_name.Length == 5 && _name[2] == '-') { // We need to keep the parent chain for the zh cultures as follows to preserve the resource lookup compatibility // zh-CN -> zh-Hans -> zh -> Invariant // zh-HK -> zh-Hant -> zh -> Invariant // zh-MO -> zh-Hant -> zh -> Invariant // zh-SG -> zh-Hans -> zh -> Invariant // zh-TW -> zh-Hant -> zh -> Invariant if ((_name[3] == 'C' && _name[4] == 'N') || // zh-CN (_name[3] == 'S' && _name[4] == 'G')) // zh-SG { parentName = "zh-Hans"; } else if ((_name[3] == 'H' && _name[4] == 'K') || // zh-HK (_name[3] == 'M' && _name[4] == 'O') || // zh-MO (_name[3] == 'T' && _name[4] == 'W')) // zh-TW { parentName = "zh-Hant"; } } else if (_name.Length > 8 && _name.AsSpan(2, 4) is "-Han" && _name[7] == '-') // cultures like zh-Hant-* and zh-Hans-* { if (_name[6] == 't') // zh-Hant-* { parentName = "zh-Hant"; } else if (_name[6] == 's') // zh-Hans-* { parentName = "zh-Hans"; } } } if (string.IsNullOrEmpty(parentName)) { culture = InvariantCulture; } else { culture = CreateCultureInfoNoThrow(parentName, _cultureData.UseUserOverride) ?? // For whatever reason our IPARENT or SPARENT wasn't correct, so use invariant // We can't allow ourselves to fail. In case of custom cultures the parent of the // current custom culture isn't installed. InvariantCulture; } Interlocked.CompareExchange<CultureInfo?>(ref _parent, culture, null); } return _parent!; } } public virtual int LCID => _cultureData.LCID; public virtual int KeyboardLayoutId => _cultureData.KeyboardLayoutId; public static CultureInfo[] GetCultures(CultureTypes types) { // internally we treat UserCustomCultures as Supplementals but v2 // treats as Supplementals and Replacements if ((types & CultureTypes.UserCustomCulture) == CultureTypes.UserCustomCulture) { types |= CultureTypes.ReplacementCultures; } return CultureData.GetCultures(types); } /// <summary> /// Returns the full name of the CultureInfo. The name is in format like /// "en-US" This version does NOT include sort information in the name. /// </summary> public virtual string Name => _nonSortName ??= (_cultureData.Name ?? string.Empty); /// <summary> /// This one has the sort information (ie: de-DE_phoneb) /// </summary> internal string SortName => _sortName ??= _cultureData.SortName; /// <summary> /// The culture name to use to interop with the underlying native globalization libraries like ICU or Windows NLS APIs. /// For example, we can have the name de_DE@collation=phonebook when using ICU for the German culture de-DE with the phonebook sorting behavior. /// </summary> internal string? InteropName => _cultureData.InteropName; public string IetfLanguageTag => // special case the compatibility cultures Name switch { "zh-CHT" => "zh-Hant", "zh-CHS" => "zh-Hans", _ => Name, }; /// <summary> /// Returns the full name of the CultureInfo in the localized language. /// For example, if the localized language of the runtime is Spanish and the CultureInfo is /// US English, "Ingles (Estados Unidos)" will be returned. /// </summary> public virtual string DisplayName { get { Debug.Assert(_name != null, "[CultureInfo.DisplayName] Always expect _name to be set"); return _cultureData.DisplayName; } } /// <summary> /// Returns the full name of the CultureInfo in the native language. /// For example, if the CultureInfo is US English, "English /// (United States)" will be returned. /// </summary> public virtual string NativeName => _cultureData.NativeName; /// <summary> /// Returns the full name of the CultureInfo in English. /// For example, if the CultureInfo is US English, "English /// (United States)" will be returned. /// </summary> public virtual string EnglishName => _cultureData.EnglishName; /// <summary> /// ie: en /// </summary> public virtual string TwoLetterISOLanguageName => _cultureData.TwoLetterISOLanguageName; /// <summary> /// ie: eng /// </summary> public virtual string ThreeLetterISOLanguageName => _cultureData.ThreeLetterISOLanguageName; /// <summary> /// Returns the 3 letter windows language name for the current instance. eg: "ENU" /// The ISO names are much preferred /// </summary> public virtual string ThreeLetterWindowsLanguageName => _cultureData.ThreeLetterWindowsLanguageName; /// <summary> /// Gets the CompareInfo for this culture. /// </summary> public virtual CompareInfo CompareInfo => _compareInfo ??= // Since CompareInfo's don't have any overrideable properties, get the CompareInfo from // the Non-Overridden CultureInfo so that we only create one CompareInfo per culture (UseUserOverride ? GetCultureInfo(_name).CompareInfo : new CompareInfo(this)); /// <summary> /// Gets the TextInfo for this culture. /// </summary> public virtual TextInfo TextInfo { get { if (_textInfo == null) { // Make a new textInfo TextInfo tempTextInfo = new TextInfo(_cultureData); tempTextInfo.SetReadOnlyState(_isReadOnly); _textInfo = tempTextInfo; } return _textInfo; } } public override bool Equals([NotNullWhen(true)] object? value) { if (ReferenceEquals(this, value)) { return true; } if (value is CultureInfo that) { // using CompareInfo to verify the data passed through the constructor // CultureInfo(String cultureName, String textAndCompareCultureName) return Name.Equals(that.Name) && CompareInfo.Equals(that.CompareInfo); } return false; } public override int GetHashCode() { return Name.GetHashCode() + CompareInfo.GetHashCode(); } /// <summary> /// Implements object.ToString(). Returns the name of the CultureInfo, /// eg. "de-DE_phoneb", "en-US", or "fj-FJ". /// </summary> public override string ToString() => _name; public virtual object? GetFormat(Type? formatType) { if (formatType == typeof(NumberFormatInfo)) { return NumberFormat; } if (formatType == typeof(DateTimeFormatInfo)) { return DateTimeFormat; } return null; } public virtual bool IsNeutralCulture => _cultureData.IsNeutralCulture; public CultureTypes CultureTypes { get { CultureTypes types = _cultureData.IsNeutralCulture ? CultureTypes.NeutralCultures : CultureTypes.SpecificCultures; if (CultureData.IsWin32Installed) { types |= CultureTypes.InstalledWin32Cultures; } if (_cultureData.IsSupplementalCustomCulture) { types |= CultureTypes.UserCustomCulture; } if (_cultureData.IsReplacementCulture) { types |= CultureTypes.ReplacementCultures; } return types; } } public virtual NumberFormatInfo NumberFormat { get { if (_numInfo == null) { NumberFormatInfo temp = new NumberFormatInfo(_cultureData); temp._isReadOnly = _isReadOnly; Interlocked.CompareExchange(ref _numInfo, temp, null); } return _numInfo!; } set { ArgumentNullException.ThrowIfNull(value); VerifyWritable(); _numInfo = value; } } /// <summary> /// Create a DateTimeFormatInfo, and fill in the properties according to /// the CultureID. /// </summary> public virtual DateTimeFormatInfo DateTimeFormat { get { if (_dateTimeInfo == null) { // Change the calendar of DTFI to the specified calendar of this CultureInfo. DateTimeFormatInfo temp = new DateTimeFormatInfo(_cultureData, this.Calendar); temp._isReadOnly = _isReadOnly; Interlocked.CompareExchange(ref _dateTimeInfo, temp, null); } return _dateTimeInfo!; } set { ArgumentNullException.ThrowIfNull(value); VerifyWritable(); _dateTimeInfo = value; } } public void ClearCachedData() { // reset the default culture values #if TARGET_WINDOWS UserDefaultLocaleName = GetUserDefaultLocaleName(); #endif s_userDefaultCulture = GetUserDefaultCulture(); s_userDefaultUICulture = GetUserDefaultUICulture(); RegionInfo.s_currentRegionInfo = null; #pragma warning disable 0618 // disable the obsolete warning TimeZone.ResetTimeZone(); #pragma warning restore 0618 TimeZoneInfo.ClearCachedData(); s_cachedCulturesByLcid = null; s_cachedCulturesByName = null; CultureData.ClearCachedData(); } /// <summary> /// Map a Win32 CALID to an instance of supported calendar. /// </summary> /// <remarks> /// Shouldn't throw exception since the calType value is from our data /// table or from Win32 registry. /// If we are in trouble (like getting a weird value from Win32 /// registry), just return the GregorianCalendar. /// </remarks> internal static Calendar GetCalendarInstance(CalendarId calType) { Debug.Assert(!GlobalizationMode.Invariant); if (calType == CalendarId.GREGORIAN) { return new GregorianCalendar(); } return GetCalendarInstanceRare(calType); } /// <summary> /// This function exists as a shortcut to prevent us from loading all of the non-gregorian /// calendars unless they're required. /// </summary> internal static Calendar GetCalendarInstanceRare(CalendarId calType) { Debug.Assert(calType != CalendarId.GREGORIAN, "calType!=CalendarId.GREGORIAN"); return calType switch { CalendarId.GREGORIAN_US or CalendarId.GREGORIAN_ME_FRENCH or CalendarId.GREGORIAN_ARABIC or CalendarId.GREGORIAN_XLIT_ENGLISH or CalendarId.GREGORIAN_XLIT_FRENCH => new GregorianCalendar((GregorianCalendarTypes)calType), CalendarId.TAIWAN => new TaiwanCalendar(), CalendarId.JAPAN => new JapaneseCalendar(), CalendarId.KOREA => new KoreanCalendar(), CalendarId.THAI => new ThaiBuddhistCalendar(), CalendarId.HIJRI => new HijriCalendar(), CalendarId.HEBREW => new HebrewCalendar(), CalendarId.UMALQURA => new UmAlQuraCalendar(), CalendarId.PERSIAN => new PersianCalendar(), _ => new GregorianCalendar(), }; } /// <summary> /// Return/set the default calendar used by this culture. /// This value can be overridden by regional option if this is a current culture. /// </summary> public virtual Calendar Calendar { get { if (_calendar == null) { // Get the default calendar for this culture. Note that the value can be // from registry if this is a user default culture. Calendar newObj = _cultureData.DefaultCalendar; Interlocked.MemoryBarrier(); newObj.SetReadOnlyState(_isReadOnly); _calendar = newObj; } return _calendar; } } /// <summary> /// Return an array of the optional calendar for this culture. /// </summary> public virtual Calendar[] OptionalCalendars { get { // This property always returns a new copy of the calendar array. if (GlobalizationMode.Invariant) { return new[] { new GregorianCalendar() }; } CalendarId[] calID = _cultureData.CalendarIds; Calendar[] cals = new Calendar[calID.Length]; for (int i = 0; i < cals.Length; i++) { cals[i] = GetCalendarInstance(calID[i]); } return cals; } } public bool UseUserOverride => _cultureData.UseUserOverride; public CultureInfo GetConsoleFallbackUICulture() { CultureInfo? temp = _consoleFallbackCulture; if (temp == null) { temp = CreateSpecificCulture(_cultureData.SCONSOLEFALLBACKNAME); temp._isReadOnly = true; _consoleFallbackCulture = temp; } return temp; } public virtual object Clone() { CultureInfo ci = (CultureInfo)MemberwiseClone(); ci._isReadOnly = false; // If this is exactly our type, we can make certain optimizations so that we don't allocate NumberFormatInfo or DTFI unless // they've already been allocated. If this is a derived type, we'll take a more generic codepath. if (!_isInherited) { if (_dateTimeInfo != null) { ci._dateTimeInfo = (DateTimeFormatInfo)_dateTimeInfo.Clone(); } if (_numInfo != null) { ci._numInfo = (NumberFormatInfo)_numInfo.Clone(); } } else { ci.DateTimeFormat = (DateTimeFormatInfo)this.DateTimeFormat.Clone(); ci.NumberFormat = (NumberFormatInfo)this.NumberFormat.Clone(); } if (_textInfo != null) { ci._textInfo = (TextInfo)_textInfo.Clone(); } if (_dateTimeInfo != null && _dateTimeInfo.Calendar == _calendar) { // Usually when we access CultureInfo.DateTimeFormat first time, we create the DateTimeFormatInfo object // using CultureInfo.Calendar. i.e. CultureInfo.DateTimeInfo.Calendar == CultureInfo.calendar. // When cloning CultureInfo, if we know it's still the case that CultureInfo.DateTimeInfo.Calendar == CultureInfo.calendar // then we can keep the same behavior for the cloned object and no need to create another calendar object. ci._calendar = ci.DateTimeFormat.Calendar; } else if (_calendar != null) { ci._calendar = (Calendar)_calendar.Clone(); } return ci; } public static CultureInfo ReadOnly(CultureInfo ci) { ArgumentNullException.ThrowIfNull(ci); if (ci.IsReadOnly) { return ci; } CultureInfo newInfo = (CultureInfo)(ci.MemberwiseClone()); if (!ci.IsNeutralCulture) { // If this is exactly our type, we can make certain optimizations so that we don't allocate NumberFormatInfo or DTFI unless // they've already been allocated. If this is a derived type, we'll take a more generic codepath. if (!ci._isInherited) { if (ci._dateTimeInfo != null) { newInfo._dateTimeInfo = DateTimeFormatInfo.ReadOnly(ci._dateTimeInfo); } if (ci._numInfo != null) { newInfo._numInfo = NumberFormatInfo.ReadOnly(ci._numInfo); } } else { newInfo.DateTimeFormat = DateTimeFormatInfo.ReadOnly(ci.DateTimeFormat); newInfo.NumberFormat = NumberFormatInfo.ReadOnly(ci.NumberFormat); } } if (ci._textInfo != null) { newInfo._textInfo = TextInfo.ReadOnly(ci._textInfo); } if (ci._calendar != null) { newInfo._calendar = Calendar.ReadOnly(ci._calendar); } // Don't set the read-only flag too early. // We should set the read-only flag here. Otherwise, info.DateTimeFormat will not be able to set. newInfo._isReadOnly = true; return newInfo; } public bool IsReadOnly => _isReadOnly; private void VerifyWritable() { if (_isReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } } /// <summary> /// For resource lookup, we consider a culture the invariant culture by name equality. /// We perform this check frequently during resource lookup, so adding a property for /// improved readability. /// </summary> internal bool HasInvariantCultureName => Name == InvariantCulture.Name; /// <summary> /// Gets a cached copy of the specified culture from an internal /// hashtable (or creates it if not found). (LCID version) /// </summary> public static CultureInfo GetCultureInfo(int culture) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(culture); Dictionary<int, CultureInfo> lcidTable = CachedCulturesByLcid; CultureInfo? result; lock (lcidTable) { if (lcidTable.TryGetValue(culture, out result)) { return result; } } try { result = new CultureInfo(culture, useUserOverride: false) { _isReadOnly = true }; } catch (ArgumentException) { throw new CultureNotFoundException(nameof(culture), culture, GetCultureNotSupportedExceptionMessage()); } lock (lcidTable) { lcidTable[culture] = result; } return result; } /// <summary> /// Gets a cached copy of the specified culture from an internal /// hashtable (or creates it if not found). (Named version) /// </summary> public static CultureInfo GetCultureInfo(string name) { ArgumentNullException.ThrowIfNull(name); name = CultureData.AnsiToLower(name); Dictionary<string, CultureInfo> nameTable = CachedCulturesByName; CultureInfo? result; lock (nameTable) { if (nameTable.TryGetValue(name, out result)) { return result; } } result = CreateCultureInfoNoThrow(name, useUserOverride: false) ?? throw new CultureNotFoundException(nameof(name), name, GetCultureNotSupportedExceptionMessage()); result._isReadOnly = true; // Remember our name as constructed. Do NOT use alternate sort name versions because // we have internal state representing the sort (so someone would get the wrong cached version). name = CultureData.AnsiToLower(result._name); lock (nameTable) { // add only if it wasn't already added nameTable.TryAdd(name, result); } return result; } /// <summary> /// Gets a cached copy of the specified culture from an internal /// hashtable (or creates it if not found). /// </summary> public static CultureInfo GetCultureInfo(string name, string altName) { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(altName); name = CultureData.AnsiToLower(name); altName = CultureData.AnsiToLower(altName); string nameAndAltName = name + "\xfffd" + altName; Dictionary<string, CultureInfo> nameTable = CachedCulturesByName; CultureInfo? result; lock (nameTable) { if (nameTable.TryGetValue(nameAndAltName, out result)) { return result; } } try { result = new CultureInfo(name, altName) { _isReadOnly = true }; result.TextInfo.SetReadOnlyState(readOnly: true); // TextInfo object is already created; we need to set it as read only. } catch (ArgumentException) { #pragma warning disable CA2208 // Instantiate argument exceptions correctly, combination of arguments used throw new CultureNotFoundException("name/altName", SR.Format(SR.Argument_OneOfCulturesNotSupported, name, altName)); #pragma warning restore CA2208 } lock (nameTable) { nameTable[nameAndAltName] = result; } return result; } public static CultureInfo GetCultureInfo(string name, bool predefinedOnly) { ArgumentNullException.ThrowIfNull(name); if (predefinedOnly && !GlobalizationMode.Invariant) { if (GlobalizationMode.UseNls ? !CultureData.NlsIsEnsurePredefinedLocaleName(name) : !CultureData.IcuIsEnsurePredefinedLocaleName(name)) { throw new CultureNotFoundException(nameof(name), name, SR.Format(SR.Argument_InvalidPredefinedCultureName, name)); } } return GetCultureInfo(name); } private static Dictionary<string, CultureInfo> CachedCulturesByName { get { Dictionary<string, CultureInfo>? cache = s_cachedCulturesByName; if (cache is null) { cache = new Dictionary<string, CultureInfo>(); cache = Interlocked.CompareExchange(ref s_cachedCulturesByName, cache, null) ?? cache; } return cache; } } private static Dictionary<int, CultureInfo> CachedCulturesByLcid { get { Dictionary<int, CultureInfo>? cache = s_cachedCulturesByLcid; if (cache is null) { cache = new Dictionary<int, CultureInfo>(); cache = Interlocked.CompareExchange(ref s_cachedCulturesByLcid, cache, null) ?? cache; } return cache; } } public static CultureInfo GetCultureInfoByIetfLanguageTag(string name) { // Disallow old zh-CHT/zh-CHS names if (name is "zh-CHT" or "zh-CHS") { throw new CultureNotFoundException(nameof(name), SR.Format(SR.Argument_CultureIetfNotSupported, name)); } CultureInfo ci = GetCultureInfo(name); // Disallow alt sorts and es-es_TS if (ci.LCID > 0xffff || ci.LCID == 0x040a) { throw new CultureNotFoundException(nameof(name), SR.Format(SR.Argument_CultureIetfNotSupported, name)); } return ci; } } }