Unicode & Internationalization
FiveWin provides a comprehensive internationalization (i18n) system with two main components: a multi-language string translation system (FWString) and Unicode support for displaying non-ASCII characters in windows and controls. This page covers both systems in detail, including encoding considerations and practical examples.
FWString() - The Translation System
FiveWin's translation system is built on a static array called aStrings defined in source\function\strings.prg. Each element is an array of strings: index 1 is English, 2 is Spanish, 3 is French, 4 is Portuguese, 5 is German, 6 is Italian. When you call FWString("some text"), it looks up the English text in the array and returns the translation for the current language.
How It Works
- On startup,
FWLanguageID()auto-detects the language fromHB_LangSelect()and maps it to a language index (1=EN, 2=ES, 3=FR, 4=PT, 5=DE, 6=IT). - All FiveWin internal UI strings (menus, buttons, messages, report previewer labels) go through
FWString(). - When the current language is English (index 1),
FWString()returns the input string immediately (no lookup needed). - For other languages, it searches
aStringsfor a case-insensitive match on the English text and returns the translated string at the current language index. - If the string is not found, it is added to a "missing strings" list and the original English text is returned.
Built-in Strings
FiveWin includes translations for hundreds of common UI strings: button labels (Ok, Cancel, Save, Print), menu items (File, Edit, View, Help), dialog titles, error messages, report preview labels (First, Previous, Next, Last, Zoom), data browser labels (Add, Edit, Del, Search, Filter), and more.
Key Functions
| Function | Description |
|---|---|
FWString( cString ) | Translates an English string to the current language. Returns the original if no translation found. |
FWSetLanguage( nLang ) | Sets the active language. Returns the previous language ID. nLang: 1=EN, 2=ES, 3=FR, 4=PT, 5=DE, 6=IT, 7+ = custom. |
FWLanguageID() | Returns the current language index. Auto-detects from HB_LangSelect() on first call. |
FWAddString( aString ) | Adds a translation entry. aString can be { "English", "Spanish", "French", ... } or a pipe-delimited string "English|Spanish|French|...". |
FWSetString( nLang, aString ) | Sets a translation for a specific language index. aString: { "EnglishKey", "TranslatedValue" }. Can also accept arrays of arrays. |
FWAddLanguage( aLang, nLang ) | Adds an entire new language column. aLang is an array of all translated strings. nLang is the target index (auto-assigned if nil). |
FWLoadStrings( cFileName ) | Loads additional translations from an INI-format file (default: "fwstrings.ini" in app directory). |
FWSaveStrings( cFileName ) | Saves all current translations to an INI-format file. |
FWMissingStrings() | Returns array of strings that were requested but not found. Writes "missing.str" file. |
FWEditStrings() | Opens an interactive XBrowse editor for all translation strings (dev tool). |
FWSetLanguage() - Setting the Active Language
Language ID Reference
| ID | Language | HB_LangSelect() Code |
|---|---|---|
| 1 | English | EN |
| 2 | Spanish | ES |
| 3 | French | FR |
| 4 | Portuguese | PT |
| 5 | German | DE |
| 6 | Italian | IT |
| 7+ | Custom (added via FWAddLanguage) | -- |
Example: Set Language at Startup
function Main()
// Set to Spanish
FWSetLanguage( 2 )
// Now all FiveWin UI strings appear in Spanish
// MsgInfo() title will say "Informacion" instead of "Information"
// Report previewer shows "Imprimir" instead of "Print", etc.
DEFINE WINDOW oWnd TITLE FWString( "My Application" )
// ...
ACTIVATE WINDOW oWnd
return nil
FWLoadStrings() - Loading Custom Translations
You can load additional translations from an INI file at runtime. The file uses a [strings] section with numbered entries, where each value contains pipe-separated translations.
INI File Format (fwstrings.ini)
[strings]
1=Customer|Cliente|Client|Cliente|Kunde|Cliente
2=Invoice|Factura|Facture|Fatura|Rechnung|Fattura
3=Product|Producto|Produit|Produto|Produkt|Prodotto
4=Total Amount|Importe Total|Montant Total|Valor Total|Gesamtbetrag|Importo Totale
5=Due Date|Fecha Vencimiento|Date Echeance|Data Vencimento|Faelligkeitsdatum|Data Scadenza
Example: Load Custom Translations
function Main()
// Load application-specific translations
FWLoadStrings( "c:\myapp\translations.ini" )
// Set language
FWSetLanguage( 2 ) // Spanish
// Now custom strings are translated too
? FWString( "Customer" ) // "Cliente"
? FWString( "Invoice" ) // "Factura"
? FWString( "Total Amount" ) // "Importe Total"
return nil
Adding Custom Translations Programmatically
FWAddString() - Add Individual Entries
// Add a single translation (all 6 languages)
FWAddString( { "Customer", "Cliente", "Client", "Cliente", "Kunde", "Cliente" } )
// Add using pipe-delimited string
FWAddString( "Invoice|Factura|Facture|Fatura|Rechnung|Fattura" )
// Add multiple entries at once (array of arrays)
FWAddString( { ;
{ "Customer", "Cliente", "Client", "Cliente", "Kunde", "Cliente" }, ;
{ "Invoice", "Factura", "Facture", "Fatura", "Rechnung", "Fattura" }, ;
{ "Product", "Producto", "Produit", "Produto", "Produkt", "Prodotto" } ;
} )
FWSetString() - Set Translation for Specific Language
// Set Spanish (language 2) translations
FWSetString( 2, { "Customer", "Cliente" } )
FWSetString( 2, { "Invoice", "Factura" } )
// Set multiple at once
FWSetString( 2, { ;
{ "Customer", "Cliente" }, ;
{ "Invoice", "Factura" }, ;
{ "Product", "Producto" } ;
} )
FWAddLanguage() - Add an Entirely New Language
// Add Japanese as language 7
// The array must match the order of the English strings in aStrings
local aJapanese := { ;
"Attention", ... } // This needs ALL strings translated
// Or more practically, add empty and fill specific strings:
local nJapanese := FWAddLanguage( {} ) // Returns the new language ID (e.g., 7)
// Then fill in translations one at a time
FWSetString( nJapanese, { "Ok", "OK" } )
FWSetString( nJapanese, { "Cancel", "Cancel" } )
FWSetString( nJapanese, { "Print", "Print" } )
FWSetLanguage( nJapanese ) // Switch to Japanese
FW_SetUnicode() - Unicode Mode
By default, FiveWin creates windows and controls using the ANSI ("A") versions of Windows API functions (e.g., CreateWindowExA). When you enable Unicode mode with FW_SetUnicode(.T.), FiveWin switches to the Wide ("W") API functions (e.g., CreateWindowExW), allowing windows and controls to display characters from any language -- Chinese, Japanese, Korean, Arabic, Hindi, and more.
How Unicode Mode Affects FiveWin
When Unicode mode is active:
- All new windows and controls are created with wide-character API calls.
- Text is internally handled as UTF-8 in Harbour and converted to UTF-16 for Windows API calls.
- Each TWindow/TControl has an
lUnicodeDATA that inherits from the global setting. - TClipBoard auto-detects UTF-8 text and uses CF_UNICODETEXT format.
- SAY controls, GET fields, menus, and message boxes all support Unicode text.
Function Reference
| Function | Description |
|---|---|
FW_SetUnicode( [lOnOff] ) | Sets or gets Unicode mode. Call with .T. to enable, .F. to disable. Call without parameter to query current state. |
FW_IsUTF8( cStr ) | Returns .T. if the string contains valid UTF-8 multi-byte sequences. |
Example: Enable Unicode
function Main()
// Enable Unicode BEFORE creating any windows
FW_SetUnicode( .T. )
local oWnd
DEFINE WINDOW oWnd TITLE "Unicode Demo"
// These will display correctly in Unicode mode:
@ 1, 1 SAY "English: Hello World" OF oWnd
@ 2, 1 SAY "Chinese: " + Chr(20320) + Chr(22909) OF oWnd
@ 3, 1 SAY "Japanese: " + Chr(12371) + Chr(12435) + Chr(12395) + Chr(12385) + Chr(12399) OF oWnd
@ 4, 1 SAY "Arabic: " + Chr(1605) + Chr(1585) + Chr(1581) + Chr(1576) + Chr(1575) OF oWnd
ACTIVATE WINDOW oWnd
return nil
UTF-8 Utility Functions
When working in Unicode mode, strings in Harbour are stored as UTF-8. FiveWin provides helper functions for UTF-8 string manipulation that correctly handle multi-byte characters.
UTF-8 String Functions
| Function | Description |
|---|---|
FW_UTF8LEFT( cStr, nLeft ) | Returns leftmost nLeft characters (not bytes) from a UTF-8 string. |
FW_UTF8RIGHT( cStr, nLen ) | Returns rightmost nLen characters from a UTF-8 string. |
FW_UTF8SUBSTR( cStr, nPos, nLen ) | Extracts a substring by character position (not byte position). |
FW_UTF8STUFF( cSrc, nPos, nDelete, cInsert ) | Inserts/replaces characters in a UTF-8 string by character position. |
FW_UTF8PADCHAR( cStr, nChars ) | Pads a UTF-8 string to nChars characters. |
FW_UTF8PADBYTE( cText, nBytes ) | Pads a UTF-8 string to nBytes total bytes. |
Why UTF-8 Functions Are Needed
Standard Harbour string functions (Left, Right, SubStr, Len) operate on bytes, not characters. A single Unicode character in UTF-8 can be 1-4 bytes. Using Left( cUTF8, 3 ) might cut a multi-byte character in half, producing garbled text. Use FW_UTF8LEFT( cUTF8, 3 ) instead to get exactly 3 characters regardless of byte length.
// Example: UTF-8 character-safe operations
local cText := "Hello" // Mixed ASCII + multi-byte
// WRONG: may split multi-byte characters
// local cFirst := Left( cText, 3 )
// CORRECT: character-aware substring
local cFirst := FW_UTF8LEFT( cText, 3 )
local cLast := FW_UTF8RIGHT( cText, 2 )
local cMid := FW_UTF8SUBSTR( cText, 2, 3 )
OEM vs ANSI vs UTF-8 Encoding
Understanding the three encoding systems is critical for correct text display in FiveWin applications, especially when mixing console output, Windows GUI, and file I/O.
Encoding Overview
| Encoding | When Used | Description |
|---|---|---|
| OEM (DOS) | Console output, DBF files, legacy data | The original IBM PC character set (CP437, CP850, etc.). Characters above 127 are line-drawing and accented characters specific to the codepage. |
| ANSI (Windows) | Windows controls, "A" API functions | Windows-1252 or locale-specific codepage. Characters above 127 include accented letters for Western European languages. |
| UTF-8 | Unicode mode, modern data, web/JSON | Variable-length encoding (1-4 bytes per character). ASCII-compatible for chars 0-127. Used with "W" API functions (converted to UTF-16). |
Console output
DOS programs"] B -->|ANSI/Windows| D["SAY controls (non-Unicode)
MsgInfo/MsgAlert
Windows API 'A' calls"] B -->|UTF-8| E["Unicode controls
JSON/REST APIs
Web content
Modern data files"] C -->|"hb_oemToAnsi()"| D D -->|"hb_AnsiToOem()"| C D -->|"AnsiToUTF8() or hb_StrToUTF8()"| E E -->|"UTF8ToAnsi() or hb_UTF8ToStr()"| D
Conversion Functions
| Function | From | To | Notes |
|---|---|---|---|
hb_oemToAnsi( cStr ) | OEM | ANSI | Harbour built-in. Converts DOS/DBF data for Windows display. |
hb_AnsiToOem( cStr ) | ANSI | OEM | Harbour built-in. Converts Windows text for DBF storage. |
hb_StrToUTF8( cStr ) | ANSI | UTF-8 | Harbour built-in. For passing ANSI text to UTF-8 contexts. |
hb_UTF8ToStr( cStr ) | UTF-8 | ANSI | Harbour built-in. May lose characters not in the ANSI codepage. |
utf8toutf16( cStr ) | UTF-8 | UTF-16LE | For Windows "W" API calls. Used internally by FiveWin Unicode mode. |
When to Use Which Encoding
- Reading from DBF files: Data is typically OEM-encoded. If displaying in a SAY or GET control in non-Unicode mode, use
hb_oemToAnsi(). In Unicode mode, convert viahb_oemToAnsi()thenhb_StrToUTF8(). - Displaying in Windows controls: In ANSI mode (default), controls expect ANSI strings. In Unicode mode (
FW_SetUnicode(.T.)), controls accept UTF-8 which FiveWin converts to UTF-16 internally. - REST API / JSON data: Always UTF-8. If displaying in non-Unicode FiveWin, convert to ANSI first. In Unicode mode, use directly.
- MsgInfo / MsgAlert: These use the window's Unicode setting. In Unicode mode, pass UTF-8 strings. In ANSI mode, pass ANSI strings.
- Writing to files: Choose encoding based on target: OEM for DBF compatibility, UTF-8 for modern interchange, ANSI for legacy Windows files.
Codepage Considerations
// Set Harbour codepage for proper OEM/ANSI conversion
REQUEST HB_CODEPAGE_ESWIN // Spanish Windows codepage
HB_CDPSELECT( "ESWIN" ) // Select it
// Or for French:
REQUEST HB_CODEPAGE_FRWIN
HB_CDPSELECT( "FRWIN" )
// Harbour language selection (affects date formats, error messages)
REQUEST HB_LANG_ES
HB_LANGSELECT( "ES" )
Legacy single-byte (non-Unicode) applications
A legacy application that stores genuine single-byte data in a Windows codepage (CP1250 Central European, CP1251 Cyrillic, CP1252 Western, CP1253 Greek, CP1254 Turkish, CP1257 Baltic, etc.) and does not use UTF-8 should select its Harbour codepage and turn Unicode mode off. This is the supported configuration -- no extra FW_SetCdp() call is needed unless the PC's ANSI codepage differs from the application's data codepage:
REQUEST HB_CODEPAGE_TRWIN // e.g. Turkish (Windows-1254)
HB_CDPSELECT( "TRWIN" )
FW_SetUnicode( .F. ) // ANSI mode: single-byte data preserved
// Only if the machine ANSI codepage is NOT the data codepage:
// FW_SetCdp( 1254 )
With a comctl32 v6 manifest the EDIT controls are created Unicode, so keystrokes arrive as UTF-16. FiveWin's GET converts each keystroke back to the selected single-byte codepage, so characters whose Unicode codepoint is above 255 (Turkish ş ğ ı İ, Polish/Czech/Baltic accents, Cyrillic and Greek letters) are stored as their correct single byte. (Builds before June 2026 had a regression that showed ? for those characters in this configuration -- see the GET documentation.)
Complete Multi-Language Application
This example shows a complete application with a language selector that dynamically switches the UI language for all FiveWin controls and custom strings.
#include "FiveWin.ch"
static oWnd, oBar, oBtnSave, oBtnPrint, oSayStatus
function Main()
// Enable Unicode for full character support
FW_SetUnicode( .T. )
// Add application-specific translations
FWAddString( { ;
{ "My Application", "Mi Aplicacion", "Mon Application", "Meu Aplicativo", "Meine Anwendung", "Mia Applicazione" }, ;
{ "Customer List", "Lista de Clientes", "Liste des Clients", "Lista de Clientes", "Kundenliste", "Lista Clienti" }, ;
{ "New Customer", "Nuevo Cliente", "Nouveau Client", "Novo Cliente", "Neuer Kunde", "Nuovo Cliente" }, ;
{ "Save Record", "Guardar Registro", "Enregistrer", "Salvar Registro", "Datensatz Speichern","Salva Record" }, ;
{ "Print Report", "Imprimir Reporte", "Imprimer Rapport", "Imprimir Relatorio", "Bericht Drucken", "Stampa Report" }, ;
{ "Language", "Idioma", "Langue", "Idioma", "Sprache", "Lingua" }, ;
{ "Ready", "Listo", "Pret", "Pronto", "Bereit", "Pronto" } ;
} )
// Default to English
FWSetLanguage( 1 )
DEFINE WINDOW oWnd TITLE FWString( "My Application" )
BuildUI()
ACTIVATE WINDOW oWnd
return nil
//------------------------------------------------------------//
function BuildUI()
// Toolbar / button bar
@ 1, 1 BUTTON oBtnSave PROMPT FWString( "Save Record" ) OF oWnd ;
SIZE 120, 28 ACTION MsgInfo( FWString( "Save Record" ) )
@ 1, 16 BUTTON oBtnPrint PROMPT FWString( "Print Report" ) OF oWnd ;
SIZE 120, 28 ACTION MsgInfo( FWString( "Print Report" ) )
// Language selector
@ 1, 32 BUTTON PROMPT FWString( "Language" ) OF oWnd ;
SIZE 100, 28 ACTION SelectLanguage()
// Status
@ 4, 1 SAY oSayStatus PROMPT FWString( "Ready" ) OF oWnd SIZE 300, 20
return nil
//------------------------------------------------------------//
function SelectLanguage()
local aLangs := { "English", "Espanol", "Francais", "Portugues", "Deutsch", "Italiano" }
local nChoice := 0
nChoice := Alert( FWString( "Language" ), aLangs )
if nChoice > 0
FWSetLanguage( nChoice )
RefreshUI()
endif
return nil
//------------------------------------------------------------//
function RefreshUI()
// Update window title
oWnd:SetText( FWString( "My Application" ) )
// Update button captions
oBtnSave:SetText( FWString( "Save Record" ) )
oBtnPrint:SetText( FWString( "Print Report" ) )
// Update status
oSayStatus:SetText( FWString( "Ready" ) )
return nil
Adding Custom Translations via INI File
For larger applications or when translations are managed by non-developers, use an INI file to store translations that are loaded at startup.
Step 1: Create the translations file
; File: translations.ini
; Format: n=English|Spanish|French|Portuguese|German|Italian
[strings]
1=Customer|Cliente|Client|Cliente|Kunde|Cliente
2=Invoice|Factura|Facture|Fatura|Rechnung|Fattura
3=Product|Producto|Produit|Produto|Produkt|Prodotto
4=Total Amount|Importe Total|Montant Total|Valor Total|Gesamtbetrag|Importo Totale
5=Due Date|Vencimiento|Echeance|Vencimento|Faelligkeit|Scadenza
6=Payment|Pago|Paiement|Pagamento|Zahlung|Pagamento
7=Discount|Descuento|Remise|Desconto|Rabatt|Sconto
8=Tax|Impuesto|Taxe|Imposto|Steuer|Imposta
9=Shipping|Envio|Expedition|Envio|Versand|Spedizione
10=Notes|Notas|Notes|Notas|Anmerkungen|Note
Step 2: Load at application startup
function Main()
// Load custom translations
FWLoadStrings( cFilePath( GetModuleFileName( GetInstance() ) ) + "translations.ini" )
// Set language (could be read from user preferences)
local oIni := TIni():New( "myapp.ini" )
local nLang := oIni:Get( "Settings", "Language", 1 )
FWSetLanguage( nLang )
// All FWString() calls now return the correct language
MsgInfo( FWString( "Customer" ) + ": John Doe" )
// In Spanish: "Cliente: John Doe"
return nil
Step 3: Save language preference when user changes it
function ChangeLanguage( nNewLang )
local oIni := TIni():New( "myapp.ini" )
FWSetLanguage( nNewLang )
oIni:Set( "Settings", "Language", nNewLang )
// Refresh all visible UI elements
RefreshAllWindows()
return nil
Finding Missing Translations
During development, use FWMissingStrings() to discover which strings in your application lack translations.
// At the end of a test run:
function CheckTranslations()
local aMissing
// Set to a non-English language to trigger lookups
FWSetLanguage( 2 ) // Spanish
// ... run through your application UI ...
// Check what's missing
aMissing := FWMissingStrings()
// This writes "missing.str" with ready-to-paste code:
// FWSetString( nLang, { ;
// { "Customer List", }, ;
// { "New Customer", }, ;
// ...
// } )
? "Missing translations:", Len( aMissing )
return nil
Interactive String Editor
// Opens an XBrowse grid with all translation strings
// You can edit translations directly and they take effect immediately
FWEditStrings()
Best Practices
1. Always use FWString() for user-visible text
// GOOD: translatable
@ 1, 1 SAY FWString( "Customer" ) OF oDlg
MsgInfo( FWString( "Record saved successfully" ) )
// BAD: hard-coded, not translatable
@ 1, 1 SAY "Customer" OF oDlg
MsgInfo( "Record saved successfully" )
2. Enable Unicode early
// Call FW_SetUnicode(.T.) BEFORE creating any windows
function Main()
FW_SetUnicode( .T. ) // First thing!
// ... create windows after this
return nil
3. Use correct codepage for your data
// For applications using DBF with accented characters:
REQUEST HB_CODEPAGE_ESWIN
HB_CDPSELECT( "ESWIN" )
// For UTF-8 data (JSON, REST APIs, modern databases):
FW_SetUnicode( .T. )
4. Handle encoding at data boundaries
// Reading from DBF (OEM) to display in Unicode window:
local cName := Customers->Name // OEM encoded
cName := hb_oemToAnsi( cName ) // Convert to ANSI
// In Unicode mode, FiveWin handles ANSI->UTF-16 automatically
// Reading from JSON API (UTF-8):
local cData := hb_jsonDecode( cResponse ) // Already UTF-8
// In Unicode mode, display directly
// In ANSI mode: cData := hb_UTF8ToStr( cData )
5. Test with non-Latin scripts
When building international applications, test with Chinese, Arabic, or Cyrillic text to verify that Unicode mode is properly enabled and all controls display correctly. Pay special attention to:
- GET fields (input length may differ from display width with multi-byte characters)
- XBrowse column widths (auto-size may need adjustment)
- Printer output (ensure the selected font supports the required characters)
- File I/O (save as UTF-8, not ANSI, if the data contains non-Latin characters)