In Here Be Monsters*, we have a story-driven, episodic MMORPG that has over 3500 items and 1500 quests, and with more text than the first three Harry Potter books combined – so it represented a fairly sizable challenge when we made the decision to localize the whole game!
From a technical point of view, the shear volume of words is of little consequence, although it is a significant cost concern. It’s the number of places that require localization that represents a maintainability headache.
With a conventional approach, the client application would consume a gettext file containing all the translations, and anywhere it needs to display some text it’ll substitute the original text with the localized text instead.
We found a number of issues with this approach:
- large number of files – Domain Objects/DTOs/game logic/view – need to change during implementation
- all future changes need to take localization into account
- need to replicate changes across client platforms (Flash, iOS, etc.)
- hard to get good test coverage given the scope, especially across all client platforms
- easy for regression to creep in during our frequent release cycles
- complicates and lengthens regression tests and puts more pressure on already stretched QA resources
Sounds like a dark cloud is about to take permanent residence above all our heads? It felt that way.
Instead, we decided to perform localization on the server as part of the pipeline that validates and publishes the data (quest,s achievements, items, etc.) captured in our custom CMS. The publishing process first generates domain objects that are consumable by our game servers, then converts them to DTOs for the clients.
This approach partially addresses points 3 and 4 above as it centralizes the bulk of the localization work. But it still leaves plenty of unanswered questions, the most important was the question of how to implement a solution that is:
- clean – it shouldn’t convolute our code base
- maintainable – it should be easy to maintain and hard to make mistakes even as we continue to evolve our code base
- scalable – it should continue to work well as we add more languages and localized DTO types
To answer this question, we derived a simple and yet effective solution:
- ingest the gettext translation file (the nuget package SecondLanguage comes in very handy here)
- use a PostSharp attribute to intercept string property setters on DTOs to replace input string with the localized version
- repeat for each language to generate a language specific version of the DTOs
For those of you who are not familiar with it, PostSharp is an Aspect-Oriented Programming (AOP) framework for .Net, very similar to AspectJ for Java.
Here is a simplified version of what our Localize attribute looks like:
To automatically apply localization to all present and future DTO types (assuming that all the DTO types are defined in one project), simply multicast the attribute and target all types that follows our naming convention:
[assembly: Localize(AttributeTargetTypes = "*DTO”)]
and voila, we have localized over 95% of the game with one line of code!
and here’s an example of how an almanac page in the game looks in both English and Brazilian Portuguese:
I hope you find this little story of how we localized our MMORPG interesting, and the morale of the story is really that there is much more to AOP than the same old examples you might have heard so many times before – logging, validation, etc.
With a powerful framework like PostSharp, you are able to do meta-programming on the .Net platform in a structured and disciplined way and tackle a whole range of problems that would otherwise be difficult to solve. To name a few that pops into mind:
- String interning
- Auto-implement INotifyPropertyChanged
- Auto add DataContract and DataMember attributes
- UI thread dispatching
- Performance monitoring
- Transaction handling
- Backing property with a registry value
- Making an event asynchronous
- Raise event when object is Finalized
- Dynamically introducing an interface
the list goes on, and many of these are available as part of the PostSharp pattern library too so you even get them out of the box.