Data language and internationalization - PowerShell scripting and automation - PowerShell in Depth, Second Edition (2015)

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.