diff --git a/_config.yml b/_config.yml index f54edddd..d6aefa8a 100644 --- a/_config.yml +++ b/_config.yml @@ -3,3 +3,4 @@ markdown: kramdown title: Recline Data Explorer and Library +exclude: [] diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index 3e6d7360..fb77d16b 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -46,6 +46,10 @@ + + + + @@ -66,6 +70,8 @@ + + diff --git a/css/grid.css b/css/grid.css index caaef192..05d6f619 100644 --- a/css/grid.css +++ b/css/grid.css @@ -193,3 +193,29 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-read-only a.row-header-menu { display: none; } + +/************************* + * WCAG 2.0 + *************************/ + +.wcag_hide { + position:absolute; + top:0; + left:-10000px; + width:1px; + height:1px; +} + +.wcag_hide2 { + clip: rect(1px, 1px, 1px, 1px); + display: block; + position: absolute; +} + +.wcag_show_on_focus { + font-size: 0 !important; +} + +.wcag_show_on_focus:focus { + font-size: 1em !important; +} diff --git a/demos/multiview/app.js b/demos/multiview/app.js index bfea4d28..e905ae92 100755 --- a/demos/multiview/app.js +++ b/demos/multiview/app.js @@ -64,10 +64,11 @@ var createMultiView = function(dataset, state) { $el.appendTo(window.explorerDiv); // customize the subviews for the MultiView + var fmt = I18nMessages('recline', recline.View.translations); var views = [ { id: 'grid', - label: 'Grid', + label: fmt.t('Grid'), view: new recline.View.SlickGrid({ model: dataset, state: { @@ -91,15 +92,14 @@ var createMultiView = function(dataset, state) { }, { id: 'graph', - label: 'Graph', + label: fmt.t('Graph'), view: new recline.View.Graph({ model: dataset - }) }, { id: 'map', - label: 'Map', + label: fmt.t('Map'), view: new recline.View.Map({ model: dataset }) diff --git a/dist/recline.css b/dist/recline.css index 5d7f6e86..f1e976fd 100644 --- a/dist/recline.css +++ b/dist/recline.css @@ -219,6 +219,32 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-read-only a.row-header-menu { display: none; } + +/************************* + * WCAG 2.0 + *************************/ + +.wcag_hide { + position:absolute; + top:0; + left:-10000px; + width:1px; + height:1px; +} + +.wcag_hide2 { + clip: rect(1px, 1px, 1px, 1px); + display: block; + position: absolute; +} + +.wcag_show_on_focus { + font-size: 0 !important; +} + +.wcag_show_on_focus:focus { + font-size: 1em !important; +} .recline-map .map { height: 500px; } diff --git a/docs/views.markdown b/docs/views.markdown index 762de79f..d29220e5 100644 --- a/docs/views.markdown +++ b/docs/views.markdown @@ -105,3 +105,94 @@ of such behaviour see the DataExplorer view. See the existing Views. +## Internationalization + +### Adding translations to templates + +Internationalization is implemented with help of [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) library and supports [ICU Message syntax](http://userguide.icu-project.org/formatparse/messages). + +Translation keys are using Mustache tags prefixed by `t.`. You can specify translated strings in two ways: + +1. Simple text with no variables is rendered using Mustache variable-tag +```javascript +$template = '{{ "{{t.Add_row"}}}}'; // will show "Add row" in defaultLocale (English) +``` + +2. Text with variables or special characters is rendered using Mustache section-tag + +```javascript +$template = '{{ "{{#t.desc"}}}}Add first_row field{{ "{{/t.desc"}}}}'; +// using special chars; will show "Add first_row field" in defaultLocale + +$template = '{{ "{{#t.num_records"}}}}{recordCount, plural, =0 {no records} =1{# record} other {# records}}{{ "{{/t.num_records"}}}}'; +// will show "no records", "1 record" or "x records" in defaultLocale (English) +``` + +When using section-tags in existing templates be sure to remove a bracket from variables inside sections: +```javascript +#template___notranslation = '{{ "{{recordCount"}}}} records'; +#template_withtranslation = '{{ "{{#t.num_records"}}}} {recordCount} records {{ "{{/t.num_records"}}}}'; +``` + + +Then setup Mustache to use translation by injecting tranlation tags in `render` function: + +```javascript +// ============== BEFORE =================== + +my.MultiView = Backbone.View.extend({ + render: function() { + var tmplData = this.model.toTemplateJSON(); + var output = Mustache.render(this.template, tmplData); + ... + } +}); + +// ============== AFTER ==================== + +my.MultiView = Backbone.View.extend({ + render: function() { + var tmplData = this.model.toTemplateJSON(); + tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); // inject Moustache formatter + var output = Mustache.render(this.template, tmplData); + ... + } +}); +``` + +### Language resolution + +By default the language is detected from the root `lang` attributes - ` and ``. + +If you want to override this functionality then override `I18nMessages.languageResolver` with your implementation. + +```html + + +``` + +Libraries can also ask for specific language: `I18nMessages('recline', recline.View.translations, 'pl')`. Language resolver is not used in that case. + +If you're creating templates using default language other than English (why?) set appropriately appHardcodedLocale: `I18nMessages('recline', recline.View.translations, undefined, 'pl')`. Then missing strings won't be reported in console and underscores in simple translations will be converted to spaces. + +### Adding new language + +Create a copy of `src/i18n/pl.js` and translate all the keys. Long English messages can be found in `en.js`. + +### Overriding defined messages + +If you want to override translations from provided locale do it in a script tag after including recline: + +```html + + +``` \ No newline at end of file diff --git a/make b/make index 324b840a..a01a23e4 100755 --- a/make +++ b/make @@ -7,6 +7,8 @@ def cat(): print("** Combining js files") cmd = 'ls src/*.js | grep -v couchdb | xargs cat > dist/recline.js' os.system(cmd) + cmd = 'ls src/**/*.js | grep -v couchdb | xargs cat >> dist/recline.js' + os.system(cmd) cmd = 'cat src/model.js src/backend.memory.js > dist/recline.dataset.js' os.system(cmd) diff --git a/package.json b/package.json index 7fb7550d..7e6a8058 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,18 @@ { "name": "Max Ogden", "email": "max@maxogden.com" + }, + { + "name": "Krzysztof Madejski", + "email": "krzysztof.madejski@epf.org.pl" } ], "dependencies": { "backbone": ">=0.5", "jquery": ">=1.6", "mustache": ">=0.5.2", - "underscore": ">=1.0" + "underscore": ">=1.0", + "intl-messageformat": "1.3.x" }, "homepage": "http://reclinejs.com/", "keywords": [ diff --git a/src/i18n/en.js b/src/i18n/en.js new file mode 100644 index 00000000..f7c26b88 --- /dev/null +++ b/src/i18n/en.js @@ -0,0 +1,14 @@ +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; +this.recline.View.translations = this.recline.View.translations || {}; + +this.recline.View.translations['en'] = { + 'date_required': "A date is required, check field field-date-format", + 'backend_error': 'There was an error querying the backend', + 'Distance_km': 'Distance (km)', + flot_Group_Column: 'Group Column (Axis 1)', + + map_mapping: 'Coordinates source', + map_mapping_lat_lon: 'Latitude / Longitude fields', + map_mapping_geojson: 'GeoJSON field' +}; \ No newline at end of file diff --git a/src/i18n/pl.js b/src/i18n/pl.js new file mode 100644 index 00000000..74c9fa1c --- /dev/null +++ b/src/i18n/pl.js @@ -0,0 +1,69 @@ +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; +this.recline.View.translations = this.recline.View.translations || {}; + +this.recline.View.translations['pl'] = { + Grid: 'Tabela', + Graph: 'Wykres', + Map: 'Mapa', + Timeline: 'Oś czasu', + Search_data: 'Wyszukaj w danych', + Search: 'Szukaj', + Add: 'Dodaj', + Add_row: 'Dodaj wiersz', + Delete_row: 'Usuń wiersz', + Reorder_row: 'Przesuń wiersz', + Update: 'Zaktualizuj', + Cancel: 'Anuluj', + Updating_row: 'Aktualizuję wiersz', + Row_updated_successfully: 'Wiersz został zaktualizowany', + Error_saving_row: 'Wystąpił błąd aktualizacji wiersza', + Filters: 'Filtry', + Add_filter: 'Dodaj filtr', + Remove_this_filter: 'Usuń filtr', + Fields: 'Kolumny', + Field: 'Kolumna', + Filter_type: 'Typ filtra', + Value: 'Wartość', + Range: 'Zakres', + Geo_distance: 'Odległość', + From: 'Od', + To: 'Do', + Longitude: 'Długość geograficzna', + Latitude: 'Szerokość geograficzna', + Distance_km: 'Odległość (km)', + backend_error: 'Wystąpił błąd połączenia z serwerem', + Unknown: '???', + Edit_this_cell: 'Edytuj komórkę', + date_required: "Data jest wymagana: sprawdź kolumnę field-date-format", + Show_field: 'Pokaż kolumnę', + Force_fit_columns: 'Dopasuj kolumny do zawartości', + Expand_and_collapse: 'Rozwiń i zwiń', + + flot_info: '

Witamy!

\ +

Jakie kolumny powinny zostać narysowane na wykresie?

\ +

Wybierz je używając menu po prawej, a wykres pojawi się automatycznie.

', + Graph_Type: 'Typ wykresu', + Lines_and_Points: 'Linie z punktami', + Lines: 'Linie', + Points: 'Punkty', + Bars: 'Słupki poziome', + Columns: 'Słupki', + flot_Group_Column: 'Kolumna (Oś X)', + Please_choose: 'Proszę wybrać', + Remove: 'Usuń', + Series: 'Seria', + Axis_2: 'Oś Y', + Add_Series: 'Dodaj serię danych', + Save: 'Zapisz', + + map_mapping: 'Źródło koordynatów', + map_mapping_lat_lon: 'Szerokość i długość geo.', + map_mapping_geojson: 'Jedna kolumna typu GeoJSON', + Latitude_field: 'Kolumna szerokości geo. (WGS84)', + Longitude_field: 'Kolumna długości geo. (WGS84)', + Auto_zoom_to_features: 'Kadruj, aby pokazać wszytkie punkty', + Cluster_markers: 'Łącz pobliskie punkty w grupy', + + num_records: '{recordCount} rekordów' +}; \ No newline at end of file diff --git a/src/view.flot.js b/src/view.flot.js index 17037b55..6d468f80 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -27,9 +27,9 @@ my.Flot = Backbone.View.extend({
\
\
\ -

Hey there!

\ + {{#t.flot_info}}

Hey there!

\

There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.

\ -

Please tell us by using the menu on the right and a graph will automatically appear.

\ +

Please tell us by using the menu on the right and a graph will automatically appear.

{{/t.flot_info}} \
\
\
\ @@ -67,7 +67,7 @@ my.Flot = Backbone.View.extend({ render: function() { var self = this; - var tmplData = this.model.toTemplateJSON(); + var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); var htmls = Mustache.render(this.template, tmplData); this.$el.html(htmls); this.$graph = this.$el.find('.panel.graph'); @@ -369,22 +369,22 @@ my.FlotControls = Backbone.View.extend({
\
\
\ - \ + \
\ - \ + \ + \ + \ + \ + \ \
\
\
\ - \ + \
\ - \ + \ {{#fields}} \ \ {{/fields}} \ @@ -395,10 +395,10 @@ my.FlotControls = Backbone.View.extend({
\
\
\ - \ + \
\ \ \ @@ -407,15 +407,10 @@ my.FlotControls = Backbone.View.extend({ templateSeriesEditor: ' \
\
\ - \ -
\ - \
\
\
\ @@ -437,6 +432,7 @@ my.FlotControls = Backbone.View.extend({ render: function() { var self = this; var tmplData = this.model.toTemplateJSON(); + tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); var htmls = Mustache.render(this.template, tmplData); this.$el.html(htmls); @@ -499,6 +495,7 @@ my.FlotControls = Backbone.View.extend({ seriesName: String.fromCharCode(idx + 64 + 1) }, this.model.toTemplateJSON()); + data = I18nMessages('recline', recline.View.translations).injectMustache(data); var htmls = Mustache.render(this.templateSeriesEditor, data); this.$el.find('.editor-series-group').append(htmls); return this; diff --git a/src/view.grid.js b/src/view.grid.js index af3733b0..d2b0792c 100644 --- a/src/view.grid.js +++ b/src/view.grid.js @@ -22,7 +22,7 @@ my.Grid = Backbone.View.extend({ var state = _.extend({ hiddenFields: [] }, modelEtc.state - ); + ); this.state = new recline.Model.ObjectState(state); }, @@ -113,7 +113,10 @@ my.Grid = Backbone.View.extend({ field.set({width: width}); } }); - var htmls = Mustache.render(this.template, this.toTemplateJSON()); + var tmplData = this.toTemplateJSON(); + tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); + + var htmls = Mustache.render(this.template, tmplData); this.$el.html(htmls); this.model.records.forEach(function(doc) { var tr = $(''); @@ -174,7 +177,7 @@ my.GridRow = Backbone.View.extend({ {{#cells}} \ \
\ -   \ +   \
{{{value}}}
\
\ \ @@ -201,7 +204,9 @@ my.GridRow = Backbone.View.extend({ render: function() { this.$el.attr('data-id', this.model.id); - var html = Mustache.render(this.template, this.toTemplateJSON()); + var tmplData = this.toTemplateJSON(); + tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); + var html = Mustache.render(this.template, tmplData); this.$el.html(html); return this; }, @@ -214,8 +219,8 @@ my.GridRow = Backbone.View.extend({ \
\
\ - \ - \ + \ + \
\
\
\ @@ -229,8 +234,10 @@ my.GridRow = Backbone.View.extend({ $(e.target).addClass("hidden"); var cell = $(e.target).siblings('.data-table-cell-value'); cell.data("previousContents", cell.text()); - var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()}); - cell.html(templated); + + var tmplData = I18nMessages('recline', recline.View.translations).injectMustache({value: cell.text()}); + var output = Mustache.render(this.cellEditorTemplate, tmplData); + cell.html(output); }, onEditorOK: function(e) { @@ -242,13 +249,15 @@ my.GridRow = Backbone.View.extend({ var newData = {}; newData[field] = newValue; this.model.set(newData); - this.trigger('recline:flash', {message: "Updating row...", loader: true}); + + var fmt = I18nMessages('recline', recline.View.translations); + this.trigger('recline:flash', {message: fmt.t("Updating_row") + "...", loader: true}); this.model.save().then(function(response) { - this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'}); + this.trigger('recline:flash', {message: fmt.t("Row_updated_successfully"), category: 'success'}); }) .fail(function() { this.trigger('recline:flash', { - message: 'Error saving row', + message: fmt.t('Error_saving_row'), category: 'error', persist: true }); diff --git a/src/view.map.js b/src/view.map.js index 4af3b54f..75d1576f 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -174,7 +174,8 @@ my.Map = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); + var htmls = Mustache.render(this.template, tmplData); this.$el.html(htmls); this.$map = this.$el.find('.panel.map'); this.redraw(); @@ -513,15 +514,16 @@ my.MapMenu = Backbone.View.extend({
\
\
\ + {{t.map_mapping}}: \ \ + {{t.map_mapping_lat_lon}} \ \ + {{t.map_mapping_geojson}} \
\
\ - \ + \
\ \
\ - \ + \
\ \ \ @@ -553,15 +555,15 @@ my.MapMenu = Backbone.View.extend({
\
\
\ - \ + \
\
\ \ + {{t.Auto_zoom_to_features}} \ \ + {{t.Cluster_markers}} \
\ \ \ @@ -589,7 +591,8 @@ my.MapMenu = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); + var htmls = Mustache.render(this.template, tmplData); this.$el.html(htmls); if (this._geomReady() && this.model.fields.length){ diff --git a/src/view.multiview.js b/src/view.multiview.js index bb8f0710..1a263714 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -108,7 +108,7 @@ my.MultiView = Backbone.View.extend({
\ \
\ - {{recordCount}} records\ + {{#t.num_records}}{recordCount} {recordCount, plural, =1{record} other{records}}{{/t.num_records}}\
\