Siegfried Locale Library Developer Guide

Using the sfliblocale.so library

This chapter contains information about how to include the sfliblocale.so library in projects und how to use the functions of the library.

The priciple of localization

The kind of localization for the sfliblocale.so library is quiet simple. Every text which should be localized is included in a table. The text is identified by the position in the table (the table row). The main function of the library returns a pointer to the text by using the row number as parameter. A Table must be created for every language that should be supported. Corresponding to the row number the text will be translated to the approriated language. Every table is saved as a single file. To get multi language support only the approriated table ("locale" file) is to be loaded and used. By adding "locale" files an application can be easy expand for new languages, without any additional development effort!


Inserting the library into a project

To use the library in a project, two files are needed:

File Folder Comment
sfliblocale.so develop/lib/x86 Locale Library for x86 plattform
sfliblocale.so develop/lib/ppc Locale Library for PPC plattform
SFLocale.h develop/include Class/function defintion for the sfliblocale.so

Insert both files simply to the project (by using "Add Files" command of the BeIDE).


Adding the library to an application

The sfliblocale.so library isn't a static link library, it's a dynamic link library. That means that the library is linked to the application during the startup of the program. By this, the library must be accessible. BeOS defines two folders for third party libraries. The first one is a local path "lib" at the folder where the application resides. The second path is "/boot/home/config/lib". That's the folder for shared libraries. If BeOS can't find the library in both folders an error message appears and the application terminates.

Which place for the library is the best can be decided by the developer. We recommended to use the local folder, because of an easier deinstallation. The user has only to delete the application folder to remove all files, he doesn't need to remember where to find other parts of the application.


Preparation/Organisation

For an optimal usage of the library all needed text of an application is to collect in a single (Include) file. A second (include) file should manage the text IDs (symbolic representation of the table row numbers).

The collected text is used as default (built-in) text for the application. If an application can't get an specific text from the "locale" file the built-in default text is used by the library automatically. This can be the case if the application is updated and the "locale" files are not. If an error occurs, the application is usable without "locale" files using the built-in text automatically. Simply the default built-in text is used. To get an application international running it's strongly recommend to use English for the default built-in text.

The naming convention of both files is free. For the example provide by us, we have named the text file "SFTexts.h" and the ID file "SFTextIDs.h".

The files should be have the following structure:

#ifndef _SFTEXTS
#define _SFTEXTS

#include "SFTextIDs.h"                       // The text IDs

//----------------------------------------------------------------------------
static const char   *mText[SF_LAST_ID] = 
{
"Ok",                                        //SFOK
"Cancel",                                    //SFCANCEL
"Continue",                                  //SFCONTINUE
    :
    :
"ERROR: There is no text to save!",          //SFERRNOTEXT
"Font",                                      //SFFONT
"Size",                                      //SFSIZE
};

//----------------------------------------------------------------------------

#endif

If a new text is needed, it is appended to the end of the list and the corresponding ID is inserted to "SFTextIDs.h".

Die Datei mit den Text-IDs baut sich dann wie folgt auf:

#ifndef _SFTEXTIDS
#define _SFTEXTIDS

#define SF_DEFAULT_LANGUAGE     "English"
#define SF_TEXT_APP_ID          "siegfried localeeditor locale"

#define MSG_SFLANGUAGE          'sflg'

//----------------------------------------------------------------------------
// Defintition for all used text data
//----------------------------------------------------------------------------

enum gSFTextID
{
SFOK,
SFCANCEL,
SFCONTINUE,
    :
    :
SFERRNOTEXT,
SFFONT,
SFSIZE,

SF_LAST_ID             //  MUST be always the last ID!!!
};

#endif

For every new text that is appended to the list of "SFTexts.h" the corresponding symbolic identifier (ID) is insert to "SFTextIDs.h". The ID is to be included at the end of the identifier list (but before label "SF_LAST_ID"), because the text position at the list and the position of the identfier must remain the same!

By inserting before the identifier "SF_LAST_ID" we automatically get the number of text list entries.

IMPORTANT: It's strictly to check that the position of the text and the position of the identifiers are the same. Differences will be result in very strange text outputs of the application :-)

The ID file defines two additional constants:

"SF_DEFAULT_LANGUAGE" is the name of the used default build-in language. Normally this should be "English".

The second one is "SF_TEXT_APP_ID". The application ID is a unique identifier for an application that is using "locale" files. The ID shows whether a "locale" file can be used by the application or not. Nothing brings up more strange effects than a "locale" file that isn't directed to the application ;-). The application ID is created by the developer of an application. All "locale" files of an application must have the same ID. Files with other IDs are ignored by the application. The developer is free to decide what kind of string is used for the application ID. It's a good idea if there is a reference to the application. For example, the Siegfried Locale Editor is using as application ID "siegfried localeeditor locale".

It's possible to use as ID the BApplication object signature of the application, because it's suffice unique.


Include the sfliblocale functionality to an application

To use the sfliblocale.so library in applications, at first there is some initializiation needed. The best place for this initialization is the startup function of the application, the inherited BApplication class. The initialization need only be done once.

The job of the initialization is to set the built-in text, the name of the build-in language and the application ID for the "locale" files. Thereby that text data and the text IDs are separated into different (Include) files the initiliazation is simple to realize.

Example:

#include "SuperApp.h"
#include "SFTextIDs.h"
#include "SFTexts.h"
int main()
{
SuperApp  *myApplication;

myApplication = new SuperApp();       // create/init application
myApplication->Run();                 // run application

delete(myApplication);                // delete application
return(0);                            // see you!!!
}
   
SuperApp::SuperApp()
    : BApplication("application/x-vnd.siegfriedsoft-superapp")
{  
SFLocale  *lang;
lang = SFLocale::GetInstance();       // get text data access
lang->SetAppID(SF_TEXT_APP_ID);       // set application ID
// ---  Set build-in text data ---
lang->SetDefaultText(mText, SF_LAST_ID, SF_DEFAULT_LANGUAGE);
// --- Now it's possible to access the text data.
// --- Initialization finished.
        :
        :
}

At this point it's a good idea to load/set the last used language by using the function "Setlanguage()".

After the initialization is finished at every point within the application it's possible to access the text data. Access can be get by the following call:

SFLang  *lang;
lang = SFLocale::GetInstance();        // Get text data access

It's recommend for classes that need access to the SFLocale object to declare a class private or for base classes a protected variable. To inititialize the variable use "GetInstance()" at the constructor function of the class (see chapter "Function overview / GetInstance()" for more information).


Usage of the text funtions

Without doubt "Text()" is the most used function of the sfliblocale.so library. Every text for output that an application needs is delivered by "Text()". As a parameter the function needs the ID number of the appropriated text data. The result of the function call is a pointer to a text string. if there was no text data for the used language, the pointer contain the english build-in text.

To localize a "BButton" the label must be replaced by the "Text()" function.

Example:

BButton   *button;
button = new BButton(BRect(0, 200, 200, 225), "btn_quit",
                     cLang->Text(SFQUIT), new Message(B_QUIT_REQUESTED),
                     B_FOLLOW_HCENTER+B_FOLLOW_BOTTOM);

In the example above the "SFLocale" object (cLang) is a class private variable and was initialized at the constructor of the class by "GetInstance()". The text ID "SFQUIT" is a user defined constant declared and managed in the file "SFTextIDs.h".

The resulting text pointers of "Text()" can be used in many ways. There is no problem to expand the text:

Example:

BButton  *button;
BString  string;

string = cLang->Text(SFSAVE);
string += "...";
button = new BButton(BRect(300, 10, 400, 30), "btn_filesave",
                     string.String(), new BMessage(MSG_SAVEFILE_PANEL),
                     B_FOLLOW_RIGHT+B_FOLLOW_TOP);

This kind of adding has the advantage that the amount of needed text will be reduced. The example above shows only that text "Save" is needed and not "Save" and "Save...".

Another kind of expanding text is to include numbers. This is simplified through the use of the the C library function "sprintf()".

Example:

// Text "You have saved %ld files to\n'%s'!"         // SFSAVECOUNT
char   buf[128];
long   count = 6587;
char   *dest = "/boot/home/savefolder";

sprintf(buf, cLang->Text(SFSAVECOUNT), count, dest);
Output:

You have saved 6587 files to
'/boot/home/savefolder'!


Changing the language

To change the language the best user guidance is to use a menu. A sub menu contains all available languages with the current one marked. In addition it's a good idea to react to changes in the folder of the "locale" files (e.g. if a new language is added).

Example:

BMenu       *smenu;
const char  *last_language;
      :
      :
smenu = new BMenu(cLang->Text(SFLANGUAGE));      // create language menu
// get the name of the last used language
last_Language = GetAppPreferences(....);
// create items for menu
BuildLanguageMenu(smenu, last_language);
cLocaleMenu = smenu;                             // save pointer to langauge menu
      :
      :

The function "BuildLanguageMenu()" creates the sub menu. The function walks through the language folder and adds all files with the correct application ID to the menu list. The last used language is also marked. Every menu item corresponds to a BMessage. The "what" field is set to "MSG_SFLANGUAGE". The message inlcudes an entry that contains the name of the language.

The setting of a new language is done by the inherited "MessageReceive()" function of the application window.

void SuperAppWin::MessageReceived(BMessage *msg)
{
entry_ref   ref;
app_info    info;
BPath       path;
BEntry      entry;
      :
      :
switch (msg->what)
    {
    :
    :
    //---- Sprache setzen ----
    case MSG_SFLANGUAGE:
        be_app->GetAppInfo(&info);    // get program info
        entry.SetTo(&info.ref);       // get access to program
        entry.GetPath(&path);         // path + filename
        path.GetParent(&path);        // Programmpfad ermitteln
        path.Append("locale");        // get only application path
        cLang->SetLanguage(&path, msg->FindString("language"));
        // save here the name of the language to the preferences
        // SetAppPreferences(....);
        break;
    //---- Default message management ----
    default:
        BWindow::MessageReceived(msg);
    }
}  

To change a language the function "SetLanguage()" is used. The function needs the path where the "locale" file is found and the name of the new language.


Managing "locale" files

In general it doesn't care where the "locale" files reside. The library checks if the file is a "locale" file for the application or not. It's a good idea to collect the "locale" files in a single folder. At best it's recommended to use a folder inside of the application folder. If the application resides in "/boot/home/superapp", as path for the "locale" files "/boot/home/superapp/locale" can be used. By this all files will be collected in one place enabling the user to easily de-install the application. One only has to remove the application folder, without the need to remember if there are other folders or files to remove. The name "locale" isn't a prescription. But we recommend the use of its name, because it's very intuitive for the user.

The node watching of BeOS is used to watch the folder where the "locale" files reside. The best place to activate the node watching is the constructor function of the application window:

SuperAppWin::SuperAppWin(....)
    : BWindow(...)
{
app_info  info;  
BEntry    entry;  
BPath     path;  
node_ref  nref;  
    :
    :
    :
// ---  activate node watching for the "locale" folde ---
be_app->GetAppInfo(&info);               // get program info  
entry.SetTo(&info.ref);                  // get access to program  
entry.GetPath(&path);                    // path + filename  
path.GetParent(&path);                   // only application path is needed  
path.Append("locale");                   // append language folder

if (entry.SetTo(path.Path()) == B_OK)    // get access to the folder  
    {  
    entry.GetNodeRef(&nref);             // get node_ref for folder
    // Überwachung aktivieren
    watch_node(&nref, B_WATCH_DIRECTORY, this);
    }
entry.Unset();
}

By using node watching it's simple to detect changes at the language folder (e.g. files added or removed). If the application detects changes the language sub menu is updated. The interpretation of the node watching message will be done in the "MessageReceived()" function of the window class.

void SuperAppWin::MessageReceived(BMessage *msg)
{
BMenuItem  *item;
BString    string;
int32      opcode;

switch (msg->what)
    {
    :
    :
    case B_NODE_MONITOR:
        if (msg->FindInt32("opcode", &opcode) == B_OK)
            {
            switch (opcode)
                {
                //---- file/folder deleted ----
                case B_ENTRY_REMOVED:
                //---- file/folder moved -----
                case B_ENTRY_MOVED:
                //---- file/folder created ----
                case B_ENTRY_CREATED:
                    if (cLocaleMenu)
                        {
                        while ((item = cLocaleMenu->RemoveItem((int32)0))> (BMenuItem *)NULL)
                            {
                            if (item->IsMarked())
                                string = item->Label();
                            delete item;
                            }
                        BuildLanguageMenu(cLocaleMenu, string.String());
                        }
                    break;
                }
            }
        break;
    //----
    :
    :
    }
}

Scripting for the Siegfried Locale Editor

To get an easy translation of the text data for an application, it's a good idea to copy the built-in text data to the Siegfried Locale Editor as a reference. Normaly the file that contains the built-in text ("SFTexts.h") is loaded to the editor. This is only possible for the developer(s) of the application. All other users can do a translation only if they acquire text data from the developer.

It's possible to simplfy this by using the scripting capabilities of BeOS. Scripting enables every user to directly acquire the built-in text, the language name and the application ID. The Siegfried Locale Editor uses these scripting capabilities to receive the data from every application that supports the sfliblocale.so library correctly.

To make an application locale scripting aware only the inherited "BApplication" class needs to be extended a little bit.

To interpret scripting commands the "MessageRecived()" function must be extended by "B_GET_PROPERTY":

void SuperApp::MessageReceived(BMessage *msg)
{
BMessage    spec, reply;
bool        found;
int32       index, what;
const char  *prop;

switch (msg->what)
    {
    //---- Scripting ----
    case B_GET_PROPERTY:  
        found = false;  
        if (msg->GetCurrentSpecifier(&index, &spec, &what, &prop) == B_OK)
            {
            reply.what = B_REPLY;
            if (strcmp(prop, "DefaultLanguage") == 0 && what == B_DIRECT_SPECIFIER)
                {
                found = true;
                reply.AddString("result", SF_DEFAULT_LANGUAGE);
                }
            if (strcmp(prop, "DefaultText") == 0 && what == B_DIRECT_SPECIFIER)
                {
                found = true;
                for (index = 0; index < SF_LAST_ID; index++)
                    reply.AddString("result", mText[index]);
                }
            if (strcmp(prop, "TextAppID") == 0 && what == B_DIRECT_SPECIFIER)
                {
                found = true;
                reply.AddString("result", SF_TEXT_APP_ID);
                }
            }
        if (found)
            msg->SendReply(&reply);
        else
            BApplication::MessageReceived(msg);
        break;
     //---- Default message management ----
    default:
        BApplication::MessageReceived(msg);
    }
}

The function will be extended by three scripting commands:

The above used constants "SF_LAST_ID", "SF_DEFUALT_LANGUAGE" and "SF_TEXT_APP_ID" can be found in the user defined files "SFTexts.h" and "SFTextIDs.h".

In addition, to implement scripting correct the BeOS hook functions "GetSupportedSuites()" and "ResolveSpecifier()" need to be implemented:

static property_info mPropList[] = {  
  { "DefaultText", {B_GET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get the default texts for localization", 0},  
  { "DefaultLanguage", {B_GET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get the default language name for localization", 0},  
  { "TextAppID", {B_GET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get the id for locale files", 0},  
  0 // terminate list  
};  

status_t SuperApp::GetSupportedSuites(BMessage *msg) 
{
BPropertyInfo PropInfo(mPropList);

msg->AddString("suites", "suite/vnd.SiegfriedSoft-locale");
msg->AddFlat("messages", &PropInfo);

return BApplication::GetSupportedSuites(msg);
}

BHandler *SuperApp::ResolveSpecifier(BMessage *msg, int32 index, BMessage *spec, int32 form, const char *prop) 
{
BPropertyInfo PropInfo(mPropList);

if (PropInfo.FindMatch(msg, index, spec, form, prop) >= 0) 
    return this;

return BApplication::ResolveSpecifier(msg, index, spec, form, prop);  
}