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 closest available language. Pass a user locale into this method before passing the return value into `compile`. [#168](https://github.com/Project-OSRM/osrm-text-instructions/pull/168)

## 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’re unsure if the user’s locale is supported, use `getBestMatchingLanguage` method to find an appropriate language.
var language = osrmTextInstructions.getBestMatchingLanguage('en-US');

var language = 'en';
response.legs.forEach(function(leg) {
leg.steps.forEach(function(step) {
instruction = osrmTextInstructions.compile(language, step, options)
Expand Down
44 changes: 44 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,47 @@ module.exports = function(version, _options) {
}

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

var codes = languages.parseLanguageIntoCodes(language);
var languageCode = codes.language;
var scriptCode = codes.script;
var regionCode = codes.region;

// Same language code and script code (lng-Scpt)
if (languages.instructions[languageCode + '-' + scriptCode]) {
return languageCode + '-' + scriptCode;
}

// Same language code and region code (lng-CC)
if (languages.instructions[languageCode + '-' + regionCode]) {
return languageCode + '-' + regionCode;
}

// Same language code (lng)
if (languages.instructions[languageCode]) {
return languageCode;
}

// Same language code and any script code (lng-Scpx) and the found language contains a script
var anyScript = languages.parsedSupportedCodes.find(function (language) {
return language.language === languageCode && language.script;
});
if (anyScript) {
return anyScript.locale;
}

// Same language code and any region code (lng-CX)
var anyCountry = languages.parsedSupportedCodes.find(function (language) {
return language.language === languageCode && language.region;
});
if (anyCountry) {
return anyCountry.locale;
}

return 'en';
}
};
};
30 changes: 29 additions & 1 deletion languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,36 @@ var grammars = {
'ru': grammarRu
};

function parseLanguageIntoCodes (language) {
var match = language.match(/(\w\w)(?:-(\w\w\w\w))?(?:-(\w\w))?/i);
var locale = [];
if (match[1]) {
match[1] = match[1].toLowerCase();
locale.push(match[1]);
}
if (match[2]) {
match[2] = match[2][0].toUpperCase() + match[2].substring(1).toLowerCase();
locale.push(match[2]);
}
if (match[3]) {
match[3] = match[3].toUpperCase();
locale.push(match[3]);
}

return {
locale: locale.join('-'),
language: match[1],
script: match[2],
region: match[3]
};
}

module.exports = {
supportedCodes: Object.keys(instructions),
parsedSupportedCodes: Object.keys(instructions).map(function(language) {
return parseLanguageIntoCodes(language);
}),
instructions: instructions,
grammars: grammars
grammars: grammars,
parseLanguageIntoCodes: parseLanguageIntoCodes
};
93 changes: 91 additions & 2 deletions test/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,101 @@ 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.equal(compiler('v5').getBestMatchingLanguage('pt-pt'), 'pt-BR');
t.end();
});

t.test('respects options.instructionStringHook', function(assert) {
var v5Compiler = compiler('v5', {
hooks: {
Expand Down
16 changes: 16 additions & 0 deletions test/languages_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,19 @@ tape.test('verify language files structure', function(assert) {

assert.end();
});

/* eslint-disable */
tape.test('parseLanguageIntoCodes', function(t) {
t.deepEqual(languages.parseLanguageIntoCodes('foo'), { region: undefined, language: 'fo', locale: 'fo', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('en-US'), { region: 'US', language: 'en', locale: 'en-US', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('zh-CN'), { region: 'CN', language: 'zh', locale: 'zh-CN', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('zh-Hant'), { region: undefined, language: 'zh', locale: 'zh-Hant', script: 'Hant' });
t.deepEqual(languages.parseLanguageIntoCodes('zh-Hant-TW'), { region: 'TW', language: 'zh', locale: 'zh-Hant-TW', script: 'Hant' });
t.deepEqual(languages.parseLanguageIntoCodes('zh'), { region: undefined, language: 'zh', locale: 'zh', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('es-MX'), { region: 'MX', language: 'es', locale: 'es-MX', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('es-ES'), { region: 'ES', language: 'es', locale: 'es-ES', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('pt-PT'), { region: 'PT', language: 'pt', locale: 'pt-PT', script: undefined });
t.deepEqual(languages.parseLanguageIntoCodes('pt'), { region: undefined, language: 'pt', locale: 'pt', script: undefined });
t.end();
});
/* eslint-enable */