PowerShell in Depth, Second Edition (2015)
Part 3. PowerShell scripting and automation
Chapter 28. Data language and internationalization
This chapter covers
· Creating localized data tables
· Using PSD1 files
· Testing localized scripts
PowerShell v2 introduced a data language element for the shell, designed to help separate text from the functional code of a script or command. By separating text, you can make it easier to swap out alternate versions of that text. The primary use case for doing so is localizing a script, meaning you swap out your original language text strings for an alternate language. Internationalization is the act of designing your code to enable this swap-out of language-specific data.
We acknowledge up front that this is a fairly specialized feature and that few administrators will typically use it, though if you’re working for a large multinational company this feature might just be a big help. We’re including it to help ensure that this book is as complete as possible, but we’ll keep it brief. You can find additional help in two of PowerShell’s help files: about_script_internationalization and about_data_sections.
28.1. Internationalization basics
Internationalization is implemented through several specific features in PowerShell:
· A data section, which we’ll discuss next, that contains all the text strings intended for display or other output.
· Two built-in variables, $PSCulture and $PSUICulture, that store the name of the user interface language in use by the current system. That way, you can detect the language that the current user is using in Windows. $PSCulture contains the language used for regional settings such as date, time, and currency formats, whereas $PSUICulture contains the language for user interface elements such as menus and text strings.
· ConvertFrom-StringData, a cmdlet that converts text strings into a hash table, which makes it easier to import a batch of strings in a specific language and then use them from within your script. By varying the batch that you import, you can dynamically vary what your script outputs.
· The PSD1 file type, which in addition to being used for module manifests, can be used to store language-specific strings. You provide a single PSD1 file for each language you want to support.
· Import-LocalizedData, a cmdlet that imports translated text strings for a specific language into a script.
Changes to the handling of culture
Don’t assume anything about the cultures that PowerShell is running. One of us is based in the UK and in PowerShell v2 gets these results returned for $PSCulture and $PSUICulture:
PS C:\> $psculture
en-GB
PS C:\> $psuiculture
en-US
Notice that $PSCulture is what you’d expect but that the UI culture is set to US English. Additional cultural information can be found by using Get-Culture and Get-UICulture.
You should also note that in PowerShell v2 the culture can be changed, but the UI culture is dependent on the cultural version of Windows installed. This can have unintended consequences when you’re trying to run a localized script.
In PowerShell v3 and v4, this changes:
PS C:\> $PSCulture
en-GB
PS C:\> $PSUICulture
en-GB
The UI culture now reflects the system settings rather than being fixed.
Windows 8, Windows 8.1, Windows Server 2012, and Windows Server 2012 R2 have an International module that enables changes to cultural settings. You do have to restart PowerShell for the changes to take effect. We present an alternative method of temporarily changing the current culture in section 28.4. This method is great for testing multiple cultural scenarios.
We figure the best way to show you all this is to dive into a sample project and explain as we go, so that’s what we’ll do. We’re going to start with a script (shown in listing 28.1) that’s functionally very simple.
Note
The scripts in this chapter are written on PowerShell v3 and have been tested on PowerShell v3 and v4. Don’t assume backward compatibility to PowerShell v2 on your system, especially if it uses a culture different from the ones we’ve used. If your machine has a culture setting different from ours, test internationalized scripts carefully because we can’t test all possible combinations of settings.
Notice that the script includes several Write-Verbose statements that output strings of text. We’ll focus on those for our internationalization efforts. For our examples, we’re using Google Translate to produce non-English text strings. We hope any native speakers of our chosen languages will forgive any translation errors.
Listing 28.1. Our starting point, Tools.psm1
Note
This listing uses the backtick (`) character so that longer lines could be broken into multiple physical lines. If you’re typing this in, be sure to include the backtick character, and make sure it’s the very last thing on the line—it can’t be followed by any spaces or tabs. We don’t think it’s the prettiest way to type code, but it makes it easier to fit it within the constraints of the printed page.
Save this script as Tools.psm1 in \Documents\WindowsPowerShell\Modules\Tools\. Doing so will enable it to be autoloaded when PowerShell starts. Alternatively, you can load it into the console by running Import-Module tools and test it by running Get-OSInfo –computername $env:COMPUTERNAME. If you’re going to follow along, make sure that you can successfully complete those steps before continuing.
28.2. Adding a data section
Currently, your script has hardcoded strings—primarily the Write-Verbose statements, which we’re going to address—but also the output object’s property names. You could also localize the property names, but we’re not going to ask you to do that. Generally speaking, even Microsoft doesn’t translate those because other bits of code might take a dependency on the property names, and translating them would break those dependencies. If you wanted the property names to display with translated column names, then you could use a custom view to do that. You also can’t localize any parameter help messages in comment-based help that you might’ve added to your scripting project.
Take a look at the next listing, where you’re adding a data section to contain your default strings.
Listing 28.2. Adding a data section to Tools.psm1
In listing 28.2, you’ve added a data section . This uses the ConvertFrom-StringData cmdlet to convert a here-string into a hash table. The end result is that you’ll have a $msgTable object, with properties named connectionTo, starting, ending, and so on. The properties will contain the English-language values shown in the script. You can then use those properties whenever you want to display the associated text. Because this is a script module, it’d ordinarily make the $msgTable variable accessible to the global shell once the module is imported. You don’t want that; you’d rather $msgTable remain internal use only within this module. So you’ve also added an Export-ModuleMember call . By exporting your Get-OSInfo function, everything else—that is, everything you don’t explicitly export—remains private to the module and accessible only to other things within the script file.
Test the changes by removing the module, reimporting it, and then running it. Be sure to use the –Verbose switch so that you can test your localized output. Here’s what it should look like:
PS C:\> remove-module tools
PS C:\> import-module tools
PS C:\> Get-OSInfo -computerName localhost
Manufacturer OSVersion ComputerName Model
------------ --------- ------------ -----
VMware, Inc. 6.1.7601 localhost VMware Virtua...
PS C:\> Get-OSInfo -computerName localhost -verbose
VERBOSE: Starting Get-OSInfo
VERBOSE: Attempting localhost
VERBOSE: Connection to localhost succeeded
Manufacturer OSVersion ComputerName Model
------------ --------- ------------ -----
VMware, Inc. 6.1.7601 localhost VMware Virtua...
VERBOSE: Ending Get-OSInfo
As you can see, your changes seem to be successful. Your verbose output is displaying with the correct English-language strings. Now you can move on to the next step: creating translated versions of those strings.
28.3. Storing translated strings
You need to set up some new text files and a directory structure to store the translated strings. Each text file will contain a copy of your data section. Begin by creating the following new directories and files:
· \Documents\WindowsPowerShell\Modules\Tools\de-DE\Tools.PSD1
· \Documents\WindowsPowerShell\Modules\Tools\es\Tools.PSD1
By doing so, you create two localized languages, German and Spanish. The “es” and “de-DE,” as well as the “en-US” used in your data section, are language codes defined by Microsoft. You have to use the correct codes, so be sure to consult the list at http://msdn.microsoft.com/en-us/library/ms533052(v=vs.85).aspx. The filenames must also match the name of the module or script file that you’re localizing.
With the files created, copy your ConvertFrom-StringData command from the original script into the two new PSD1 files. You’ll then translate the strings. Listings 28.3 and 28.4 show the final result. As we said, you’re just using Google Translate here—we’re sure the results will be amusing to anyone who knows what these mean.
Listing 28.3. German version of Tools.PSD1
ConvertFrom-StringData @'
attempting = Versuch
connectionTo = Der anschluss an
failed = gescheitert
succeeded = gelungen
starting = Ab Get-OSInfo
ending = Ende Get-OSInfo
'@
Listing 28.4. Spanish version of Tools.PSD1
ConvertFrom-StringData @'
attempting = Intentar
connectionTo = Conexion a
failed = fracasado
succeeded = exito
starting = A partir Get-OSInfo
ending = Final Get-OSInfo
'@
Note
The way in which you type the here-strings is very specific. The closing '@ can’t be indented—it must be typed in the first two characters of a line, all by itself. Read about_here_strings in PowerShell for more information on them.
You also have to move the en-US version of the data out into its own PSD1 file; otherwise, you’ll see this sort of error when you try to import the module:
PS C:\> Import-Module tools –Force
Import-LocalizedData : Cannot find PowerShell data file
'toolsPSD1' in directory 'C:\Scripts\Modules\tools\en-US\' or
any parent culture directories.
At C:\Scripts\Modules\tools\tools.psm1:12 char:1
+ Import-LocalizedData -BindingVariable $msgTable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Scripts\Modu...n-
US\toolsPSD1:String) [Import-LocalizedData],
PSInvalidOperationException + FullyQualifiedErrorId :
ImportLocalizedData,
Microsoft.PowerShell.Commands.ImportLocalizedData
If you allow automatic loading of the module to occur (PowerShell v3 and v4), you’ll get an error that looks like this:
Write-Verbose : Cannot bind argument to parameter 'Message' because it is null
But the output should be produced. There are no guarantees on cultures we haven’t tested. The following listing shows the file. Save it in an en-US subfolder of your module folder.
Listing 28.5. en-US version of Tools.PSD1
ConvertFrom-StringData @'
attempting = Attempting
connectionTo = Connection to
failed = failed
succeeded = succeeded
starting = Starting Get-OSInfo
ending = Ending Get-OSInfo
'@
You’re not quite ready to retest the script; you must modify it to load the translated data. That’s done with the Import-LocalizedData cmdlet, and one of the two built-in variables we mentioned earlier will play a role. The cmdlet automatically uses $PSUICulture’s contents to figure out which PSD1 file to import. That means it can be tricky to test on a single-language Windows installation. We’ve called upon our international MVP contacts, who own localized versions of Windows, to help us test this. The following listing shows the changes to Tools.psm1.
Listing 28.6. Modifying tools.psm1 to import the current language
Listing 28.6 adds the Import-LocalizedData command . Because it isn’t contained in a function, it’s executed when your module is loaded. The binding variable will be used to define a hash table of localized strings. Make sure you don’t insert a $ in front of the variable. The neat thing about this command is that it automatically reads $PSUICulture, which we’ve mentioned, and looks for the PSD1 file in the appropriate subfolder. If it doesn’t find the right file, it throws an error as shown.
28.4. Testing localization
Testing nonnative localization is bit more difficult. Ideally you’ll want to test on a computer running the appropriate language. But there’s a workaround—okay, a hack—that you can use to test localization. You can’t just assign a new value to the $PSUICulture variable. You must start a temporary PowerShell thread using a new culture, as shown in the next listing.
Listing 28.7. Testing localization with Using-Culture.ps1
Param (
[Parameter(Position=0,Mandatory=$True,`
HelpMessage="Enter a new culture like de-DE")]
[ValidateNotNullOrEmpty()]
[System.Globalization.CultureInfo]$culture,
[Parameter(Position=1,Mandatory=$True,`
HelpMessage="Enter a script block or command to run.")]
[ValidateNotNullorEmpty()]
[scriptblock]$Scriptblock
)
Write-Verbose "Testing with culture $culture"
#save current culture values
$OldCulture = $PSCulture
$OldUICulture = $PSUICulture
#define a trap in case something goes wrong so we can revert back.
#better safe than sorry
trap
{
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture
Continue
}
#set the new culture
[System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture
#run the command
Invoke-command $ScriptBlock
#roll culture settings back
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture
To use this test function, specify a culture and a script block of PowerShell commands to execute. The script modifies the culture of the thread and then invokes the script block. Use the following to test your module:
PS C:\Scripts> .\Using-Culture.ps1 de-de {import-module tools -force;
get-osinfo client2 -verbose}
VERBOSE: Ab Get-OSInfo
VERBOSE: Versuch client2
VERBOSE: Der anschluss an client2 gelungen
Model ComputerName OSVersion Manufacturer
----- ------------ --------- ------------
VirtualBox client2 6.1.7601 innotek GmbH
VERBOSE: Ende Get-OSInfo
The –Force parameter is used when importing the module to ensure that the culture is refreshed correctly. It isn’t necessary to run PowerShell with elevated privileges to work with cultures in this way. We do recommend that you check carefully that your settings have been put back to the correct values when you’ve finished.
Although we’ve been demonstrating using a module, you can localize individual scripts and functions as well. Jeff has done a fair amount of localization work for a client that includes many stand-alone functions. Let’s look at another localization example that also demonstrates how to incorporate variables into your localized strings using the –f operator.
The following listing is the main script that contains a single function.
Listing 28.8. A localized function, Get-Data.ps1
Import-LocalizedData -BindingVariable msgTable
Function Get-Data {
[cmdletbinding()]
Param()
Write-Verbose ($msgtable.msg3 -f (Get-Date),$myinvocation.mycommand)
Write-Host $msgtable.msg5 -foreground Magenta
$svc=Get-Service | where {$_.status -eq "running"}
Write-Host ($msgtable.msg1 -f $svc.count)
Write-Host $msgtable.msg6 -foreground Magenta
$procs=Get-Process
Write-Host ($msgtable.msg2 -f $procs.count,$env:computername)
Write-verbose ($msgtable.msg4 -f (Get-Date),$myinvocation.mycommand)
}
The function in listing 28.8 isn’t the most groundbreaking function, but it makes a nice demonstration. Notice that you’ve moved the message strings to a culture-specific PSD1 file. Again, this will require a subfolder named for the appropriate culture. You’re testing with en-US and de-DE (listings 28.9 and 28.10).
Listing 28.9. English Get-DataPSD1
#English US strings
ConvertFrom-StringData @"
MSG1= Found {0} services that are running.
MSG2= Found {0} processes on the computer {1}.
MSG3= {0} Starting command {1}
MSG4= {0} Ending command {1}
MSG5= Getting the list of services that are currently running.
MSG6= Getting all of the running processes.
"@
Listing 28.10. German Get-DataPSD1
#localized German strings
ConvertFrom-StringData @"
MSG1= Gefunden {0} Dienste, die ausgeführt.
MSG2= Gefunden {0} Prozesse auf dem Computer {1}.
MSG3= {0} Ab Befehl {1}
MSG4= {0} Ende-Befehl {1}
MSG5= Getting der Liste der Dienste, die derzeit ausgeführt werden.
MSG6= Getting alle laufenden Prozesse.
"@
First, run the function on a computer that uses the en-US culture:
PS C:\> get-data -verbose
VERBOSE: 11/25/2013 8:35:19 PM Starting command Get-Data
Getting the list of services that are currently running.
Found 67 services that are running.
Getting all of the running processes.
Found 37 processes on the computer CLIENT2.
VERBOSE: 11/25/2013 8:35:19 PM Ending command Get-Data
Now, test it with your Using-Culture script:
PS C:\Scripts> .\Using-Culture.ps1 de-de {. f:\get-data.ps1;
get-data -verbose}
VERBOSE: 25.11.2013 20:37:59 Ab Befehl Get-Data
Getting der Liste der Dienste, die derzeit ausgeführt werden.
Gefunden 67 Dienste, die ausgeführt.
Getting alle laufenden Prozesse.
Gefunden 37 Prozesse auf dem Computer CLIENT2.
VERBOSE: 25.11.2013 20:37:59 Ende-Befehl Get-Data
Notice that the values have been inserted into the placeholders. Also notice that the date time format was affected by the change in culture.
Richard took the en-US folder and copied it as en-GB (British English). The date was displayed correctly for that culture. This shows how you can deal with minor cultural differences as well as language issues.
A bit more about data sections
The data section in a script or a PSD1 file has a strict syntax. In general, it can contain only supported cmdlets like ConvertFrom-StringData. It can also support PowerShell operators (except –match), so that you can do some logical decision making using the If...ElseIf...Elseconstruct; no other scripting language constructs are permitted. You can access the $PSCulture, $PSUICulture, $True, $False, and $Null built-in variables but no others. You can add comments, too. There’s a bit more to them, but that’s the general overview of what’s allowed. You’re not meant to put much code in there; they’re intended to separate string data from your code, not to contain a bunch more code.
28.5. Summary
We don’t see a lot of cases where administrators need to write localized scripts, but we can certainly imagine them. Larger, international organizations might well want to make the effort to localize scripts, especially when the output will be shown to end users rather than other administrators. PowerShell’s built-in support for handling multilanguage scripts is fairly straightforward to use, and as you’ve seen here it’s not difficult to convert a single-language script to this multilanguage format.