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.

graph TD subgraph "String Translation System" A["aStrings static array"] --> B["FWString( cEnglish )"] B --> C{"FWLanguageID()"} C -->|1| D[English] C -->|2| E[Spanish] C -->|3| F[French] C -->|4| G[Portuguese] C -->|5| H[German] C -->|6| I[Italian] C -->|7+| J[Custom Languages] end subgraph "Unicode System" K["FW_SetUnicode(.T.)"] --> L["Windows W APIs"] L --> M["CreateWindowExW"] L --> N["SetWindowTextW"] L --> O["SendMessageW"] end

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

  1. On startup, FWLanguageID() auto-detects the language from HB_LangSelect() and maps it to a language index (1=EN, 2=ES, 3=FR, 4=PT, 5=DE, 6=IT).
  2. All FiveWin internal UI strings (menus, buttons, messages, report previewer labels) go through FWString().
  3. When the current language is English (index 1), FWString() returns the input string immediately (no lookup needed).
  4. For other languages, it searches aStrings for a case-insensitive match on the English text and returns the translated string at the current language index.
  5. 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

FunctionDescription
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

IDLanguageHB_LangSelect() Code
1EnglishEN
2SpanishES
3FrenchFR
4PortuguesePT
5GermanDE
6ItalianIT
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

graph LR A["FW_SetUnicode(.T.)"] --> B["Window Creation"] A --> C["Control Text"] A --> D["Clipboard"] A --> E["Dialog Creation"] B --> F["CreateWindowExW()"] C --> G["SetWindowTextW()"] D --> H["CF_UNICODETEXT"] E --> I["DialogBoxIndirectW()"]

When Unicode mode is active:

Function Reference

FunctionDescription
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

FunctionDescription
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

EncodingWhen UsedDescription
OEM (DOS)Console output, DBF files, legacy dataThe 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 functionsWindows-1252 or locale-specific codepage. Characters above 127 include accented letters for Western European languages.
UTF-8Unicode mode, modern data, web/JSONVariable-length encoding (1-4 bytes per character). ASCII-compatible for chars 0-127. Used with "W" API functions (converted to UTF-16).
graph TD A[Source Text] --> B{Encoding} B -->|OEM/DOS| C["DBF data
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

FunctionFromToNotes
hb_oemToAnsi( cStr )OEMANSIHarbour built-in. Converts DOS/DBF data for Windows display.
hb_AnsiToOem( cStr )ANSIOEMHarbour built-in. Converts Windows text for DBF storage.
hb_StrToUTF8( cStr )ANSIUTF-8Harbour built-in. For passing ANSI text to UTF-8 contexts.
hb_UTF8ToStr( cStr )UTF-8ANSIHarbour built-in. May lose characters not in the ANSI codepage.
utf8toutf16( cStr )UTF-8UTF-16LEFor Windows "W" API calls. Used internally by FiveWin Unicode mode.

When to Use Which Encoding

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: