+4 votes
by (710 points)
Its a complex technical question (for me anyway) but I would love to implant a language option in the Settings menu. Is it doable?

I know I could simply put a 'choose your language' passage at the beginning of the story, but its not the same as 'switching language on the fly' like I had seen in many text based apps.

1 Answer

+3 votes
by (68.6k points)
selected by
 
Best answer

An on-the-fly language changing setting can be done, yes.  What you're asking about is known as internationalization (i18n).  To fully internationalize a SugarCube project would, besides the language changing setting and attendant plumbing, involve creating language localizations (l10n) of two key parts:

  • Various strings displayed as part of SugarCube's UI.  A primer on which may be found at SugarCube's website as the Localization document.  Unfortunately, user labels on the settings themselves cannot be internationalized at this time.
  • The text of the a project's passages.

I'd probably suggest something like the following: (in Twee notation)

:: StoryTitle
SugarCube i18n example


:: Language Switching [script]
/***********************************************************
	Set up a `i18n` object on SugarCube's `setup` object.
***********************************************************/
setup.i18n = {
	/*
		Map of language labels to codes for all supported
		languages go here.
	*/
	langs : {

		// NOTE: User customization required here.

		'Deutsche' : 'de',
		'English'  : 'en',
		'Français' : 'fr',

	},

	/*
		Utility code.  You probably do not need to worry
		about any of these.
	*/
	codes : function () {
		return Object.keys(this.langs).map(function (label) {
			return this.langs[label];
		}, this);
	},

	labels : function () {
		return Object.keys(this.langs);
	},

	labelFromCode : function (code) {
		var label = Object.keys(this.langs).find(function (label) {
			return this.langs[label] === code;
		}, this);

		if (!label) {
			throw new Error('unknown language code "' + code + '"');
		}

		return label;
	}
};


/***********************************************************
	Language switching setting.
***********************************************************/
function initLanguage() {
	/*
		Set the `l10nStrings` properties to the appropriate
		values for each supported language.

		English need not receive a case, unless you simply
		want to alter the default values, as it is the
		default language.
	*/
	switch (setup.i18n.langs[settings.lang]) {

	// NOTE: User customization required here.

	case 'de':
		l10nStrings.settingsTitle = 'Einstellungen';
		l10nStrings.settingsOff   = 'Deaktivieren';
		l10nStrings.settingsOn    = 'Aktivieren';
		l10nStrings.settingsReset = 'Auf Standardeinstellung zurücksetzen';
		break;

	case 'fr':
		l10nStrings.settingsTitle = 'Paramètres';
		l10nStrings.settingsOff   = 'Désactiver';
		l10nStrings.settingsOn    = 'Activer';
		l10nStrings.settingsReset = 'Réinitialiser les paramètres par défaut';
		break;

	}

	/*
		Set the `lang` attribute on the document element to
		the appropriate value.  This is mostly notational,
		though it could also be used to enable localization
		specific styling.
	*/
	$('html').attr('lang', setup.i18n.langs[settings.lang]);
}

function changeLanguage() {
	/*
		Reload the application to ensure that the proper
		localizations are loaded.
	*/
	window.location.reload();
}

Setting.addList('lang', {
	label    : 'Language.',
	list     : setup.i18n.labels(),
	default  : setup.i18n.labelFromCode('en'),
	onInit   : initLanguage,
	onChange : changeLanguage
});


/***********************************************************
	Set up a `postrender` task which renders the appropriate
	language code suffixed passage into each non-suffixed
	passage.
***********************************************************/
postrender['i18n-passage-include'] = function (content) {
	var passage = State.passage + '_' + setup.i18n.langs[settings.lang];

	$(content).empty().wiki(Story.get(passage).processText());
};



:: Start
/*

The non-suffixed passages are here to prevent Twine from
complaining endlessly about missing passages, SugarCube from
showing broken links, and to keep the story history free of
suffixed passage names so that changing languages on the fly
will work.

The actual passages are the language code suffixed versions.
For example, for this starting passage the German version
would be Start_de, the French Start_fr, and English Start_en.

When creating links between passages, you must always link to
the non-suffixed versions and never to the suffixed versions.
The postrender task will handle displaying the correct version
based on the currently selected language.

NOTE: While nothing within the non-suffixed passages will be
displayed, they will be rendered.  Meaning side-effects from
included code will be in effect—e.g. modifications to story
variables.  Thus, you should either leave them empty or place
all contents within comment blocks—like this one.

*/



:: Start_de
Deutsche.

[[Nächster|Next]]



:: Start_en
English.

[[Next]]



:: Start_fr
Français.

[[Prochain|Next]]



:: Next
/* This should be empty. */



:: Next_de
Nächster



:: Next_en
Next



:: Next_fr
Prochain



NOTE: The few translations within the l10nStrings section of the example are machine translations, so please refrain from telling me I'm doing it wrong.

 

by (710 points)
edited by

That work perfectly! A huge thanks! smiley

by (68.6k points)

That was exported directly from a working project.

Try downlading the SugarCube v2 basic internationalization (i18n) example (in Twee notation) gist—you want the Download ZIP button—and use the File > Import > Twee Source Code… menu item to import it into a new project.

 

by (710 points)

Good idea. For anyone who might be interested, there are some more strings for a French UI. (I did put French as the default language, but this is for some testing on my side.)

 

setup.i18n = {
	langs : {
		'English'  : 'en',
		'Français' : 'fr',
	},

	codes : function () {
		return Object.keys(this.langs).map(function (label) {
			return this.langs[label];
		}, this);
	},

	labels : function () {
		return Object.keys(this.langs);
	},

	labelFromCode : function (code) {
		var label = Object.keys(this.langs).find(function (label) {
			return this.langs[label] === code;
		}, this);

		if (!label) {
			throw new Error('unknown language code "' + code + '"');
		}

		return label;
	}
};

function initLanguage() {
	switch (setup.i18n.langs[settings.lang]) {

	case 'fr':
		l10nStrings.settingsTitle = 'Paramètres';
		l10nStrings.settingsOff   = 'Off';
		l10nStrings.settingsOn    = 'On';
		l10nStrings.settingsReset = 'Réinitialiser';
		l10nStrings.cancel =  "Annuler";
		l10nStrings.restartTitle = "Recommencer";
		l10nStrings.restartPrompt = "Êtes-vous sûr de vouloir recommencer? Tout progrès non sauvegardé sera perdu";
		l10nStrings.savesTitle = "Sauvegardes";
		l10nStrings.savesEmptySlot = "emplacement vide";
		l10nStrings.savesLabelDelete = "Effacer";
		l10nStrings.savesLabelExport = "Sauvegarder au Disque";
		l10nStrings.savesLabelImport = "Charger du Disque";
		l10nStrings.savesLabelLoad = "Charger";
		l10nStrings.savesLabelClear = "Tout Effacer";
		l10nStrings.savesLabelSave = "Sauvegarder";
		break;
}

	$('html').attr('lang', setup.i18n.langs[settings.lang]);
}

function changeLanguage() {
	window.location.reload();
}

Setting.addList('lang', {
	label    : 'Language',
	list     : setup.i18n.labels(),
	default  : setup.i18n.labelFromCode('fr'),
	onInit   : initLanguage,
	onChange : changeLanguage
});

postrender['i18n-passage-include'] = function (content) {
	var passage = State.passage + '_' + setup.i18n.langs[settings.lang];

	$(content).empty().wiki(Story.get(passage).processText());
};

 

...