3

I have been trying to implement a way to make my program bilingual : the user could chose if the program should display French or English (in my case). I have made lots of researches and googling but I still cannot find a good example on how to do that :/

I read about gettext, but since this is for a school's project we are not allowed to use external libraries (and I must admit I have nooo idea how to make it work even though I tried !)

Someone also suggested to me the use of arrays one for each language, I could definitely make this work but I find the solution super ugly.

Another way I thought of is to have to different files, with sentences on each line and I would be able to retrieve the right line for the right language when I need to. I think I could make this work but it also doesn't seem like the most elegant solution.

At last, a friend said I could use DLL for that. I have looked up into that and it indeed seems to be one of the best ways I could find... the problem is that most resources I could find on that matter were coded for C# and C++ and I still have no idea how I would do to implement in C :/ I can grasp the idea behind it, but have no idea how to handle it in C (at all ! I do not know how to create the DLL, call it, retrieve the right stuff from it or anything >_<)

Could someone point me to some useful resources that I could use, or write a piece of code to explain the way things work or should be done ? It would be seriously awesome !

Thanks a lot in advance !

(Btw, I use visual studio 2012 and code in C) ^^

Zoé de Moffarts
  • 165
  • 1
  • 2
  • 9

3 Answers3

2

If you can't use a third party lib then write your own one! No need for a dll.

The basic idea is the have a file for each locale witch contains a mapping (key=value) for text resources.

The name of the file could be something like

resources_<locale>.txt

where <locale> could be something like en, fr, de etc.

When your program stars it reads first the resource file for specified locale.

Preferably you will have to store each key/value pair in a simple struct.

Your read function reads all key/value pair into a hash table witch offers a very good access speed. An alternative would be to sort the array containing the key/value pairs by key and then use binary search on lookup (not the best option, but far better than iterating over all entries each time).

Then you'll have to write a function get_text witch takes as argument the key of the text resource to be looked up an return the corresponding text in as read for the specified locale. You have to handle keys witch have no mapping, the simplest way would be to return key back.

Here is some sample code (using qsort and bsearch):

#include<stdio.h> 
#include<stdlib.h>
#include<string.h>

#define DEFAULT_LOCALE "en"
#define NULL_ARG "[NULL]"

typedef struct localized_text {
    char* key;
    char* value;
} localized_text_t;

localized_text_t* localized_text_resources = NULL;
int counter = 0;

char* get_text(char*);
void read_localized_text_resources(char*);
char* read_line(FILE*);
void free_localized_text_resources();
int compare_keys(const void*, const void*);
void print_localized_text_resources();


int main(int argc, char** argv)  
{
    argv++;
    argc--;

    char* locale = DEFAULT_LOCALE;

    if(! *argv) {
        printf("No locale provided, default to %s\n", locale);
    } else {
        locale = *argv;
        printf("Locale provided is %s\n", locale);
    }

    read_localized_text_resources(locale);

    printf("\n%s, %s!\n", get_text("HELLO"), get_text("WORLD"));
    printf("\n%s\n", get_text("foo"));

    free_localized_text_resources();

    return 0;  
} 


char* get_text(char* key)
{
    char* text = NULL_ARG;
    if(key) {
        text = key;
        localized_text_t tmp;
        tmp.key = key;
        localized_text_t* result = bsearch(&tmp, localized_text_resources, counter, sizeof(localized_text_t), compare_keys);
        if(result) {
            text = result->value;
        }
    }    
    return text;
}

void read_localized_text_resources(char* locale)
{
    if(locale) {
        char localized_text_resources_file_name[64];
        sprintf(localized_text_resources_file_name, "resources_%s.txt", locale);
        printf("Read localized text resources from file %s\n", localized_text_resources_file_name);
        FILE* localized_text_resources_file = fopen(localized_text_resources_file_name, "r");
        if(! localized_text_resources_file) {
            perror(localized_text_resources_file_name);
            exit(1);
        }
        int size = 10;
        localized_text_resources = malloc(size * sizeof(localized_text_t));
        if(! localized_text_resources) {
            perror("Unable to allocate memory for text resources");
        }

        char* line;
        while((line = read_line(localized_text_resources_file))) {
            if(strlen(line) > 0) {
                if(counter == size) {
                    size += 10;
                    localized_text_resources = realloc(localized_text_resources, size * sizeof(localized_text_t));
                }
                localized_text_resources[counter].key = line;
                while(*line != '=') {
                    line++;
                }
                *line = '\0';
                line++;
                localized_text_resources[counter].value = line;
                counter++;
            }
        }
        qsort(localized_text_resources, counter, sizeof(localized_text_t), compare_keys);
        // print_localized_text_resources();
        printf("%d text resource(s) found in file %s\n", counter, localized_text_resources_file_name);
    }
}


char* read_line(FILE* p_file)
{
    int len = 10, i = 0, c = 0;
    char* line = NULL;

    if(p_file) {
        line = malloc(len * sizeof(char));
        c = fgetc(p_file);
        while(c != EOF) {
            if(i == len) {
                len += 10;
                line = realloc(line, len * sizeof(char));
            }
            line[i++] = c;
            c = fgetc(p_file);
            if(c == '\n' || c == '\r') {
                break;
            }
        }

        line[i] = '\0';

        while(c == '\n' || c == '\r') {
            c = fgetc(p_file);
        }
        if(c != EOF) {
            ungetc(c, p_file);
        }

        if(strlen(line) == 0 && c == EOF) {
            free(line);
            line = NULL;
        }
    }

    return line;
}


void free_localized_text_resources()
{
    if(localized_text_resources) {
        while(counter--) {
            free(localized_text_resources[counter].key);
        }
        free(localized_text_resources);
    }
}


int compare_keys(const void* e1, const void* e2)
{
    return strcmp(((localized_text_t*) e1)->key, ((localized_text_t*) e2)->key);
}


void print_localized_text_resources() 
{
    int i = 0;
    for(; i < counter; i++) {
        printf("Key=%s  value=%s\n", localized_text_resources[i].key, localized_text_resources[i].value);
    }
}

Used with the following resource files

resources_en.txt

WORLD=World
HELLO=Hello

resources_de.txt

HELLO=Hallo
WORLD=Welt

resources_fr.txt

HELLO=Hello
WORLD=Monde

run

(1) out.exe     /* default */
(2) out.exe en
(3) out.exe de
(4) out.exe fr

output

(1) Hello, World!
(2) Hello, World!
(3) Hallo, Welt!
(4) Hello, Monde!
A4L
  • 17,353
  • 6
  • 49
  • 70
  • This is a pretty cool solution ! :D Thanks ! I had to adapt the code a tiny bit for some stuff (VS was complaining about some cast missing, but nothing too bad). quick questions though : if I compile it as .cpp file it works just fine, if I change the extension to .c, it starts complaining about a bunch of variables not being defined ? I split the code into a .h file and a .cpp file : it then starts giving LINK2005 error, saying that localized_text_resources and counter is already defined. Removing them from the header and place them on top of .cpp file seems to fix it though... but why ? ^^ – Zoé de Moffarts Apr 25 '13 at 13:47
  • You're welcome! The code was compiled with `GNU` `gcc` compiler (the C compiler not g++ witch is the C++ compiler) as a single `.c` file. Regarding the cast warnings, if you compile code containing `malloc` calls with `C++` compiler then you need to cast it to the target type, this is not mandatory if you compile the code with a `C` compiler. Regarding not defined variables, i can't think of a reason why since all variables were defined as global and before any use, MS tend to make their own standards :/ . You need to look into the docs of the VS c compiler. – A4L Apr 25 '13 at 18:27
  • Regarding Link2005 maybe you had then in both files O_o. Changing the extension from `.c` to `.cpp` lets the IDE (VS in your case) automatically choose the appropriate compiler. So one time you are compiling using C compiler and C rules and the other time using the C++ compiler and the C++ rules. – A4L Apr 25 '13 at 18:30
0

gettext is the obvious answer but it seems it's not possible in your case. Hmmm. If you really, really need a custom solution... throwing out a wild idea here...

1: Create a custom multilingual string type. The upside is that you can easily add new languages afterwards, if you want. The downside you'll see in #4.

//Terrible name, change it
typedef struct
{
    char *french;
    char *english;
} MyString; 

2: Define your strings as needed.

MyString s;
s.french = "Bonjour!";
s.english = "Hello!";

3: Utility enum and function

enum
{
    ENGLISH,
    FRENCH
};

char* getLanguageString(MyString *myStr, int language)
{
    switch(language)
    {
        case ENGLISH:
            return myStr->english;
            break;
        case FRENCH:
            return myStr->french;
            break;
        default:
            //How you handle other values is up to you. You could decide on a default, for instance
            //TODO
    }
}

4: Create wrapper functions instead of using plain old C standard functions. For instance, instead of printf :

//Function should use the variable arguments and allow a custom format, too
int myPrintf(const char *format, MyString *myStr, int language, ...)
{
    return printf(format, getLanguageString(myStr, language));
}

That part is the painful one : you'll need to override every function you use strings with to handle custom strings. You could also specify a global, default language variable to use when one isn't specified.

Again : gettext is much, much better. Implement this only if you really need to.

SolarBear
  • 4,534
  • 4
  • 37
  • 53
  • That's could be done simpler. For example some my old CGI used [macros](http://eddy-em.livejournal.com/5319.html) (text in russian!). – Eddy_Em Apr 24 '13 at 19:03
0

the main idea of making programs translatable is using in all places you use texts any kind of id. Then before displaying the test you get the text using the id form the appropriate language-table.

Example:

instead of writing

printf("%s","Hello world");

You write

printf("%s",myGetText(HELLO_WORLD)); 

Often instead of id the native-language string itself is used. e.g.:

printf("%s",myGetText("Hello world"));

Finally, the myGetText function is usually implemented as a Macro, e.g.:

printf("%s", tr("Hello world")); 

This macro could be used by an external parser (like in gettext) for identifying texts to be translated in source code and store them as list in a file.

The myGetText could be implemented as follows:

std::map<std::string, std::map<std::string, std::string> > LangTextTab;
std::string GlobalVarLang="en"; //change to de for obtaining texts in German

void readLanguagesFromFile()
{

  LangTextTab["de"]["Hello"]="Hallo";
  LangTextTab["de"]["Bye"]="Auf Wiedersehen";
  LangTextTab["en"]["Hello"]="Hello";
  LangTextTab["en"]["Bye"]="Bye";
}

const char * myGetText( const char* origText )
{
  return LangTextTab[GlobalVarLang][origText ].c_str();
}

Please consider the code as pseudo-code. I haven't compiled it. Many issues are still to mention: unicode, thread-safety, etc... I hope however the example will give you the idea how to start.

Valentin H
  • 7,240
  • 12
  • 61
  • 111
  • Your idea is a very slow variant of `gettext`. Gettext is better because it stores localisations in a BD-like file, so to find some text it don't need any time scan all strings, but get a string it needs by hash. Your variant will scan all translations every time you run `myGetText`. – Eddy_Em Apr 24 '13 at 19:59
  • Seems to me like it could be a nice way to do it, but I think your code is in C++ and I am really not familiar with it syntax so I have no idea what is happening there for example : std::map > LangTextTab; std::string GlobalVarLang="en"; – Zoé de Moffarts Apr 25 '13 at 13:49
  • @ZoédeMoffarts map is a key-value map container in the C++ standard library. Among other things (e.g. providing fast key-search as it is a Red-Black tree, not a list :) it provides also subscript operator - it looks like array but for any index-types not only numbers. It wasn't my intention to provide you the code but to explain the idea. Looks, like you've got it now. P.S: I can't see many reasons for using C on systems offering C++ compilers. Just take a look at the example I provided and the one of A4L. They do pretty the same with dramatically different LOCs :-) – Valentin H Apr 25 '13 at 20:44