Chapter 29: Using MessageTrans
Despite considering quite a few aspects of our application, one thing that we haven’t yet thought about is the possibility that anyone might wish to use it in a language other than English. And whilst English appears to be the default language for many RISC OS applications, a significant number of users will be working in a different one day to day. It would clearly be desirable to allow our application to be translated for use in different languages so that, even if we can’t do it ourselves, other people can provide translations if they wish to do so.
Unfortunately, there’s a problem with this: we can translate the window and icon text in the Templates file, and the contents of the !Help file, but there is are several pieces of user-facing text embedded into the !RunImage file itself. Translating this would seem to require us to build a new copy of the application for each target language, or at least engage in some complicated conditional logic for selecting the appropriate pieces of text.
The Messages file
Fortunately RISC OS has a better solution, in the form of the MessageTrans module. This provides a number of SWI calls which allow us to move all of our user-facing text out of !RunImage and into a separate text file, then look the text up using simple tokens. We can add this file to the list for translation, alongside Templates and !Help, should another language be requested.
The first thing to do is to create our Messages file. Looking through our application’s source files for text which is presented to the user we can find a number of strings which will need attention, and the result is the file shown in Listing 29.1. It’s made up of a piece of text that can be written to a display icon, the menu titles and entries, and the error messages which can be presented to the user when things go wrong.
The file does not contain the application name, as this is a ‘reserved’ name which has been registered through the allocations system and therefore can’t be changed. We also haven’t included text like sprite names, which the user does not see.
# Example 29.1 # # (c) Stephen Fryatt, 2026 # # File: Messages # Calculation messages NA:n/a # Iconbar menu IBarMenu/MainMenu:Example IBarMenu0:Info IBarMenu1:Help... IBarMenu2:Quit # Main menu MainMenu0:1 Decimal place MainMenu1:2 Decimal places MainMenu2:3 Decimal places # Shapes menu ShapeMenu:Shapes ShapeMenu0:Circle ShapeMenu1:Triangle ShapeMenu2:Square # Error messages BadSprites:Failed to load application Sprites BadIBarMenu:Failed to create Iconbar Menu BadMainMenu:Failed to create Main Menu BadShapeMenu:Failed to create Shapes Menu BadMainTempl:Failed to load Main template BadInfoTempl:Failed to load ProgInfo template
Listing 29.1 (Messages): The new Messages file for MessageTrans
The format of the file is fairly simple: each line consists of a token, followed by a colon (:) and then the text which is to be associated with it. Tokens can contain any character except for a colon (since that denotes their end), comma (,), closing bracket ()), forward slash (/) or question mark (?).
Blank lines, and lines starting with a #, are ignored or treated as comments.
As an example, if we looked up the token “MainMenu2” in the file above, we would have the text “3 Decimal Places” returned to us, whilst looking up the token “NA” would return the text “n/a”. The tokens are case sensitive, so attempting to look up “mainmenu2” or “na” would not match the same entries (but these could be separate entries in their own right).
It is possible to associate two or more tokens with a single piece of text by separating the token names with a forward slash or a new line. We have used this in our file to have a single entry for the menu title associated with both the “IBarMenu” and “MainMenu” tokens – this makes sense, as both titles are based on the name of the application. We could also have written this as
IBarMenu MainMenu:Example
and it is up to the developer which they choose. In addition to our menu titles, this feature can be extremely useful when working with things like repetitive interactive help messages.
Within token names, the ? is treated as a single-character wildcard, which provides another way of handling duplicated texts. If we had the line
Help.IBar.Menu?:This is an entry in the Icon Bar menu.in our messages file, it would match the tokens “Help.IBarMenu0”, “Help.IBarMenu1”, “Help.IBarMenuA” – and a whole lot more variations!
The token names that we have selected are fairly arbitrary. The aim is to keep them short, but also to make them clear and memorable so that there is no question what they refer to. That said, other approaches to naming tokens do exist – in the end, it is up to the developer to decide what they are most comfortable with (and what they expect any translators to appreciate)!
Loading the Messages file
The MessageTrans module provides a number of SWIs to help us use this messages file: MessageTrans_FileInfo will supply information about the file that we will need in order to call MessageTrans_OpenFile, and MessageTrans_CloseFile will tidy up when our application exits.
The SWIs provided by MessageTrans, and the description in the Programmer’s Reference Manual, look complicated because the module supports a number of different use cases – including ROM-based modules looking their messages up directly in the ROM-based ResourceFS. For a standard disc-based application, things can be a little bit simpler: we ask MessageTrans to load the file into our application space using memory that we supply, which removes a lot of the potential pitfalls. However, if you do plan to use the SWIs in different ways to the one described here, make sure that you have read and understood all of the requirements in the PRM!
extern void messagetrans_file_info( char const *file_name, messagetrans_file_flags *flags, int *size );
We call MessageTrans_FileInfo, which is defined by OSLib as shown above, with *file_name being a pointer to the filename of our file. If we also supply pointers to variables in which to return flags and the file size, it will return us some information about the file and the amount of memory required to load it into memory. In practice, the only flag that is defined is messagetrans_DIRECT_ACCESS, which indicates that the file is already in memory (it may be stored in ResourceFS, for example) and can be accessed directly from there without it needing to be loaded into RAM first. As previously mentioned, this feature tends to be used by ROM modules, and therefore isn’t something that will affect us.
Armed with the size of the file, we can allocate the memory that we need, and also allocate space for a messagetrans_control_block structure which is what MessageTrans uses for a file handle.
struct messagetrans_control_block { int cb [4]; }; typedef struct messagetrans_control_block messagetrans_control_block; extern void messagetrans_open_file( messagetrans_control_block *cb, char const *file_name, char *buffer );
With the preparations in place, we can call MessageTrans_LoadFile using the OSLib definition here, to load the file into memory and ready it for use. Tracking message files is a fairly standard process which is similar across most applications, and SFLib provides a msgs library which will do most of the work for us. We’ll make use of it, but will look inside first to see what it’s doing.
The library starts by defining a couple of global variables, *message_block and *message_buffer, which will point to the MessageTrans control block and the block of application memory into which the file will be loaded. These can be seen in Listing 29.2.
static messagetrans_control_block *message_block = NULL; static char *message_buffer = NULL;
Listing 29.2: The global variables used by the msgs library
When we come to load a file, the first step is to pass the filename – pointed to by *messages_file – to MessageTrans_FileInfo so that the amount of memory required to load it can be determined. This value is returned from the SWI call via the pointer that we supply to the message_size variable.
int message_size = 0; messagetrans_file_info(messages_file, NULL, &message_size);
When the SWI returns, we allocate two blocks of memory: a MessageTrans control block whose pointer goes into the static *message_block variable, and the memory that we will be asking MessageTrans to load the file into, whose pointer goes into the static *message_buffer variable.
message_block = malloc(sizeof (messagetrans_control_block)); message_buffer = malloc(message_size);
The final step is to call MessageTrans_LoadFile with the filename, and the pointers to the two memory blocks, to load the file into memory and set it up as an actual MessageTrans instance.
messagetrans_open_file(message_block, messages_file, message_buffer);
Our messages file is now held in the memory pointed to by *message_buffer and we can access it through the control block pointed to by message_block.
Putting all of the blocks together and adding in the missing error handling gives us the msgs_initialise() function seen in Listing 29.3.
osbool msgs_initialise(char *messages_file) { int message_size = 0; /* Validate the inputs. */ if (message_block != NULL || message_buffer != NULL || messages_file == NULL) return FALSE; /* Read the file details and allocate the necessary memory. */ if (xmessagetrans_file_info(messages_file, NULL, &message_size) != NULL) return FALSE; message_block = malloc(sizeof (messagetrans_control_block)); message_buffer = malloc(message_size); if (message_block == NULL || message_buffer == NULL) { if (message_block != NULL) free(message_block); if (message_buffer != NULL) free(message_buffer); message_block = NULL; message_buffer = NULL; return FALSE; } /* Load the file */ if (xmessagetrans_open_file(message_block, messages_file, message_buffer) != NULL) { if (message_block != NULL) free(message_block); if (message_buffer != NULL) free(message_buffer); message_block = NULL; message_buffer = NULL; return FALSE; } /* Return successfully. */ return TRUE; }
Listing 29.3: Loading a MessageTrans file
Closing the file and freeing up the memory used is a bit easier, and uses the MessageTrans_CloseFile SWI, which is defined by OSLib like this.
void messagetrans_close_file( messagetrans_control_block const *cb );
After checking that there is actually a file open (and that *message_block is not NULL), we simply call MessageTrans_CloseFile and pass it the pointer to the MessageTrans contol block. The memory which had been allocated is then freed.
The full msgs_terminate() function can be seen in Listing 29.4.
osbool msgs_terminate(void) { if (message_block == NULL) return FALSE; messagetrans_close_file(message_block); free(message_block); if (message_buffer != NULL) free(message_buffer); message_block = NULL; message_buffer = NULL; return TRUE; }
Listing 29.4: Closing a MessageTrans file
Looking up messages
With our messages file loaded, we will need to be able to look up the tokens and convert them into user-facing text. There are a number of ways to do this, but the one which is most likely to be useful is the MessageTrans_Lookup SWI – which OSLib defines as follows.
extern char *messagetrans_lookup( messagetrans_control_block const *cb, char const *token, char *buffer, int size, char const *arg0, char const *arg1, char const *arg2, char const *arg3, int *used );
It takes a number of parameters, including a pointer to our control block in *cb, a pointer to the token to be looked up in *token, a pointer to a buffer to hold the resulting text in *buffer, and the size of that buffer in size. It returns a pointer to the output text (which in our case should just be a pointer to the buffer pointed to by *buffer) and the length of the returned string if we supply a pointer in *used.
The three parameters *arg0, *arg1, *arg2 and *arg0 are optional pointers to strings which can be substituted into the text from the messages file if it contains placeholders %0, %1, %2 or %3 respectively. This allows run-time values to be included in messages. For example, if we had the token
OnWall:%0 Green Bottlesand called MessageTrans_Lookup with
messagetrans_lookup(cb, "OnWall", buffer, BUFFER_LEN, "10", NULL, NULL, NULL, NULL);
then we could expect the memory pointed to by *buffer to contain the text “10 Green Bottles” afterwards. Note that the four parameters are pointers to strings: it is up to the application to convert numbers or other data into string form before passing them to MessageTrans_Lookup.
SFLib’s msgs library contains a number of functions which can be used to access MessageTrans_Lookup, which ultimately all end up by calling msgs_param_lookup_result() as defined in Listing 29.5. Given C’s vagaries around the use of strings, care is taken to try to ensure that a valid '\0'-terminated string is returned in most circumstances. This should only fail if the supplied buffer itself is invalid – in which situation, our application probably has other more important problems to worry about first!
osbool msgs_param_lookup_result(char *token, char *buffer, size_t buffer_size, char *a, char *b, char *c, char *d) { os_error *error; if (buffer == NULL || buffer_size == 0) return FALSE; /* If there's no token, return an empty buffer. */ if (token == NULL) { *buffer = '\0'; return FALSE; } /* If there's no message block, instead of using the Global block, return the supplied token. */ if (message_block == NULL) { string_copy(buffer, token, buffer_size); return FALSE; } /* Look up the token. */ error = xmessagetrans_lookup(message_block, token, buffer, buffer_size, a, b, c, d, NULL, NULL); /* If there was an error, return an empty buffer. */ if (error != NULL) { *buffer = '\0'; return FALSE; } return TRUE; }
Listing 29.5: Looking up a message token
The function starts by checking that *buffer isn’t NULL and that size is non-zero. If either of these checks fail, then it can’t safely write anything to the buffer and so simply returns FALSE to indicate failure.
Once we know that we have a buffer, we check that *token isn’t NULL; if it is, we terminate the buffer and then return FALSE. The next check is to see if msgs_initialise() has been called (because *message_block will be NULL otherwise); if it hasn’t, we copy the token into the buffer to give some indication of what the intent was, and return FALSE. The final step is to call messagetrans_lookup(), terminating the buffer and returning FALSE if an error occurred. If nothing went wrong, we return TRUE.
The full set of parameters expected by msgs_param_lookup_result() can be a bit cumbersome in many circumstances, so the msgs library defines a few more functions to give us other options: these can be seen in Listing 29.6. A sibling function – in that it returns success or failure but doesn’t need the four parameter pointers to be passed if they are all NULL – is msgs_lookup_result().
The other two functions are msgs_param_lookup() and msgs_lookup(), which both return pointers to the buffer supplied to them instead of success or failure. This can be useful when we need to pass a pointer to a message into another function.
char *msgs_lookup(char *token, char *buffer, size_t buffer_size) { msgs_param_lookup_result(token, buffer, buffer_size, NULL, NULL, NULL, NULL); return buffer; } char *msgs_param_lookup(char *token, char *buffer, size_t buffer_size, char *a, char *b, char *c, char *d) { msgs_param_lookup_result(token, buffer, buffer_size, a, b, c, d); return buffer; } osbool msgs_lookup_result(char *token, char *buffer, size_t buffer_size) { return msgs_param_lookup_result(token, buffer, buffer_size, NULL, NULL, NULL, NULL); }
Listing 29.6: More ways to look up a message token
Updating the code
To add MessageTrans support to our application, we will start in c.main by adding some lines to main_initialise() and main_terminate(). In main_initialise() we add a call to msgs_initialise() just before we call wimp_initialise(), exiting with a fatal error if MessageTrans failed to set itself up. The full code can be seen in Listing 29.7.
static void main_initialise(void) { os_error *error; osspriteop_area *sprites; /* Load the messages file. */ if (!msgs_initialise("<ExamplApp$Dir>.Messages")) error_report_fatal("Failed to load application Messages"); /* Initialise with the Wimp. */ wimp_initialise(wimp_VERSION_RO3, main_application_name, NULL, NULL); error_initialise(main_application_name, main_application_sprite, NULL); event_add_message_handler(message_QUIT, EVENT_MESSAGE_INCOMING, main_message_quit); /* Initialise library modules. */ url_initialise(); /* Load the application sprites. */ sprites = resources_load_user_sprite_area("<ExamplApp$Dir>.Sprites"); if (sprites == NULL) error_msgs_report_fatal("BadSprites:Failed to load application Sprites"); /* Open the templates file. */ error = xwimp_open_template("<ExamplApp$Dir>.Templates"); if (error != NULL) error_report_program(error); /* Initialise the program modules. */ calc_initialise(); ibar_initialise(main_application_sprite); win_initialise(sprites); /* Close the templates file. */ wimp_close_template(); }
Listing 29.7: Initialising the application with MessageTrans
Since the call to error_report_fatal() won't return we can be confident that, if we get as far as wimp_initialise(), then we probably have a working instance of MessageTrans. There’s no point tokenising the error message, as if it gets used we know that MessageTrans probably isn’t working for us anyway!
Further down the file, we can tokenise the error reported if the sprites fail to load simply by swapping the call to error_report_fatal() into the similarly-named error_msgs_report_fatal(). SFLib’s errors library provides sibling MessageTrans calls for many of its reporting functions, which simply go on to call msgs_lookup() for us. Here we are showing another feature offered by MessageTrans: if the text supplied to MessageTrans_Lookup contains a colon, then what follows is treated as a fallback text in case the lookup fails for any reason. This lets us provide a backup error message, to reduce the chance that the user is faced with nothing should things go wrong.
In main_terminate(), we simply call msgs_terminate() after calling wimp_close_down() as seen in Listing 29.8.
static void main_terminate(void) { wimp_close_down(0); msgs_terminate(); }
Listing 29.8: Tidying up MessageTrans on exit
Translating the menus
A significant source of the text within our messages file are our application’s menus, so perhaps the next place to look to convert is the menu generation within c.menus. Currently the menu_create() and menu_entry() functions take a pointer to the text which we want to appear in the menu title or entry, so it would be useful if these could take a pointer to a MessageTrans token instead. If we look at the code to set up the menu title in menu_create(), we find the following.
len = strlen(title); if (len > 12) buffer = malloc(len + 1); if (buffer != NULL) { strncpy(buffer, title, len); buffer[len] = '\0'; menu->title_data.indirected_text.text = buffer; } else { strncpy(menu->title_data.text, title, 12); }
If the length of the text is twelve characters or less then we simply copy it straight into the menu block, otherwise we allocate a buffer and copy the text into that so that we can create an indirected icon. At present, *title is one of the parameters to menu_create(), but we change that parameter to *token and instead declare title[] as a local variable on the stack.
char title[MENU_MAX_LINE_LEN]
We can then insert a message token lookup ahead of the code above.
msgs_lookup(token, title, MENU_MAX_LINE_LEN);
Taken together, we end up with the function seen in Listing 29.9. Note that we have made an arbitrary decision that the texts for menu titles (and, as we’ll see soon, menu entries) won’t be more than 63 characters long. Working with strings in C often results in these sorts of compromises, and it’s worth bearing in mind that the messages files are to an extent part of the application that we’re in control of. Also, if a text is too long it will simply be truncated: the application won’t crash.
We can also make a very similar change to menu_entry(), as seen in Listing 29.10.
Armed with these new versions of menu_create() and menu_entry(), we can change ibar_initialise() in c.ibar so that it uses MessageTrans tokens instead of the original text. The errors raised if there are problems loading the program information window template or creating the iconbar menu have been updated in the same way that we did with the error when loading the application sprites – although we haven’t provided any fallback text this time.
/* Program Info Window. */ window_definition = windows_load_template("ProgInfo"); if (window_definition == NULL) { error_msgs_report_error("BadInfoTempl"); return; } prog_info = wimp_create_window(window_definition); free(window_definition); event_add_window_icon_click(prog_info, IBAR_PROGINFO_ICON_WEB, ibar_proginfo_web_click); /* Iconbar Menu. */ ibar_menu = menu_create("IBarMenu", 3); if (ibar_menu == NULL) { error_msgs_report_error("BadIBarMenu"); return; } menu_entry(ibar_menu, IBAR_MENU_INFO, "IBarMenu0", (wimp_menu *) prog_info); menu_entry(ibar_menu, IBAR_MENU_HELP, "IBarMenu1", NULL); menu_entry(ibar_menu, IBAR_MENU_QUIT, "IBarMenu2", NULL);
We can also update win_initialise() within c.win in a similar way.
/* Load and create the window. */ window_definition = windows_load_template("Main"); if (window_definition == NULL) { error_msgs_report_error("BadMainTempl"); return; } icons = window_definition->icons; icons[WIN_ICON_SHAPE].data.indirected_sprite.area = sprites; win_handle = wimp_create_window(window_definition); free(window_definition); /* Window Menu. */ win_menu = menu_create("MainMenu", 3); if (win_menu == NULL) { error_msgs_report_error("BadMainMenu"); return; } menu_entry(win_menu, WIN_MENU_DECIMAL1, "MainMenu0", NULL); menu_entry(win_menu, WIN_MENU_DECIMAL2, "MainMenu1", NULL); menu_entry(win_menu, WIN_MENU_DECIMAL3, "MainMenu2", NULL); /* Shapes Menu. */ win_shape_menu = menu_create("ShapeMenu", 3); if (win_shape_menu == NULL) { error_msgs_report_error("BadShapeMenu"); return; } menu_entry(win_shape_menu, WIN_MENU_SHAPE_CIRCLE, "ShapeMenu0", NULL); menu_entry(win_shape_menu, WIN_MENU_SHAPE_TRIANGLE, "ShapeMenu1", NULL); menu_entry(win_shape_menu, WIN_MENU_SHAPE_SQUARE, "ShapeMenu2", NULL);
Tying up loose ends
The other place that we have some user-facing text to translate is in c.calc, where we have “n/a” being used to denote “not applicable”.
static char *calc_not_applicable = "n/a";
This change isn’t quite as simple, but we convert calc_not_applicable from a pointer to a character array and then look the token up in calc_initialise(). Once again, we are making an arbitrary decision on how long this text can be – although in this case, there is also the limit from the length of the icon that the text is displayed in which needs to be considered. That limit is set by the indirected text length of the icons in the template file.
#define CALC_NOT_APPLICABLE_LENGTH 8 static char calc_not_applicable[CALC_NOT_APPLICABLE_LENGTH]; void calc_initialise(void) { calc_set_format(2); calc_set_shape(CALC_SHAPE_NONE); msgs_lookup("NA", calc_not_applicable, CALC_NOT_APPLICABLE_LENGTH); }
Finally, with this change in place, our application no longer has any hard-coded English text within the !RunImage file – meaning that it can be translated into other languages by editing the Messages, Templates and !Help files. From an end-user’s perspective, however, it should remain functionally identical.
The full set of code can be found in Download 29.1. Why not try changing the text in the Messages, to see what effect it has?
Another approach
A sizeable amount of the work in this chapter was spent on translating our application’s menus, so before moving on we should probably mention that there are other ways to approach this particular requirement. Instead of building the menu structures in memory at run-time using our menu_create() and menu_entry() functions, we could take a similar approach to the one used by window templates: that is, to store the ready-built structures in a file and load them as the application initialises.
The advantage of this approach is that it separates the menu data from the application code, just as templates do for the contents of windows. The text used in menu titles and entries exists in one place, and can be translated easily; the menu data files can then be swapped out in exactly the same way that window templates can. It also reduces the amount of data stored within the !RunImage.
The problem is that, unlike window templates where the Wimp gives us the Wimp_OpenTemplate, Wimp_LoadTemplate and Wimp_CloseTemplate SWIs to make this work, there’s no similar facility offered for menus. A number of third-party soluations exist and, in my own software, I use a tool called MenuGen – which can be found elsewhere on my website. This builds the menu blocks up into a data file which can be loaded in a similar manner to window templates. At the application side, SFLib provides three functions – menus_load_templates(), menus_link_dbox() and menus_get_menu() – which can load the data into memory, link in dialogue boxes and give us access to the menus within. For those who are working in BASIC, my WimpLib has similar functaionality. This is all a long way from anything that’s “standard practice”, however, so we’ll leave it as a footnote for now; it may be something to return to in a future chapter if there’s enough interest.
And, of course, this ability to load different files is somewhat academic at present, as our application is currently unable to support more than one translation at at time; we’ll find out how to resolve this problem in the next chapter.


