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 13e88350c1..4ad72b4927 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 diff --git a/package.json b/package.json index 059fdd390f..b94c8d66eb 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", diff --git a/src/AddThreepid.js b/src/AddThreepid.js index c89de4f5fa..8be7a19b13 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -16,6 +16,7 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +import { _t } from './languageHandler'; /** * Allows a user to add a third party identifier to their Home Server and, @@ -44,7 +45,7 @@ class AddThreepid { return res; }, function(err) { if (err.errcode == 'M_THREEPID_IN_USE') { - err.message = "This email address is already in use"; + err.message = _t('This email address is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -69,7 +70,7 @@ class AddThreepid { return res; }, function(err) { if (err.errcode == 'M_THREEPID_IN_USE') { - err.message = "This phone number is already in use"; + err.message = _t('This phone number is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -91,7 +92,7 @@ class AddThreepid { id_server: identityServerDomain }, this.bind).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) { err.message += ` (Status ${err.httpStatus})`; diff --git a/src/CallHandler.js b/src/CallHandler.js index 5199ef0a67..c36ca382f2 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") { @@ -203,8 +204,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 +224,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 +234,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 +250,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 +276,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 +295,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 +313,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 d516cf07bf..8b7a2f3f20 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,9 +16,36 @@ limitations under the License. */ 'use strict'; +import { _t } from './languageHandler'; -const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -const 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; @@ -34,20 +62,37 @@ function twelveHourTime(date) { module.exports = { formatDate: function(date) { var now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { return this.formatTime(date); } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()] + " " + this.formatTime(date); + // 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() + " " + this.formatTime(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() + " " + this.formatTime(date); + 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: this.formatTime(date), + }); }, formatTime: function(date, showTwelveHour=false) { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f34aeae0e5..0e3e52fe40 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -27,6 +27,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 +230,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(); return q.reject(new Error( - "Unable to restore previous session: " + msg, + _t('Unable to restore previous session') + ': ' + msg, )); } diff --git a/src/Notifier.js b/src/Notifier.js index 6473ab4d9c..eeedbcf365 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -21,6 +21,7 @@ import TextForEvent from './TextForEvent'; import Avatar from './Avatar'; import dis from './dispatcher'; import sdk from './index'; +import { _t } from './languageHandler'; import Modal from './Modal'; /* @@ -134,13 +135,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; diff --git a/src/PasswordReset.js b/src/PasswordReset.js index a03a565459..668b8f67da 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/SlashCommands.js b/src/SlashCommands.js index bd68f1a6fe..d2c0eda3ff 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(); }), diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3f200a089d..2d53c5db9f 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,54 @@ 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 inivitation.', + {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 + '"'; + return _t('%(senderDisplayName)s changed the room name to %(roomName)s', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name}); } function textForMessageEvent(ev) { @@ -122,66 +127,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 +209,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 +217,14 @@ 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/UserSettingsStore.js b/src/UserSettingsStore.js index 9de291249f..5b96692dc9 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -14,7 +14,6 @@ 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'; @@ -23,10 +22,14 @@ import Notifier from './Notifier'; * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. */ -module.exports = { +/* + * TODO: Find a way to translate the names of LABS_FEATURES. In other words, guarantee that languages were already loaded before building this array. + */ + +export default { LABS_FEATURES: [ { - name: 'New Composer & Autocomplete', + name: "New Composer & Autocomplete", id: 'rich_text_editor', default: false, }, 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/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 60171bc72f..34d0110131 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,8 +1,10 @@ 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', @@ -68,7 +70,7 @@ export default class CommandProvider extends AutocompleteProvider { component: (), range, }; @@ -78,7 +80,7 @@ export default class CommandProvider extends AutocompleteProvider { } getName() { - return '*️⃣ Commands'; + return '*️⃣ ' + _t('Commands'); } static getInstance(): CommandProvider { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index d488ac53ae..7b9e8fb669 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,4 +1,5 @@ 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 +40,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..4a2dd09eb5 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,4 +1,5 @@ import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; @@ -50,7 +51,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..281b6fd40d 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,4 +1,5 @@ import React from 'react'; +import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; @@ -51,7 +52,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({
-
-