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

Multiple language support #107

Merged
merged 10 commits into from
May 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. For change

- put future changes here

- Breaking API change: move language selection from module loading to `compile(language, step)`

# 0.2.1 2017-04-05

- Add Spanish translation (thanks @josek5494)
Expand Down
21 changes: 10 additions & 11 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,25 @@ OSRM Text Instructions has been translated into [several languages](https://gith

### JavaScript Usage

```
```js
var version = 'v5';
var language = 'en';
var options = {};
var osrmTextInstructions = require('osrm-text-instructions')(version, language, options);
var osrmTextInstructions = require('osrm-text-instructions')(version);

// make your request against the API
// make your request against the API, save result to response variable

var language = 'en';
response.legs.forEach(function(leg) {
leg.steps.forEach(function(step) {
instruction = osrmTextInstructions.compile(step)
instruction = osrmTextInstructions.compile(language, step)
});
});
```

parameter | required? | values | description
---|----|----|---
`version` | required | `v5` | Major OSRM version
`language` | required | `en` `de` `zh-Hans` `fr` `nl` `ru` | Language identifier
`options.hooks.tokenizedInstruction` | optional | `function(instruction)` | A function to change the raw instruction string before tokens are replaced. Useful to inject custom markup for tokens
`language` | required | `en` `de` `zh-Hans` `fr` `nl` `ru` [and more](https://github.com/Project-OSRM/osrm-text-instructions/tree/master/languages/translations/) | Compiling instructions for the selected language code.

### Development
#### Architecture
Expand All @@ -61,17 +60,17 @@ Fixtures are programatically created and updated via `test/fixtures_test`. To up

#### Translations

The main language of this project is English `en`. We support other languages via translations, as seen in `languages/translations`.
The main language of this project is English `en`. We support other languages via translations, as seen in [`languages/translations`](https://github.com/Project-OSRM/osrm-text-instructions/tree/master/languages/translations/).

You can help translating on the web via [Transifex](https://www.transifex.com/project-osrm/osrm-text-instructions/)

To add an own translations:

- Go to [Transifex](https://www.transifex.com/project-osrm/osrm-text-instructions/) and create the new translation there
- When the translation on Transifex is ready, pull in the translation file:
- Create an empty translation file `echo "{}" > languages/translations/{language_tag}.json`
- Add the new translation file and language tag to `./languages.js`
- If needed: make overrides in `languages/overrides/{language_tag}.json`
- Create an empty translation file `echo "{}" > languages/translations/{language_code}.json`
- Add the new translation file and language code to `./languages.js`
Copy link
Member

Choose a reason for hiding this comment

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

the new translation file

This can be removed, since the code for loading each language file is not present anymore. It's only an array of language codes now that needs to be edited.

- If needed: make overrides in `languages/overrides/{language_code}.json`
- `npm run transifex`
- Generate fixture strings for the tests via `UPDATE=1 npm test` (see changes in `git diff`)
- Make a PR
Expand Down
76 changes: 43 additions & 33 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
module.exports = function(version, language, options) {
// load instructions
var instructions = require('./languages').get(language);
if (Object !== instructions.constructor) throw 'instructions must be object';
if (!instructions[version]) { throw 'invalid version ' + version; }
var languages = require('./languages');
var instructions = languages.instructions;

module.exports = function(version, _options) {
var options = {};
options.hooks = {};
options.hooks.tokenizedInstruction = ((_options || {}).hooks || {}).tokenizedInstruction;

Object.keys(instructions).forEach(function(code) {
if (!instructions[code][version]) { throw 'invalid version ' + version + ': ' + code + ' not supported'; }
});

return {
capitalizeFirstLetter: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
ordinalize: function(number) {
ordinalize: function(language, number) {
// Transform numbers to their translated ordinalized value
return instructions[version].constants.ordinalize[number.toString()] || '';
if (!language) throw new Error('No language code provided');

return instructions[language][version].constants.ordinalize[number.toString()] || '';
},
directionFromDegree: function(degree) {
directionFromDegree: function(language, degree) {
// Transform degrees to their translated compass direction
if (!language) throw new Error('No language code provided');
if (!degree && degree !== 0) {
// step had no bearing_after degree, ignoring
return '';
} else if (degree >= 0 && degree <= 20) {
return instructions[version].constants.direction.north;
return instructions[language][version].constants.direction.north;
} else if (degree > 20 && degree < 70) {
return instructions[version].constants.direction.northeast;
return instructions[language][version].constants.direction.northeast;
} else if (degree >= 70 && degree <= 110) {
return instructions[version].constants.direction.east;
return instructions[language][version].constants.direction.east;
} else if (degree > 110 && degree < 160) {
return instructions[version].constants.direction.southeast;
return instructions[language][version].constants.direction.southeast;
} else if (degree >= 160 && degree <= 200) {
return instructions[version].constants.direction.south;
return instructions[language][version].constants.direction.south;
} else if (degree > 200 && degree < 250) {
return instructions[version].constants.direction.southwest;
return instructions[language][version].constants.direction.southwest;
} else if (degree >= 250 && degree <= 290) {
return instructions[version].constants.direction.west;
return instructions[language][version].constants.direction.west;
} else if (degree > 290 && degree < 340) {
return instructions[version].constants.direction.northwest;
return instructions[language][version].constants.direction.northwest;
} else if (degree >= 340 && degree <= 360) {
return instructions[version].constants.direction.north;
return instructions[language][version].constants.direction.north;
} else {
throw new Error('Degree ' + degree + ' invalid');
}
Expand All @@ -59,7 +68,9 @@ module.exports = function(version, language, options) {

return config.join('');
},
compile: function(step) {
compile: function(language, step) {
if (!language) throw new Error('No language code provided');
if (languages.supportedCodes.indexOf(language) === -1) throw new Error('language code ' + language + ' not loaded');
if (!step.maneuver) throw new Error('No step maneuver provided');

var type = step.maneuver.type;
Expand All @@ -69,7 +80,7 @@ module.exports = function(version, language, options) {
if (!type) { throw new Error('Missing step maneuver type'); }
if (type !== 'depart' && type !== 'arrive' && !modifier) { throw new Error('Missing step maneuver modifier'); }

if (!instructions[version][type]) {
if (!instructions[language][version][type]) {
// Log for debugging
console.log('Encountered unknown instruction type: ' + type); // eslint-disable-line no-console
// OSRM specification assumes turn types can be added without
Expand All @@ -80,23 +91,23 @@ module.exports = function(version, language, options) {

// Use special instructions if available, otherwise `defaultinstruction`
var instructionObject;
if (instructions[version].modes[mode]) {
instructionObject = instructions[version].modes[mode];
} else if (instructions[version][type][modifier]) {
instructionObject = instructions[version][type][modifier];
if (instructions[language][version].modes[mode]) {
instructionObject = instructions[language][version].modes[mode];
} else if (instructions[language][version][type][modifier]) {
instructionObject = instructions[language][version][type][modifier];
} else {
instructionObject = instructions[version][type].default;
instructionObject = instructions[language][version][type].default;
}

// Special case handling
var laneInstruction;
switch (type) {
case 'use lane':
laneInstruction = instructions[version].constants.lanes[this.laneConfig(step)];
laneInstruction = instructions[language][version].constants.lanes[this.laneConfig(step)];

if (!laneInstruction) {
// If the lane combination is not found, default to continue straight
instructionObject = instructions[version]['use lane'].no_lanes;
instructionObject = instructions[language][version]['use lane'].no_lanes;
}
break;
case 'rotary':
Expand Down Expand Up @@ -147,9 +158,8 @@ module.exports = function(version, language, options) {
instruction = instructionObject.default;
}

var tokenizedInstructionHook = ((options || {}).hooks || {}).tokenizedInstruction;
if (tokenizedInstructionHook) {
instruction = tokenizedInstructionHook(instruction);
if (options.hooks.tokenizedInstruction) {
instruction = options.hooks.tokenizedInstruction(instruction);
}

// Replace tokens
Expand All @@ -158,15 +168,15 @@ module.exports = function(version, language, options) {
instruction = instruction
.replace('{way_name}', wayName)
.replace('{destination}', (step.destinations || '').split(',')[0])
.replace('{exit_number}', this.ordinalize(step.maneuver.exit || 1))
.replace('{exit_number}', this.ordinalize(language, step.maneuver.exit || 1))
.replace('{rotary_name}', step.rotary_name)
.replace('{lane_instruction}', laneInstruction)
.replace('{modifier}', instructions[version].constants.modifier[modifier])
.replace('{direction}', this.directionFromDegree(step.maneuver.bearing_after))
.replace('{modifier}', instructions[language][version].constants.modifier[modifier])
.replace('{direction}', this.directionFromDegree(language, step.maneuver.bearing_after))
.replace('{nth}', nthWaypoint)
.replace(/ {2}/g, ' '); // remove excess spaces

if (instructions.meta.capitalizeFirstLetter) {
if (instructions[language].meta.capitalizeFirstLetter) {
instruction = this.capitalizeFirstLetter(instruction);
}

Expand Down
27 changes: 5 additions & 22 deletions languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ var instructionsSv = require('./languages/translations/sv.json');
var instructionsVi = require('./languages/translations/vi.json');
var instructionsZhHans = require('./languages/translations/zh-Hans.json');

// Match tag to required language files
var tags = {

// Create a list of supported codes
var instructions = {
'de': instructionsDe,
'en': instructionsEn,
'es': instructionsEs,
Expand All @@ -25,25 +26,7 @@ var tags = {
'zh-Hans': instructionsZhHans
};

// A tag can redirect to another tag via the language tag as string value
var redirects = {
'zh': 'zh-Hans'
};

module.exports = {
tags: tags,
redirects: redirects,
get: function(tag) {
if (this.redirects[tag]) {
// redirect to other tag
this.get(this.redirects[tag]);
}

var language = this.tags[tag];
if (!language) {
throw 'invalid language ' + tag;
}

return language;
}
supportedCodes: Object.keys(instructions),
instructions: instructions
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"request": "2.79.0"
},
"scripts": {
"lint": "eslint *.js test/*.js",
"lint": "eslint *.js test/*.js scripts/*.js",
"pretest": "npm run lint",
"test": "tape test/*_test.js",
"transifex": "node scripts/transifex.js"
Expand Down
15 changes: 8 additions & 7 deletions scripts/transifex.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,24 @@ if (!auth.user || !auth.pass) throw 'invalid transifex.auth';
var urls = {};
urls.api = 'https://www.transifex.com/api/2';
urls.project = 'project/osrm-text-instructions';
urls.translation = `${urls.api}/${urls.project}/resource/enjson/translation`
urls.translation = `${urls.api}/${urls.project}/resource/enjson/translation`;

Object.keys(languages.tags).forEach((tag) => {
if (tag === 'en') { return }; // no need to download english
Object.keys(languages.codes).forEach((code) => {
// no need to download english
if (code === 'en') return;

// Download from Transifex
request.get(`${urls.translation}/${tag}`, { auth: auth }, (err, resp, body) => {
request.get(`${urls.translation}/${code}`, {auth: auth}, (err, resp, body) => {
if (err) throw err;
var content = JSON.parse(JSON.parse(body).content);

// Apply language-specific overrides
var override = `${__dirname}/../languages/overrides/${tag}.js`
if(fs.existsSync(override)) {
var override = `${__dirname}/../languages/overrides/${code}.js`;
if (fs.existsSync(override)) {
content = require(override)(content);
}

// Write language file
fs.writeFileSync(`${__dirname}/../languages/translations/${tag}.json`, JSON.stringify(content, null, 4));
fs.writeFileSync(`${__dirname}/../languages/translations/${code}.json`, JSON.stringify(content, null, 4));
});
});
5 changes: 1 addition & 4 deletions test/fixtures_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ var constants = require('./constants');
var instructions = require('../index.js');

// Load instructions files for each language
var languages = {};
Object.keys(require('../languages').tags).forEach((l) => {
languages[l] = instructions('v5', l);
});
var languages = instructions('v5');

tape.test('verify existance/update fixtures', function(assert) {
function clone(obj) {
Expand Down
Loading