Macro i18n methods/best practices

Thoughts, Help, Feature Requests, Bug Reports, Developing code for...

Moderators: dorpond, trevor, Azhrei

Forum rules
PLEASE don't post images of your entire desktop, attach entire campaign files when only a single file is needed, or generally act in some other anti-social behavior. :)
Post Reply
User avatar
JoeDuncan
Giant
Posts: 118
Joined: Sun Nov 22, 2020 9:02 pm

Macro i18n methods/best practices

Post by JoeDuncan »

Hey,

I'm looking into how to provide internationalisation of macros/macro libraries within Maptool, and I'm looking for some tips or "best practices" on how to do it.

How do I find out the client locale?

Are there any i18n specific macro functions I should be aware of? (haven't found any obvious ones)

How do other people generally do it in their macro frameworks? Giant lookup tables?
"Joe's Ugly Hacks - Get 'em while their hot! Guaranteed ugliest hacks around or your money back!"

User avatar
bubblobill
Giant
Posts: 167
Joined: Sun Jan 24, 2010 3:07 pm

Re: Macro i18n methods/best practices

Post by bubblobill »

There are currently two libraries that have been released into the wild that contain i18n features, one of which I can't remember the name of and Lib:TheDoor.

In writing them I checked out various resources to see if there was a general standard or practise to meet and came up with much contradictory information. I put together what I hoped would become more or less the standard for MT macro use.

You need 5 things.
1. A function for fetching translated text. getText is the most commonly used function name for this so that is what I used.
2. Locale. Simple enough. Prompt the user and store in a library property until macros can get access to system locale.
3. A giant JSON full of translated text stored somewhere,
4. A function for editing and storing that text,
5. A function for updating macro details.

There is a feature request for proper functions so I suggest you have a look at that and make some noise.

JSON should be called messages.json to be the most compliant with general use and structured like this

Code: Select all

{"en_au":
    {
        "locale":"Locale",
        "translationsUpdated", "Translations property updated."
    } ,
"nl":
    {
        "locale":"Locale",
        "translationsUpdated":"Vertaling eigenschap bijgewerkt in het Nederlands."
     }
}
This is my macro for storing the translation text(much snipping done, currently sits at around 250 lines.

Code: Select all

[h:"<!-- -------------------------- BEGIN messages.json ---------------------------- -->"]
[h:"<!-- search ## for messages with no translation text -->"]
[r: json.messages = json.set("",
"en_au", json.set("", 
	"locale"             , "Locale",
	"translationsUpdated", "Translations property updated.",
	"word.yes"           , "yes"),
                         
"nl"                     , json.set("", 
	"locale"             , "Locale",
	"translationsUpdated", "Vertaling eigenschap bijgewerkt in het Nederlands.",
	"word.yes"           , "ja" )
)]
[h: setLibProperty("messages.json", json.messages, getMacroLocation())]
[r, g: i18n.getText("translationsUpdated")]
[h:"<!-- -------------------------- END messages.json ---------------------------- -->"]
This is my macro for the *getText* function. It takes a keyword/phrase as single argument and returns the translated text.

Code: Select all

[h:"<!-- -------------------------- BEGIN getText ---------------------------- -->"]
[h: assert(argCount(), "No argument provided to <b>"+getMacroName()+"()</b> on <i>"+getMacroLocation()+".")]
[h: vLookup		= arg(0)]
[h: vLocale		= getLibProperty("locale", getMacroLocation())]
[h: oTranslations	= getLibProperty("messages.json", getMacroLocation())]
[h: vPath			= strformat('["%{vLocale}"]["%{vLookup}"]')]
[h: vText			= json.path.read(oTranslations, vPath, "SUPPRESS_EXCEPTIONS")]
[h:"<!-- ------------------ END getText (before return 0) --------------------- -->"]
[h, if(vText!="null"), code:{ 
	[return(0, vText)]
};{	
	[vText			=	json.path.read(oTranslations, strformat('["%{vLocale}"]["translationMissing"]'), "SUPPRESS_EXCEPTIONS")]
	[broadcast(vText+" <i>search key:</i>"+vLookup, "gm-self")]
}]
[h, if(vText!="null"):
	return(0, vText);
	return(0, "Translation lookup failed for "+vLookup+".")
]
[return(0, "i18n.getText() should never reach this point.")
[h:"<!-- -------------------------- END getText ---------------------------- -->"]
Now you have a problem. To be properly internationalised you need to update macro labels and tooltips that face the user.
The issue is that once you change the name of the macro to one language, how do you know which macro you need to change if it needs to be changed again?

Beware Jay's Ugly Hack.

The maxWidth macro property is an unused artifact. It is also capable of holding a string. So I store the actual macro name that needs to be looked up in the maxWidth property.
Behold!

Code: Select all

[h:"<!-- ----------------------- BEGIN setMacroLabels ----------------------- -->"]
[h: groupIndices = getMacroGroup("0. Campaign Macros", "json")]
[h, foreach(macroIndex, groupIndices), code:{
	[macroProps=getMacroProps(macroIndex, "json")]
	[macroName = json.get(macroProps, "maxWith")]
	[if(macroName==""): macroName = json.get(macroProps, "maxWidth")]
	[switch(macroName), code:
		case"FIRSTTIMEUSE":{[macroLabel=i18n.getText("macro.label.1stTime")][macroTip=i18n.getText("macro.tooltip.1stTime")]};
		case"ToggleDoorFunction":{[macroLabel=i18n.getText("macro.label.toggleFunction")][macroTip=i18n.getText("macro.tooltip.toggleFunction")]};
		case"SetupDoor":{[macroLabel=i18n.getText("macro.label.setupDoor")][macroTip=i18n.getText("macro.tooltip.setupDoor")]};
		case"SmartSettings":{[macroLabel=i18n.getText("macro.label.smartSettings")][macroTip=i18n.getText("macro.tooltip.smartSettings")]};
		case"ConverttoDoor":{[macroLabel=i18n.getText("macro.label.convert2Door")][macroTip=i18n.getText("macro.tooltip.convert2Door")]};
		case"GMDoorPanel":{[macroLabel=i18n.getText("macro.label.GMPanel")][macroTip=i18n.getText("macro.tooltip.GMPanel")]};
		case"Settings":{[macroLabel=i18n.getText("macro.label.settings")][macroTip=i18n.getText("macro.tooltip.settings")]}
	]
	[macroProps=json.set(macroProps, "label", macroLabel, "tooltip", macroTip)]
	[if(macroName!=""):setMacroProps(macroIndex, macroProps, "json")]
}]
[h:"<!-- -------------------------- END getText ---------------------------- -->"]
if you stick with all this stylistically we should be consistent going forwards.
Bubblobill on the forum.
@Reverend on the MapTool Discord Server

Responsible for less atrocities than most... I do accept responsibility for these though: SmartDoors, Simple d20 Framework - barebones, Drop-in: All 3.5 SRD Monsters, Drop in: Simple Character Editor, Battletech Framework

Post Reply

Return to “MapTool”