more components updated/translated

This commit is contained in:
or-else
2018-10-28 14:18:22 +03:00
parent d7fb701983
commit dc838776ec
19 changed files with 266 additions and 84 deletions
+52 -4
View File
@@ -8,12 +8,18 @@ var path = require('path');
var globSync = require('glob').sync;
var mkdirpSync = require('mkdirp').sync;
const DEFAULT_TRANSLATION = 'en.json';
const MESSAGES_PATTERN = './src/i18n/src/**/*.json';
const LANG_DIR = './src/i18n/';
const LANG_PATTERN = './src/i18n/*.json';
const FINAL_FILE = './src/messages.json';
// Read message files generated by react-intl-babel-plugin.
const MISSING_MARKER = ':!![MISSING]!!:';
const OBSOLETE_MARKER = ':!![OBSOLETE]!!:';
// The react-intl-babel-plugin saves extracted messages
// into files, one fine per component. Rewrite them into
// a single file en.json.
let messages = globSync(MESSAGES_PATTERN)
.map((filename) => fs.readFileSync(filename, 'utf8'))
.map((file) => JSON.parse(file))
@@ -28,17 +34,59 @@ let messages = globSync(MESSAGES_PATTERN)
return collection;
}, {});
// Write English translation.
let filename = path.join(LANG_DIR, 'en.json');
let filename = path.join(LANG_DIR, DEFAULT_TRANSLATION);
mkdirpSync(path.dirname(filename));
fs.writeFileSync(filename, JSON.stringify(messages, null, 2));
// Find messages present in en.json but missing in translations and vice versa.
// Mark missing or obsolete messages as such in translation files.
globSync(LANG_PATTERN)
.map((filename) => {
const parts = filename.split('/');
if (parts[parts.length-1] != DEFAULT_TRANSLATION) {
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
// Find missing messages.
Object.entries(messages).forEach((ent) => {
if (!translated.hasOwnProperty(ent[0])) {
translated[ent[0]] = MISSING_MARKER + ent[1];
}
});
// Find no longer needed messages.
Object.entries(translated).forEach((ent) => {
if (!messages.hasOwnProperty(ent[0])) {
let val = translated[ent[0]];
if (val.indexOf(MISSING_MARKER) == 0) {
val = val.substring(0, MISSING_MARKER.length);
}
if (val.indexOf(OBSOLETE_MARKER) != 0) {
val = OBSOLETE_MARKER + val;
}
translated[ent[0]] = val;
}
});
fs.writeFileSync(filename, JSON.stringify(translated, null, 2));
}
});
// Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"};
// Remove messages marked as missing or obsolete.
// Read all translations.
messages = globSync(LANG_PATTERN)
.map((filename) => {
const parts = filename.split('/');
const locale = parts[parts.length - 1].split('.json')[0];
return { [locale]: JSON.parse(fs.readFileSync(filename, 'utf8')) };
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
// Strip messages prefixed with MISSING or OBSOLETE markers.
for (let id in translated) {
if (translated[id].indexOf(OBSOLETE_MARKER) == 0 ||
translated[id].indexOf(MISSING_MARKER) == 0) {
delete translated[id];
}
}
return { [locale]: translated };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
+15 -1
View File
@@ -10,7 +10,7 @@
"label_default_access_mode": "Default access mode:",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS)",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"title_tag_manager": "Tags (user discovery)",
"button_logout": "Logout",
"login_prompt": "Login",
@@ -28,6 +28,7 @@
"tabtitle_find_user": "find",
"tabtitle_new_group": "new group",
"tabtitle_group_by_id": "by id",
"search_for_contacts": "Use search to find contacts",
"label_server_to_use": "Server to use:",
"label_wire_transport": "Wire transport:",
"button_update": "Update",
@@ -38,11 +39,24 @@
"sidepanel_title_newtpk": "Start New Chat",
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
"phone": "phone",
"email": "email",
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
"numeric_confirmation_code_prompt": "Numbers only",
"button_confirm": "Confirm",
"invalid_content": "invalid content",
"button_ok": "OK",
"button_cancel": "Cancel",
"label_file_name": "File name:",
"label_content_type": "Content type:",
"label_size": "Size:",
"message_sending": "sending...",
"message_sending_failed": "failed",
"search_placeholder": "List like email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Messaging disabled",
"new_message_prompt": "New message",
"file_attachment_too_large": "The file size {size} exceeds the {limit} limit.",
"cannot_initiate_file_upload": "Cannot initiate file upload.",
"tags_not_found": "No tags defined. Add some.",
"tags_editor_no_tags": "Add some tags",
"title_manage_tags": "Manage tags"
+21 -2
View File
@@ -40,5 +40,24 @@
"sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги",
"title_manage_tags": "Редактор тагов"
}
"title_manage_tags": "Редактор тагов",
"message_sending": "шлём...",
"message_sending_failed": "ошибка",
"search_placeholder": "Список, напр. email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Отправка недоступна",
"new_message_prompt": "Новое сообщение",
"file_attachment_too_large": "Размер файла {size} превышает {limit} лимит.",
"cannot_initiate_file_upload": "Ошибка загрузки файла.",
"search_for_contacts": ":!![MISSING]!!:Use search to find contacts",
"phone": "телефон",
"email": "емейл",
"enter_confirmation_code_prompt": "Введите код подтверждения {method}:",
"numeric_confirmation_code_prompt": "Только цифры",
"button_confirm": "Подтвердить",
"button_ok": "OK",
"button_cancel": "Отменить",
"invalid_content": "сообщение не читается",
"label_file_name": "Имя файла:",
"label_content_type": "Тип:",
"label_size": "Размер:"
}
+2 -1
View File
@@ -1,4 +1,5 @@
// File and image helper functions.
import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js';
// Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
@@ -204,7 +205,7 @@ export function base64ReEncode(str) {
try {
str = btoa(atob(str));
} catch(err) {
console.log("Failed to base64 re-decode string");
console.log("Failed to base64 re-encode string");
str = null;
}
}
+34 -2
View File
@@ -11,7 +11,7 @@
"label_default_access_mode": "Default access mode:",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS)",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"title_tag_manager": "Tags (user discovery)",
"button_logout": "Logout",
"login_prompt": "Login",
@@ -29,6 +29,7 @@
"tabtitle_find_user": "find",
"tabtitle_new_group": "new group",
"tabtitle_group_by_id": "by id",
"search_for_contacts": "Use search to find contacts",
"label_server_to_use": "Server to use:",
"label_wire_transport": "Wire transport:",
"button_update": "Update",
@@ -39,11 +40,24 @@
"sidepanel_title_newtpk": "Start New Chat",
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
"phone": "phone",
"email": "email",
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
"numeric_confirmation_code_prompt": "Numbers only",
"button_confirm": "Confirm",
"invalid_content": "invalid content",
"button_ok": "OK",
"button_cancel": "Cancel",
"label_file_name": "File name:",
"label_content_type": "Content type:",
"label_size": "Size:",
"message_sending": "sending...",
"message_sending_failed": "failed",
"search_placeholder": "List like email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Messaging disabled",
"new_message_prompt": "New message",
"file_attachment_too_large": "The file size {size} exceeds the {limit} limit.",
"cannot_initiate_file_upload": "Cannot initiate file upload.",
"tags_not_found": "No tags defined. Add some.",
"tags_editor_no_tags": "Add some tags",
"title_manage_tags": "Manage tags"
@@ -90,6 +104,24 @@
"sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги",
"title_manage_tags": "Редактор тагов"
"title_manage_tags": "Редактор тагов",
"message_sending": "шлём...",
"message_sending_failed": "ошибка",
"search_placeholder": "Список, напр. email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Отправка недоступна",
"new_message_prompt": "Новое сообщение",
"file_attachment_too_large": "Размер файла {size} превышает {limit} лимит.",
"cannot_initiate_file_upload": "Ошибка загрузки файла.",
"phone": "телефон",
"email": "емейл",
"enter_confirmation_code_prompt": "Введите код подтверждения {method}:",
"numeric_confirmation_code_prompt": "Только цифры",
"button_confirm": "Подтвердить",
"button_ok": "OK",
"button_cancel": "Отменить",
"invalid_content": "сообщение не читается",
"label_file_name": "Имя файла:",
"label_content_type": "Тип:",
"label_size": "Размер:"
}
}
+2 -2
View File
@@ -196,8 +196,8 @@ export default class EditAccountView extends React.Component {
description="Label for push notifications switch" />
:
<FormattedMessage id="label_push_notifications_disabled"
defaultMessage="Notification alerts (requires HTTPS)"
description="Label for push notifications switch (disabled):" />
defaultMessage="Notification alerts (requires HTTPS):"
description="Label for push notifications switch (disabled)" />
}
</label>
<CheckBox name="alert" id="desktop-alerts"
+4 -2
View File
@@ -1,8 +1,8 @@
/* InfoView: panel with topic/user info */
// InfoView: panel with topic/user info.
import React from 'react';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import ContactList from '../widgets/contact-list.jsx';
import GroupManager from '../widgets/group-manager.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
@@ -11,6 +11,8 @@ import MoreButton from '../widgets/more-button.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
export default class InfoView extends React.Component {
constructor(props) {
super(props);
+12 -9
View File
@@ -90,15 +90,18 @@ export default class NewTopicView extends React.Component {
<div className="flex-column">
<SearchContacts type="p2p"
onSearchContacts={this.props.onSearchContacts} />
<ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage="Use search to find contacts"
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleContactSelected} />
<FormattedMessage id="search_for_contacts" defaultMessage="Use search to find contacts"
description="Prompt in search field.">{
(search_for_contacts) => <ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage={search_for_contacts}
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleContactSelected} />
}</FormattedMessage>
</div>}
</div>
);
+1 -1
View File
@@ -1,4 +1,4 @@
/* A password reset form */
// A password reset form.
import React from 'react';
import VisiblePassword from '../widgets/visible-password.jsx';
+33 -8
View File
@@ -1,7 +1,21 @@
// ValidationView: panel for confirming credentials, like email or phone.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
export default class ValidationView extends React.PureComponent {
const messages = defineMessages({
phone: {
id: 'phone',
defaultMessage: 'phone',
description: "The word 'phone'",
},
email: {
id: 'email',
defaultMessage: 'email',
description: "The word 'email'",
}
});
class ValidationView extends React.PureComponent {
constructor(props) {
super(props);
@@ -32,24 +46,35 @@ export default class ValidationView extends React.PureComponent {
}
render() {
var methods = {'email': "email", 'tel': "phone"};
var method = methods[this.props.credMethod] || this.props.credMethod;
const { formatMessage } = this.props.intl;
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
let method = methods[this.props.credMethod] || this.props.credMethod;
return (
<div className="panel-form">
<div className="panel-form-row">
<label className="small" htmlFor="enter-confirmation-code">
Enter confirmation code sent to you by {method}:
<FormattedMessage id="enter_confirmation_code_prompt"
defaultMessage="Enter confirmation code sent to you by {method}:"
description="Request to enter confirmation code" />
</label>
</div>
<div className="panel-form-row">
<input type="text" id="enter-confirmation-code" placeholder="Numbers only"
value={this.state.code} onChange={this.handleChange}
onKeyPress={this.handleKeyPress} required />
<FormattedMessage id="numeric_confirmation_code_prompt"
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
(numbers_only) => <input type="text" id="enter-confirmation-code"
placeholder={numbers_only}
value={this.state.code} onChange={this.handleChange}
onKeyPress={this.handleKeyPress} required />
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className="blue" onClick={this.handleSubmit}>Confirm</button>
<button className="blue" onClick={this.handleSubmit}>
<FormattedMessage id="button_confirm" defaultMessage="Confirm" description="Button [Confirm]" />
</button>
</div>
</div>
);
}
};
export default injectIntl(ValidationView);
+15 -9
View File
@@ -1,11 +1,15 @@
// Single message, sent or received.
import React from 'react';
import {Drafty} from 'tinode-sdk'
import { FormattedMessage } from 'react-intl';
import { Drafty } from 'tinode-sdk'
import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
/* Single message, sent or received. */
import { REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js';
export default class ChatMessage extends React.Component {
constructor(props) {
super(props);
@@ -50,17 +54,16 @@ export default class ChatMessage extends React.Component {
}
render() {
var elementKey = 0;
var formatter = function(style, data, values) {
let elementKey = 0;
const formatter = function(style, data, values) {
elementKey += 1;
var el = Drafty.tagName(style);
let el = Drafty.tagName(style);
if (el) {
var attr = Drafty.attrValue(style, data) || {};
let attr = Drafty.attrValue(style, data) || {};
attr.key = elementKey;
if (style == 'IM') {
// Additional processing for images
var dim = fitImageSize(data.width, data.height,
let dim = fitImageSize(data.width, data.height,
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
attr.className = 'inline-image';
attr.style = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } : null;
@@ -95,7 +98,10 @@ export default class ChatMessage extends React.Component {
}, this);
content = React.createElement('span', null, Drafty.format(content, formatter, this));
} else if (typeof content != 'string') {
content = <span><i className="material-icons">error_outline</i> <i>invalid content</i></span>
content = <span><i className="material-icons">error_outline</i> <i>
<FormattedMessage id="invalid_content"
defaultMessage="invalid content" description="Shown when message is unreadable" />
</i></span>
}
return (
+10 -10
View File
@@ -1,10 +1,10 @@
// A single topic or user.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import LetterTile from './letter-tile.jsx';
import UnreadBadge from './unread-badge.jsx';
/* A single topic or user */
export default class Contact extends React.Component {
constructor(props) {
super(props);
@@ -28,20 +28,20 @@ export default class Contact extends React.Component {
}
render() {
var title = this.props.title;
let title = this.props.title;
if (!title) {
title = <i>unknown</i>;
title = <i><FormattedMessage id="unnamed_topic" /></i>;
} else if (title.length > 30) {
// FIXME: this is wrong for RTL languages.
// FIXME: this is probably wrong for RTL languages.
title = title.substring(0, 28) + '...';
}
var online = this.props.now ? 'online' : 'offline';
var avatar = this.props.avatar ? this.props.avatar : true;
var badges = [];
let online = this.props.now ? 'online' : 'offline';
let avatar = this.props.avatar ? this.props.avatar : true;
let badges = [];
if (this.props.badges && this.props.badges.length > 0) {
var count = 0;
let count = 0;
this.props.badges.map(function(b) {
var style = 'badge' + (b.color ? ' ' + b.color : '');
let style = 'badge' + (b.color ? ' ' + b.color : '');
badges.push(<span className={style} key={count}>{b.name}</span>);
count ++;
});
+1 -1
View File
@@ -1,6 +1,6 @@
// Context Menu: popup/dropdown menu.
import React from 'react';
import ReactDom from 'react-dom';
import ReactDOM from 'react-dom';
import { REM_SIZE } from '../config.js';
+8 -4
View File
@@ -1,6 +1,6 @@
/* GroupMembers: control for managing a list of group members */
// GroupMembers: control for managing a list of group members.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ChipInput from './chip-input.jsx';
import ContactList from './contact-list.jsx';
@@ -156,8 +156,12 @@ export default class GroupManager extends React.Component {
showUnread={false}
onTopicSelected={this.handleContactSelected} />
<div id="group-manager-buttons" className="panel-form-row">
<button className="blue" onClick={this.handleSubmit}>OK</button>
<button className="white" onClick={this.handleCancel}>Cancel</button>
<button className="blue" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
</button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
</button>
</div>
</div>
);
+17 -3
View File
@@ -1,4 +1,9 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js';
import { bytesToHumanSize } from '../lib/strformat.js';
export default class ImagePreview extends React.PureComponent {
constructor(props) {
@@ -43,10 +48,19 @@ export default class ImagePreview extends React.PureComponent {
<img src={this.props.content.url} style={size} />
</div>
<div id="image-preview-footer">
<div><div><b>File name</b>:</div><div><span title={this.props.content.filename}>{filename}</span></div></div>
<div><div><b>Content type</b>:</div><div>{this.props.content.type}</div></div>
<div>
<div><b>Size</b>:</div>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
description="Label for a file name" /></b></div>
<div><span title={this.props.content.filename}>{filename}</span></div>
</div>
<div>
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
description="Label for file content type (mime)" /></b></div>
<div>{this.props.content.type}</div>
</div>
<div>
<div><b><FormattedMessage id="label_size" defaultMessage="Size:"
description="Label for file size" /></b></div>
<div>{this.props.content.width} &times; {this.props.content.height} px; {bytesToHumanSize(this.props.content.size)}</div>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
// In-place text editor. Shows text with an icon which toggles it into an input field.
import React from 'react';
import ReactDom from 'react-dom';
import ReactDOM from 'react-dom';
import VisiblePassword from './visible-password.jsx';
+19 -5
View File
@@ -1,8 +1,11 @@
// Send message form.
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import { KEYPRESS_DELAY } from '../config.js';
import { KEYPRESS_DELAY, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, filePasted, imageFileToBase64, imageFileScaledToBase64 } from '../lib/blob-helpers.js';
import { bytesToHumanSize } from '../lib/strformat.js';
const messages = defineMessages({
'messaging_disabled': {
@@ -14,7 +17,17 @@ const messages = defineMessages({
id: 'new_message_prompt',
defaultMessage: 'New message',
description: 'Prompt in SendMessage in read-only topic'
}
},
'file_attachment_too_large': {
id: 'file_attachment_too_large',
defaultMessage: 'The file size {size} exceeds the {limit} limit.',
description: 'Error message when attachment is too large'
},
'cannot_initiate_upload': {
id: 'cannot_initiate_file_upload',
defaultMessage: 'Cannot initiate file upload.',
description: 'Generic error messagewhen attachment fails'
},
});
class SendMessage extends React.PureComponent {
@@ -99,17 +112,18 @@ class SendMessage extends React.PureComponent {
}
handleAttachFile(e) {
const {formatMessage} = this.props.intl;
if (e.target.files && e.target.files.length > 0) {
var file = e.target.files[0];
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
// Too large.
this.props.onError("The file size " + bytesToHumanSize(file.size) +
" exceeds the " + bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE) + " limit.", 'err');
this.props.onError(formatMessage(messages.file_attachment_too_large,
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE)}), 'err');
} else if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
// Too large to send inband - uploading out of band and sending as a link.
let uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError("Cannot initiate file upload.");
this.props.onError(formatMessage(messages.cannot_initiate_upload));
return;
}
// Format data and initiate upload.
+13 -13
View File
File diff suppressed because one or more lines are too long
+6 -6
View File
File diff suppressed because one or more lines are too long