The localization of Nocturnarya took exactly one month. It was a complex project, with numerous dialogue lines and a few challenging sections, as discussed in a previous post. Some terms posed difficulties during translation, such as Blood Cartridge, Fang Sword, Bloody Spirit, Diabolical Nurse, and others. We included the option to switch between vampiress and vampire identities for narrative purposes, without any impact on gameplay (our tests confirmed that there were no bugs resulting from this feature).
Submissions of the localized versions of Nocturnarya (German, French, Spanish, Dutch, Italian and Portuguese) are already underway, to our regular publishers. These localized versions will be available for Windows only, and there are no plans for a Steam release. All in all, considering both the time spent on localization and the development of the original English game, Nocturnarya stands as our largest project to date. Since Spooky Dwellers 2 was already localized in September, we can now shift our focus towards developing a new game.
Regarding this new project, we’re still discussing a few ideas. One of our goals for 2024 is creating at least one Match 3 game, that’s for sure. However, we’d like to explore other genres, if time and budget permit. We’ll likely write a post when we have clearer ideas about our next title. Stay tuned! 🙂
Our current project is the localization of Nocturnarya Collector’s Edition. As usual, our target languages are: German, French, Dutch, Spanish, Portuguese and Italian. It’ll be a Windows-only release. This localization is a daunting task, which comprises a few intricate parts:
The Story: Nocturnarya’s story is a bit long, as the game builds a particular vampire lore, with adventure, scheming, military invaders, treasons and loyalty. The game portrays several characters of which the most important is you, the player. At its core, it’s a battle between you and Grysmore, the boss of the human invaders.
The vampire and the vampiress: In the original English game, you can choose how you want to be referred to as the story progresses: as a vampire, a vampiress, or a rockstar renegade. Localizing this is tricky, because of articles and genre-specific details in all the target languages. We’ll be removing the rockstar renegade option, and will do our best to keep the vampire and vampiress options. However, if it proves to be too error-prone we’ll just scrap this option altogether.
The Village: The game has a section that allows you to build a 3D village for your vampire tribe. However, the help window and the tutorials amount to a lot of text to translate.
The Match 3 levels: This includes all the help texts, instructions and user interface of the Match 3 levels.
Collector’s Edition goodies: Last but not least, we’ll have to translate the extra stories and minigames offered as Collector’s Edition exclusive bonuses.
If everything goes smoothly, this localization should be completed by the end of this month.
Localization in Unity with No Such Localization is easy and truly convenient. We used that package to localize our game 10th Corpse into several languages, and it was a straightforward process. When the time came to localize our game, we evaluated the localization options available for Unity. The canon choice, so to speak, is Unity’s own Localization package which, however, required us to update our project and also, at that time, did not have an expedited installation (it had to be installed manually.) In the long run, Unity’s Localization package is likely to become the standard and the first choice of Unity developers, but for now it’s still at the preview stage.
For 10th Corpse, however, No Such Localization was a wise choice. In fact, No Such Localization offers much more than what we really needed to localize our game, which only required localization of strings. No images and no audio had to be localized in 10th Corpse, but if your project needs that, No Such Localization easily allows for it.
What is No Such Localization?
It’s a localization package for Unity, and it comes in 2 versions: Lite and Pro. Both versions are available in the Unity Asset Store. The Pro version (paid) offers even more features, such as support for Right-To-Left (RTL) languages (Hebrew, etc.), automatic variable replacement, and translation source classes for JSON and CSV files (more about this in a while.) Even the Lite version is awesome and, as told above, it offers much more than what our game’s localization required. No Such Localization integrates naturally with the Unity editor, and requires no coding unless you want to extend its functionality. And that’s the beauty of the package: its architecture, which makes it a breeze understanding and extending its functionality.
How to work with No Such Localization?
Let’s first understand the architecture of No Such Localization. The official documentantion is here and is very good. You’ll work with 3 classes of components. The most important of these components is the LocalizationService. It’s the core of your localization, and you should have a single instance in your scenes (ideally, use DontDestroyOnLoad to have single, persistent LocalizationService object if your project comprises multiple scenes.) Just create an empty object and append a LocalizationService component. Now, in the Locales list, add the languages you want to support, e.g., English and German. There are a few attributes, but you’ll be mostly changing Current Locale. Once you complete the localization process, modifying this Current Locale will immediately switch your project’s content to the localized strings and assets corresponding to such language.
So, main component: LocalizationService. We have yet to work with other two components: ComponentLocalizer and BaseTranslationSource. LocalizationService is our core, our hub, the ruler of all the localization of your game. However, it needs to know what scene’s objects (strictly speaking, components) it has to localize, and it also needs to know where it’s going to find the content with which it will localize those objects. For instance, let’s suppose you have a Text object (again, strictly speaking, an object with a Text component) displaying a “Welcome” string. The LocalizationService can automatically localize that object, but it needs to know that such object has to be localized (that’s the responsibility of ComponentLocalizer), and it needs to know what content it will use to localize the object (that’s the responsibility of BaseTranslationSource.) You can subclass both ComponentLocalizer and BaseTranslationSource to adapt them to the requirements of your project, but No Such Localization comes with classes that will suffice for basic requirements. For instance, the package includes a TextLocalizer component.
In continuation of our example, let’s add a TextLocalizer to our Text object having the “Welcome” string. You’ll notice that the TextLocalizer component has a Phrase attribute. When localizing, you’ll have to remove string literals (e.g., “Welcome”) from your components and replace them with unique IDs. The localization components will used those unique IDs to identify the content. Place unique IDs in that Phrase attribute, e.g., welcome_text. Now we need a Translation Source with an entry for that unique ID, welcome_text. The package comes with a very basic translation source, StandaloneTranslationSource which we can recur to for a simple use case like this. Create another object, add a StandaloneTranslationSource component, and edit its Translation List attribute to add welcome_text as an entry of such list, with translations for Locale English and Locale German.
That’s all. All the components of No Such Localization will connect among them automatically, and if you go back to your LocalizationService component and change the Current Locale you’ll see your text component modifying its content accordingly. It’s like magic!
Of course, if your project is medium or large sized, working with the StandaloneTranslationSource can be tiresome and error-prone. You might want to read your content from a file (the Pro version of this component comes with translation sources for JSON and CSV files.) The wonderful thing about architecture of No Such Localization is that you can extend it as you wish (we created our own Translation Source class for 10th Corpse.) And you can also create your own ComponentLocalizer for your specialized widgets, for example. You can localize text, images, sound, etc. Further information in the official documentation. An excellent tool for localizing your Unity project!
Languages supported: English, German, French, Dutch, Italian, Portuguese and Spanish.
Updated libraries.
DragonScales 3 was the first DragonScales game with Gravity Mode and falling skulls. Furthermore, the game showcases 5 boss battles combining a new gravity approach with the classic DragonScales tile-matching gameplay. DragonScales 3’s story is based on a sinister prophecy, the Eternal Prophecy of Darkness. There is a group of dark cultists who want this prophecy to come true. Their plan is to bring back Specktr to life. Specktr! The ultimate embodiment of malice! Rumor has it that Specktr, once resurrected, will grant immortality to his followers. One of such followers, incidentally, is the powerful Archmage Cedric. DragonScales 3 is also the first DragonScales 3 with references to a fantasy kingdom, the Gravity Kingdom.
When localizing DragonScales 3 we experienced a baffling issue with an internal tool whose purpose is simply to replace text in a group of files. Those UTF-8 encoded files contain messages loaded by the game from the very beginning. However, after running the tool, the game started crashing when reading such files. By using an old buddy, fc /B, we found out that our tool was “injecting” a few extra bytes at the start of the file: EF BB BF. In short, the tool was altering the encoding of files from UTF-8 to UTF-8 with BOM. That was the cause for the crashing, as our game expects the files to be UTF-8 encoded without BOM.
What’s this BOM, anyway? Simply put, it’s just a sequence of bytes (EF BB BF) used to signal readers about the file being UTF-8 encoded. It seems such mark might be useful in some specific contexts, with some specific programs. Not our case, so we had to remove the BOM with a little batch script like this:
for /r ".\DE\scenes" %%i in (*.*) do (
copy %%i .\tmp.txt /Y
sed -i '1s/^\xEF\xBB\xBF//' .\tmp.txt
attrib -R .\tmp.txt
move /Y .\tmp.txt %%i
)
In this snippet we remove the BOM via sed. Files are those under a fictitious directory, .\DE\scenes. Those copies and attribs help to circumvent some problems with permissions of files created by our sed version on Windows.
Typically, we need to translate hundreds of strings when localizing our games. Most strings are text messages which the game loads from some database or simple text file. However, we often have to handle localization of several PNG images, such as the one below.
AWESOME! message in DragonScales
Such PNG images are exported from PSD files which must obviously contain at least one Text Layer. To speed up the localization process we have a little Photoshop script which opens the PSD files and extracts all the text we have to translate. PSD files are grouped in directories corresponding to the tileset they belong to. For instance, this would be a typical directory structure for the DragonScales games:
A simplified but functional version of the script we use is this:
#target photoshop
var target = "/C/projects/ds/images";
var toLocalize = new Array();
var totalProcessed = 0;
var warningsFiles = new Array();
function log(msg) {
$.writeln(msg);
}
function processPSDFolder(dir) {
var files = dir.getFiles("*.psd");
log("===============================================");
log(dir + " -> "+ files.length);
log("===============================================");
for (var i = 0; i < files.length; i++) {
var doc = app.open(files[i]);
log(" file: " + files[i]);
totalProcessed++;
if ( doc.artLayers.length == 0 ) {
log(" -> WARNING: ZERO TEXT LAYERS? THEY SHOULD NOT BE IN GROUPS.");
warningsFiles.push(files[i]);
}
for (var j = 0; j < doc.artLayers.length; j++) {
var lyr = doc.artLayers[j];
if (lyr.kind == LayerKind.TEXT) {
var lyr = doc.artLayers[j];
log(" ->" + lyr.textItem.contents);
toLocalize.push(lyr.textItem.contents);
}
}
doc.close(SaveOptions.DONOTSAVECHANGES);
}
}
function saveStrings() {
var out = new File(target + "/strings.txt");
out.open("w");
for (var i = 0; i < toLocalize.length; i++) {
var str = toLocalize[i];
out.writeln(str);
}
out.close();
}
var root = Folder(target).getFiles();
for ( var i = 0; i < root.length; i++ ) {
var fileFoldObj = root[i];
if ( fileFoldObj instanceof File ) {
// Discard files at this level
} else {
processPSDFolder( Folder(fileFoldObj) );
}
}
saveStrings();
log("Total PSDs processed: " + totalProcessed);
log("Warnings: " + warningsFiles.length);
for ( var i = 0; i < warningsFiles.length; i++ ) {
log(" " + warningsFiles[i]);
}
Observations:
target is the path to your directory structure holding the PSD files.
The strings to be translated will we written to file strings.txt under your target directory.
This script looks for text layers on the top level of the PSD. It can be easily extended to inspect layers in groups, though.
We use warnings to be notified about files not containing Text Layers. These might be files requiring special exporting and extra formatting, and therefore we’ll have to handle such files exceptionally.