more components updated and translated

This commit is contained in:
or-else
2018-10-27 11:55:32 +03:00
parent 71bbfb4fdd
commit 48d5732c16
15 changed files with 153 additions and 81 deletions
+22 -2
View File
@@ -1,7 +1,8 @@
// Flattens messages extracted via the React Intl Babel plugin. An error will be // 1.Flattens messages extracted via the React Intl Babel plugin. An error will be
// thrown if there are messages with the same `id`. The result is a file containing // thrown if there are messages with the same `id`. The result is a file containing
// `id: message` pairs in "en" locale. // `id: message` pairs in "en" locale.
//
// 2. Takes all translations and groups them into a single file.
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var globSync = require('glob').sync; var globSync = require('glob').sync;
@@ -9,7 +10,10 @@ var mkdirpSync = require('mkdirp').sync;
const MESSAGES_PATTERN = './src/i18n/src/**/*.json'; const MESSAGES_PATTERN = './src/i18n/src/**/*.json';
const LANG_DIR = './src/i18n/'; 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.
let messages = globSync(MESSAGES_PATTERN) let messages = globSync(MESSAGES_PATTERN)
.map((filename) => fs.readFileSync(filename, 'utf8')) .map((filename) => fs.readFileSync(filename, 'utf8'))
.map((file) => JSON.parse(file)) .map((file) => JSON.parse(file))
@@ -24,6 +28,22 @@ let messages = globSync(MESSAGES_PATTERN)
return collection; return collection;
}, {}); }, {});
// Write English translation.
let filename = path.join(LANG_DIR, 'en.json'); let filename = path.join(LANG_DIR, 'en.json');
mkdirpSync(path.dirname(filename)); mkdirpSync(path.dirname(filename));
fs.writeFileSync(filename, JSON.stringify(messages, null, 2)); fs.writeFileSync(filename, JSON.stringify(messages, null, 2));
// 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')) };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
}, {});
// Write file with all translations combined.
mkdirpSync(path.dirname(FINAL_FILE));
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages, null, 2));
+8 -3
View File
@@ -2,12 +2,12 @@
"contacts_not_found": "You have no chats<br />¯\\\\_(ツ)_/¯", "contacts_not_found": "You have no chats<br />¯\\\\_(ツ)_/¯",
"full_name_prompt": "Full name, e.g. John Doe", "full_name_prompt": "Full name, e.g. John Doe",
"email_prompt": "Email, e.g. jdoe@example.com", "email_prompt": "Email, e.g. jdoe@example.com",
"sign_up_button": "Sign up", "button_sign_up": "Sign up",
"login_prompt": "Login", "login_prompt": "Login",
"password_prompt": "Password", "password_prompt": "Password",
"stay_logged_in": "Stay logged in", "stay_logged_in": "Stay logged in",
"forgot_password_link": "Forgot password?", "forgot_password_link": "Forgot password?",
"sign_in_button": "Sign in", "button_sign_in": "Sign in",
"label_client": "Client:", "label_client": "Client:",
"label_server": "Server:", "label_server": "Server:",
"online_now": "online now", "online_now": "online now",
@@ -15,6 +15,9 @@
"title_not_found": "Not found", "title_not_found": "Not found",
"unnamed_topic": "Unnamed", "unnamed_topic": "Unnamed",
"messages_not_readable": "no access to messages", "messages_not_readable": "no access to messages",
"tabtitle_find_user": "find",
"tabtitle_new_group": "new group",
"tabtitle_group_by_id": "by id",
"label_server_to_use": "Server to use:", "label_server_to_use": "Server to use:",
"label_wire_transport": "Wire transport:", "label_wire_transport": "Wire transport:",
"button_update": "Update", "button_update": "Update",
@@ -24,5 +27,7 @@
"sidepanel_title_edit_account": "Edit Account", "sidepanel_title_edit_account": "Edit Account",
"sidepanel_title_newtpk": "Start New Chat", "sidepanel_title_newtpk": "Start New Chat",
"sidepanel_title_cred": "Confirm Credentials", "sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password" "sidepanel_title_reset": "Reset Password",
"tags_not_found": "No tags defined. Add some.",
"tags_editor_no_tags": "Add some tags"
} }
+5 -2
View File
@@ -2,12 +2,12 @@
"contacts_not_found": "Чатов нет<br />¯\\\\_(ツ)_/¯", "contacts_not_found": "Чатов нет<br />¯\\\\_(ツ)_/¯",
"full_name_prompt": "Полное имя, напр. Иван Петров", "full_name_prompt": "Полное имя, напр. Иван Петров",
"email_prompt": "Email, напр. ivan@example.com", "email_prompt": "Email, напр. ivan@example.com",
"sign_up_button": "Создать счет", "button_sign_up": "Создать счет",
"login_prompt": "Логин", "login_prompt": "Логин",
"password_prompt": "Пароль", "password_prompt": "Пароль",
"stay_logged_in": "Запомнить", "stay_logged_in": "Запомнить",
"forgot_password_link": "Напомнить пароль", "forgot_password_link": "Напомнить пароль",
"sign_in_button": "Войти", "button_sign_in": "Войти",
"label_client": "Клиент:", "label_client": "Клиент:",
"label_server": "Сервер:", "label_server": "Сервер:",
"online_now": "онлайн", "online_now": "онлайн",
@@ -15,6 +15,9 @@
"title_not_found": "Не найден", "title_not_found": "Не найден",
"unnamed_topic": "Без названия", "unnamed_topic": "Без названия",
"messages_not_readable": "нет доступа к сообщениям", "messages_not_readable": "нет доступа к сообщениям",
"tabtitle_find_user": "найти",
"tabtitle_new_group": "создать",
"tabtitle_group_by_id": "по id",
"label_server_to_use": "Использовать сервер:", "label_server_to_use": "Использовать сервер:",
"label_wire_transport": "Соединение:", "label_wire_transport": "Соединение:",
"button_update": "Применить", "button_update": "Применить",
+10 -3
View File
@@ -6,8 +6,7 @@ import ReactDOM from 'react-dom';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import ruData from 'react-intl/locale-data/ru'; import ruData from 'react-intl/locale-data/ru';
addLocaleData(ruData); addLocaleData(ruData);
import en from './i18n/en.json'; import allMessages from './messages.json';
import ru from './i18n/ru.json';
import TinodeWeb from './views/tinode-web.jsx'; import TinodeWeb from './views/tinode-web.jsx';
@@ -19,8 +18,16 @@ const language = (params && params.hl) ||
navigator.language || navigator.language ||
navigator.userLanguage; navigator.userLanguage;
const baseLanguage = language.toLowerCase().split(/[-_]+/)[0];
// Try full locale, try locale without region code, fallback to 'en'
const messages =
allMessages[language] ||
allMessages[baseLanguage] ||
allMessages.en;
ReactDOM.render( ReactDOM.render(
<IntlProvider locale={language} messages={en} > <IntlProvider locale={language} messages={messages} >
<TinodeWeb /> <TinodeWeb />
</IntlProvider>, </IntlProvider>,
document.getElementById('mountPoint') document.getElementById('mountPoint')
+5 -3
View File
@@ -1,5 +1,7 @@
// Odds and ends
// Make shortcut icon appear with a green dot + show unread count in title. // Make shortcut icon appear with a green dot + show unread count in title.
function updateFavicon(count) { export function updateFavicon(count) {
var oldIcon = document.getElementById('shortcut-icon'); var oldIcon = document.getElementById('shortcut-icon');
if (oldIcon) { if (oldIcon) {
var head = document.head || document.getElementsByTagName('head')[0]; var head = document.head || document.getElementsByTagName('head')[0];
@@ -15,7 +17,7 @@ function updateFavicon(count) {
} }
// Create VCard which represents topic 'public' info // Create VCard which represents topic 'public' info
function vcard(fn, imageDataUrl) { export function vcard(fn, imageDataUrl) {
var card = null; var card = null;
if ((fn && fn.trim()) || imageDataUrl) { if ((fn && fn.trim()) || imageDataUrl) {
@@ -35,7 +37,7 @@ function vcard(fn, imageDataUrl) {
} }
// Deep-shallow compare two arrays: shallow compare each element. // Deep-shallow compare two arrays: shallow compare each element.
function arrayEqual(a, b) { export function arrayEqual(a, b) {
// Compare lengths first. // Compare lengths first.
if (a.length != b.length) { if (a.length != b.length) {
return false; return false;
+7 -3
View File
@@ -1,9 +1,11 @@
/* ContactsView holds all contacts-related stuff */ /* ContactsView holds all contacts-related stuff */
import React from 'react'; import React from 'react';
import { defineMessages } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ContactList from '../widgets/contact-list.jsx'; import ContactList from '../widgets/contact-list.jsx';
import { updateFavicon } from '../lib/utils.js';
// Panel titles for translation. // Panel titles for translation.
const messages = defineMessages({ const messages = defineMessages({
'contacts_not_found': { 'contacts_not_found': {
@@ -13,7 +15,7 @@ const messages = defineMessages({
} }
}); });
export default class ContactsView extends React.Component { class ContactsView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -37,7 +39,7 @@ export default class ContactsView extends React.Component {
} }
render() { render() {
const {formatMessage} = this.props.intl; const {formatHTMLMessage} = this.props.intl;
return ( return (
<ContactList <ContactList
connected={this.props.connected} connected={this.props.connected}
@@ -52,3 +54,5 @@ export default class ContactsView extends React.Component {
); );
} }
}; };
export default injectIntl(ContactsView);
+2 -2
View File
@@ -7,7 +7,7 @@ import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx'; import VisiblePassword from '../widgets/visible-password.jsx';
import LocalStorageUtil from '../lib/local-storage.js'; import LocalStorageUtil from '../lib/local-storage.js';
import vcard from '../lib/utils.js'; import { vcard } from '../lib/utils.js';
export default class CreateAccountView extends React.PureComponent { export default class CreateAccountView extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -115,7 +115,7 @@ export default class CreateAccountView extends React.PureComponent {
</div> </div>
<div className="dialog-buttons"> <div className="dialog-buttons">
<button className={submitClasses} type="submit"> <button className={submitClasses} type="submit">
<FormattedMessage id="sign_up_button" defaultMessage="Sign up" <FormattedMessage id="button_sign_up" defaultMessage="Sign up"
description="Create account button [Sign Up]" /> description="Create account button [Sign Up]" />
</button> </button>
</div> </div>
+1 -1
View File
@@ -85,7 +85,7 @@ export default class LoginView extends React.Component {
</div> </div>
<div className="dialog-buttons"> <div className="dialog-buttons">
<button className={submitClasses} type="submit"> <button className={submitClasses} type="submit">
<FormattedMessage id="sign_in_button" defaultMessage="Sign in" <FormattedMessage id="button_sign_in" defaultMessage="Sign in"
description="Button [Sign In]" /> description="Button [Sign In]" />
</button> </button>
</div> </div>
+19 -7
View File
@@ -1,12 +1,15 @@
/* Create new topic and invite users or send an invite */ /* Create new topic and invite users or send an invite */
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
import ContactList from '../widgets/contact-list.jsx'; import ContactList from '../widgets/contact-list.jsx';
import NewTopicById from '../widgets/new-topic-by-id.jsx'; import NewTopicById from '../widgets/new-topic-by-id.jsx';
import NewTopicGroup from '../widgets/new-topic-group.jsx'; import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx'; import SearchContacts from '../widgets/search-contacts.jsx';
import HashNavigation from '../lib/navigation.js';
export default class NewTopicView extends React.Component { export default class NewTopicView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -34,24 +37,24 @@ export default class NewTopicView extends React.Component {
handleTabClick(e) { handleTabClick(e) {
e.preventDefault(); e.preventDefault();
navigateTo(addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id)); HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({tabSelected: e.currentTarget.dataset.id}); this.setState({tabSelected: e.currentTarget.dataset.id});
} }
handleContactSelected(sel) { handleContactSelected(sel) {
if (this.state.tabSelected === 'p2p') { if (this.state.tabSelected === 'p2p') {
navigateTo(removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(sel, undefined); this.props.onCreateTopic(sel, undefined);
} }
} }
handleNewGroupSubmit(name, dataUrl, priv, tags) { handleNewGroupSubmit(name, dataUrl, priv, tags) {
navigateTo(removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags); this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags);
} }
handleGroupByID(topicName) { handleGroupByID(topicName) {
navigateTo(removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(topicName); this.props.onCreateTopic(topicName);
} }
@@ -60,13 +63,22 @@ export default class NewTopicView extends React.Component {
<div className="flex-column"> <div className="flex-column">
<ul className="tabbar"> <ul className="tabbar">
<li className={this.state.tabSelected === "p2p" ? "active" : null}> <li className={this.state.tabSelected === "p2p" ? "active" : null}>
<a href="javascript:;" data-id="p2p" onClick={this.handleTabClick}>find</a> <a href="javascript:;" data-id="p2p" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_find_user" defaultMessage="find"
description="Tab title Find" />
</a>
</li> </li>
<li className={this.state.tabSelected === "grp" ? "active" : null}> <li className={this.state.tabSelected === "grp" ? "active" : null}>
<a href="javascript:;" data-id="grp" onClick={this.handleTabClick}>new group</a> <a href="javascript:;" data-id="grp" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_new_group" defaultMessage="new group"
description="Tab title New Group" />
</a>
</li> </li>
<li className={this.state.tabSelected === "byid" ? "active" : null}> <li className={this.state.tabSelected === "byid" ? "active" : null}>
<a href="javascript:;" data-id="byid" onClick={this.handleTabClick}>by id</a> <a href="javascript:;" data-id="byid" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_group_by_id" defaultMessage="by id"
description="Tab title Find topic by ID" />
</a>
</li> </li>
</ul> </ul>
{this.state.tabSelected === 'grp' ? {this.state.tabSelected === 'grp' ?
+1
View File
@@ -13,6 +13,7 @@ import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx'; import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js'; import { API_KEY, APP_NAME, MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { isLocalHost, isSecureConnection } from '../lib/host-name.js'; import { isLocalHost, isSecureConnection } from '../lib/host-name.js';
import LocalStorageUtil from '../lib/local-storage.js'; import LocalStorageUtil from '../lib/local-storage.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
+2
View File
@@ -2,6 +2,8 @@ import React from 'react';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
/* ContactList: component for showing a list of contacts, /* ContactList: component for showing a list of contacts,
* such as a list of group members in a group chat */ * such as a list of group members in a group chat */
export default class ContactList extends React.Component { export default class ContactList extends React.Component {
+2
View File
@@ -2,6 +2,8 @@ import React from 'react';
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
import { stringHash } from '../lib/strformat.js';
export default class LetterTile extends React.PureComponent { export default class LetterTile extends React.PureComponent {
render() { render() {
let avatar; let avatar;
+20 -8
View File
@@ -1,8 +1,11 @@
// TagManager: edit topic or user tags. // TagManager: edit topic or user tags.
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
import ChipInput from './chip-input.jsx'; import ChipInput from './chip-input.jsx';
import { arrayEqual } from '../lib/utils.js';
export default class TagManager extends React.Component { export default class TagManager extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -10,8 +13,7 @@ export default class TagManager extends React.Component {
this.state = { this.state = {
tags: this.props.tags, tags: this.props.tags,
tagInput: '', tagInput: '',
activated: this.props.activated, activated: this.props.activated
noTagsMessage: "Add some tags"
}; };
this.handleShowTagManager = this.handleShowTagManager.bind(this); this.handleShowTagManager = this.handleShowTagManager.bind(this);
@@ -99,7 +101,9 @@ export default class TagManager extends React.Component {
tags.push(<span className="badge" key={tags.length}>{tag}</span>); tags.push(<span className="badge" key={tags.length}>{tag}</span>);
}); });
if (tags.length == 0) { if (tags.length == 0) {
tags = <i>No tags defined. Add some.</i>; tags = (<i>
<FormattedMessage id="tags_not_found" defaultMessage="No tags defined. Add some." description="" />
</i>);
} }
} }
return ( return (
@@ -109,26 +113,34 @@ export default class TagManager extends React.Component {
</div> </div>
{this.state.activated ? {this.state.activated ?
<div> <div>
<ChipInput <FormattedMessage id="tags_editor_no_tags" defaultMessage="Add some tags"
description="Tag editor prompt when no tags are found.">{
(add_tags_prompt) => <ChipInput
chips={tags} chips={tags}
avatarDisabled={true} avatarDisabled={true}
prompt={this.state.noTagsMessage} prompt={add_tags_prompt}
onEnter={this.handleAddTag} onEnter={this.handleAddTag}
onFocusLost={this.handleAddTag} onFocusLost={this.handleAddTag}
onCancel={this.handleCancel} onCancel={this.handleCancel}
onChipRemoved={this.handleRemoveTag} onChipRemoved={this.handleRemoveTag}
filterFunc={this.handleTagInput} /> filterFunc={this.handleTagInput} />
}</FormattedMessage>
{this.props.onSubmit || this.props.onCancel ? {this.props.onSubmit || this.props.onCancel ?
<div id="tag-manager-buttons" className="panel-form-row"> <div id="tag-manager-buttons" className="panel-form-row">
<button className="blue" onClick={this.handleSubmit}>OK</button> <button className="blue" onClick={this.handleSubmit}>
<button className="white" onClick={this.handleCancel}>Cancel</button> <FormattedMessage id="button_ok" defautMessage="OK" description="Confirmation button [OK]" />
</button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defautMessage="Cancel" description="Rejection button [Cancel]" />
</button>
</div> </div>
: null} : null}
</div> </div>
: :
<div> <div>
<a href="javascript:;" className="flat-button" onClick={this.handleShowTagManager}> <a href="javascript:;" className="flat-button" onClick={this.handleShowTagManager}>
<i className="material-icons">edit</i> Manage tags <i className="material-icons">edit</i> <FormattedMessage id="title_manage_tags" defautMessage="Manage tags"
description="Section title in TagManager" />
</a> </a>
<span>{tags}</span> <span>{tags}</span>
</div> </div>
+36 -34
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