Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-13966] TiAPI: Select Locale for L() at runtime

GitHub Issuen/a
TypeNew Feature
PriorityHigh
StatusClosed
ResolutionFixed
Resolution Date2019-01-10T18:59:25.000+0000
Affected Version/sRelease 3.1.0
Fix Version/sRelease 8.0.0, Release 7.5.0, Release 7.5.1
ComponentsCore
Labelslanguage, localization, runtime
ReporterMads Moller
AssigneeJoshua Quick
Created2013-05-16T10:13:41.000+0000
Updated2021-03-15T18:43:30.000+0000

Description

*Problem* It would be great if it was possible to let the user select which language to use at runtime. So instead the L() would only look at device language, it could also check if a Ti.App.LanguageXMLSetting property has been defined. That way we do not need to create homemade localization systems anymore. The use case is included below. The problem is that L is undefined in style.tss because I redefine it in the controller. *alloy.js*
// Language helper
Alloy.LanguageHelper = require('languageHelper');
Alloy.L = Alloy.LanguageHelper.fetchString;
*Controller*
//top of the controller
var L = require("languageHelper");
*Style*
 
"#menuBtn":{
	title: L("MenuButton")
}
*View*
<Alloy>
	<Window>
		<Button id="menuBtn"></Button>
	</Window>
</Alloy>
*Custom languageHelper*
/**
 * Language Helper
 * Makes it possible to change language at runtime 
 * @version 1.0
 */

var lookup = null;
exports.locale = Ti.App.Properties.getString('SETTING_LANGUAGE', Ti.Locale.getCurrentLanguage() );


/**
 * Private function
 * Loads the language xml file.
 * It has fallback for english if file does not exist.
 */
function loadFile() {
	lookup = {};
	
	// LOAD FILE
	var file = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'i18n/' + exports.locale + '/strings.xml');
	if (!file.exists()) {
		file = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'i18n/en/strings.xml');
		if (!file.exists()) {
			Ti.API.warn("Language file for both '" + exports.locale + "' and 'en' fallback do not exist");
			return;	
		}
	}
	
	// PARSE XML
	var xml_string = file.read().text;
  	var doc = Ti.XML.parseString(xml_string);
	var strings = doc.getElementsByTagName("string");
	for (var i = 0; i < strings.length; i++) {
		var node = strings.item(i);
		var value = node.text;
		if (node.hasAttributes()) {
			for (var att_index = 0; att_index < node.attributes.length; att_index++) {
				var att = node.attributes.item(att_index);
				if (att.nodeName === "name") {
					lookup[att.nodeValue] = value;
				}
			}
		}
	}
	
}

/**
 * Set the current language of the app. 
 * @param {String} language
 */
exports.setLanguage = function(language){
	if(language === null){
		return;
	}
	// clean old lookup
	exports.clear();
	
	// save language
	Ti.App.Properties.setString('SETTING_LANGUAGE', language);
	exports.locale = language;
	
	
	// parse new lookup
	loadFile();
	
	
	return lookup;
}

/**
 * Clear the parsed xml translations
 */
exports.clear = function() {
	lookup = null;
};

/**
 * Lookup function. require this at each controller
 * Example usage:
 * var L = require('languageHelper').fetchString;
 * @param {Object} string
 * @param {Object} hint
 */
exports.fetchString = function(string, hint) {
	if (lookup === null) {
		loadFile();
	}
	return lookup[string] !== undefined ? lookup[string].replace(/\\n/g, "\n") : (hint || string);
}; 

Comments

  1. Mads Moller 2013-05-16

    The big issue here is Alloy. I can write my own localization function, and use the i18n structure, if I place the folder inside Resources. But in Alloy, the controller code is compiled after the view/style. This means that I cannot have use L() in mycontroller.tss or markup. I will need to have all my text in the controller. This is bad design, and not really useful.
  2. Tony Lukasavage 2013-05-16

    Can you include some code that exhibits the issue? Particularly can you show me what you are shopping now to work appc the issue, and what you would expect to be able to do when this issue is resolved. The more clear and concise in these samples toy can be, the more likely it is that they can be implemented sooner rather than later.
  3. Mads Moller 2013-05-23

    Hi Tony, This issue is not just an Alloy bug/feature. The best solution is the ability to change locale at runtime. If thats possible, then Alloy can just continue working as it does.
       // this is the best and most flexible solution. 
       Ti.Locale.setString("da");
       
    cheers, Mads
  4. Dan Tamas 2014-03-08

    This should work out of the box using
       Ti.Locale.setLanguage('es');
       
    This is an undocumented method that exists in Ti [here](https://github.com/appcelerator/titanium_mobile/blob/master/iphone/Classes/LocaleModule.m#L73) for iOS. On android seems [not to be supported](https://github.com/appcelerator/titanium_mobile/blob/master/android/modules/locale/src/java/ti/modules/titanium/locale/LocaleModule.java#L89) :( I works for me in [AsturPlaya](https://itunes.apple.com/es/app/asturplaya/id662587421?ls=1&mt=8) combined with [this module](https://github.com/rborn/tiapprestart) However this is a classic Ti app, not an Alloy one, and didn't test it yet. But because the app is restarted it should work with no issues.
  5. Mads Moller 2018-08-02

    Hurrah @Hans Knöchel. After 5 years its now in progress ;)
  6. Hans Knöchel 2018-08-02

    Ha, I know. It's even reported by you, funny. Let get this party started.
  7. Hans Knöchel 2018-08-02

    PR: https://github.com/appcelerator/titanium_mobile/pull/10227 Test-Setup (requires a hello_world language file in i18n/de and i18n/en):
       var win = Ti.UI.createWindow({
           backgroundColor: '#fff'
        });
       
        var label = Ti.UI.createLabel({
            text: 'Current Locale: ' + Ti.Locale.currentLanguage + ' (' + L('hello_world') + ')',
            top: 100
        })
        
        var btn = Ti.UI.createButton({
          title: 'Change to German'
        });
        
        btn.addEventListener('click', function() {
          Ti.Locale.setLanguage('de');
        });
        
        win.add(btn);
        win.add(label);
        win.open();
       
    Some notes here: - It seems like (different to the initial discussion) the API [is documented](https://docs.appcelerator.com/platform/latest/#!/api/Titanium.Locale-method-setLanguage) by now - It seems like (different to the initial discussion) the API is also [supported on Android](https://github.com/appcelerator/titanium_mobile/blob/master/android/modules/locale/src/java/ti/modules/titanium/locale/LocaleModule.java#L95) already This PR will fix one remaining issue on iOS that prevents the locale to be forgotten across app-launches. I added a simple UserDefaults setting to hook into that behavior and clean it up. The only remaining thing I am wondering is if we should expose the Ti.App.restart() method as well, to restart the app for the user. This is pretty much against the native behavior and could lead in an app rejection, so I'd like to put down my vote against this for now. [~gmathews] [~jquick] Are we good on Android?
  8. Samir Mohammed 2018-10-19

    Verified feature on SDK Version 7.5.0.v20181018133006. Tested using the test case provided by [~hknoechel] on iOS. *Test Environment*
       APPC Studio: 5.1.0.201808080937
       iPhone 6 Sim (iOS 12)
       APPC CLI: 7.0.6
       Operating System Name: Mac OS Mojave
       Operating System Version: 10.14
       Node.js Version: 8.9.1
       Xcode 10.0
  9. Samir Mohammed 2018-10-26

    *Closing ticket* If any issue remains please reopen.
  10. Teun Klijn 2018-11-16

    Doesn't seem to work correctly on *Android*, setting the textid directly does work. When you get a string with L('key') it doesn't work, what is strange is that it does seem to work when you run the app with liveview enabled. Test on: Samung s9 (Android 8.0.0) and emulator (Android 8.0.0). SDK: 7.5.0.GA Example (does need to have the strings.xml file):
        // est-Setup (requires a hello_world language file in i18n/de and i18n/en)
        
        var win = Ti.UI.createWindow({
          backgroundColor: '#fff'
        });
        
        var label = Ti.UI.createLabel({
          text: 'Current Locale: ' + Ti.Locale.currentLanguage + ' (' + L('hello_world') + ')',
          top: 100,
          color: 'black'
        });
        
        var btn = Ti.UI.createButton({
          title: 'Change'
        });
        
        btn.addEventListener('click', function () {
          var newLang = Ti.Locale.currentLanguage  == 'de' ? 'en' : 'de';
          Ti.Locale.setLanguage(newLang);
          label.text = 'Current Locale: ' + Ti.Locale.currentLanguage + ' (' + L('hello_world') + ')';
        });
        
        win.add(btn);
        win.add(label);
        win.open();
        
  11. Ahmed Mohamed 2018-12-13

    Also doesn't work with me but it was working great with 7.4.1
  12. Josh Longton 2018-12-14

    Verified that it is an issue on 7.5.0.GA on android, it has been fixed on master tested with SDK 8.0.0.v20181213061450
  13. Hans Knöchel 2018-12-14

    Will this be backported into 7.5.1?
  14. Abir Mukherjee 2018-12-15

    [~vijaysingh] Can you please create a 7.5.1 backport?
  15. Abir Mukherjee 2018-12-15

    Reopened to update fix version to include 7.5.1 backport
  16. Ahmed Mohamed 2018-12-17

    Did this backport has been merged yet?
  17. Hans Knöchel 2018-12-17

    Can someone clarify if locales are stable in 7.5.1 (both iOS and Android)?
  18. Josh Longton 2018-12-18

    I found that it was specific to the android version, results are below. || ||7.4.1.GA||7.5.0.GA||8.0.0.v20181217103214|| |4.4.2|(/)|(/)|(/)| |6.0|(/)|(/)|(/)| |8.1|(/)|(x)|(x)| |9.0|(/)|(x)|(x)|
  19. Yordan Banev 2018-12-19

    This PR should fix this. https://github.com/appcelerator/titanium_mobile/pull/10549 Currently for master. We will discuss a backport in case it passes CR and FR.
  20. Joshua Quick 2019-01-07

    Please note that changing language on Android broke in Titanium 7.5.0 due to an undocumented breaking-change on Google's end. It'll only happen if your app targets Android P/9 (aka: API Level 28). Yordan's above PR fixes it in Titanium 8.0.0. You can also work-around the issue by "targeting" API Level 27.
  21. Prashant Saini 2019-01-08

    @Joshua, if we change the targetSDK version to 27 with SDK 7.5.x, will our app continue to work fine on Android OS 28 as well? I would like to stick to L('string') method so that when we get 8.x.x release, we won't need to change anything. so what is the best possible solution at present as we can't go back to any SDK than 7.5.x?
  22. Prashant Saini 2019-01-08

    @Joshua, I just changed the Target-SDK with 7.5.1.RC build and it still doesn't work properly.
  23. Josh Longton 2019-01-08

    *FR Passed*. Waiting on Jenkins to merge.
  24. Christopher Williams 2019-01-08

    merged to master/8_0_X
  25. Joshua Quick 2019-01-09

    Re-opening... Yordan's PR does fix the issue with L(), but now titleid and textid no longer read from the selected language/locale. And it happens on all Android OS versions when building with Titanium 8.0.0. (Building with 7.5.0 is fine.) _*Edit:*_ _I've isolated the issue. The problem is that the old code (before the last merged PR) did not load resources from the same context objects. The L() function loaded strings from the application context. The localized string ID properties load strings from the top-most or previous activity context. The issue here is that Android 8.0 and above's context objects no longer share the same resources/configuration objects. Best solution is to have them load resources from the same object for consistency. Yordan's last PR which updates the language/locale of the application context is the right move since backing out of the app and relaunch the main activity will preserve the last set language/locale setting._
  26. Joshua Quick 2019-01-09

    PR (master): https://github.com/appcelerator/titanium_mobile/pull/10591 PR (8.0.x): https://github.com/appcelerator/titanium_mobile/pull/10592
  27. Lokesh Choudhary 2019-01-10

    FR Passed. PR's merged.
  28. Ahmed Mohamed 2019-01-11

    Will this PR merge to 7.5.1?
  29. Joshua Quick 2019-01-11

    [~ahmed.mohamed20320], no plans at the moment. Currently, all resources are on 8.0.0 to get a release candidate out in the near future. Stay tuned.
  30. Ahmed Mohamed 2019-01-12

    Ok, I will check it out, I ask because the fix version is for 8.0.0,7.5.0 and 7.51
  31. Joshua Quick 2019-01-12

    [~ahmed.mohamed20320], This issue was resolved on iOS in 7.5.0. https://github.com/appcelerator/titanium_mobile/pull/10227 This issue will be resolved on Android in 8.0.0. https://github.com/appcelerator/titanium_mobile/pull/10549 https://github.com/appcelerator/titanium_mobile/pull/10592 We don't normally mix platforms and fixed releases like this in the same ticket. I apologize for the confusion. We should have split this into 2 separate tickets.
  32. Josh Longton 2019-01-14

    *Closing ticket.* Fixes from [PR/10227](https://github.com/appcelerator/titanium_mobile/pull/10227), [PR/10549](https://github.com/appcelerator/titanium_mobile/pull/10549) and [PR/10592](https://github.com/appcelerator/titanium_mobile/pull/10592) are all present in SDK {noformat} 8.0.0.v20190114020205 {noformat}
  33. said chaouche 2019-02-13

    workaround in 7.5.0 https://github.com/genocsb/titanium_i18n
  34. Hans Knöchel 2021-03-13

    Quick question in the round: Does this work across app launches? Or does it have to be manually applied on each app start?
  35. Joshua Quick 2021-03-15

    [~hknoechel], on Android, you have to call setLanguage() manually on every app startup or else it'll use the system's locale/language.

JSON Source