You don't need your hash to be reversible.
At runtime, all your lookups to recover an actual string should be done using the an integer ID value, so you might have a function called getLocalizedString
to perform such a lookup. Given storage for all the strings for the active language that looks something like
std::unordered_map<int, std::string> localizedStrings;
that function can be simply
std::string getLocalizedString(int id) {
return localizedStrings[id];
}
Naturally, you don't actually want to write code like getLocalizedString(892)
, you want to use a human-readable token like AURA_POISON_CLOUD
in place of 892. The simple approach is just define all such tokens
constexpr int AURA_POISON_CLOUD = 892;
but that's also a maintenance headache, so a common approach is to hash the token itself into an integer value using a macro and/or constexpr
function. For example:
#define LOCSTR(token) hash(#token)
constexpr int hash(const char* string) {
return string % 0xFFFF; // replace with a real hash function though
}
The Fowler-Noll-Vo hash function is simple to implement as a constexpr
function in C++ and works well for this purpose.
With this, you can call getLocalizedString(LOCSTR(AURA_POISON_CLOUD))
as well as using LOCSTR(AURA_POISON_CLOUD)
all over your code to refer to the string. The hash value will be computed at compile-time; the runtime will still see a call to getLocalizedString(892)
.
Just like you don't want to write 892
all over the place in code, you don't want the translation team to see 892
anywhere. You want them to see AURA_POISON_CLOUD
. The file you give them should be something that associates the original English strings with those identifiers, as a .csv
or spreadsheet or text file. The latter might look like this:
AURA_POISON_CLOUD Cloud of Poison
AURA_FLAME_CLOUD Cloud of Flame
UI_CHOOSE_CHAR Choose Your Character
UI_PLAYER_DIED You have died!
...
You give them the source file, they returning it to you having replaced the English text with whatever translations. Your game then has to load that file, and when it does, it iterates through every pair of (token, translation) and adds them to localizedStrings
:
// pseudo-code
foreach(pair in the file) {
localizedStrings.insert(hash(pair.token), pair.translation)
}
Having populated that database of translated text, getLocalizedString
will now work fine. At no point will you need to expose the actual hashed ID to a translator or person writing code. They're all able to use the human-readable token.
(Note this approach still works if you don't have constexpr
functions, you just pay the hash cost at runtime, so you might write your code a little differently, but it should generally be fine.)
(Also note that while you don't need to perform the reverse lookup ever, keeping the information for the reverse lookup around in debug builds can be helpful, but while stepping through the code in the debugger, you will see the raw hash results and having the ability to reverse the lookup in the watch window or something can be useful; you don't need this for shipping builds at all, though.)
You may also be interested in reading an approach to localization described on the Our Machinery blog.