diff --git a/.gitignore b/.gitignore index dcfe1c355d..b99c9f1145 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,5 @@ npm-debug.log # test reports created by karma /karma-reports -# ignore auto-generated component index +/.idea /src/component-index.js diff --git a/.travis.yml b/.travis.yml index 9a8f804644..a405b9ef35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,6 @@ install: - npm install - (cd node_modules/matrix-js-sdk && npm install) script: - - npm run test - - ./.travis-test-riot.sh + # don't run the riot tests unless the react-sdk tests pass, otherwise + # the output is confusing. + - npm run test && ./.travis-test-riot.sh diff --git a/README.md b/README.md index 3627225299..0f5ef73365 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should be considered as a single project (for instance, matrix-react-sdk bugs are currently filed against vector-im/riot-web rather than this project). +Translation Status +================== +[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget) + Developer Guide =============== @@ -190,4 +194,3 @@ Alternative instructions: * Create an index.html file pulling in your compiled javascript and the CSS bundle from the skin you use. For now, you'll also need to manually import CSS from any skins that your skin inherts from. - diff --git a/karma.conf.js b/karma.conf.js index 3495a981be..d544248332 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -55,11 +55,18 @@ module.exports = function (config) { // some images to reduce noise from the tests {pattern: 'test/img/*', watched: false, included: false, served: true, nocache: false}, + // translation files + {pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true}, + {pattern: 'test/i18n/*', watched: false, included: false, served: true}, ], - // redirect img links to the karma server proxies: { + // redirect img links to the karma server "/img/": "/base/test/img/", + // special languages.json file for the tests + "/i18n/languages.json": "/base/test/i18n/languages.json", + // and redirect i18n requests + "/i18n/": "/base/src/i18n/strings/", }, // list of files to exclude @@ -166,11 +173,15 @@ module.exports = function (config) { 'sinon': 'sinon/pkg/sinon.js', }, root: [ - path.resolve('./src'), path.resolve('./test'), ], }, devtool: 'inline-source-map', + externals: { + // Don't try to bundle electron: leave it as a commonjs dependency + // (the 'commonjs' here means it will output a 'require') + "electron": "commonjs electron", + }, }, webpackMiddleware: { diff --git a/package.json b/package.json index 059fdd390f..a6076a56d2 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "browser-request": "^0.3.3", "classnames": "^2.1.2", "commonmark": "^0.27.0", + "counterpart": "^0.18.0", "draft-js": "^0.8.1", "draft-js-export-html": "^0.5.0", "draft-js-export-markdown": "^0.2.0", @@ -63,7 +64,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.8", + "matrix-js-sdk": "0.7.9", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", diff --git a/scripts/check-i18n.pl b/scripts/check-i18n.pl new file mode 100755 index 0000000000..fa11bc5292 --- /dev/null +++ b/scripts/check-i18n.pl @@ -0,0 +1,192 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Cwd 'abs_path'; + +# script which checks how out of sync the i18ns are drifting + +# example i18n format: +# "%(oneUser)sleft": "%(oneUser)sleft", + +$|=1; + +$0 =~ /^(.*\/)/; +my $i18ndir = abs_path($1."/../src/i18n/strings"); +my $srcdir = abs_path($1."/../src"); + +my $en = read_i18n($i18ndir."/en_EN.json"); + +my $src_strings = read_src_strings($srcdir); +my $src = {}; + +print "Checking strings in src\n"; +foreach my $tuple (@$src_strings) { + my ($s, $file) = (@$tuple); + $src->{$s} = $file; + if (!$en->{$s}) { + if ($en->{$s . '.'}) { + printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s); + } + else { + $s =~ /^(.*)\.?$/; + if ($en->{$1}) { + printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s); + } + else { + printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s); + } + } + } +} + +print "\nChecking en_EN\n"; +my $count = 0; +my $remaining_src = {}; +foreach (keys %$src) { $remaining_src->{$_}++ }; + +foreach my $k (sort keys %$en) { + # crappy heuristic to ignore country codes for now... + next if ($k =~ /^(..|..-..)$/); + + if ($en->{$k} ne $k) { + printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k); + } + + if (!$src->{$k}) { + if ($src->{$k. '.'}) { + printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k); + } + else { + $k =~ /^(.*)\.?$/; + if ($src->{$1}) { + printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k); + } + else { + printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k); + } + } + } + else { + $count++; + delete $remaining_src->{$k}; + } +} +printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n"); +foreach (keys %$remaining_src) { + print "missing: $_\n"; +} + +opendir(DIR, $i18ndir) || die $!; +my @files = readdir(DIR); +closedir(DIR); +foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) { + print "\nChecking $lang\n"; + + my $map = read_i18n($i18ndir."/".$lang); + my $count = 0; + + my $remaining_en = {}; + foreach (keys %$en) { $remaining_en->{$_}++ }; + + foreach my $k (sort keys %$map) { + { + no warnings 'uninitialized'; + my $vars = {}; + while ($k =~ /%\((.*?)\)s/g) { + $vars->{$1}++; + } + while ($map->{$k} =~ /%\((.*?)\)s/g) { + $vars->{$1}--; + } + foreach my $var (keys %$vars) { + if ($vars->{$var} != 0) { + printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k); + } + } + } + + if ($en->{$k}) { + if ($map->{$k} eq $k) { + printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k); + } + $count++; + delete $remaining_en->{$k}; + } + else { + if ($en->{$k . "."}) { + printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k); + next; + } + + $k =~ /^(.*)\.?$/; + if ($en->{$1}) { + printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k); + next; + } + + printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k); + } + } + + if (scalar keys %$remaining_en < 100) { + foreach (keys %$remaining_en) { + printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_); + } + } + + printf ("$count/" . (scalar keys %$en) . " strings translated\n"); +} + +sub read_i18n { + my $path = shift; + my $map = {}; + $path =~ /.*\/(.*)$/; + my $lang = $1; + + open(FILE, "<", $path) || die $!; + while() { + if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) { + my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5); + $src =~ s/\\"/"/g; + $dst =~ s/\\"/"/g; + + if ($map->{$src}) { + printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src); + } + $map->{$src} = $dst; + } + } + close(FILE); + + return $map; +} + +sub read_src_strings { + my $path = shift; + + use File::Find; + use File::Slurp; + + my $strings = []; + + my @files; + find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path ); + foreach my $file (@files) { + my $src = read_file($file); + $src =~ s/'\s*\+\s*'//g; + $src =~ s/"\s*\+\s*"//g; + + $file =~ s/^.*\/src/src/; + while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) { + my $s = $1; + $s =~ s/\\'/'/g; + push @$strings, [$s, $file]; + } + while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) { + push @$strings, [$1, $file]; + } + } + + return $strings; +} \ No newline at end of file diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl new file mode 100755 index 0000000000..247b2b663f --- /dev/null +++ b/scripts/fix-i18n.pl @@ -0,0 +1,104 @@ +#!/usr/bin/perl -ni + +use strict; +use warnings; + +# script which synchronises i18n strings to include punctuation. +# i've cherry-picked ones which seem to have diverged between the different translations +# from TextForEvent, causing missing events all over the place + +BEGIN { +$::fixups = [split(/\n/, < + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { getCurrentLanguage } from './languageHandler'; +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import SdkConfig from './SdkConfig'; + +function redact(str) { + return str.replace(/#\/(room|user)\/(.+)/, "#/$1/"); +} + +const customVariables = { + 'App Platform': 1, + 'App Version': 2, + 'User Type': 3, + 'Chosen Language': 4, +}; + + +class Analytics { + constructor() { + this._paq = null; + this.disabled = true; + this.firstPage = true; + } + + /** + * Enable Analytics if initialized but disabled + * otherwise try and initalize, no-op if piwik config missing + */ + enable() { + if (this._paq || this._init()) { + this.disabled = false; + } + } + + /** + * Disable Analytics calls, will not fully unload Piwik until a refresh, + * but this is second best, Piwik should not pull anything implicitly. + */ + disable() { + this.disabled = true; + } + + _init() { + const config = SdkConfig.get(); + if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return; + + const url = config.piwik.url; + const siteId = config.piwik.siteId; + const self = this; + + window._paq = this._paq = window._paq || []; + + this._paq.push(['setTrackerUrl', url+'piwik.php']); + this._paq.push(['setSiteId', siteId]); + + this._paq.push(['trackAllContentImpressions']); + this._paq.push(['discardHashTag', false]); + this._paq.push(['enableHeartBeatTimer']); + this._paq.push(['enableLinkTracking', true]); + + const platform = PlatformPeg.get(); + this._setVisitVariable('App Platform', platform.getHumanReadableName()); + platform.getAppVersion().then((version) => { + this._setVisitVariable('App Version', version); + }).catch(() => { + this._setVisitVariable('App Version', 'unknown'); + }); + + this._setVisitVariable('Chosen Language', getCurrentLanguage()); + + (function() { + const g = document.createElement('script'); + const s = document.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js'; + + g.onload = function() { + console.log('Initialised anonymous analytics'); + self._paq = window._paq; + }; + + s.parentNode.insertBefore(g, s); + })(); + + return true; + } + + trackPageChange() { + if (this.disabled) return; + if (this.firstPage) { + // De-duplicate first page + // router seems to hit the fn twice + this.firstPage = false; + return; + } + this._paq.push(['setCustomUrl', redact(window.location.href)]); + this._paq.push(['trackPageView']); + } + + trackEvent(category, action, name) { + if (this.disabled) return; + this._paq.push(['trackEvent', category, action, name]); + } + + logout() { + if (this.disabled) return; + this._paq.push(['deleteCookies']); + } + + login() { // not used currently + const cli = MatrixClientPeg.get(); + if (this.disabled || !cli) return; + + this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]); + } + + _setVisitVariable(key, value) { + this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']); + } + + setGuest(guest) { + if (this.disabled) return; + this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In'); + } +} + +if (!global.mxAnalytics) { + global.mxAnalytics = new Analytics(); +} +module.exports = global.mxAnalytics; diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 6eed22f436..d0d8e0c74e 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -29,6 +29,11 @@ export default class BasePlatform { this.errorDidOccur = false; } + // Used primarily for Analytics + getHumanReadableName(): string { + return 'Base Platform'; + } + setNotificationCount(count: number) { this.notificationCount = count; } @@ -66,11 +71,14 @@ export default class BasePlatform { displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { } + loudNotification(ev: Event, room: Object) { + } + /** * Returns a promise that resolves to a string representing * the current version of the application. */ - getAppVersion() { + getAppVersion(): Promise { throw new Error("getAppVersion not implemented!"); } @@ -79,10 +87,12 @@ export default class BasePlatform { * with getUserMedia, return a string explaining why not. * Otherwise, return null. */ - screenCaptureErrorString() { + screenCaptureErrorString(): string { return "Not implemented"; } + isElectron(): boolean { return false; } + /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/CallHandler.js b/src/CallHandler.js index 5199ef0a67..b2ccf65df7 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -55,6 +55,7 @@ var MatrixClientPeg = require('./MatrixClientPeg'); var PlatformPeg = require("./PlatformPeg"); var Modal = require('./Modal'); var sdk = require('./index'); +import { _t } from './languageHandler'; var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); @@ -142,8 +143,8 @@ function _setCallListeners(call) { play("busyAudio"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Call Timeout", - description: "The remote side failed to pick up." + title: _t('Call Timeout'), + description: _t('The remote side failed to pick up') + '.', }); } else if (oldState === "invite_sent") { @@ -179,7 +180,8 @@ function _setCallState(call, roomId, status) { } dis.dispatch({ action: 'call_state', - room_id: roomId + room_id: roomId, + state: status, }); } @@ -203,8 +205,8 @@ function _onAction(payload) { console.log("Can't capture screen: " + screenCapErrorString); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Unable to capture screen", - description: screenCapErrorString + title: _t('Unable to capture screen'), + description: screenCapErrorString, }); return; } @@ -223,8 +225,8 @@ function _onAction(payload) { if (module.exports.getAnyActiveCall()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Existing Call", - description: "You are already in a call." + title: _t('Existing Call'), + description: _t('You are already in a call.'), }); return; // don't allow >1 call to be placed. } @@ -233,8 +235,8 @@ function _onAction(payload) { if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "VoIP is unsupported", - description: "You cannot place VoIP calls in this browser." + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), }); return; } @@ -249,7 +251,7 @@ function _onAction(payload) { if (members.length <= 1) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "You cannot place a call with yourself." + description: _t('You cannot place a call with yourself.'), }); return; } @@ -275,14 +277,14 @@ function _onAction(payload) { if (!ConferenceHandler) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "Conference calls are not supported in this client" + description: _t('Conference calls are not supported in this client'), }); } else if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "VoIP is unsupported", - description: "You cannot place VoIP calls in this browser." + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), }); } else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { @@ -294,14 +296,14 @@ function _onAction(payload) { // Therefore we disable conference calling in E2E rooms. const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "Conference calls are not supported in encrypted rooms", + description: _t('Conference calls are not supported in encrypted rooms'), }); } else { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { - title: "Warning!", - description: "Conference calling is in development and may not be reliable.", + title: _t('Warning!'), + description: _t('Conference calling is in development and may not be reliable.'), onFinished: confirm=>{ if (confirm) { ConferenceHandler.createNewMatrixCall( @@ -312,8 +314,8 @@ function _onAction(payload) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { - title: "Failed to set up conference call", - description: "Conference call failed. " + ((err && err.message) ? err.message : ""), + title: _t('Failed to set up conference call'), + description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''), }); }); } diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 4ab982c98f..315c312b9f 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -21,6 +21,7 @@ var extend = require('./extend'); var dis = require('./dispatcher'); var MatrixClientPeg = require('./MatrixClientPeg'); var sdk = require('./index'); +import { _t } from './languageHandler'; var Modal = require('./Modal'); var encrypt = require("browser-encrypt-attachment"); @@ -347,14 +348,14 @@ class ContentMessages { }, function(err) { error = err; if (!upload.canceled) { - var desc = "The file '"+upload.fileName+"' failed to upload."; + var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.'; if (err.http_status == 413) { - desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads"; + desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName}); } var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Upload Failed", - description: desc + title: _t('Upload Failed'), + description: desc, }); } }).finally(() => { diff --git a/src/DateUtils.js b/src/DateUtils.js index c58c09d4de..0bce7c8a16 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,40 +16,89 @@ limitations under the License. */ 'use strict'; +import { _t } from './languageHandler'; -var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function getDaysArray() { + return [ + _t('Sun'), + _t('Mon'), + _t('Tue'), + _t('Wed'), + _t('Thu'), + _t('Fri'), + _t('Sat'), + ]; +} + +function getMonthsArray() { + return [ + _t('Jan'), + _t('Feb'), + _t('Mar'), + _t('Apr'), + _t('May'), + _t('Jun'), + _t('Jul'), + _t('Aug'), + _t('Sep'), + _t('Oct'), + _t('Nov'), + _t('Dec'), + ]; +} function pad(n) { return (n < 10 ? '0' : '') + n; } +function twelveHourTime(date) { + let hours = date.getHours() % 12; + const minutes = pad(date.getMinutes()); + const ampm = date.getHours() >= 12 ? 'PM' : 'AM'; + hours = pad(hours ? hours : 12); + return `${hours}:${minutes}${ampm}`; +} + module.exports = { formatDate: function(date) { - // date.toLocaleTimeString is completely system dependent. - // just go 24h for now - var now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { - return pad(date.getHours()) + ':' + pad(date.getMinutes()); + return this.formatTime(date); } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date)}); } else if (now.getFullYear() === date.getFullYear()) { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); - } - else { - return this.formatFullDate(date); + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + time: this.formatTime(date), + }); } + return this.formatFullDate(date); }, - formatFullDate: function(date) { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + formatFullDate: function(date, showTwelveHour=false) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date), + }); }, - formatTime: function(date) { + formatTime: function(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date); + } return pad(date.getHours()) + ':' + pad(date.getMinutes()); - } + }, }; - diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 4acb314c2f..8af1894c79 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -360,7 +360,7 @@ export function bodyToHtml(content, highlights, opts) { 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtml, }); - return ; + return ; } export function emojifyText(text) { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f34aeae0e5..a3bec14492 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; @@ -27,6 +28,7 @@ import DMRoomMap from './utils/DMRoomMap'; import RtsClient from './RtsClient'; import Modal from './Modal'; import sdk from './index'; +import { _t } from './languageHandler'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -229,14 +231,16 @@ function _handleRestoreFailure(e) { let msg = e.message; if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { - msg = "You need to log back in to generate end-to-end encryption keys " - + "for this device and submit the public key to your homeserver. " - + "This is a once off; sorry for the inconvenience."; + msg = _t( + 'You need to log back in to generate end-to-end encryption keys' + + ' for this device and submit the public key to your homeserver.' + + ' This is a once off; sorry for the inconvenience.', + ); - _clearLocalStorage(); + _clearStorage(); return q.reject(new Error( - "Unable to restore previous session: " + msg, + _t('Unable to restore previous session') + ': ' + msg, )); } @@ -254,7 +258,7 @@ function _handleRestoreFailure(e) { return def.promise.then((success) => { if (success) { // user clicked continue. - _clearLocalStorage(); + _clearStorage(); return false; } @@ -275,6 +279,8 @@ export function initRtsClient(url) { export function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); + Analytics.setGuest(credentials.guest); + console.log( "setLoggedIn: mxid:", credentials.userId, "deviceId:", credentials.deviceId, @@ -326,6 +332,10 @@ export function setLoggedIn(credentials) { } // stop any running clients before we create a new one with these new credentials + // + // XXX: why do we have any running clients here? Maybe on sign-in after + // initial use as a guest? but what about our persistent storage? we need to + // be careful not to leak e2e data created as one user into another session. stopMatrixClient(); MatrixClientPeg.replaceUsingCreds(credentials); @@ -396,12 +406,19 @@ export function startMatrixClient() { * a session has been logged out / ended. */ export function onLoggedOut() { - _clearLocalStorage(); - stopMatrixClient(); + stopMatrixClient(true); dis.dispatch({action: 'on_logged_out'}); } -function _clearLocalStorage() { +function _clearStorage() { + Analytics.logout(); + + const cli = MatrixClientPeg.get(); + if (cli) { + // TODO: *really* ought to wait for the promise to complete + cli.clearStores().done(); + } + if (!window.localStorage) { return; } @@ -418,9 +435,13 @@ function _clearLocalStorage() { } /** - * Stop all the background processes related to the current client + * Stop all the background processes related to the current client. + * + * Optionally clears persistent stores. + * + * @param {boolean} clearStores true to clear the persistent stores. */ -export function stopMatrixClient() { +export function stopMatrixClient(clearStores) { Notifier.stop(); UserActivity.stop(); Presence.stop(); @@ -429,7 +450,13 @@ export function stopMatrixClient() { if (cli) { cli.stopClient(); cli.removeAllListeners(); - cli.store.deleteAllData(); - MatrixClientPeg.unset(); } + + if (clearStores) { + // note that we have to do this *after* stopping the client, but + // *before* clearing the MatrixClientPeg. + _clearStorage(); + } + + MatrixClientPeg.unset(); } diff --git a/src/Login.js b/src/Login.js index 107a8825e9..87731744e9 100644 --- a/src/Login.js +++ b/src/Login.js @@ -16,6 +16,7 @@ limitations under the License. */ import Matrix from "matrix-js-sdk"; +import { _t } from "./languageHandler"; import q from 'q'; import url from 'url'; @@ -97,9 +98,9 @@ export default class Login { }; }, (error) => { if (error.httpStatus === 403) { - error.friendlyText = "Guest access is disabled on this Home Server."; + error.friendlyText = _t("Guest access is disabled on this Home Server."); } else { - error.friendlyText = "Failed to register as guest: " + error.data; + error.friendlyText = _t("Failed to register as guest:") + ' ' + error.data; } throw error; }); @@ -158,12 +159,12 @@ export default class Login { }, function(error) { if (error.httpStatus == 400 && loginParams.medium) { error.friendlyText = ( - 'This Home Server does not support login using email address.' + _t('This Home Server does not support login using email address.') ); } else if (error.httpStatus === 403) { error.friendlyText = ( - 'Incorrect username and/or password.' + _t('Incorrect username and/or password.') ); if (self._fallbackHsUrl) { var fbClient = Matrix.createClient({ @@ -187,7 +188,7 @@ export default class Login { } else { error.friendlyText = ( - 'There was a problem logging in. (HTTP ' + error.httpStatus + ")" + _t("There was a problem logging in.") + ' (HTTP ' + error.httpStatus + ")" ); } throw error; diff --git a/src/Modal.js b/src/Modal.js index 7be37da92e..8d53b2da7d 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var ReactDOM = require('react-dom'); +import Analytics from './Analytics'; import sdk from './index'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -104,6 +105,9 @@ class ModalManager { } createDialog(Element, props, className) { + if (props && props.title) { + Analytics.trackEvent('Modal', props.title, 'createDialog'); + } return this.createDialogAsync((cb) => {cb(Element);}, props, className); } diff --git a/src/Notifier.js b/src/Notifier.js index 6473ab4d9c..40a65d4106 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -18,9 +18,11 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import TextForEvent from './TextForEvent'; +import Analytics from './Analytics'; import Avatar from './Avatar'; import dis from './dispatcher'; import sdk from './index'; +import { _t } from './languageHandler'; import Modal from './Modal'; /* @@ -120,6 +122,9 @@ const Notifier = { setEnabled: function(enable, callback) { const plaf = PlatformPeg.get(); if (!plaf) return; + + Analytics.trackEvent('Notifier', 'Set Enabled', enable); + // make sure that we persist the current setting audio_enabled setting // before changing anything if (global.localStorage) { @@ -134,13 +139,11 @@ const Notifier = { if (result !== 'granted') { // The permission request was dismissed or denied const description = result === 'denied' - ? 'Riot does not have permission to send you notifications' - + ' - please check your browser settings' - : 'Riot was not given permission to send notifications' - + ' - please try again'; + ? _t('Riot does not have permission to send you notifications - please check your browser settings') + : _t('Riot was not given permission to send notifications - please try again'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createDialog(ErrorDialog, { - title: 'Unable to enable Notifications', + title: _t('Unable to enable Notifications'), description, }); return; @@ -200,6 +203,8 @@ const Notifier = { setToolbarHidden: function(hidden, persistent = true) { this.toolbarHidden = hidden; + Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); + // XXX: why are we dispatching this here? // this is nothing to do with notifier_enabled dis.dispatch({ @@ -245,6 +250,7 @@ const Notifier = { this._displayPopupNotification(ev, room); } if (actions.tweaks.sound && this.isAudioEnabled()) { + PlatformPeg.get().loudNotification(ev, room); this._playAudioNotification(ev, room); } } diff --git a/src/PasswordReset.js b/src/PasswordReset.js index a03a565459..0739ca0a24 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -15,6 +15,7 @@ limitations under the License. */ var Matrix = require("matrix-js-sdk"); +import { _t } from './languageHandler'; /** * Allows a user to reset their password on a homeserver. @@ -53,7 +54,7 @@ class PasswordReset { return res; }, function(err) { if (err.errcode == 'M_THREEPID_NOT_FOUND') { - err.message = "This email address was not found"; + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -78,10 +79,10 @@ class PasswordReset { } }, this.password).catch(function(err) { if (err.httpStatus === 401) { - err.message = "Failed to verify email address: make sure you clicked the link in the email"; + err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); } else if (err.httpStatus === 404) { - err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; + err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.'); } else if (err.httpStatus) { err.message += ` (Status ${err.httpStatus})`; diff --git a/src/Roles.js b/src/Roles.js index cef8670aad..8c1f711bbe 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -13,14 +13,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -export const LEVEL_ROLE_MAP = { - undefined: 'Default', - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import { _t } from './languageHandler'; + +export function levelRoleMap() { + return { + undefined: _t('Default'), + 0: _t('User'), + 50: _t('Moderator'), + 100: _t('Admin'), + }; +} export function textualPowerLevel(level, userDefault) { + const LEVEL_ROLE_MAP = this.levelRoleMap(); if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); } else { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index dbb7e405df..8c591f7cb2 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -125,6 +125,7 @@ const SdkConfig = require('./SdkConfig'); const MatrixClientPeg = require("./MatrixClientPeg"); const MatrixEvent = require("matrix-js-sdk").MatrixEvent; const dis = require("./dispatcher"); +import { _t } from './languageHandler'; function sendResponse(event, res) { const data = JSON.parse(JSON.stringify(event.data)); @@ -150,7 +151,7 @@ function inviteUser(event, roomId, userId) { console.log(`Received request to invite ${userId} into room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } const room = client.getRoom(roomId); @@ -170,7 +171,7 @@ function inviteUser(event, roomId, userId) { success: true, }); }, function(err) { - sendError(event, "You need to be able to invite users to do that.", err); + sendError(event, _t('You need to be able to invite users to do that.'), err); }); } @@ -181,7 +182,7 @@ function setPlumbingState(event, roomId, status) { console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => { @@ -189,7 +190,7 @@ function setPlumbingState(event, roomId, status) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); } @@ -197,7 +198,7 @@ function setBotOptions(event, roomId, userId) { console.log(`Received request to set options for bot ${userId} in room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { @@ -205,20 +206,20 @@ function setBotOptions(event, roomId, userId) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); } function setBotPower(event, roomId, userId, level) { if (!(Number.isInteger(level) && level >= 0)) { - sendError(event, "Power level must be positive integer."); + sendError(event, _t('Power level must be positive integer.')); return; } console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } @@ -235,7 +236,7 @@ function setBotPower(event, roomId, userId, level) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); }); } @@ -258,12 +259,12 @@ function botOptions(event, roomId, userId) { function returnStateEvent(event, roomId, eventType, stateKey) { const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } const room = client.getRoom(roomId); if (!room) { - sendError(event, "This room is not recognised."); + sendError(event, _t('This room is not recognised.')); return; } const stateEvent = room.currentState.getStateEvents(eventType, stateKey); @@ -313,13 +314,13 @@ const onMessage = function(event) { const roomId = event.data.room_id; const userId = event.data.user_id; if (!roomId) { - sendError(event, "Missing room_id in request"); + sendError(event, _t('Missing room_id in request')); return; } let promise = Promise.resolve(currentRoomId); if (!currentRoomId) { if (!currentRoomAlias) { - sendError(event, "Must be viewing a room"); + sendError(event, _t('Must be viewing a room')); return; } // no room ID but there is an alias, look it up. @@ -331,7 +332,7 @@ const onMessage = function(event) { promise.then((viewingRoomId) => { if (roomId !== viewingRoomId) { - sendError(event, "Room " + roomId + " not visible"); + sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId})); return; } @@ -345,7 +346,7 @@ const onMessage = function(event) { } if (!userId) { - sendError(event, "Missing user_id in request"); + sendError(event, _t('Missing user_id in request')); return; } switch (event.data.action) { @@ -370,7 +371,7 @@ const onMessage = function(event) { } }, (err) => { console.error(err); - sendError(event, "Failed to lookup current room."); + sendError(event, _t('Failed to lookup current room') + '.'); }); }; diff --git a/src/Skinner.js b/src/Skinner.js index 4482f2239c..0688c9fc26 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -23,22 +23,28 @@ class Skinner { if (this.components === null) { throw new Error( "Attempted to get a component before a skin has been loaded."+ - "This is probably because either:"+ + " This is probably because either:"+ " a) Your app has not called sdk.loadSkin(), or"+ - " b) A component has called getComponent at the root level" + " b) A component has called getComponent at the root level", ); } - var comp = this.components[name]; - if (comp) { - return comp; - } + let comp = this.components[name]; // XXX: Temporarily also try 'views.' as we're currently // leaving the 'views.' off views. - var comp = this.components['views.'+name]; - if (comp) { - return comp; + if (!comp) { + comp = this.components['views.'+name]; } - throw new Error("No such component: "+name); + + if (!comp) { + throw new Error("No such component: "+name); + } + + // components have to be functions. + const validType = typeof comp === 'function'; + if (!validType) { + throw new Error(`Not a valid component: ${name}.`); + } + return comp; } load(skinObject) { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index bd68f1a6fe..185ea504ac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -18,6 +18,7 @@ import MatrixClientPeg from "./MatrixClientPeg"; import dis from "./dispatcher"; import Tinter from "./Tinter"; import sdk from './index'; +import { _t } from './languageHandler'; import Modal from './Modal'; @@ -41,7 +42,7 @@ class Command { } getUsage() { - return "Usage: " + this.getCommandWithArgs(); + return _t('Usage') + ': ' + this.getCommandWithArgs(); } } @@ -68,8 +69,8 @@ const commands = { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createDialog(ErrorDialog, { - title: "/ddg is not a command", - description: "To use it, just wait for autocomplete results to load and tab through them.", + title: _t('/ddg is not a command'), + description: _t('To use it, just wait for autocomplete results to load and tab through them.'), }); return success(); }), @@ -143,7 +144,7 @@ const commands = { dis.dispatch({ action: 'view_room', - roomAlias: roomAlias, + room_alias: roomAlias, auto_join: true, }); @@ -185,7 +186,7 @@ const commands = { if (targetRoomId) { break; } } if (!targetRoomId) { - return reject("Unrecognised room alias: " + roomAlias); + return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); } } } @@ -302,14 +303,14 @@ const commands = { const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId); if (!device) { - return reject(`Unknown (user, device) pair: (${userId}, ${deviceId})`); + return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`); } if (device.isVerified()) { if (device.getFingerprint() === fingerprint) { - return reject(`Device already verified!`); + return reject(_t(`Device already verified!`)); } else { - return reject(`WARNING: Device already verified, but keys do NOT MATCH!`); + return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`)); } } @@ -321,12 +322,15 @@ const commands = { // Tell the user we verified everything! const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { - title: "Verified key", + title: _t("Verified key"), description: (

- The signing key you provided matches the signing key you received - from { userId }'s device { deviceId }. Device marked as verified. + { + _t("The signing key you provided matches the signing key you received " + + "from %(userId)s's device %(deviceId)s. Device marked as verified.", + {userId: userId, deviceId: deviceId}) + }

), @@ -335,9 +339,13 @@ const commands = { return success(); } else { - return reject(`WARNING: KEY VERIFICATION FAILED! The signing key for ${userId} and device - ${deviceId} is "${device.getFingerprint()}" which does not match the provided key - "${fingerprint}". This could mean your communications are being intercepted!`); + const fprint = device.getFingerprint(); + return reject( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key' + + ' "%(fingerprint)s". This could mean your communications are being intercepted!', + {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}) + ); } } } @@ -382,7 +390,7 @@ module.exports = { if (commands[cmd]) { return commands[cmd].run(roomId, args); } else { - return reject("Unrecognised command: " + input); + return reject(_t("Unrecognised command:") + ' ' + input); } } return null; // not a command diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3f200a089d..fa78f9d61b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -16,7 +16,7 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); - +import { _t } from './languageHandler'; import * as Roles from './Roles'; function textForMemberEvent(ev) { @@ -25,45 +25,45 @@ function textForMemberEvent(ev) { var targetName = ev.target ? ev.target.name : ev.getStateKey(); var ConferenceHandler = CallHandler.getConferenceHandler(); var reason = ev.getContent().reason ? ( - " Reason: " + ev.getContent().reason + _t('Reason') + ': ' + ev.getContent().reason ) : ""; switch (ev.getContent().membership) { case 'invite': var threePidContent = ev.getContent().third_party_invite; if (threePidContent) { if (threePidContent.display_name) { - return targetName + " accepted the invitation for " + - threePidContent.display_name + "."; + return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name}); } else { - return targetName + " accepted an invitation."; + return _t('%(targetName)s accepted an invitation.', {targetName: targetName}); } } else { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return senderName + " requested a VoIP conference"; + return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName}); } else { - return senderName + " invited " + targetName + "."; + return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName}); } } case 'ban': - return senderName + " banned " + targetName + "." + reason; + return _t( + '%(senderName)s banned %(targetName)s.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; case 'join': if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { - return ev.getSender() + " changed their display name from " + - ev.getPrevContent().displayname + " to " + - ev.getContent().displayname; + return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname}); } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { - return ev.getSender() + " set their display name to " + ev.getContent().displayname; + return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname}); } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { - return ev.getSender() + " removed their display name (" + ev.getPrevContent().displayname + ")"; + return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname}); } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { - return senderName + " removed their profile picture"; + return _t('%(senderName)s removed their profile picture.', {senderName: senderName}); } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { - return senderName + " changed their profile picture"; + return _t('%(senderName)s changed their profile picture.', {senderName: senderName}); } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { - return senderName + " set a profile picture"; + return _t('%(senderName)s set a profile picture.', {senderName: senderName}); } else { // suppress null rejoins return ''; @@ -71,49 +71,57 @@ function textForMemberEvent(ev) { } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return "VoIP conference started"; + return _t('VoIP conference started.'); } else { - return targetName + " joined the room."; + return _t('%(targetName)s joined the room.', {targetName: targetName}); } } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return "VoIP conference finished"; + return _t('VoIP conference finished.'); } else if (ev.getPrevContent().membership === "invite") { - return targetName + " rejected the invitation."; + return _t('%(targetName)s rejected the invitation.', {targetName: targetName}); } else { - return targetName + " left the room."; + return _t('%(targetName)s left the room.', {targetName: targetName}); } } else if (ev.getPrevContent().membership === "ban") { - return senderName + " unbanned " + targetName + "."; + return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName}); } else if (ev.getPrevContent().membership === "join") { - return senderName + " kicked " + targetName + "." + reason; + return _t( + '%(senderName)s kicked %(targetName)s.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; } else if (ev.getPrevContent().membership === "invite") { - return senderName + " withdrew " + targetName + "'s invitation." + reason; + return _t( + '%(senderName)s withdrew %(targetName)s\'s invitation.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; } else { - return targetName + " left the room."; + return _t('%(targetName)s left the room.', {targetName: targetName}); } } } function textForTopicEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - - return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"'; + return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic}); } function textForRoomNameEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - - return senderDisplayName + ' changed the room name to "' + ev.getContent().name + '"'; + + if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { + return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName}); + } + return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name}); } function textForMessageEvent(ev) { @@ -122,66 +130,66 @@ function textForMessageEvent(ev) { if (ev.getContent().msgtype === "m.emote") { message = "* " + senderDisplayName + " " + message; } else if (ev.getContent().msgtype === "m.image") { - message = senderDisplayName + " sent an image."; + message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName}); } return message; } function textForCallAnswerEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " answered the call." + supported; + var senderName = event.sender ? event.sender.name : _t('Someone'); + var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported; } function textForCallHangupEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " ended the call." + supported; + var senderName = event.sender ? event.sender.name : _t('Someone'); + var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s ended the call.', {senderName: senderName}) + ' ' + supported; } function textForCallInviteEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; + var senderName = event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? var type = "voice"; if (event.getContent().offer && event.getContent().offer.sdp && event.getContent().offer.sdp.indexOf('m=video') !== -1) { type = "video"; } - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " placed a " + type + " call." + supported; + var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported; } function textForThreePidInviteEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); - return senderName + " sent an invitation to " + event.getContent().display_name + - " to join the room."; + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name}); } function textForHistoryVisibilityEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); var vis = event.getContent().history_visibility; - var text = senderName + " made future room history visible to "; + // XXX: This i18n just isn't going to work for languages with different sentence structure. + var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' '; if (vis === "invited") { - text += "all room members, from the point they are invited."; + text += _t('all room members, from the point they are invited') + '.'; } else if (vis === "joined") { - text += "all room members, from the point they joined."; + text += _t('all room members, from the point they joined') + '.'; } else if (vis === "shared") { - text += "all room members."; + text += _t('all room members') + '.'; } else if (vis === "world_readable") { - text += "anyone."; + text += _t('anyone') + '.'; } else { - text += " unknown (" + vis + ")"; + text += ' ' + _t('unknown') + ' (' + vis + ').'; } return text; } function textForEncryptionEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); - return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; + return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm}); } // Currently will only display a change if a user's power level is changed @@ -204,6 +212,7 @@ function textForPowerEvent(event) { } ); let diff = []; + // XXX: This is also surely broken for i18n users.forEach((userId) => { // Previous power level const from = event.getPrevContent().users[userId]; @@ -211,16 +220,21 @@ function textForPowerEvent(event) { const to = event.getContent().users[userId]; if (to !== from) { diff.push( - userId + - ' from ' + Roles.textualPowerLevel(from, userDefault) + - ' to ' + Roles.textualPowerLevel(to, userDefault) + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { + userId: userId, + fromPowerLevel: Roles.textualPowerLevel(from, userDefault), + toPowerLevel: Roles.textualPowerLevel(to, userDefault) + }) ); } }); if (!diff.length) { return ''; } - return senderName + ' changed the power level of ' + diff.join(', '); + return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { + senderName: senderName, + powerLevelDiffText: diff.join(", ") + }); } var handlers = { diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 2aa0573e22..2b1cf23380 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -22,7 +22,7 @@ let isDialogOpen = false; const onAction = function(payload) { if (payload.action === 'unknown_device_error' && !isDialogOpen) { - var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; Modal.createDialog(UnknownDeviceDialog, { devices: payload.err.devices, @@ -33,17 +33,17 @@ const onAction = function(payload) { // https://github.com/vector-im/riot-web/issues/3148 console.log('UnknownDeviceDialog closed with '+r); }, - }, "mx_Dialog_unknownDevice"); + }, 'mx_Dialog_unknownDevice'); } -} +}; let ref = null; -export function startListening () { +export function startListening() { ref = dis.register(onAction); } -export function stopListening () { +export function stopListening() { if (ref) { dis.unregister(ref); ref = null; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 9de291249f..84d85e7565 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -14,24 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; import q from 'q'; import MatrixClientPeg from './MatrixClientPeg'; import Notifier from './Notifier'; +import { _t } from './languageHandler'; /* * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. */ -module.exports = { +export default { LABS_FEATURES: [ { - name: 'New Composer & Autocomplete', + name: "-", id: 'rich_text_editor', default: false, }, ], + // horrible but it works. The locality makes this somewhat more palatable. + doTranslations: function() { + this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete"); + }, + loadProfileInfo: function() { const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 4502b0ccd9..f3d89f0ff2 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -15,6 +15,7 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +import { _t } from './languageHandler'; module.exports = { usersTypingApartFromMe: function(room) { @@ -56,18 +57,18 @@ module.exports = { if (whoIsTyping.length == 0) { return ''; } else if (whoIsTyping.length == 1) { - return whoIsTyping[0].name + ' is typing'; + return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name}); } const names = whoIsTyping.map(function(m) { return m.name; }); - if (othersCount) { - const other = ' other' + (othersCount > 1 ? 's' : ''); - return names.slice(0, limit - 1).join(', ') + ' and ' + - othersCount + other + ' are typing'; + if (othersCount==1) { + return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')}); + } else if (othersCount>1) { + return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); - return names.join(', ') + ' and ' + lastPerson + ' are typing'; + return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); } } }; diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index ba706e0aa5..3a6ca4e6b7 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ var React = require("react"); +import { _t } from '../../../languageHandler'; var sdk = require('../../../index'); var MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -78,33 +79,33 @@ module.exports = React.createClass({ _renderDeviceInfo: function() { var device = this.state.device; if (!device) { - return (unknown device); + return ({ _t('unknown device') }); } - var verificationStatus = (NOT verified); + var verificationStatus = ({ _t('NOT verified') }); if (device.isBlocked()) { - verificationStatus = (Blacklisted); + verificationStatus = ({ _t('Blacklisted') }); } else if (device.isVerified()) { - verificationStatus = "verified"; + verificationStatus = _t('verified'); } return ( - + - + - + - + @@ -119,32 +120,32 @@ module.exports = React.createClass({
Name{ _t('Name') } { device.getDisplayName() }
Device ID{ _t('Device ID') } { device.deviceId }
Verification{ _t('Verification') } { verificationStatus }
Ed25519 fingerprint{ _t('Ed25519 fingerprint') } {device.getFingerprint()}
- + - - + + - - + + - - + + { event.getContent().msgtype === 'm.bad.encrypted' ? ( - + ) : null } - - + +
User ID{ _t('User ID') } { event.getSender() }
Curve25519 identity key{ event.getSenderKey() || none }{ _t('Curve25519 identity key') }{ event.getSenderKey() || { _t('none') } }
Claimed Ed25519 fingerprint key{ event.getKeysClaimed().ed25519 || none }{ _t('Claimed Ed25519 fingerprint key') }{ event.getKeysClaimed().ed25519 || { _t('none') } }
Algorithm{ event.getWireContent().algorithm || unencrypted }{ _t('Algorithm') }{ event.getWireContent().algorithm || { _t('unencrypted') } }
Decryption error{ _t('Decryption error') } { event.getContent().body }
Session ID{ event.getWireContent().session_id || none }{ _t('Session ID') }{ event.getWireContent().session_id || { _t('none') } }
@@ -166,18 +167,18 @@ module.exports = React.createClass({ return (
- End-to-end encryption information + { _t('End-to-end encryption information') }
-

Event information

+

{ _t('Event information') }

{this._renderEventInfo()} -

Sender device information

+

{ _t('Sender device information') }

{this._renderDeviceInfo()}
{buttons}
diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 56b9d56cc9..d6f16a7105 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; +import { _t } from '../../../languageHandler'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -52,11 +53,11 @@ export default React.createClass({ const passphrase = this.refs.passphrase1.value; if (passphrase !== this.refs.passphrase2.value) { - this.setState({errStr: 'Passphrases must match'}); + this.setState({errStr: _t('Passphrases must match')}); return false; } if (!passphrase) { - this.setState({errStr: 'Passphrase must not be empty'}); + this.setState({errStr: _t('Passphrase must not be empty')}); return false; } @@ -109,24 +110,28 @@ export default React.createClass({ return (

- This process allows you to export the keys for messages - you have received in encrypted rooms to a local file. You - will then be able to import the file into another Matrix - client in the future, so that client will also be able to - decrypt these messages. + { _t( + 'This process allows you to export the keys for messages ' + + 'you have received in encrypted rooms to a local file. You ' + + 'will then be able to import the file into another Matrix ' + + 'client in the future, so that client will also be able to ' + + 'decrypt these messages.' + ) }

- The exported file will allow anyone who can read it to decrypt - any encrypted messages that you can see, so you should be - careful to keep it secure. To help with this, you should enter - a passphrase below, which will be used to encrypt the exported - data. It will only be possible to import the data by using the - same passphrase. + { _t( + 'The exported file will allow anyone who can read it to decrypt ' + + 'any encrypted messages that you can see, so you should be ' + + 'careful to keep it secure. To help with this, you should enter ' + + 'a passphrase below, which will be used to encrypt the exported ' + + 'data. It will only be possible to import the data by using the ' + + 'same passphrase.' + ) }

{this.state.errStr} @@ -135,7 +140,7 @@ export default React.createClass({
@@ -148,7 +153,7 @@ export default React.createClass({
@@ -161,11 +166,11 @@ export default React.createClass({
-
diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index ddd13813e2..61d2aeec74 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -19,6 +19,7 @@ import React from 'react'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) => { @@ -112,20 +113,23 @@ export default React.createClass({ return (

- This process allows you to import encryption keys - that you had previously exported from another Matrix - client. You will then be able to decrypt any - messages that the other client could decrypt. + { _t( + 'This process allows you to import encryption keys ' + + 'that you had previously exported from another Matrix ' + + 'client. You will then be able to decrypt any ' + + 'messages that the other client could decrypt.' + ) }

- The export file will be protected with a passphrase. - You should enter the passphrase here, to decrypt the - file. + { _t( + 'The export file will be protected with a passphrase. ' + + 'You should enter the passphrase here, to decrypt the file.' + ) }

{this.state.errStr} @@ -134,7 +138,7 @@ export default React.createClass({
@@ -147,7 +151,7 @@ export default React.createClass({
@@ -160,11 +164,11 @@ export default React.createClass({
-
diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 5c90990295..cbdb839ce3 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,3 +1,20 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; import type {Completion, SelectionRange} from './Autocompleter'; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 1bf1b1dc14..f8564a43a0 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,3 +1,19 @@ +/* +Copyright 2016 Aviral Dasgupta + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + // @flow import type {Component} from 'react'; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 60171bc72f..205a3737dc 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,8 +1,27 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import Fuse from 'fuse.js'; import {TextualCompletion} from './Components'; +// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file const COMMANDS = [ { command: '/me', @@ -43,10 +62,10 @@ const COMMANDS = [ command: '/ddg', args: '', description: 'Searches DuckDuckGo for results', - } + }, ]; -let COMMAND_RE = /(^\/\w*)/g; +const COMMAND_RE = /(^\/\w*)/g; let instance = null; @@ -60,15 +79,15 @@ export default class CommandProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let {command, range} = this.getCurrentCommand(query, selection); + const {command, range} = this.getCurrentCommand(query, selection); if (command) { - completions = this.fuse.search(command[0]).map(result => { + completions = this.fuse.search(command[0]).map((result) => { return { completion: result.command + ' ', component: (), range, }; @@ -78,12 +97,11 @@ export default class CommandProvider extends AutocompleteProvider { } getName() { - return '*️⃣ Commands'; + return '*️⃣ ' + _t('Commands'); } static getInstance(): CommandProvider { - if (instance == null) - {instance = new CommandProvider();} + if (instance === null) instance = new CommandProvider(); return instance; } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 4595f7456d..b26a217ec6 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -1,3 +1,19 @@ +/* +Copyright 2016 Aviral Dasgupta + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index bffd924976..9c996bb1cc 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,4 +1,22 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import 'whatwg-fetch'; @@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } getName() { - return '🔍 Results from DuckDuckGo'; + return '🔍 ' + _t('Results from DuckDuckGo'); } static getInstance(): DuckDuckGoProvider { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index d488ac53ae..810212315b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,4 +1,22 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; @@ -39,7 +57,7 @@ export default class EmojiProvider extends AutocompleteProvider { } getName() { - return '😃 Emoji'; + return '😃 ' + _t('Emoji'); } static getInstance() { diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8d1e555e56..be35c53e5d 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,4 +1,22 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; @@ -50,7 +68,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return '💬 Rooms'; + return '💬 ' + _t('Rooms'); } static getInstance() { diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 4d40fbdf94..fedebb3618 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,4 +1,22 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; @@ -51,7 +69,7 @@ export default class UserProvider extends AutocompleteProvider { } getName() { - return '👥 Users'; + return '👥 ' + _t('Users'); } setUserList(users) { diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 24ebfea07f..8b3d035dc1 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -16,15 +16,16 @@ limitations under the License. 'use strict'; -var React = require("react"); -var MatrixClientPeg = require("../../MatrixClientPeg"); -var PresetValues = { +import React from 'react'; +import q from 'q'; +import { _t } from '../../languageHandler'; +import sdk from '../../index'; +import MatrixClientPeg from '../../MatrixClientPeg'; +const PresetValues = { PrivateChat: "private_chat", PublicChat: "public_chat", Custom: "custom", }; -var q = require('q'); -var sdk = require('../../index'); module.exports = React.createClass({ displayName: 'CreateRoom', @@ -231,7 +232,7 @@ module.exports = React.createClass({ if (curr_phase == this.phases.ERROR) { error_box = (
- An error occured: {this.state.error_string} + {_t('An error occured: %(error_string)s', {error_string: this.state.error_string})}
); } @@ -248,27 +249,27 @@ module.exports = React.createClass({
-
-