You usually use the C library gettext with a _() macro that returns a localized version of the text passed in parameter.
For example:
will result in a runtime search of the string "Hello", and _() eventually returns the localized version of the string. I assume the search is implemented using some sort of binary search, so it's probably extremely fast.
It's fast, but we can get rid of the search altogether using constexpr functions!
The trick is to use a constexpr function that at compile time gets the ID of the given string. Then it's a matter of doing an array lookup in the table of strings for the desired locale. The array lookup is still unavoidable because we don't know at compile time which locale the app will use.
In my case, _() is implemented using I18NStringIndex which looks like this:
constexpr bool I18N_EQ(char const * a, char const * b) { return std::string_view(a)==b; } constexpr int I18NStringIndex(const char* s) { if (I18N_EQ(s, "off")) { return 0; } if (I18N_EQ(s, "on")) { return 1; } if (I18N_EQ(s, "ok")) { return 2; } if (I18N_EQ(s, "Quick pause")) { return 3; } if (I18N_EQ(s, "Quick pause %s")) { return 4; } if (I18N_EQ(s, "Continue")) { return 5; } if (I18N_EQ(s, "Exit")) { return 6; } if (I18N_EQ(s, "Pause")) { return 7; } if (I18N_EQ(s, "Restart")) { return 8; } if (I18N_EQ(s, "Game Over")) { return 9; } if (I18N_EQ(s, "Play again")) { return 10; } if (I18N_EQ(s, "Cancel")) { return 11; } if (I18N_EQ(s, "Game is full")) { return 12; } if (I18N_EQ(s, "Local lobby")) { return 13; } if (I18N_EQ(s, "Ready")) { return 14; } if (I18N_EQ(s, "Waiting for other players...")) { return 15; } etc...
It is brutal, but the compiler still manages to transform I18NStringIndex("Game Over") to 9, and without noticeably slowing down the compilation!
It is used together with static arrays of strings, such as this one:
std::array<const char*, 138> kStrings_FR = { "off", "on", "ok", "Mini-pause", "Mini-pause %s", "Continuer", "Quitter", "Pause", "Recommencer", "Game Over", "Rejouer", "Annuler", "Partie déjà complète", "Partie locale", "Je suis prêt !", "En attente des autres joueurs...", etc...
I18NStringIndex is generated by going over the code with a regex looking for patterns like _(".*"), and kStrings_LANG is generated using the .po files the translators filled.
One danger of this technique is that if for whatever reason the compiler can't run I18NStringIndex at compile time, then you pay a heavy price at runtime. Fortunately in C++20 you can specify the function to be consteval to make sure it does not happen.
Another downside of this technique is that whenever I18NStringIndex is re-generated, all the files containing _() need to be recompiled.
It's not a problem for me because I re-generate that function only when I'm about to send the .po files to the translators, which does not happen often.
Comments
Post a Comment