Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback code for unsupported locales #168

Merged
merged 25 commits into from
Oct 5, 2017
Merged
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"lines-around-comment": "error",
"max-depth": "error",
"max-len": "off",
"max-lines": "error",
"max-lines": "off",
"max-nested-callbacks": "error",
"max-params": "error",
"max-statements": "off",
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log
All notable changes to this project will be documented in this file. For change log formatting, see http://keepachangelog.com/

## Master

- Added `getBestMatchingLanguage` for determining the best language when it's unknown if the users locale is supported.

## 0.8.0 2017-10-04

- Added grammatical cases support for Russian way names [#102](https://github.com/Project-OSRM/osrm-text-instructions/pull/102)
Expand Down
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Grammatical cases and other translated strings customization after [Transifex](h
var version = 'v5';
var osrmTextInstructions = require('osrm-text-instructions')(version);

// make your request against the API, save result to response variable
// If you are unsure if the users locale is supported, use `getBestMatchingLanguage` method to find an appropriate language.
var language = osrmTextInstructions.getBestMatchingLanguage('en');

var language = 'en';
response.legs.forEach(function(leg) {
leg.steps.forEach(function(step) {
instruction = osrmTextInstructions.compile(language, step, options)
Expand Down
46 changes: 46 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module.exports = function(version, _options) {
getWayName: function(language, step, options) {
var classes = options ? options.classes || [] : [];
if (typeof step !== 'object') throw new Error('step must be an Object');
if (!language) throw new Error('No language code provided');
if (!Array.isArray(classes)) throw new Error('classes must be an Array or undefined');

var wayName;
Expand Down Expand Up @@ -206,6 +207,7 @@ module.exports = function(version, _options) {
return this.tokenize(language, instruction, replaceTokens);
},
grammarize: function(language, name, grammar) {
if (!language) throw new Error('No language code provided');
// Process way/rotary name with applying grammar rules if any
if (name && grammar && grammars && grammars[language] && grammars[language][version]) {
var rules = grammars[language][version][grammar];
Expand All @@ -225,6 +227,7 @@ module.exports = function(version, _options) {
return name;
},
tokenize: function(language, instruction, tokens) {
if (!language) throw new Error('No language code provided');
// Keep this function context to use in inline function below (no arrow functions in ES4)
var that = this;
var output = instruction.replace(/\{(\w+):?(\w+)?\}/g, function(token, tag, grammar) {
Expand All @@ -243,6 +246,49 @@ module.exports = function(version, _options) {
}

return output;
},
getBestMatchingLanguage: function(language) {
if (languages.instructions[language]) return language;

var codes = languages.parseLanguageIntoCodes(language);
var languageCode = codes.languageCode;
var scriptCode = codes.scriptCode;
var countryCode = codes.countryCode;

var supportedLanguageCodes = languages.supportedCodes.map(function(language) {
return language.toLowerCase().split('-')[0];
});

// Same language code and script code (lng-Scpt)
if (languages.instructions[languageCode + '-' + scriptCode]) {
return languageCode + '-' + scriptCode;
// Same language code and country code (lng-CC)
} else if (languages.instructions[languageCode + '-' + countryCode]) {
return languageCode + '-' + countryCode;
// Same language code (lng)
} else if (supportedLanguageCodes[languageCode]) {
return languageCode;
// Same language code and any script code (lng-Scpx) and the found language contains a script
} else if (languages.parsedSupportedCodes.find(function (language) {
return language.language === languageCode && language.scriptCode;
})) {
return languages.parsedSupportedCodes.find(function (language) {
return language.languageCode === languageCode && language.scriptCode;
}).locale;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of repeating the find() call, store the result of the first find. It’d be a lot easier to store things in local variables if you replace all the else ifs with ifs – after all, we’re returning early in each case.

// Same language code and any country code (lng-CX)
} else if (supportedLanguageCodes.indexOf(languageCode) > -1 && countryCode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the passed-in language is pt-PT and we support pt-BR (but not pt), this line checks whether pt-PT is supported (it isn’t) and pt-PT has a country code (it does). Consequently, we fail to go in here. Let’s add a test for this case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return languages.supportedCodes[supportedLanguageCodes.indexOf(languageCode)];
// Only language code provided, but we on support this language code
// with either script/country code.
} else if (languages.parsedSupportedCodes.find(function (language) {
return language.languageCode === languageCode;
})) {
return (languages.parsedSupportedCodes.find(function (language) {
return language.languageCode === languageCode;
})).locale;
} else {
return 'en';
}
}
};
};
43 changes: 42 additions & 1 deletion languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,49 @@ var grammars = {
'ru': grammarRu
};

function parseLanguageIntoCodes (language) {
// If the language is not found, try a little harder
var splitLocale = language.toLowerCase().split('-');
var languageCode = splitLocale[0];
var scriptCode = false;
var countryCode = false;

/**
Documentation on how the language tag is being split: https://en.wikipedia.org/wiki/IETF_language_tag#Syntax_of_language_tags

Example: zh-Hant-TW
language code: zh
script code: Hant
country code: TW

Example: en-US
language code: en
country code: US
*/

if (splitLocale.length === 2 && splitLocale[1].length === 4) {
scriptCode = splitLocale[1];
} else if (splitLocale.length === 2 && splitLocale[1].length === 2) {
countryCode = splitLocale[1];
} else if (splitLocale.length === 3) {
scriptCode = splitLocale[1];
countryCode = splitLocale[2];
}

return {
locale: language,
languageCode: languageCode,
scriptCode: scriptCode,
countryCode: countryCode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s a bit redundant to say “code” in each of these keys.

};
}

module.exports = {
supportedCodes: Object.keys(instructions),
parsedSupportedCodes: Object.keys(instructions).map(function(language) {
return parseLanguageIntoCodes(language);
}),
instructions: instructions,
grammars: grammars
grammars: grammars,
parseLanguageIntoCodes: parseLanguageIntoCodes
};
108 changes: 106 additions & 2 deletions test/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,116 @@ tape.test('v5 compile', function(t) {

assert.throws(function() {
v5Compiler.compile('foo');
}, /language code foo not loaded/
);
}, /language code foo not loaded/);

assert.end();
});

t.test('en-US fallback to en', function(assert) {
var v5Compiler = compiler('v5');
var language = v5Compiler.getBestMatchingLanguage('en-us');

assert.equal(v5Compiler.compile(language, {
maneuver: {
type: 'turn',
modifier: 'left'
},
name: 'Way Name'
}), 'Turn left onto Way Name');

assert.end();
});

t.test('zh-CN fallback to zh-Hans', function(assert) {
var v5Compiler = compiler('v5');
var language = v5Compiler.getBestMatchingLanguage('zh-CN');

assert.equal(v5Compiler.compile(language, {
maneuver: {
type: 'turn',
modifier: 'left'
},
name: 'Way Name'
}), '左转,上Way Name');

assert.end();
});

t.test('zh-Hant fallback to zh-Hanz', function(assert) {
var v5Compiler = compiler('v5');
var language = v5Compiler.getBestMatchingLanguage('zh-Hant');

assert.equal(v5Compiler.compile(language, {
maneuver: {
type: 'turn',
modifier: 'left'
},
name: 'Way Name'
}), '左转,上Way Name');

assert.end();
});

t.test('zh-Hant-TW fallback to zh-Hant', function(assert) {
var v5Compiler = compiler('v5');
var language = v5Compiler.getBestMatchingLanguage('zh-Hant-TW');

assert.equal(v5Compiler.compile(language, {
maneuver: {
type: 'turn',
modifier: 'left'
},
name: 'Way Name'
}), '左转,上Way Name');

assert.end();
});

t.test('es-MX fallback to es', function(assert) {
var v5Compiler = compiler('v5');
var language = v5Compiler.getBestMatchingLanguage('es-MX');

assert.equal(v5Compiler.compile(language, {
maneuver: {
type: 'turn',
modifier: 'straight'
},
name: 'Way Name'
}), 'Ve recto en Way Name');

assert.end();
});

t.test('getBestMatchingLanguage', function(t) {
t.equal(compiler('v5').getBestMatchingLanguage('foo'), 'en');
t.equal(compiler('v5').getBestMatchingLanguage('en-US'), 'en');
t.equal(compiler('v5').getBestMatchingLanguage('zh-CN'), 'zh-Hans');
t.equal(compiler('v5').getBestMatchingLanguage('zh-Hant'), 'zh-Hans');
t.equal(compiler('v5').getBestMatchingLanguage('zh-Hant-TW'), 'zh-Hans');
t.equal(compiler('v5').getBestMatchingLanguage('zh'), 'zh-Hans');
t.equal(compiler('v5').getBestMatchingLanguage('es-MX'), 'es');
t.equal(compiler('v5').getBestMatchingLanguage('es-ES'), 'es-ES');
t.equal(compiler('v5').getBestMatchingLanguage('pt-PT'), 'pt-BR');
t.equal(compiler('v5').getBestMatchingLanguage('pt'), 'pt-BR');
t.end();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s add some tests of divideLanguageCodes() or whatever we call it once we move it to languages.js.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/* eslint-disable */
t.test('parseLanguageIntoCodes', function(t) {
t.deepEqual(languages.parseLanguageIntoCodes('foo'), { countryCode: false, languageCode: 'foo', locale: 'foo', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('en-US'), { countryCode: 'us', languageCode: 'en', locale: 'en-US', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('zh-CN'), { countryCode: 'cn', languageCode: 'zh', locale: 'zh-CN', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('zh-Hant'), { countryCode: false, languageCode: 'zh', locale: 'zh-Hant', scriptCode: 'hant' });
t.deepEqual(languages.parseLanguageIntoCodes('zh-Hant-TW'), { countryCode: 'tw', languageCode: 'zh', locale: 'zh-Hant-TW', scriptCode: 'hant' });
t.deepEqual(languages.parseLanguageIntoCodes('zh'), { countryCode: false, languageCode: 'zh', locale: 'zh', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('es-MX'), { countryCode: 'mx', languageCode: 'es', locale: 'es-MX', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('es-ES'), { countryCode: 'es', languageCode: 'es', locale: 'es-ES', scriptCode: false });
t.deepEqual(languages.parseLanguageIntoCodes('pt-PT'), { countryCode: 'pt', languageCode: 'pt', locale: 'pt-PT', scriptCode: false });
Copy link
Member

@1ec5 1ec5 Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Directions API currently allows a language parameter to be specified in any case, such as pt-pt or PT-Pt. If the Directions API is to use getBestMatchingLanguage(), we should test that this method matches case insensitively, so pt-pt should also produce pt-BR.

t.deepEqual(languages.parseLanguageIntoCodes('pt'), { countryCode: false, languageCode: 'pt', locale: 'pt', scriptCode: false });
t.end();
});
/* eslint-enable */

t.test('respects options.instructionStringHook', function(assert) {
var v5Compiler = compiler('v5', {
hooks: {
Expand Down