diff --git a/.eslintrc.json b/.eslintrc.json index ab5122222..009b344cf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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", diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d19fa01..c7167a94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Readme.md b/Readme.md index 3a29d3eb3..f00c36d81 100644 --- a/Readme.md +++ b/Readme.md @@ -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) diff --git a/index.js b/index.js index faa7f9728..1d432c359 100644 --- a/index.js +++ b/index.js @@ -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; @@ -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]; @@ -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) { @@ -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'; } }; }; diff --git a/languages.js b/languages.js index d5d0f59b6..4d3fff26b 100755 --- a/languages.js +++ b/languages.js @@ -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 }; diff --git a/test/index_test.js b/test/index_test.js index e16ebab6a..414bc9189 100644 --- a/test/index_test.js +++ b/test/index_test.js @@ -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: { diff --git a/test/languages_test.js b/test/languages_test.js index a60327786..83272e80f 100644 --- a/test/languages_test.js +++ b/test/languages_test.js @@ -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 */