How is i18n handled within Sencha touch? (I am talking of localization support for strings, but also of localized components)
A more specific question: I have a form that contains a date picker, how do I make sure that the date will be displayed and picked in european format when I access the application using a french android phone?
Cheers
There isn't an official API for i18n in SenchaTouch, yet. Although in Ext 4 there are localization files for all components in the /locale folder.
There is an old tutorial that indicates a way, by dynamically in setting the src attribute of a script tag according to the locale.
<script type="text/javascript" id="extlocale"></script>
<script type="text/javascript">
var browserLang = window.navigator.language; // get the browsers language
var locales = [ 'fr', 'es', 'pt', 'pt-BR', 'pt-PT' ]; // available locale files
var locale = 'fr'; // default locale
// check browser language against available locale files
for (var i = locales.length - 1; i >= 0; i--) {
if (browserLang === locales[i]) {
locale = browserLang;
break;
}
};
// Insert src attribute to extlocale
if(locale) {
Ext.fly('extlocale').set({src:'ext/locale/ext-lang-' + locale + '.js'});
}
</script>
Use window.navigator.language to check the browser's language.
Locale files must be set in /ext/locale/ext-lang-fr.js
Where you can override the components properties.
Ext.onReady(function() {
if(Date){
Date.shortMonthNames = [
"Janv",
"Févr",
"Mars",
"Avr",
"Mai",
"Juin",
"Juil",
"Août",
"Sept",
"Oct",
"Nov",
"Déc"
];
Date.getShortMonthName = function(month) {
return Date.shortMonthNames[month];
};
Date.monthNames = [
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre"
];
Date.monthNumbers = {
"Janvier" : 0,
"Février" : 1,
"Mars" : 2,
"Avril" : 3,
"Mai" : 4,
"Juin" : 5,
"Juillet" : 6,
"Août" : 7,
"Septembre" : 8,
"Octobre" : 9,
"Novembre" : 10,
"Décembre" : 11
};
Date.getMonthNumber = function(name) {
return Date.monthNumbers[Ext.util.Format.capitalize(name)];
};
Date.dayNames = [
"Dimanche",
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi"
];
Date.getShortDayName = function(day) {
return Date.dayNames[day].substring(0, 3);
};
Date.parseCodes.S.s = "(?:er)";
Ext.override(Date, {
getSuffix : function() {
return (this.getDate() == 1) ? "er" : "";
}
});
}
});
I made a working prototype you can check out here:
http://lab.herkulano.com/sencha-touch/date-picker-i18n/
For your own strings (not talking about native touch component) you could do something like this.
1) In your index.html in the head section, load a LocaleManager.js file (whatever the name) before the
<script id="microloader" type="text/javascript" src="sdk/microloader/development.js"></script>
In your localeManager, depending on your browser's language load the resource file corresponding to your language (myStrings-fr.js | myStrings-en.js )
You can do something like this to get the language in the browser:
window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage || window.navigator.systemLanguage
2) Create your resources files with your translated string
It should look like this for the english version (myStrings-en.js) :
var myStrings = {
MyPackage1: {
myString1: 'Seach...'
}};
It should look like this for the french version (myStrings-fr.js) for example :
var myStrings = {
MyPackage1: {
myString1: 'Recherchez...'
}};
3) In your sencha touch code, for example for a searchfield place holder value
xtype: 'searchfield',
id: 'searchToolbarItem',
placeHolder: myStrings.MyPackage1.myString1
Hope it will help.
Another solution could be to modify the build process of your sencha touch app and create localized versions of your app when building it. So there would be one version per language. Then depending on the brower language you would load the right version of your app when loading the app in the browser.
I wrote my own i18n class, here it is:
Ext.define('MyApp.util.I18n', {
singleton : true,
config : {
defaultLanguage : 'en',
translations : {
'en' : {
'signIn' : 'Sign in',
'name' : 'Name',
'hello' : 'Hello {0} !',
'enOnly' : 'English only !'
'fr' : {
'signIn' : 'Identifiez-vous',
'name' : 'Nom',
'hello' : 'Bonjour {0} !'
}
}
},
constructor: function(config) {
this.initConfig(config);
this.callParent([config]);
},
translate: function (key) {
// Get browser language
var browserLanguage = window.navigator.userLanguage || window.navigator.language;
// Is it defined ? if not : use default
var language = this.getTranslations()[browserLanguage] === undefined ? this.getDefaultLanguage() : browserLanguage;
// Translate
var translation = "[" + key + "]";
if (this.getTranslations()[language][key] === undefined) {
// Key not found in language : tries default one
if (this.getTranslations()[this.getDefaultLanguage()][key] !== undefined) {
translation = this.getTranslations()[this.getDefaultLanguage()][key];
}
} else {
// Key found
translation = this.getTranslations()[language][key];
}
// If there is more than one argument : format string
if (arguments.length > 1) {
var tokenCount = arguments.length - 2;
for( var token = 0; token <= tokenCount; token++ )
{
translation = translation.replace( new RegExp( "\\{" + token + "\\}", "gi" ), arguments[ token + 1 ] );
}
}
return translation;
}
});
Then you can call it like this:
MyApp.util.I18n.translate('signIn');
// Gives 'Identifiez-vous'
MyApp.util.I18n.translate('hello', 'Gilles');
// Gives 'Bonjour Gilles !'
MyApp.util.I18n.translate('enOnly');
// Gives 'English only !' (defaults to en, as the key is not defined for fr)
MyApp.util.I18n.translate('missing');
// Gives '[missing]' (the key is never defined, so it is returned into brackets)
For more information, you can read the original blog post here: http://zippy1978.tumblr.com/post/36131938659/internationalization-i18n-with-sencha-touch
Here is my implementation:
I want me app to be translated to English and Spanish, based in the device's language, if the language is not Spanish, then it takes the default (English).
Here are my classes:
Ext.define('MyApp.i18n.I18N', {
alternateClassName: 'i18n',
singleton: true,
constructor: function(config) {
var lang = window.navigator.userLanguage || window.navigator.language
if(lang==='undefined' || lang.substring(0,2) != 'es'){ //don't care about region
lang = 'defaultLocale';
}
return Ext.create('MyApp.i18n.locales.' + lang, config);
}
});
Ext.define('MyApp.i18n.locales.defaultLocale', {
hello:'Hello'
});
Ext.define('MyApp.i18n.locales.es', {
extend: 'MyApp.i18n.locales.defaultLocale',
hello:'Hola'
});
And here is how you call it:
i18n.hello
I hope it helps.
locale folder is under /src folder in sencha touch
Related
I am new to Cypress 12.3 and trying to find out the best possible way to run my automation on different viewports. My website in some places uses different css selector on mobile e.g. on Search of a product, Later on checkout.
I was writing the code below where depending on the viewport size being mobile or desktop it will execute those steps. However in this process I realised that some steps would be repetitive in both if and else when the css selectors are same on mobile and desktop.
Please can some one assists in what is the best way to write the tests?
import AllowCookies from "../pagesObjects/components/AllowCookies"
import Header from "../pagesObjects/components/Header"
import PLPPage from "../pagesObjects/pages/PLPPage"
import BasePage from "../pagesObjects/pages/BasePage"
const sizes = [BasePage.setiPhoneViewport(),BasePage.setAndriodViewport(), BasePage.setTabletViewport(), [1280, 768]]
describe('Add random product to bag', () => {
let testData
beforeEach(() => {
cy.fixture('general').then(function (data) {
testData = data
cy.visit(data.baseURL + data.urlHomePath)
})
AllowCookies.clickOnAllowAll()
})
sizes.forEach((size) => {
it(` Add random products ${size} `, () => {
cy.currencyPopup()
if (Cypress._.isArray(size)) {
cy.viewport(size[0], size[1])
} else {
cy.viewport(size)
}
if (size === testData.sizeiPhone || size === testData.sizeAndroid || size === testData.sizeiPad) {
Header.searchMobile(testData.validSearchTxt)
PLPPage.clickARandomProduct()
}
else {
Header.search(testData.validSearchTxt)
PLPPage.clickARandomProduct()
}
})
})
})
Try adding "metadata" to the BasePage return values
// BasePage
setiPhoneViewport() {
return {
device: 'iphone-6',
isMobile: 'mobile'
}
}
// Test
if (Cypress._.isArray(size)) {
cy.viewport(size[0], size[1])
} else {
cy.viewport(size.device)
}
const search = size.isMobile ? Header.searchMobile : Header.search;
search(testData.validSearchTxt)
PLPPage.clickARandomProduct()
If you have a "custom" size that happens to be mobile,
const sizes = [
BasePage.setiPhoneViewport(),
BasePage.setAndriodViewport(),
BasePage.setTabletViewport(),
[1280, 768],
[320, 250], // is mobile
]
then include it in the expression
// Test
function getSearchMethod(size) {
return
}
if (Cypress._.isArray(size)) {
cy.viewport(size[0], size[1])
} else {
cy.viewport(size.device)
}
const search = size.isMobile || (Cypress._.isArray(size) && size[0] < 350) ?
Header.searchMobile : Header.search;
search(testData.validSearchTxt)
PLPPage.clickARandomProduct()
I tried reactive form valueChanges but valueChanges method doesn't return input field name which has changed.
I thought code like this. but I think this is not smart way. Because I have to compare every each input field. so I need more smart way.
// get init view data from local storage
this.localstorageService.getPlaceDetail().subscribe(data => {
this.initPlaceDetail = data;
// watch changed value
this.editPlaceForm.valueChanges.subscribe(chengedVal => {
if (chengedVal['ja_name'] !== this.initPlaceDetail.languages.ja.name.text) {
this.changedJA = true;
}
if (chengedVal['ja_romaji'] !== this.initPlaceDetail.languages.ja.name.romaji) {
this.changedJA = true;
}
// ...... I have to check all input fields??
});
});
I'm adding form controls from an array and something like this worked for me. Just reference the item you need instead of expecting the valueChanges observable to pass it to you.
myFields.forEach(item => {
const field = new FormControl("");
field.setValue(item.value);
field.valueChanges.subscribe(v => {
console.log(item.name + " is now " + v);
});
});
This is my way to get changed control in form.
I shared for whom concerned.
Method to get list control changed values
private getChangedProperties(form): any[] {
let changedProperties = [];
Object.keys(form.controls).forEach((name) => {
let currentControl = form.controls[name];
if (currentControl.dirty)
changedProperties.push({ name: name, value: currentControl.value });
});
return changedProperties;
}
If you only want to get latest changed control you can use
var changedProps = this.getChangedProperties(this.ngForm.form);
var latestChanged = changedProps.reduce((acc, item) => {
if (this.changedProps.find(c => c.name == item.name && c.value == item.value) == undefined) {
acc.push(item);
}
return acc;
}, []);
Instead of listening to whole form changes you can listen to value changes event for each form control as shown in below code:
this.myForm.get('ja_name').valueChanges.subscribe(val => {
this.formattedMessage = `My name is ${val}.`;
});
I'm using the DropdownList component from react-widget. In my code, there are a couple of dropdowns that get their values from an Ajax call. Some of them, like a list of languages, are too big and it's very slow to get the list from Ajax and render it (takes 4 to 5 seconds!). I would like to provide to the dropdwon a small list of languages and an 'Extend' or 'Load Full List' option; if clicking on Extend the dropdown would be refreshed with the full list of languages.
Here is my solution: the code of the parent component:
const languages = ajaxCalls.getLanguages();
const langs = {"languages": [["eng", "English"], ["swe", "Swedish"], ["deu", "German"], ["...", "Load Full List"]]};
const common_langs = langs.languages.map(([id, name]) => ({id, name}));
<SelectBig data={common_langs} extend={languages} onSelect={x=>this.setValue(schema, path, x)} value={this.getValue(path)} />;
And here is the code for SelectBig component:
import React from 'react/lib/ReactWithAddons';
import { DropdownList } from 'react-widgets';
const PT = React.PropTypes;
export const SelectBig = React.createClass({
propTypes: {
data: PT.array,
value: PT.string,
onSelect: PT.func.isRequired,
},
maxResults: 50,
shouldComponentUpdate(nextProps, nextState) {
console.log("nextProps = " , nextProps, " , nextState = ", nextState);
const len = x => (x && x.length !== undefined) ? x.length : 0;
// fast check, not exact, but should work for our use case
return nextProps.value !== this.props.value
|| len(nextProps.data) !== len(this.props.data);
},
getInitialState(){
return {
lastSearch: '',
results: 0,
dataList: [],
};
},
select(val) {
if(val.id === "..."){
this.setState({dataList: this.props.extend})
}
this.props.onSelect(val.id);
},
filter(item, search) { .... },
renderField(item) { .... },
render() {
const busy = !this.props.data;
let data;
if(!this.props.extend){
data = this.props.data || [];
} else {
data = this.state.dataList;
}
return (
<DropdownList
data={data}
valueField='id'
textField={this.renderField}
value={this.props.value}
onChange={this.select}
filter={this.filter}
caseSensitive={false}
minLength={2}
busy={busy} />
);
}
});
But it doesn't look good: When the user chooses 'Load Full List', the dropdown list will be closed and user need to click again to see the updated list. Does anyone have a better solution or a suggestion to improve my code?
The picture shows how it looks like right now!
https://www.npmjs.com/package/react-select-2
Better go with this link, it will work
Looking on the pluralization part in Spanish here, as an example:
I see that
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
apparently, all is in English
can anyone explain if this is a bug?
thanks very much
Lior
By glancing the code I can see it's a set of simple pluralization rules. Every locale has this constant. So no, it's not a bug.
Here is how I am doing my i18n work, it seems to be working great! It is based off a set of localized resource files that get initialized at runtime. At the bottom is how I handle pluralization using this approach.
I18n module to hold string id map and parameter insertion
.factory('I18n', ['$http', 'User', function($http, User) {
// Resource File
var LANG_FILE;
// Fetch Resource File
function init() {
return $http.get('resources/locales/' + User.locale + '.json')
.then(function(response) {
LANG_FILE = response.data;
});
}
function lang(stringId, params) {
var string = LANG_FILE[stringId] || stringId;
if (params && params.length) {
for (var i = 0; i < params.length; i++) {
string = string.replace('%' + (i + 1), params[i]);
}
}
return string;
}
return {
init: init,
lang: lang
};
}]);
This can be initialized using a .run block
.run(['I18n', function(I18n) {
I18n.init();
}]);
And used anywhere to translate a string like this
.controller(['$scope', 'I18n', function($scope, I18n) {
$scope.title = I18n.lang(some_string_id);
}]);
Custom i18n DIRECTIVE to handle one time translations
.directive('i18n', ['I18n', function(I18n) {
return {
restrict: 'A',
scope: {},
link: function(scope, $el, attrs) {
$el[0].innerHTML = I18n.lang(attrs.i18n);
}
};
}]);
Which can be used like this.
<div i18n="some_string_id"></div>
Custom PLURALIZE directive that matches string ids from your resource file with the count as the parameter.
.directive('pluralize', ['I18n', function(I18n) {
return {
restrict: 'A',
scope: {
count: '='
},
link: function($scope, $el, attrs) {
var when = JSON.parse(attrs.when)
, param = [$scope.count];
if (when[$scope.count]) {
$el[0].innerHTML = I18n.lang(when[$scope.count], param);
} else {
$el[0].innerHTML = I18n.lang(when['other'], param);
}
}
};
}]);
And can be used like this.
<div pluralize count="{{obj.count}}" when="{1:'single_item','other': 'multiple_item'}"></div>
The String Resource file would be located at resources/locales/en-US.json, and would look something like this.
{
some_string_id: 'This is in English',
single_item: '%1 item',
multiple_item: '%1 items'
}
The other locales would have the same string ids, with different translated texts.
This is my code,
myText.enableKeyEvents = true; // **
myText.on('keyup', function(t){ console.log(t.getValue()); });
It not work, I think it may has some invoke method.
Any one has an idea ?
Full code
// function
var txfJump = function(txf){
var next = txf.nextSibling();
if(next.xtype == 'textfield'){
txf.enableKeyEvents = true;
txf.on('keyup', function(t, e){
if(t.getValue().length == t.maxLength){
next.focus();
}
});
txfJump(next);
}
};
// form
var p = new Ext.Panel({
title : 'test panel',
width : 400,
defaults: {
xtype : 'textfield',
},
items : [
{ ref : 'one', maxLength : 5 },
{ ref : 'two', maxLength : 5 },
{ ref : 'three',maxLength : 5 },
{
xtype : 'button',
text : 'SAMPLE'
},
{ ref : 'four', maxLength : 5 },
],
renderTo: Ext.getBody()
});
Ext.onReady(function(){
txfJump(p.one);
});
It's not possible without hackish tricks. Having read the TextField's source, i found out, that there is a private method initEvents, that sets up the callbacks for the html-elements if and only if enableKeyEvents is set. So, changing enableKeyEvents after initEvents was called has no effect, until it is called again.
To inject it, one could try to trigger a re-rendering of the component, or one could copy the behaviour of the relevant parts of TextField's initEvent. But there doesn't seem to be an official way.
you have to call txf.initEvents(), this needs to be called after txf.enableKeyEvents = true;
var txfJump = function(txf){
var next = txf.nextSibling();
if(next.xtype == 'textfield'){
txf.enableKeyEvents = true;
txf.initEvents(); //<----- this is what you have missing
txf.on('keyup', function(t, e){
if(t.getValue().length == t.maxLength){
next.focus();
}
});
txfJump(next);
}
You should pass "enableKeyEvents": true when getting new Ext.form.TextField instance. Here is the example usage
var textField = new Ext.form.TextField({
. // your configs
.
enableKeyEvents: true,
.
.
})