How to: Save Custom Cultures Without Administrative Privileges

This topic describes a technique for saving custom culture data to a file without administrative privileges. An application that is running with administrative privileges registers a custom culture by using the CultureAndRegionInfoBuilder.Register method, as described in the topic How to: Create Custom Cultures. Internally this method calls the CultureDefinition.Compile method, which does not require administrative privileges.

To save custom cultures without administrative privileges, you can use reflection to access the CultureDefinition.Compile method directly.

Caution noteCaution

Calling the CultureDefinition.Compile method directly is unsupported. The method can change or even be removed without notice. This topic describes its behavior for the .NET Framework version 4 only. Misuse of this method can leave your computer in an unstable state, possibly resulting in application crashes or data loss.

About CultureDefinition.Compile

The CultureDefinition.Compile method is a member of the internal System.Globalization.CultureDefinition class in the sysglobl.dll assembly. CultureDefinition.Compile writes information about a custom culture to a file. However, because it does not write anything to the registry, it does not require administrative privileges.

Syntax

internal static void Compile(
   CultureAndRegionInfoBuilder builder,
   String outFile
);

Parameters

  • builder
    The CultureAndRegionInfoBuilder object to read. The conditions on this object are the same as for CultureAndRegionInfoBuilder.Register.

  • outFile
    A string that represents the full path to the output file. The method writes the custom culture file but does not register it. The custom culture definition contained in the output file will not be recognized even if it is placed in the %windir%\globalization directory.

Exceptions

Because this is an unsupported private method, any exception should be considered a failure.

Remarks

The following are the differences between CultureAndRegionInfoBuilder.Register and CultureDefinition.Compile:

  • CultureAndRegionInfoBuilder.Register is a supported public method. As an internal method, CultureDefinition.Compile is subject to change or removal in future versions and should not be relied upon.

  • CultureAndRegionInfoBuilder.Register requires administrative permissions, but CultureDefinition.Compile writes to any file the user has permission to create.

  • CultureAndRegionInfoBuilder.Register performs more validation than CultureDefinition.Compile. Thus, calling the latter method directly can create an invalid culture that normally cannot be installed on a computer. The created culture might contain irregular data or data that can cause the operating system or the .NET Framework to fail.

  • CultureAndRegionInfoBuilder.Register always creates its output file in the %windir%\globalization directory. CultureDefinition.Compile writes to any output file you specify.

  • CultureAndRegionInfoBuilder.Register might throw an exception if the CultureAndRegionInfoBuilder object specified by the builder parameter contains inconsistent or unexpected data. However, this method does not validate as thoroughly as CultureAndRegionInfoBuilder.Register.

    Caution noteCaution

    Because CultureDefinition.Compile uses limited validation mechanisms, it can create invalid or inconsistent cultures, potentially leading to application crashes or even to data loss.

  • CultureAndRegionInfoBuilder.Register registers the custom culture file in the registry. CultureDefinition.Compile creates a custom culture but does not register (install) it on the local operating system. Unregistered files in the %windir%\globalization directory fail and are not fully functional in the .NET Framework 4. Although they might appear operable, improperly registered custom culture files can cause erratic behavior or failures if accessed by the operating system or by the .NET Framework.

  • When you call the CultureAndRegionInfoBuilder.Register method, it creates the custom culture file. It also sets the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CustomLocale registry key, for which it creates a string value that names the culture. The CultureDefinition.Compile method creates the custom culture file, but it does not create the corresponding registry key.

    Note

    Starting with the .NET Framework version 3.5, the CultureDefinition.Compile method no longer sets the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\IetfLanguage registry key, because the key is not used by the .NET Framework. Culture names have been fixed to conform to RFC 4646.

Using CultureDefinition.Compile Directly

To save custom cultures by using CultureDefinition.Compile:

  1. Include code in your application to perform any necessary validations. Remember that the validations performed by the CultureAndRegionInfoBuilder.Register method are not performed when you use the CultureDefinition.Compile method directly. Although the resulting custom culture file will be accessible, improperly constructed files might exhibit illegal or erratic behavior.

  2. Instead of calling CultureAndRegionInfoBuilder.Register, call the CultureDefinition.Compile method and pass it the CultureAndRegionInfoBuilder object along with an appropriate file name.

  3. Register the resulting custom culture file as described in the next procedure.

Registering (Installing) the Custom Culture File

To register (install) the custom culture file:

  1. Include code in your application to write the output of the CultureDefinition.Compile call to the %windir%\globalization subdirectory.

  2. To enable the custom culture to succeed when running under the .NET Framework 4, include code to write to the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CustomLocale registry key.

Example

The following example uses the CultureAndRegionInfoBuilder class to create a custom culture named x-en-US-example that is based on the English (United States) culture but that uses a different currency symbol (USD instead of $). The example uses reflection to compile the culture definition by calling the private CultureDefinition.Compile method. It then copies the compiled .nlp file to the Globalization subdirectory of the Windows directory and registers the custom culture in the registry. Next, it instantiates a CultureInfo object that represents the custom culture and uses it in a formatting operation. Finally, the example calls the CultureAndRegionInfoBuilder.Unregister method to remove the custom culture definition. Note that a reference to sysglobl.dll must be added to the project to successfully compile the example.

Imports Microsoft.Win32
Imports System.Globalization
Imports System.IO
Imports System.Reflection

Module Example
   Private Const MAX_PATH As Integer = 260
   Private Const CUSTOM_KEY As String = "SYSTEM\CurrentControlSet\Control\Nls\CustomLocale"
   Private Declare Function GetWindowsDirectory Lib "Kernel32" _
           Alias "GetWindowsDirectoryA" _ 
           (lpBuffer As String, nSize As Integer) As Integer

   Private cultureName As String = "x-en-US-example"

   Public Sub Main()
      ' Create an alternate en-US culture.
      Dim enUS As New CultureAndRegionInfoBuilder(cultureName, CultureAndRegionModifiers.None)
      enUS.LoadDataFromCultureInfo(CultureInfo.CreateSpecificCulture("en-US"))
      enUS.LoadDataFromRegionInfo(New RegionInfo("US"))
      enUS.NumberFormat.CurrencySymbol = "USD"
      enUS.NumberFormat.CurrencyPositivePattern = 2
      enUS.NumberFormat.CurrencyNegativePattern = 12

      ' Use reflection to get the CultureDefinition.Compile method.
      Dim assem As Assembly = Assembly.Load("sysglobl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")            
      Dim defType As Type = assem.GetType("System.Globalization.CultureDefinition")
      Dim method As MethodInfo = defType.GetMethod("Compile", BindingFlags.NonPublic Or BindingFlags.Static)
      Dim tempPath As String = ".\" + cultureName + ".nlp"
      Dim args() As Object = { enUS, tempPath }

      ' Delete target file if it already exists.
      If File.Exists(tempPath) Then File.Delete(tempPath)

      ' Compile the culture definition.
      method.Invoke(Nothing, args)  

      ' Copy the file.
      Try
         Dim buffer As New String(ChrW(0), MAX_PATH)
         Dim charsWritten As Integer = GetWindowsDirectory(buffer, MAX_PATH)
         Dim fileName As String = String.Format("{0}{1}Globalization{1}{2}.nlp", 
                                                buffer.Substring(0, charsWritten),
                                                Path.DirectorySeparatorChar,
                                                cultureName) 
         File.Copy(tempPath, fileName, True)
         WriteToRegistry(CUSTOM_KEY, cultureName)       
      Catch e As UnauthorizedAccessException
         Console.WriteLine("You must run this application as an administrator")
         Console.WriteLine("so that you can install culture definition files.") 
         Exit Sub
      End Try

      ' Create and use the new culture.
      Try
         Dim value As Decimal = 1603.42d
         Dim ci As New CultureInfo(cultureName)
         Console.WriteLine(String.Format(ci, "{0:C2}", value))
      Catch e As CultureNotFoundException
         Console.WriteLine("Unable to create the '{0}' culture.", cultureName)
      End Try

      CultureAndRegionInfoBuilder.Unregister(cultureName)
   End Sub

   Public Sub WriteToRegistry(keyName As String, valueName As String)
      Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey(keyName, True)
      ' Create the key if it does not already exist.
      If key Is Nothing      
         key = Registry.LocalMachine.CreateSubKey(keyName)
         If key Is Nothing Then Throw New NullReferenceException("Cannot create the registry key")
      End If
      ' Set the new name
      key.SetValue(valueName, valueName)
      key.Close()
   End Sub
End Module
' The example displays the following output:
'         USD 1,603.42
using Microsoft.Win32;
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

public class Example
{
   private const int MAX_PATH = 260;
   private const string CUSTOM_KEY = @"SYSTEM\CurrentControlSet\Control\Nls\CustomLocale";

   [DllImport("kernel32", SetLastError=true)]
   private static extern int GetWindowsDirectory(StringBuilder lpBuffer, 
                                                  int nSize);

   private static string cultureName = "x-en-US-example";

   public static void Main()
   {
      // Create an alternate en-US culture.
      CultureAndRegionInfoBuilder enUS = new CultureAndRegionInfoBuilder(cultureName, CultureAndRegionModifiers.None);
      enUS.LoadDataFromCultureInfo(CultureInfo.CreateSpecificCulture("en-US"));
      enUS.LoadDataFromRegionInfo(new RegionInfo("US"));
      enUS.NumberFormat.CurrencySymbol = "USD";
      enUS.NumberFormat.CurrencyPositivePattern = 2;
      enUS.NumberFormat.CurrencyNegativePattern = 12;

      // Use reflection to get the CultureDefinition.Compile method.
      Assembly assem = Assembly.Load("sysglobl, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");            
      Type defType = assem.GetType("System.Globalization.CultureDefinition");
      MethodInfo method = defType.GetMethod("Compile", BindingFlags.NonPublic | BindingFlags.Static);
      string tempPath = @".\" + cultureName + ".nlp";
      object[] args = { enUS, tempPath };
      // Delete target file if it already exists.
      if (File.Exists(tempPath))
         File.Delete(tempPath);

      // Compile the culture definition.
      method.Invoke(null, args);  
      // Copy the file.
      try {
         StringBuilder buffer = new StringBuilder(MAX_PATH);
         int charsWritten = GetWindowsDirectory(buffer, MAX_PATH);
         string fileName = String.Format("{0}{1}Globalization{1}{2}.nlp", 
                                         buffer.ToString().Substring(0, charsWritten),
                                         Path.DirectorySeparatorChar,
                                         cultureName); 
         File.Copy(tempPath, fileName, true);
         WriteToRegistry(CUSTOM_KEY, cultureName);       
      }
      catch (UnauthorizedAccessException) {
         Console.WriteLine("You must run this application as an administrator");
         Console.WriteLine("so that you can install culture definition files."); 
         return;
      }

      // Create and use the new culture.
      try {
         decimal value = 1603.42m;
         CultureInfo ci = new CultureInfo(cultureName);
         Console.WriteLine(String.Format(ci, "{0:C2}", value));
      }
      catch (CultureNotFoundException) {
         Console.WriteLine("Unable to create the '{0}' culture.", cultureName);
      }

      CultureAndRegionInfoBuilder.Unregister(cultureName);
   }

   public static void WriteToRegistry(string keyName, string valueName)
   {
      RegistryKey key = Registry.LocalMachine.OpenSubKey(keyName, true);
      // Create the key if it does not already exist.
      if (key == null) {      
         key = Registry.LocalMachine.CreateSubKey(keyName);
         if (key == null) throw new NullReferenceException("Cannot create the registry key");
      }
      // Set the new name
      key.SetValue(valueName, valueName);
      key.Close();
   }
}
// The example displays the following output:
//        USD 1,603.42

See Also

Tasks

How to: Create Custom Cultures