Commit 52729e6a authored by Adrien Pavie's avatar Adrien Pavie
Browse files

Merge branch 'feature/translation' into develop

parents c7d32abe d5921229
......@@ -87,6 +87,8 @@ coreprepare:
# Prepare fonts
mkdir -p $(DIST_FOLDER)/fonts
cp node_modules/font-awesome/fonts/* $(DIST_FOLDER)/fonts/
# Prepare locales
cp -r $(SRC_FOLDER)/locales $(DIST_FOLDER)/
corebundlejs:
$(BROWSERIFY) $(SRC_FOLDER)/js/OLU.js --debug --s OLU > $(JS_OUT)
......@@ -103,7 +105,7 @@ coreminifycss:
$(NM_FOLDER)/uglifycss/uglifycss --ugly-comments $(CSS_OUT) > $(CSS_MIN_OUT)
corebundleminifyjs:
$(BROWSERIFY) $(SRC_FOLDER)/js/OLU.js --s OLU | uglifyjs -c > $(JS_MIN_OUT)
$(BROWSERIFY) $(SRC_FOLDER)/js/OLU.js --s OLU | $(NM_FOLDER)/uglify-js/bin/uglifyjs -c > $(JS_MIN_OUT)
# Test
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -84,6 +84,7 @@ var OLU = {
Routing: require("./ctrl/service/Routing"),
URL: require("./ctrl/service/URL"),
Search: require("./ctrl/service/Search"),
Translator: require("./ctrl/service/Translator")
}
},
util: require("./utils")
......
......@@ -17,6 +17,7 @@
var $ = require("jquery");
var L = require("leaflet");
var yaml = require("js-yaml");
var RuleSet = require("../model/mapcss/RuleSet");
var MainView = require("../view/Main");
......@@ -27,6 +28,7 @@ var ServiceSearch = require("./service/Search");
var ProviderDataOSM = require("./provider/data/OSM");
var ProviderDataNotes = require("./provider/data/Notes");
var ProviderPictureFlickr = require("./provider/picture/Flickr");
var Translator = require("./service/Translator");
/**
* Main controller is the master of puppets.
......@@ -57,6 +59,11 @@ var Main = function(divId, mode, config) {
"styles/details.mapcss"
],
"locales": [
"locales/en.yaml",
"locales/fr.yaml"
],
"api": {
"overpass": "http://api.openstreetmap.fr/oapi/interpreter/?data=",
"overpass_alt": "http://www.overpass-api.de/api/interpreter?data=",
......@@ -328,6 +335,9 @@ var Main = function(divId, mode, config) {
/** Search **/
this.search = new ServiceSearch(this.config.api.nominatim);
/** Translator **/
this.translator = new Translator();
/*
* Providers
*/
......@@ -353,6 +363,24 @@ var Main = function(divId, mode, config) {
options.tile = (this.url.getParameter("tile") != null) ? parseInt(this.url.getParameter("tile")) : undefined;
options.style = (this.url.getParameter("style") != null) ? this.url.getParameter("style") : undefined;
/*
* Init locales
*/
for(var lngId=0; lngId < this.config.locales.length; lngId++) {
$.ajax({
url: this.config.locales[lngId],
async: false,
dataType: 'text',
success: function(data) {
var locale = yaml.safeLoad(data);
var localeName = Object.keys(locale)[0];
this.translator.addTranslation(localeName, locale[localeName]);
}.bind(this)
}).fail(function() { throw new Error("[Controller] Error while retrieving locales file: "+this.config.locales[lngId]); }.bind(this));
}
//Set locale according to browser
this.translator.setLocale(window.navigator.userLanguage || window.navigator.language);
/*
* Init main view
*/
......@@ -365,6 +393,7 @@ var Main = function(divId, mode, config) {
this.search,
this.data.osm,
this.pictures.flickr,
this.translator.t.bind(this.translator),
options
);
......
/*
* This file is part of OpenLevelUp!.
*
* OpenLevelUp! is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* OpenLevelUp! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with OpenLevelUp!. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Allows label translating according to given translations.
*/
var Translator = function () {
/** The loaded translations **/
this.translations = {};
/** The currently used locale **/
this.currentLocale = 'en';
};
/** Change the current locale **/
Translator.prototype.setLocale = function(lng) {
if(this.translations[lng] !== undefined) {
this.currentLocale = lng;
}
else if(this.translations[lng.split('-')[0]]) {
this.currentLocale = lng.split('-')[0];
}
};
/** Add a new language **/
Translator.prototype.addTranslation = function(lng, data) {
this.translations[lng] = data;
};
/**
* Given a string identifier, try to find that string in the current
* language, and return it.
* @param s string identifier
* @param o Optional parameters to replace in the label
* @param lng Optional, the locale to use
* @returns locale string
*/
Translator.prototype.t = function(s, o, loc) {
loc = loc || this.currentLocale;
var path = s.split('.').reverse();
var rep = this.translations[loc];
while (rep !== undefined && path.length) rep = rep[path.pop()];
if (rep !== undefined) {
if (o) for (var k in o) rep = rep.replace('{' + k + '}', o[k]);
return rep;
}
if (loc !== 'en') {
return this.t(s, o, 'en');
}
if (o && 'default' in o) {
return o.default;
}
console.error('[Translator] Missing ' + loc + ' translation: ' + s);
return 'Missing label';
};
module.exports = Translator;
......@@ -140,7 +140,7 @@ var Level = L.Control.extend({
//If too many levels
if(btnOverflow) {
$(this.container).addClass("level-control-container-full");
var btnUp = this._createButton("", "See higher levels", 'level-control-scroll-up', this.container, this._scroll.bind(this, "up"));
var btnUp = this._createButton("", this.options.t("map.level.up.desc"), 'level-control-scroll-up', this.container, this._scroll.bind(this, "up"));
if(!util.isMobileBrowser()) {
L.DomEvent.on(btnUp, 'mouseover', this._scroll.bind(this, "up", "start"));
L.DomEvent.on(btnUp, 'mouseout', this._scroll.bind(this, "up", "stop"));
......@@ -155,7 +155,7 @@ var Level = L.Control.extend({
this._createLevels(levelsContainer);
var btnDown = this._createButton("", "See lower levels", 'level-control-scroll-down', this.container, this._scroll.bind(this, "down"));
var btnDown = this._createButton("", this.options.t("map.level.down.desc"), 'level-control-scroll-down', this.container, this._scroll.bind(this, "down"));
if(!util.isMobileBrowser()) {
L.DomEvent.on(btnDown, 'mouseover', this._scroll.bind(this, "down", "start"));
L.DomEvent.on(btnDown, 'mouseout', this._scroll.bind(this, "down", "stop"));
......
......@@ -43,9 +43,10 @@ var Spinner = require("./Spinner");
* @param searchService The search service
* @param osmProvider The OSM data provider
* @param flickrProvider The flickr pictures provider
* @param t The translation function
* @param options Some init options, like { tile: integer, level: float }
*/
var Main = function(divId, mode, config, rulesets, newNoteService, searchService, osmProvider, flickrProvider, options) {
var Main = function(divId, mode, config, rulesets, newNoteService, searchService, osmProvider, flickrProvider, t, options) {
//ATTRIBUTES
/** The view mode (basic, advanced) **/
this.mode = (mode == "advanced") ? mode : "basic";
......@@ -130,18 +131,18 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
}
}.bind(this));
this.map.attributionControl.setPrefix('<img src="img/favicon.png" /> <a href="http://openlevelup.net">OpenLevelUp!</a>');
this.layersManager = new MapLayersManager(this.map);
this.map.attributionControl.setPrefix('<img src="img/favicon.png" /> <a href="http://openlevelup.net">'+t("olu.name")+'</a>');
this.layersManager = new MapLayersManager(this.map, t);
var hash = new L.Hash(this.map);
/***************************************************************************
* Create controls
*/
this.side = Side("olu-map-side", { autoPan: false });
this.side = Side("olu-map-side", { autoPan: false, t: t });
this.messages = Messages("olu-messages");
this.spinner = Spinner("olu-spinner");
this.level = Level({ position: "topright" });
this.level = Level({ position: "topright", t: t });
var levelControl = this.level; //To have access in editor control
var zoomControl = L.control.zoom({ position: "topright" });
var scaleControl = L.control.scale({ position: "bottomleft" });
......@@ -151,24 +152,24 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
function(btn, map){
this.side.show("newnote");
}.bind(this),
'Propose a map correction',
t("notes.desc"),
{ position: "topleft" }
);
var searchButton = L.easyButton(
'fa-search',
function(btn, map){ this.side.show("search"); }.bind(this),
'Find a place in the world'
t("search.desc")
);
var layersButton = L.easyButton(
'fa-globe',
function(btn, map){ this.side.show("layers"); }.bind(this),
'Change map background and style',
t("map.layers.desc"),
{ position: "bottomleft" }
);
var routingButton = L.easyButton(
'fa-map-signs',
function(btn, map){ this.side.show("routing"); }.bind(this),
'Find direction between two places'
t("routing.desc")
);
var navbar = L.easyBar([ searchButton, routingButton ], { position: "bottomright", id: "olu-control-navbar" });
var editorControl = new L.Control.EditInOSM({
......@@ -263,7 +264,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
this.side.contents.newnote.set(
newNoteService,
function() {
this.messages.show("The note was successfully sent", "info");
this.messages.show(t("notes.sent"), "info");
this.side.hide();
//TODO Reload notes data if already shown
......@@ -379,7 +380,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
if(zoom >= this.config.map.dataMinZoom) {
//Download new data
if(this.bounds.features == undefined || !this.bounds.features.contains(bounds)) {
this.spinner.show("Retrieving building data", "download");
this.spinner.show(t("data.download.building"), "download");
this.bounds.features = bounds.pad(1.1 + 0.5 * (zoom - this.config.map.dataMinZoom));
//Retrieve pictures
......@@ -390,7 +391,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
this.pictures.flickr = photos;
}.bind(this),
function() {
this.messages.show("Flickr pictures can't be retrieved", "error");
this.messages.show(t("data.pictures.fail", { provider: "Flickr" }), "error");
}.bind(this)
);
......@@ -445,7 +446,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
this.spinner.hide();
}.bind(this),
function(e) {
this.messages.show(e.message || "Can't retrieve data", "error");
this.messages.show(e.message || t("data.download.fail"), "error");
this.spinner.hide();
}.bind(this)
);
......@@ -497,7 +498,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
if(zoom >= this.config.map.clusterMinZoom && zoom < this.config.map.dataMinZoom) {
//Download new data
if(this.bounds.cluster == undefined || !this.bounds.cluster.contains(bounds)) {
this.spinner.show("Retrieving location data", "download");
this.spinner.show(t("data.download.location"), "download");
this.bounds.cluster = bounds.pad(1.1);
console.log("[Main] Start download cluster data");
osmProvider.downloadCluster(
......@@ -507,7 +508,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
this.spinner.hide();
}.bind(this),
function(e) {
this.messages.show(e.message || "Can't retrieve data", "error");
this.messages.show(e.message || t("data.download.fail"), "error");
this.spinner.hide();
}.bind(this)
);
......@@ -524,7 +525,7 @@ var Main = function(divId, mode, config, rulesets, newNoteService, searchService
if(zoom < this.config.map.clusterMinZoom) {
clearPreviousDataLayer();
//TODO Better handling of those messages
this.messages.show("Zoom in to see data", "info", 1000);
this.messages.show(t("map.zoom.showdata"), "info", 1000);
}
}.bind(this);
......
......@@ -26,7 +26,7 @@ var util = require("../utils");
* Manager for map layers, reading external data and creating appropriate map layers.
* Adding/removing layers from map is not handled here.
*/
var MapLayersManager = function(map) {
var MapLayersManager = function(map, t) {
//ATTRIBUTES
/** The leaflet map **/
this.map = map;
......@@ -34,6 +34,9 @@ var MapLayersManager = function(map) {
/** The layers **/
this.layers = { notes: null, cluster: null, features: null, tiles: null };
/** The translator **/
this.t = t;
/** The markers **/
this.markers = {
noteOpen: L.icon({
......@@ -73,11 +76,11 @@ MapLayersManager.prototype.setNotes = function(notes) {
L.marker(
n.coordinates,
{
title: "Note #"+n.id,
title: this.t("notes.id", { id: n.id }),
icon: (n.status == "closed") ? this.markers.noteClosed : this.markers.noteOpen
}
).bindPopup(
"<h3>Note #"+n.id+"</h3>"+
"<h3>"+this.t("notes.id", { id: n.id })+"</h3>"+
"<p>Status: "+n.status+"<br /><a href=\"\">Details</a></p>"
)
);
......
......@@ -32,23 +32,25 @@ var SideContentLayers = require('./sidecontent/Layers');
*/
var Side = L.Control.Sidebar.extend({
//ATTRIBUTES
contents: {
search: new SideContentSearch(),
routing: new SideContentRouting(),
notes: new SideContentNotes(),
newnote: new SideContentNewNote(),
parameters: new SideContentParameters(),
help: new SideContentHelp(),
feature: new SideContentFeature(),
layers: new SideContentLayers()
},
halfsize: [ "search", "routing", "notes", "newnote", "feature" ],
//OTHER METHODS
initialize: function(placeholder, options) {
L.Control.Sidebar.prototype.initialize.call(this, placeholder, options);
if(options.t) { this.t = options.t; }
this.contents = {
search: new SideContentSearch(this.t),
routing: new SideContentRouting(),
notes: new SideContentNotes(this.t),
newnote: new SideContentNewNote(this.t),
parameters: new SideContentParameters(),
help: new SideContentHelp(),
feature: new SideContentFeature(this.t),
layers: new SideContentLayers(this.t)
};
//Create each side content
for(var c in this.contents) {
L.DomUtil.create("div", "side-content side-content-"+c, this.getContainer())
......
......@@ -21,7 +21,7 @@ var Mapillary = require("mapillary-js");
/**
* Feature content for side bar
*/
var Feature = function() {
var Feature = function(t) {
//ATTRIBUTES
/** The current feature **/
this._feature = null;
......@@ -40,6 +40,9 @@ var Feature = function() {
/** The mapillary viewer **/
this._mapillaryViewer = null;
/** The translation function **/
this.t = t;
};
//ACCESSORS
......@@ -52,7 +55,7 @@ Feature.prototype.get = function() {
if(this._feature != null) {
var title = $(
"<div class=\"side-content-feature-title\"><h2>"+((this._feature.name) ? this._feature.name : ((this._feature.category) ? this._feature.category : this._feature.id))+"</h2>"+
((this._feature.category != this._feature.name && this._feature.category != null) ? "<span>"+this._feature.category+"</span>" : "<span>Feature</span>")+
((this._feature.category != this._feature.name && this._feature.category != null) ? "<span>"+this._feature.category+"</span>" : "<span>"+this.t("feature.name")+"</span>")+
"</div>"
);
element.append(title);
......@@ -81,7 +84,7 @@ Feature.prototype.get = function() {
if(this._feature.getImages().count() > 0) {
var images = this._feature.getImages().get();
for(var i=0; i < images.length; i++) {
pictures.append($("<div class=\"side-content-feature-picture basic\"><img src=\""+images[i].img+"\" alt=\"Loading image...\" /></div>").hide());
pictures.append($("<div class=\"side-content-feature-picture basic\"><img src=\""+images[i].img+"\" alt=\""+this.t("pictures.loading")+"\" /></div>").hide());
}
}
......@@ -94,7 +97,7 @@ Feature.prototype.get = function() {
}
//Stub image
else {
pictures.append("<div class=\"side-content-feature-picture basic\"><img src=\"img/stub_feature.jpg\" alt=\"Loading image...\" /></div>");
pictures.append("<div class=\"side-content-feature-picture basic\"><img src=\"img/stub_feature.jpg\" alt=\""+this.t("pictures.loading")+"\" /></div>");
}
element.append(pictures);
......@@ -128,7 +131,7 @@ Feature.prototype.get = function() {
*/
if(this._options.showTags) {
var tags = $("<div class=\"side-content-feature-tags\"></div>");
tags.append("<h3>Tags</h3>");
tags.append("<h3>"+this.t("features.tags")+"</h3>");
var table = $("<table></table>");
......@@ -253,7 +256,7 @@ Feature.prototype._detail = function(osmKey, d) {
result += "<a href=\""+val+"\">"+val+"</a>";
}
else {
result += "Invalid URL";
result += this.t("features.url.invalid");
}
break;
......@@ -262,12 +265,12 @@ Feature.prototype._detail = function(osmKey, d) {
result += "<a href=\"mailto:"+val+"\">"+val+"</a>";
}
else {
result += "Invalid email address";
result += this.t("features.email.invalid");
}
break;
case "hours":
result += "<a href=\"http://projets.pavie.info/yohours/?oh="+encodeURIComponent(val)+"\" target=\"_blank\">Click to see hours</a>";
result += "<a href=\"http://projets.pavie.info/yohours/?oh="+encodeURIComponent(val)+"\" target=\"_blank\">"+this.t("features.hours.show")+"</a>";
break;
case "text_bind":
......
......@@ -20,7 +20,7 @@ var $ = require('jquery');
/**
* Layers content for side bar
*/
var Layers = function() {
var Layers = function(t) {
//ATTRIBUTES
/** The map **/
this._map = null;
......@@ -42,6 +42,9 @@ var Layers = function() {
/** The map coordinates **/
this._coords = { lat: 48.12114, lng: -1.71129, zoom: 18 };
/** The translator function **/
this.t = t;
};
//ACCESSORS
......@@ -53,7 +56,7 @@ Layers.prototype.get = function() {
var element = $("<div/>");
if(this._styles) {
element.append("<h2>Style</h2>");
element.append("<h2>"+this.t("map.layers.name")+"</h2>");
var styles = $("<div id=\"side-content-layers-styles\"></div>");
//Read each meta style
......@@ -91,7 +94,7 @@ Layers.prototype.get = function() {
}
if(this._bases) {
element.append("<h2>Background</h2>");
element.append("<h2>"+this.t("map.layers.background")+"</h2>");
var bases = $("<div id=\"side-content-layers-bases\"></div>");
this._basesId = [];
......
......@@ -20,7 +20,7 @@ var $ = require('jquery');
/**
* New note content for side bar
*/
var NewNote = function() {
var NewNote = function(t) {
//ATTRIBUTES
/** The new note service **/
this._newNote = null;
......@@ -36,6 +36,9 @@ var NewNote = function() {
/** The fail handler **/
this._fail = null;
/** The translator function **/
this.t = t;
};
//ACCESSORS
......@@ -45,22 +48,22 @@ var NewNote = function() {
NewNote.prototype.get = function() {
var element = $("<div/>");
element.append("<h2>Add a note</h2>");
element.append("<p>Spotted a mistake or something missing? Let other people know so we can fix it. Move the marker to the correct position and type a note to explain the problem.<br />(Please don't enter personal information or information from copyrighted maps or directory listings)</p>");
element.append("<textarea id=\"sidecontent-newnote-textarea\">"+((this._level !== null) ? "Level "+this._level+": " : "")+"</textarea>");
element.append("<h2>"+this.t("notes.add.name")+"</h2>");
element.append("<p>"+this.t("notes.add.desc")+"<br />"+this.t("notes.add.privacy")+"</p>");
element.append("<textarea id=\"sidecontent-newnote-textarea\">"+((this._level !== null) ? this.t("map.level.value", { l: this._level })+": " : "")+"</textarea>");
var buttons = $("<div class=\"sidecontent-newnote-buttons\"></div>");
//Send button
var sendBtnDiv = $("<div class=\"sidecontent-newnote-button\">");
var sendBtn = $("<button id=\"sidecontent-newnote-send\" class=\"success\" type=\"button\">Add</button>");
var sendBtn = $("<button id=\"sidecontent-newnote-send\" class=\"success\" type=\"button\">"+this.t("buttons.add")+"</button>");
sendBtn.click(this._sendNote.bind(this));
sendBtnDiv.append(sendBtn);
buttons.append(sendBtnDiv);
//Cancel button
var cancelBtnDiv = $("<div class=\"sidecontent-newnote-button\">");
var cancelBtn = $("<button id=\"sidecontent-newnote-cancel\" class=\"warning\" type=\"button\">Cancel</button>");
var cancelBtn = $("<button id=\"sidecontent-newnote-cancel\" class=\"warning\" type=\"button\">"+this.t("buttons.cancel")+"</button>");
cancelBtnDiv.append(cancelBtn);
buttons.append(cancelBtnDiv);
......@@ -87,8 +90,8 @@ NewNote.prototype.set = function(service, success, fail) {
*/
NewNote.prototype.setLevel = function(l) {
var text = $("#sidecontent-newnote-textarea");
if(text.val() == undefined || text.val().trim().length == 0 || text.val().trim() == "Level "+this._level+":") {
text.val("Level "+l+": ");
if(text.val() == undefined || text.val().trim().length == 0 || text.val().trim() == this.t("map.level.value", { l: this._level })+":") {
text.val(this.t("map.level.value", { l: l })+": ");
}
this._level = l;
......@@ -112,7 +115,7 @@ NewNote.prototype._sendNote = function() {
this._coords == null
|| textVal == undefined
|| textVal.trim().length == 0
|| textVal.trim() == "Level "+this._level+":"
|| textVal.trim() == this.t("map.level.value", { l: this._level })+":"
) {
this._fail(new Error("view.sidecontent.NewNote.invalidParameters"));
}
......
......@@ -20,9 +20,12 @@ var $ = require('jquery');
/**
* Notes content for side bar
*/
var Notes = function() {
var Notes = function(t) {
//ATTRIBUTES
this.note = null;