Internationalization Cookbook
This is my personal blog. The views expressed on these pages are mine alone and not those of my employer.

Customized GetNumberFormat and GetCurrencyFormat

Q. I know about GetNumberFormat, but I want 4 decimal digits, not two. And I want a GetCurrencyFormat with my own currency.

A. You have to fill the NUMBERFMT and CURRENCYFMT with the right values.

2007.02.22: Fixed formats "3;2" / "3" that needed to become 320 / 30. Bug reported and explained by Shawn Steele. Thank you!

2007.02.22: Fixed LOCALE_STHOUSAND in NumberFormat (I was using LOCALE_SMONTHOUSANDSEP), thanks Jason Owen.

2007.02.22: Added test application, and all the code is now available for download here.

Now, why would you want to do that? The standard API is enough, isn’t it? Well, yes and no. For instance GetCurrencyFormat takes a pointer to a CURRENCYFMT or NULL. If you pass NULL, everything is formatted according with the LCID passed. But if you pass a real structure, all fields have to be filled. What if I want to format something “almost” like the standard, just slightly off?

So, what should you do if you want to display a currency without having the currency symbol changed?

Let’s say a car costs 12345.67 USD. Displaying this with US locales gives you $12,345.67, but using a Japanese locale will return ¥12.345,45. Right now, one US dollar is about 100 Japanese Yen. Offering it for ¥12.345,45 would be quite a loss.

Also, the $ symbol is used for many other dollars, not only the U.S. one (Argentina, Australia, Brunei, Canada, Chile, Columbia, Ecuador, El Salvador, Mexico, New Zealand, Singapore), so even for the American dollar you might want to use “USD” once your application “goes abroad,” to say so.

Ok, so this is what I would like to get for 12345.67 without losing the USD:
    en_US: “$12,345.67” or “USD 12,345.67”
    ro_RO: 12.345,67 dolari americani” or “12.345,67 USD”

Recently I have found out that I am not the only one who needs this (from comments to MishKa’s blog and also from some newsgroups), and since in my answers I have promised to think about making the code public, here it is (after doing the thinking).

Ok, now that we established the need, let's see how we solve this problem.
The available API is enough? Yes and no. There is nothing to fill the structures for us. But we do have GetLocaleInfo to help us do it. It is easy, but I will write some classes which will help. I will explain later why C++ and classes instead of plain C, like all the Windows API.

Now, here is the code to fill a NUMBERFMT. Quite straightforward, except for the Grouping part. But that should be clear, if you read the comment.

You can take it and use it.

// NumberFormatStruct.h
// Copyright © January 2004, Mihai Nita

#pragma once

#include <windows.h>
#include <tchar.h>

class CNumberFormatStruct {
private:
    TCHAR	decimalSep[10];
    TCHAR	thousandSep[10];
public:
    NUMBERFMT struNumberFormat;
    virtual ~CNumberFormatStruct() {};
    CNumberFormatStruct( LCID lcid = CP_ACP ) {
        GetDefaultFormat( lcid );
    };
    int GetDefaultFormat( LCID lcid );
};
// NumberFormatStruct.cpp
// Copyright © January 2004, Mihai Nita

#include "NumberFormatStruct.h"

#define	DIM(a)	(sizeof(a)/sizeof(a[0]))

int CNumberFormatStruct::GetDefaultFormat( LCID lcid )
{
    TCHAR	buff[100]; // huge, but just in case
    int	nRez = 1;

    memset( &struNumberFormat, 0, sizeof(struNumberFormat) );

    // Not using LOCALE_RETURN_NUMBER, is not available for Win 95.
    // I want to be compatible all the way back (I wonder why :-))
    // And in fact the ugliest one is LOCALE_SGROUPING, which does not support LOCALE_RETURN_NUMBER

    nRez &= GetLocaleInfo( lcid, LOCALE_IDIGITS, buff, DIM(buff) ); // max 2
    struNumberFormat.NumDigits = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_ILZERO, buff, DIM(buff) ); // max 2
    struNumberFormat.LeadingZero = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_SDECIMAL, decimalSep, DIM(decimalSep) ); // max 4
    struNumberFormat.lpDecimalSep = decimalSep;

    nRez &= GetLocaleInfo( lcid, LOCALE_STHOUSAND, thousandSep, DIM(thousandSep) ); // max 4
    struNumberFormat.lpThousandSep = thousandSep;

    nRez &= GetLocaleInfo( lcid, LOCALE_INEGNUMBER, buff, DIM(buff) ); // max 2
    struNumberFormat.NegativeOrder = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_SGROUPING, buff, DIM(buff) ); //max undocumented
    // Now the ugly part. Have to convert from something like string "3;2;0" to int 32
    TCHAR	*parseGrpS = buff;
    while( *parseGrpS ) {
        if( (*parseGrpS >= _T('1')) && (*parseGrpS <= _T('9')) )
            struNumberFormat.Grouping = struNumberFormat.Grouping * 10 + (*parseGrpS-_T('0'));
        if( (*parseGrpS != _T('0')) && !parseGrpS[1] )
			struNumberFormat.Grouping *= 10;
        parseGrpS++;
    }

    return nRez;
}

Now, why did I choose to create a class instead of plain C?

Because of the decimal and the thousand separators, which are TCHAR * in the original structure. (Ok, I am lying here, there is no NUMBERFMT structure. There is a NUMBERFMTA structure with WCHAR * and a NUMBERFMTW with char *. See How SBCS-MBCS-Unicode application interact with Windows).

So, a function trying to fill a structure will have to receive the two TCHAR * pointing to some buffers, big enough to receive the result (not too much), but the call would be something like:

NUMBERFMT nf;
TCHAR decimalSep[10];
TCHAR thousandSep[10];
 
nf.lpDecimalSep = decimalSep;
nf.lpThousandSep = thousandSep;
 
FilleStructure( lcid, &nf );
nf.NumDigits = 4;
GetNumberFormat( ..., &nf, ... );

Quite ugly, if you ask me.

Another option would be to have two static buffers in the FillStructure function.

int FillStructure( LCID lcid, NUMBERFMT &nf ) {
    static TCHAR decimalSep[10];
    static TCHAR thousandSep[10];

    if( ! nf )
        return ERROR; // or ASSERT( nf ); if you like

    ... // Fill the structure using GetLocaleInfo

    nf.lpDecimalSep = decimalSep;
    nf.lpThousandSep = thousandSep;
}

void Test( void ) {
    NUMBERFMT nf

    FilleStructure( lcid, &nf );
    nf.NumDigits = 4;
    GetNumberFormat( ..., &nf, ... );
}

But this restricts the use of the API. If one calls FillStructure with various LCIDs, the structure and the two buffers have to be saved somewhere, and even then we might have a problem in multithreaded use.

The third option is to have some kind of map storing a full collection of buffers, each for each LCID ever requested. Not easy to implement, and quite cumbersome. Ok, one can use a std::map, but this means again C++, so why not our own class.

Then it becomes quite easy to use:

CNumberFormatStruct	nf(lcid);
nf.struNumberFormat.NumDigits = 4;
GetNumberFormat( ..., &nf.struNumberFormat, ... );

Clean and nice, I think.

For the GetCurrencyFormat I am just going to give you the code. There is no major difference, just taking care of the lpCurrencySymbol field and to use the currency-related constants in GetLocaleInfo (but you already knew that a stand-alone number format can be different than the same number as part of the currency, didn't you?)

// CurrencyFormatStruct.h
// Copyright © January 2004, Mihai Nita
 
#pragma once

#include <windows.h>
#include <tchar.h>

class CCurrencyFormatStruct {
private:
    TCHAR	decimalSep[10];
    TCHAR	thousandSep[10];
    TCHAR	currencySymbol[10];
public:
    CURRENCYFMT struCurrencyFormat;
    virtual ~CCurrencyFormatStruct() {};
    CCurrencyFormatStruct( LCID lcid = CP_ACP ) {
        GetDefaultFormat( lcid );
    };
    int GetDefaultFormat( LCID lcid );
};
// CurrencyFormatStruct.cpp
// Copyright © January 2004, Mihai Nita

#include "CurrencyFormatStruct.h"

#define	DIM(a)	(sizeof(a)/sizeof(a[0]))

int CCurrencyFormatStruct::GetDefaultFormat( LCID lcid )
{
    TCHAR	buff[100]; // huge, but just in case
    int	nRez = 1;

    memset( &struCurrencyFormat, 0, sizeof(struCurrencyFormat) );

    // Not using LOCALE_RETURN_NUMBER, is not available for Win 95.
    // I want to be compatible all the way back (I wonder why :-))
    // And in fact the ugliest one is LOCALE_SGROUPING, which does not support LOCALE_RETURN_NUMBER

    nRez &= GetLocaleInfo( lcid, LOCALE_ICURRDIGITS, buff, DIM(buff) ); // max 3
    struCurrencyFormat.NumDigits = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_ILZERO, buff, DIM(buff) ); // max 2
    struCurrencyFormat.LeadingZero = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_SMONDECIMALSEP, decimalSep, DIM(decimalSep) ); // max 4
    struCurrencyFormat.lpDecimalSep = decimalSep;

    nRez &= GetLocaleInfo( lcid, LOCALE_SMONTHOUSANDSEP, thousandSep, DIM(thousandSep) ); // max 4
    struCurrencyFormat.lpThousandSep = thousandSep;

    nRez &= GetLocaleInfo( lcid, LOCALE_SCURRENCY, currencySymbol, DIM(currencySymbol) ); // max 6
    struCurrencyFormat.lpCurrencySymbol = currencySymbol;

    nRez &= GetLocaleInfo( lcid, LOCALE_INEGCURR, buff, DIM(buff) ); // max 3
    struCurrencyFormat.NegativeOrder = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_ICURRENCY, buff, DIM(buff) ); // max 3
    struCurrencyFormat.PositiveOrder = _tcstoul( buff, NULL, 0 );

    nRez &= GetLocaleInfo( lcid, LOCALE_SMONGROUPING, buff, DIM(buff) ); // max 4
    // Now the ugly part. Have to convert from something like string "3;2;0" to int 32
    TCHAR	*parseGrpS = buff;
    while( *parseGrpS ) {
        if( (*parseGrpS >= _T('1')) && (*parseGrpS <= _T('9')) )
            struCurrencyFormat.Grouping = struCurrencyFormat.Grouping * 10 + (*parseGrpS-_T('0'));
        if( (*parseGrpS != _T('0')) && !parseGrpS[1] )
			struCurrencyFormat.Grouping *= 10;
        parseGrpS++;
    }
    return nRez;
}

How to use it to localize some USD ammount without affecting the value?

You can load the localized USD name from the resources (i.e. “dolari americani”):

TCHAR currencyName[100];
LoadString( hResInstance, IDS_LOCALIZED_USD_NAME, currencyName, DIM(currencyName) );

or use GetLocaleInfo to get the ISO 4217 name:

LCID lcidUS = MAKELCID( MAKELANGID( LANG_ENGLISH, SUBLANG_ENGLISH_US ), SORT_DEFAULT );
TCHAR currencyName[100];
GetLocaleInfo( lcidUS, LOCALE_SINTLSYMBOL, currencyName, DIM(currencyName) );

And now you can use the obtained currency string with a pre-filled structure:

CCurrencyFormatStruct cfs(lcid);
cfs.struCurrencyFormat.lpCurrencySymbol = currencyName;
GetCurrencyFormat( ..., &cfs.struCurrencyFormat, ... );

Leave a comment