Make front-end localizable

This commit is contained in:
Toby Zerner 2015-07-17 17:43:28 +09:30
parent 9c5a6560e0
commit 0a1191d56c
49 changed files with 438 additions and 148 deletions

View File

@ -49,7 +49,7 @@ export default class ActivityPage extends UserPage {
footer = (
<div className="ActivityPage-loadMore">
{Button.component({
children: 'Load More',
children: app.trans('core.load_more'),
className: 'Button--default',
onclick: this.loadMore.bind(this)
})}

View File

@ -62,7 +62,7 @@ export default class AvatarEditor extends Component {
items.add('upload',
Button.component({
icon: 'upload',
children: 'Upload',
children: app.trans('core.upload'),
onclick: this.upload.bind(this)
})
);
@ -70,7 +70,7 @@ export default class AvatarEditor extends Component {
items.add('remove',
Button.component({
icon: 'times',
children: 'Remove',
children: app.trans('core.remove'),
onclick: this.remove.bind(this)
})
);

View File

@ -28,7 +28,7 @@ export default class ChangeEmailModal extends Modal {
}
title() {
return 'Change Email';
return app.trans('core.change_email');
}
content() {
@ -38,9 +38,11 @@ export default class ChangeEmailModal extends Modal {
return (
<div className="Modal-body">
<div class="Form Form--centered">
<p class="helpText">We've sent a confirmation email to <strong>{this.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p>
<p class="helpText">{m.trust(app.trans('core.confirmation_email_sent', {email: this.email()}))}</p>
<div class="Form-group">
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
{app.trans('core.go_to', {location: emailProviderName})}
</a>
</div>
</div>
</div>
@ -58,7 +60,9 @@ export default class ChangeEmailModal extends Modal {
disabled={this.loading}/>
</div>
<div class="Form-group">
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>Save Changes</button>
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>
{app.trans('core.save_changes')}
</button>
</div>
</div>
</div>

View File

@ -10,16 +10,18 @@ export default class ChangePasswordModal extends Modal {
}
title() {
return 'Change Password';
return app.trans('core.change_password');
}
content() {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">Click the button below and check your email for a link to change your password.</p>
<p className="helpText">{app.trans('core.change_password_help')}</p>
<div className="Form-group">
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>Send Password Reset Email</button>
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>
{app.trans('core.send_password_reset_email')}
</button>
</div>
</div>
</div>

View File

@ -434,28 +434,28 @@ class Composer extends Component {
if (this.position === Composer.PositionEnum.FULLSCREEN) {
items.add('exitFullScreen', ComposerButton.component({
icon: 'compress',
title: 'Exit Full Screen',
title: app.trans('core.exit_full_screen'),
onclick: this.exitFullScreen.bind(this)
}));
} else {
if (this.position !== Composer.PositionEnum.MINIMIZED) {
items.add('minimize', ComposerButton.component({
icon: 'minus minimize',
title: 'Minimize',
title: app.trans('core.minimize'),
onclick: this.minimize.bind(this),
itemClassName: 'App-backControl'
}));
items.add('fullScreen', ComposerButton.component({
icon: 'expand',
title: 'Full Screen',
title: app.trans('core.full_screen'),
onclick: this.fullScreen.bind(this)
}));
}
items.add('close', ComposerButton.component({
icon: 'times',
title: 'Close',
title: app.trans('core.close'),
onclick: this.close.bind(this)
}));
}

View File

@ -23,7 +23,7 @@ export default class DeleteAccountModal extends Modal {
}
title() {
return 'Delete Account';
return app.trans('core.delete_account');
}
content() {
@ -31,10 +31,10 @@ export default class DeleteAccountModal extends Modal {
<div className="Modal-body">
<div className="Form Form--centered">
<div className="helpText">
<p>Hold up! If you delete your account, there&#39;s no going back. Keep in mind:</p>
<p>{app.trans('core.delete_account_help')}</p>
<ul>
<li>Your username will be released, so someone else will be able to sign up with your name.</li>
<li>All of your posts will remain, but no longer associated with your account.</li>
<li>{app.trans('core.username_will_be_released')}</li>
<li>{app.trans('core.posts_will_remain')}</li>
</ul>
</div>
<div className="Form-group">

View File

@ -26,10 +26,10 @@ export default class DiscussionComposer extends ComposerBody {
static initProps(props) {
super.initProps(props);
props.placeholder = props.placeholder || 'Write a Post...';
props.submitLabel = props.submitLabel || 'Post Discussion';
props.confirmExit = props.confirmExit || 'You have not posted your discussion. Do you wish to discard it?';
props.titlePlaceholder = props.titlePlaceholder || 'Discussion Title';
props.placeholder = props.placeholder || app.trans('core.write_a_post');
props.submitLabel = props.submitLabel || app.trans('core.post_discussion');
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_discussion');
props.titlePlaceholder = props.titlePlaceholder || app.trans('core.discussion_title');
}
headerItems() {

View File

@ -53,7 +53,7 @@ export default class DiscussionList extends Component {
loading = LoadingIndicator.component();
} else if (this.moreResults) {
loading = Button.component({
children: 'Load More',
children: app.trans('core.load_more'),
className: 'Button',
onclick: this.loadMore.bind(this)
});

View File

@ -12,6 +12,7 @@ import PostPreview from 'flarum/components/PostPreview';
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
import DiscussionControls from 'flarum/utils/DiscussionControls';
import slidable from 'flarum/utils/slidable';
import extractText from 'flarum/utils/extractText';
/**
* The `DiscussionListItem` component shows a single discussion in the
@ -66,7 +67,7 @@ export default class DiscussionListItem extends Component {
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
<a href={startUser ? app.route.user(startUser) : '#'}
className="DiscussionListItem-author"
title={'Started by ' + (startUser ? startUser.username() : '[deleted]') + ' ' + humanTime(discussion.startTime())}
title={extractText(app.trans('core.discussion_started', {user: startUser, ago: humanTime(discussion.startTime())}))}
config={function(element) {
$(element).tooltip({placement: 'right'});
m.route.apply(this, arguments);
@ -87,7 +88,7 @@ export default class DiscussionListItem extends Component {
<span className="DiscussionListItem-count"
onclick={this.markAsRead.bind(this)}
title={showUnread ? 'Mark as Read' : ''}>
title={showUnread ? app.trans('core.mark_as_read') : ''}>
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
</span>

View File

@ -21,6 +21,6 @@ export default class DiscussionRenamedNotification extends Notification {
}
content() {
return [username(this.props.notification.sender()), ' changed the title'];
return app.trans('core.discussion_renamed_notification', {user: this.props.notification.sender()});
}
}

View File

@ -18,6 +18,10 @@ export default class DiscussionRenamedPost extends EventPost {
const oldTitle = post.content()[0];
const newTitle = post.content()[1];
return ['changed the title from ', m('strong.DiscussionRenamedPost-old', oldTitle), ' to ', m('strong.DiscussionRenamedPost-new', newTitle), '.'];
return app.trans('core.discussion_renamed', {
user: this.props.post.user(),
old: <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
new: <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
});
}
}

View File

@ -28,11 +28,11 @@ export default class DiscussionsSearchSource {
const results = this.results[query] || [];
return [
<li className="Dropdown-header">Discussions</li>,
<li className="Dropdown-header">{app.trans('core.discussions')}</li>,
<li>
{LinkButton.component({
icon: 'search',
children: 'Search all discussions for "' + query + '"',
children: app.trans('core.search_all_discussions', {query}),
href: app.route('index', {q: query})
})}
</li>,

View File

@ -15,8 +15,8 @@ export default class EditPostComposer extends ComposerBody {
static initProps(props) {
super.initProps(props);
props.submitLabel = props.submitLabel || 'Save Changes';
props.confirmExit = props.confirmExit || 'You have not saved your changes. Do you wish to discard them?';
props.submitLabel = props.submitLabel || app.trans('core.save_changes');
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit');
props.originalContent = props.originalContent || props.post.content();
props.user = props.user || props.post.user();
}
@ -29,7 +29,7 @@ export default class EditPostComposer extends ComposerBody {
<h3>
{icon('pencil')}{' '}
<a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
Post #{post.number()} in {post.discussion().title()}
{app.trans('core.editing_post', {number: post.number(), discussion: post.discussion().title()})}
</a>
</h3>
));

View File

@ -26,7 +26,7 @@ export default class FooterSecondary extends Component {
items.add('poweredBy', (
<a href="http://flarum.org?r=forum" target="_blank">
Powered by Flarum
{app.trans('core.powered_by_flarum')}
</a>
));

View File

@ -33,7 +33,7 @@ export default class ForgotPasswordModal extends Modal {
}
title() {
return 'Forgot Password';
return app.trans('core.forgot_password');
}
content() {
@ -43,9 +43,11 @@ export default class ForgotPasswordModal extends Modal {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">We've sent you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.</p>
<p className="helpText">{app.trans('core.password_reset_email_sent')}</p>
<div className="Form-group">
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
{app.trans('core.go_to', {location: emailProviderName})}
</a>
</div>
</div>
</div>
@ -55,16 +57,16 @@ export default class ForgotPasswordModal extends Modal {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">Enter your email address and we will send you a link to reset your password.</p>
<p className="helpText">{app.trans('core.forgot_password_help')}</p>
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder="Email"
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>
Recover Password
{app.trans('core.recover_password')}
</button>
</div>
</div>

View File

@ -37,7 +37,7 @@ export default class HeaderSecondary extends Component {
} else {
items.add('signUp',
Button.component({
children: 'Sign Up',
children: app.trans('core.sign_up'),
className: 'Button Button--link',
onclick: () => app.modal.show(new SignUpModal())
})
@ -45,7 +45,7 @@ export default class HeaderSecondary extends Component {
items.add('logIn',
Button.component({
children: 'Log In',
children: app.trans('core.log_in'),
className: 'Button Button--link',
onclick: () => app.modal.show(new LogInModal())
})

View File

@ -139,7 +139,7 @@ export default class IndexPage extends Component {
items.add('newDiscussion',
Button.component({
children: 'Start a Discussion',
children: app.trans('core.start_a_discussion'),
icon: 'edit',
className: 'Button Button--primary IndexPage-newDiscussion',
itemClassName: 'App-primaryControl',
@ -171,7 +171,7 @@ export default class IndexPage extends Component {
items.add('allDiscussions',
LinkButton.component({
href: app.route('index', params),
children: 'All Discussions',
children: app.trans('core.all_discussions'),
icon: 'comments-o'
})
);
@ -191,7 +191,7 @@ export default class IndexPage extends Component {
const sortOptions = {};
for (const i in app.cache.discussionList.sortMap()) {
sortOptions[i] = i.substr(0, 1).toUpperCase() + i.substr(1);
sortOptions[i] = app.trans('core.sort_' + i);
}
items.add('sort',
@ -216,7 +216,7 @@ export default class IndexPage extends Component {
items.add('refresh',
Button.component({
title: 'Refresh',
title: app.trans('core.refresh'),
icon: 'refresh',
className: 'Button Button--icon',
onclick: () => app.cache.discussionList.refresh()
@ -226,7 +226,7 @@ export default class IndexPage extends Component {
if (app.session.user) {
items.add('markAllAsRead',
Button.component({
title: 'Mark All as Read',
title: app.trans('core.mark_all_as_read'),
icon: 'check',
className: 'Button Button--icon',
onclick: this.markAllAsRead.bind(this)

View File

@ -6,6 +6,6 @@ import Activity from 'flarum/components/Activity';
*/
export default class JoinedActivity extends Activity {
description() {
return 'Joined the forum';
return app.trans('core.joined_the_forum');
}
}

View File

@ -35,7 +35,7 @@ export default class LogInModal extends Modal {
}
title() {
return 'Log In';
return app.trans('core.log_in');
}
content() {
@ -43,14 +43,14 @@ export default class LogInModal extends Modal {
<div className="Modal-body">
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="email" placeholder="Username or Email"
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder="Password"
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
@ -67,11 +67,11 @@ export default class LogInModal extends Modal {
</div>,
<div className="Modal-footer">
<p className="LogInModal-forgotPassword">
<a onclick={this.forgotPassword.bind(this)}>Forgot password?</a>
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
</p>
<p className="LogInModal-signUp">
Don't have an account?{' '}
<a onclick={this.signUp.bind(this)}>Sign Up</a>
{app.trans('core.before_sign_up_link')}{' '}
<a onclick={this.signUp.bind(this)}>{app.trans('core.sign_up')}</a>
</p>
</div>
];
@ -122,12 +122,12 @@ export default class LogInModal extends Modal {
if (response && response.code === 'confirm_email') {
this.alert = Alert.component({
children: ['You need to confirm your email before you can log in. We\'ve sent a confirmation email to ', <strong>{response.email}</strong>, '. If it doesn\'t arrive soon, check your spam folder.']
children: app.trans('core.email_confirmation_required', {email: response.email})
});
} else {
this.alert = Alert.component({
type: 'error',
children: 'Your login details were incorrect.'
children: app.trans('core.invalid_login')
});
}

View File

@ -21,8 +21,8 @@ export default class NotificationGrid extends Component {
* @type {Array}
*/
this.methods = [
{name: 'alert', icon: 'bell', label: 'Alert'},
{name: 'email', icon: 'envelope-o', label: 'Email'}
{name: 'alert', icon: 'bell', label: app.trans('core.alert')},
{name: 'email', icon: 'envelope-o', label: app.trans('core.email')}
];
/**
@ -181,7 +181,7 @@ export default class NotificationGrid extends Component {
items.add('discussionRenamed', {
name: 'discussionRenamed',
label: [icon('pencil'), ' Someone renames a discussion I started']
label: [icon('pencil'), ' ', app.trans('core.notify_discussion_renamed')]
});
return items;

View File

@ -59,12 +59,12 @@ export default class NotificationList extends Component {
{Button.component({
className: 'Button Button--icon Button--link',
icon: 'check',
title: 'Mark All as Read',
title: app.trans('core.mark_all_as_read'),
onclick: this.markAllAsRead.bind(this)
})}
</div>
<h4 className="App-titleControl App-titleControl--text">Notifications</h4>
<h4 className="App-titleControl App-titleControl--text">{app.trans('core.notifications')}</h4>
</div>
<div className="NotificationList-content">
@ -98,7 +98,7 @@ export default class NotificationList extends Component {
);
})
: !this.loading
? <div className="NotificationList-empty">No Notifications</div>
? <div className="NotificationList-empty">{app.trans('core.no_notifications')}</div>
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</div>
</div>

View File

@ -25,7 +25,7 @@ export default class NotificationsDropdown extends Component {
data-toggle="dropdown"
onclick={this.onclick.bind(this)}>
<span className="Button-icon">{unread || icon('bell')}</span>
<span className="Button-label">Notifications</span>
<span className="Button-label">{app.trans('core.notifications')}</span>
</a>
<div className="Dropdown-menu Dropdown-menu--right">
{this.showing ? NotificationList.component() : ''}

View File

@ -1,6 +1,7 @@
import Component from 'flarum/Component';
import icon from 'flarum/helpers/icon';
import humanTime from 'flarum/utils/humanTime';
import extractText from 'flarum/utils/extractText';
/**
* The `PostEdited` component displays information about when and by whom a post
@ -14,7 +15,7 @@ export default class PostEdited extends Component {
view() {
const post = this.props.post;
const editUser = post.editUser();
const title = 'Edited ' + (editUser ? 'by ' + editUser.username() + ' ' : '') + humanTime(post.editTime());
const title = extractText(app.trans('core.post_edited', {user: editUser, ago: humanTime(post.editTime())}));
return (
<span className="PostEdited" title={title}>{icon('pencil')}</span>

View File

@ -33,7 +33,7 @@ export default class PostMeta extends Component {
</a>
<div className="Dropdown-menu dropdown-menu">
<span className="PostMeta-number">Post #{post.number()}</span>{' '}
<span className="PostMeta-number">{app.trans('core.post_number', {number: post.number()})}</span>{' '}
{fullTime(time)}
{touch
? <a href="Button PostMeta-permalink" href={permalink}>{permalink}</a>

View File

@ -205,7 +205,7 @@ class PostStream extends mixin(Component, evented) {
if (dt > 1000 * 60 * 60 * 24 * 4) {
content = [
<div className="PostStream-timeGap">
<span>{moment.duration(dt).humanize()} later</span>
<span>{app.trans('core.period_later', {period: moment.duration(dt).humanize()})}</span>
</div>,
content
];

View File

@ -71,12 +71,10 @@ export default class PostStreamScrubber extends Component {
const unreadCount = this.props.stream.discussion.unreadCount();
const unreadPercent = Math.min(this.count() - this.index, unreadCount) / this.count();
const viewing = [
<span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
' of ',
<span className="Scrubber-count">{formatNumber(this.count())}</span>,
' posts '
];
const viewing = app.trans('core.viewing_posts', {
index: <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
count: <span className="Scrubber-count">{formatNumber(this.count())}</span>
});
function styleUnread(element, isInitialized, context) {
const $element = $(element);
@ -103,7 +101,7 @@ export default class PostStreamScrubber extends Component {
<div className="Dropdown-menu dropdown-menu">
<div className="Scrubber">
<a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
{icon('angle-double-up')} Original Post
{icon('angle-double-up')} {app.trans('core.original_post')}
</a>
<div className="Scrubber-scrollbar">
@ -118,12 +116,12 @@ export default class PostStreamScrubber extends Component {
<div className="Scrubber-after"/>
<div className="Scrubber-unread" config={styleUnread}>
{formatNumber(unreadCount)} unread
{app.trans('core.unread_posts', {count: unreadCount})}
</div>
</div>
<a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
{icon('angle-double-down')} Now
{icon('angle-double-down')} {app.trans('core.now')}
</a>
</div>
</div>

View File

@ -15,7 +15,7 @@ export default class PostedActivity extends Activity {
description() {
const post = this.props.activity.subject();
return post.number() === 1 ? 'Started a discussion' : 'Posted a reply';
return app.trans(post.number() === 1 ? 'core.started_a_discussion' : 'core.posted_a_reply');
}
content() {

View File

@ -16,9 +16,9 @@ export default class ReplyComposer extends ComposerBody {
static initProps(props) {
super.initProps(props);
props.placeholder = props.placeholder || 'Write a Reply...';
props.submitLabel = props.submitLabel || 'Post Reply';
props.confirmExit = props.confirmExit || 'You have not posted your reply. Do you wish to discard it?';
props.placeholder = props.placeholder || app.trans('core.write_a_reply');
props.submitLabel = props.submitLabel || app.trans('core.post_reply');
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_reply');
}
headerItems() {
@ -66,7 +66,7 @@ export default class ReplyComposer extends ComposerBody {
// transition to their new post when clicked.
let alert;
const viewButton = Button.component({
children: 'View',
children: app.trans('core.view'),
onclick: () => {
m.route(app.route.post(post));
app.alerts.dismiss(alert);
@ -75,7 +75,7 @@ export default class ReplyComposer extends ComposerBody {
app.alerts.show(
alert = new Alert({
type: 'success',
message: 'Your reply was posted.',
message: app.trans('reply_posted'),
controls: [viewButton]
})
);

View File

@ -25,7 +25,7 @@ export default class ReplyPlaceholder extends Component {
<article className="Post ReplyPlaceholder" onclick={reply} onmousedown={triggerClick}>
<header className="Post-header">
{avatar(app.session.user, {className: 'PostUser-avatar'})}{' '}
Write a Reply...
{app.trans('core.write_a_reply')}
</header>
</article>
);

View File

@ -82,7 +82,7 @@ export default class Search extends Component {
})}>
<div className="Search-input">
<input className="FormControl"
placeholder="Search Forum"
placeholder={app.trans('core.search_forum')}
value={this.value()}
oninput={m.withAttr('value', this.value)}
onfocus={() => this.hasFocus = true}

View File

@ -47,7 +47,7 @@ export default class SessionDropdown extends Dropdown {
items.add('profile',
LinkButton.component({
icon: 'user',
children: 'Profile',
children: app.trans('core.profile'),
href: app.route.user(user)
}),
100
@ -56,7 +56,7 @@ export default class SessionDropdown extends Dropdown {
items.add('settings',
LinkButton.component({
icon: 'cog',
children: 'Settings',
children: app.trans('core.settings'),
href: app.route('settings')
}),
50
@ -66,7 +66,7 @@ export default class SessionDropdown extends Dropdown {
items.add('administration',
LinkButton.component({
icon: 'wrench',
children: 'Administration',
children: app.trans('core.administration'),
href: app.forum.attribute('baseUrl') + '/admin',
target: '_blank',
config: () => {}
@ -80,7 +80,7 @@ export default class SessionDropdown extends Dropdown {
items.add('logOut',
Button.component({
icon: 'sign-out',
children: 'Log Out',
children: app.trans('core.log_out'),
onclick: app.session.logout.bind(app.session)
}),
-100

View File

@ -18,7 +18,7 @@ export default class SettingsPage extends UserPage {
super(...args);
this.init(app.session.user);
app.setTitle('Settings');
app.setTitle(app.trans('core.settings'));
app.drawer.hide();
}
@ -40,7 +40,7 @@ export default class SettingsPage extends UserPage {
items.add('account',
FieldSet.component({
label: 'Account',
label: app.trans('core.account'),
className: 'Settings-account',
children: this.accountItems().toArray()
})
@ -48,7 +48,7 @@ export default class SettingsPage extends UserPage {
items.add('notifications',
FieldSet.component({
label: 'Notifications',
label: app.trans('core.notifications'),
className: 'Settings-notifications',
children: [NotificationGrid.component({user: this.user})]
})
@ -56,7 +56,7 @@ export default class SettingsPage extends UserPage {
items.add('privacy',
FieldSet.component({
label: 'Privacy',
label: app.trans('core.privacy'),
className: 'Settings-privacy',
children: this.privacyItems().toArray()
})
@ -75,7 +75,7 @@ export default class SettingsPage extends UserPage {
items.add('changePassword',
Button.component({
children: 'Change Password',
children: app.trans('core.change_password'),
className: 'Button',
onclick: () => app.modal.show(new ChangePasswordModal())
})
@ -83,7 +83,7 @@ export default class SettingsPage extends UserPage {
items.add('changeEmail',
Button.component({
children: 'Change Email',
children: app.trans('core.change_email'),
className: 'Button',
onclick: () => app.modal.show(new ChangeEmailModal())
})
@ -91,7 +91,7 @@ export default class SettingsPage extends UserPage {
items.add('deleteAccount',
Button.component({
children: 'Delete Account',
children: app.trans('core.delete_account'),
className: 'Button Button--danger',
onclick: () => app.modal.show(new DeleteAccountModal())
})
@ -131,7 +131,7 @@ export default class SettingsPage extends UserPage {
items.add('discloseOnline',
Switch.component({
children: 'Allow others to see when I am online',
children: app.trans('disclose_online'),
state: this.user.preferences().discloseOnline,
onchange: (value, component) => {
this.user.pushAttributes({lastSeenTime: null});

View File

@ -49,7 +49,7 @@ export default class SignUpModal extends Modal {
}
title() {
return 'Sign Up';
return app.trans('core.sign_up');
}
content() {
@ -67,21 +67,21 @@ export default class SignUpModal extends Modal {
const body = [(
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="username" placeholder="Username"
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
value={this.username()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder="Email"
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder="Password"
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
@ -91,7 +91,7 @@ export default class SignUpModal extends Modal {
<button className="Button Button--primary Button--block"
type="submit"
disabled={this.loading}>
Sign Up
{app.trans('core.sign_up')}
</button>
</div>
</div>
@ -111,13 +111,21 @@ export default class SignUpModal extends Modal {
<div className="darkenBackground"/>
<div className="container">
{avatar(user)}
<h3>Welcome, {user.username()}!</h3>
<h3>{app.trans('core.welcome_user', {user})}</h3>
{user.isConfirmed() ? [
<p>We've sent a confirmation email to <strong>{user.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p>,
<p><a href={`http://${emailProviderName}`} className="Button Button--primary">Go to {emailProviderName}</a></p>
<p>{app.trans('core.confirmation_email_sent', {email: user.email()})}</p>,
<p>
<a href={`http://${emailProviderName}`} className="Button Button--primary">
{app.trans('core.go_to', {location: emailProviderName})}
</a>
</p>
] : (
<p><button className="Button Button--primary" onclick={this.hide.bind(this)}>Dismiss</button></p>
<p>
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
{app.trans('core.dismiss')}
</button>
</p>
)}
</div>
</div>
@ -130,8 +138,8 @@ export default class SignUpModal extends Modal {
footer() {
return [
<p className="SignUpModal-logIn">
Already have an account?{' '}
<a onclick={this.logIn.bind(this)}>Log In</a>
{app.trans('core.before_log_in_link')}{' '}
<a onclick={this.logIn.bind(this)}>{app.trans('core.log_in')}</a>
</p>
];
}

View File

@ -20,9 +20,10 @@ export default class TerminalPost extends Component {
return (
<span>
{username(user)}{' '}
{lastPost ? 'replied ' : 'started '}
{humanTime(time)}
{app.trans('core.discussion_' + (lastPost ? 'replied' : 'started'), {
user,
ago: humanTime(time)
})}
</span>
);
}

View File

@ -26,12 +26,6 @@ export default class TextEditor extends Component {
this.value = m.prop(this.props.value || '');
}
static initProps(props) {
super.initProps(props);
props.submitLabel = props.submitLabel || 'Submit';
}
view() {
return (
<div className="TextEditor">

View File

@ -1,4 +1,5 @@
import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import classList from 'flarum/utils/classList';
/**
@ -29,19 +30,19 @@ export default class UserBio extends Component {
let content;
if (this.editing) {
content = <textarea className="FormControl" placeholder="Write something about yourself" rows="3"/>;
content = <textarea className="FormControl" placeholder={app.trans('core.bio_placeholder')} rows="3" value={user.bio()}/>;
} else {
let subContent;
if (this.loading) {
subContent = <p className="UserBio-placeholder">Saving</p>;
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component()}</p>;
} else {
const bioHtml = user.bioHtml();
if (bioHtml) {
subContent = m.trust(bioHtml);
} else if (this.props.editable) {
subContent = <p className="UserBio-placeholder">Write something about yourself</p>;
subContent = <p className="UserBio-placeholder">{app.trans('core.bio_placeholder')}</p>;
}
}

View File

@ -89,13 +89,13 @@ export default class UserCard extends Component {
items.add('lastSeen', (
<span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
{online
? [icon('circle'), ' Online']
? [icon('circle'), ' ', app.trans('core.online')]
: [icon('clock-o'), ' ', humanTime(lastSeenTime)]}
</span>
));
}
items.add('joined', ['Joined ', humanTime(user.joinTime())]);
items.add('joined', app.trans('core.joined', {ago: humanTime(user.joinTime())}));
return items;
}

View File

@ -137,7 +137,7 @@ export default class UserPage extends Component {
items.add('activity',
LinkButton.component({
href: app.route('user.activity', {username: user.username()}),
children: 'Activity',
children: app.trans('core.activity'),
icon: 'user'
})
);
@ -145,7 +145,7 @@ export default class UserPage extends Component {
items.add('discussions',
LinkButton.component({
href: app.route('user.discussions', {username: user.username()}),
children: ['Discussions', <span className="Button-badge">{user.discussionsCount()}</span>],
children: [app.trans('core.discussions'), <span className="Button-badge">{user.discussionsCount()}</span>],
icon: 'reorder'
})
);
@ -153,7 +153,7 @@ export default class UserPage extends Component {
items.add('posts',
LinkButton.component({
href: app.route('user.posts', {username: user.username()}),
children: ['Posts', <span className="Button-badge">{user.commentsCount()}</span>],
children: [app.trans('core.posts'), <span className="Button-badge">{user.commentsCount()}</span>],
icon: 'comment-o'
})
);
@ -163,7 +163,7 @@ export default class UserPage extends Component {
items.add('settings',
LinkButton.component({
href: app.route('settings'),
children: 'Settings',
children: app.trans('core.settings'),
icon: 'cog'
})
);

View File

@ -22,7 +22,7 @@ export default class UsersSearchResults {
if (!results.length) return '';
return [
<li className="Dropdown-header">Users</li>,
<li className="Dropdown-header">{app.trans('core.users')}</li>,
results.map(user => (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<a href={app.route.user(user)} config={m.route}>

View File

@ -4,6 +4,7 @@ import LogInModal from 'flarum/components/LogInModal';
import Button from 'flarum/components/Button';
import Separator from 'flarum/components/Separator';
import ItemList from 'flarum/utils/ItemList';
import extractText from 'flarum/utils/extractText';
/**
* The `DiscussionControls` utility constructs a list of buttons for a
@ -54,14 +55,14 @@ export default {
!app.session.user || discussion.canReply()
? Button.component({
icon: 'reply',
children: app.session.user ? 'Reply' : 'Log In to Reply',
children: app.session.user ? app.trans('core.reply') : app.trans('core.log_in_to_reply'),
onclick: this.replyAction.bind(discussion, true, false)
})
: Button.component({
icon: 'reply',
children: 'Can\'t Reply',
children: app.trans('core.cannot_reply'),
className: 'disabled',
title: 'You don\'t have permission to reply to this discussion.'
title: app.trans('core.cannot_reply_help')
})
);
}
@ -84,7 +85,7 @@ export default {
if (discussion.canRename()) {
items.add('rename', Button.component({
icon: 'pencil',
children: 'Rename',
children: app.trans('core.rename'),
onclick: this.renameAction.bind(discussion)
}));
}
@ -107,7 +108,7 @@ export default {
if (discussion.canDelete()) {
items.add('delete', Button.component({
icon: 'times',
children: 'Delete',
children: app.trans('core.delete'),
onclick: this.deleteAction.bind(discussion)
}));
}
@ -176,7 +177,7 @@ export default {
* Delete the discussion after confirming with the user.
*/
deleteAction() {
if (confirm('Are you sure you want to delete this discussion?')) {
if (confirm(extractText(app.trans('core.confirm_delete_discussion')))) {
this.delete();
// If there is a discussion list in the cache, remove this discussion.
@ -197,7 +198,7 @@ export default {
*/
renameAction() {
const currentTitle = this.title();
const title = prompt('Enter a new title for this discussion:', currentTitle);
const title = prompt(extractText(app.trans('core.prompt_rename_discussion')), currentTitle);
// If the title is different to what it was before, then save it. After the
// save has completed, update the post stream as there will be a new post

View File

@ -60,13 +60,13 @@ export default {
if (post.isHidden()) {
items.add('restore', Button.component({
icon: 'reply',
children: 'Restore',
children: app.trans('core.restore'),
onclick: this.restoreAction.bind(post)
}));
} else {
items.add('edit', Button.component({
icon: 'pencil',
children: 'Edit',
children: app.trans('core.edit'),
onclick: this.editAction.bind(post)
}));
}
@ -91,13 +91,13 @@ export default {
if (post.contentType() === 'comment' && !post.isHidden() && post.canEdit()) {
items.add('hide', Button.component({
icon: 'times',
children: 'Delete',
children: app.trans('core.delete'),
onclick: this.hideAction.bind(post)
}));
} else if ((post.contentType() !== 'comment' || post.isHidden()) && post.canDelete()) {
items.add('delete', Button.component({
icon: 'times',
children: 'Delete Forever',
children: app.trans('core.delete_forever'),
onclick: this.deleteAction.bind(post)
}));
}

View File

@ -58,7 +58,7 @@ export default {
if (user.canEdit()) {
items.add('edit', Button.component({
icon: 'pencil',
children: 'Edit',
children: app.trans('core.edit'),
onclick: this.editAction.bind(user)
}));
}
@ -81,7 +81,7 @@ export default {
if (user.canDelete()) {
items.add('delete', Button.component({
icon: 'times',
children: 'Delete',
children: app.trans('core.delete'),
onclick: this.deleteAction.bind(user)
}));
}

View File

@ -0,0 +1,19 @@
/**
* Extract the text nodes from a virtual element.
*
* @param {VirtualElement} vdom
* @return {String}
*/
export default function extractText(vdom) {
let text = '';
if (vdom instanceof Array) {
text += vdom.map(element => extractText(element)).join('');
} else if (typeof vdom === 'object') {
text += extractText(vdom.children);
} else {
text += vdom;
}
return text;
}

View File

@ -1,3 +1,8 @@
import User from 'flarum/models/User';
import username from 'flarum/helpers/username';
import extractText from 'flarum/utils/extractText';
import extract from 'flarum/utils/extract';
/**
* The `Translator` class translates strings using the loaded localization.
*/
@ -30,9 +35,10 @@ export default class Translator {
*
* @param {String} key
* @param {Object} input
* @return {String}
* @param {VirtualElement} fallback
* @return {VirtualElement}
*/
trans(key, input = {}) {
trans(key, input = {}, fallback) {
const parts = key.split('.');
let translation = this.translations;
@ -46,19 +52,33 @@ export default class Translator {
// in the input, we'll work out which option to choose using the `plural`
// method.
if (typeof translation === 'object' && typeof input.count !== 'undefined') {
translation = translation[this.plural(input.count)];
translation = translation[this.plural(extractText(input.count))];
}
// If we've been given a user model as one of the input parameters, then
// we'll extract the username and use that for the translation. In the
// future there should be a hook here to inspect the user and change the
// translation key. This will allow a gender property to determine which
// translation key is used.
if (input.user instanceof User) {
input.username = username(extract(input, 'user'));
}
// If we've found the appropriate translation string, then we'll sub in the
// input.
if (typeof translation === 'string') {
for (const i in input) {
translation = translation.replace(new RegExp('{' + i + '}', 'gi'), input[i]);
}
translation = translation.split(new RegExp('({[^}]+})', 'gi'));
translation.forEach((part, i) => {
const match = part.match(/^{(.+)}$/i);
if (match) {
translation[i] = input[match[1]];
}
});
return translation;
}
return key;
return fallback || [key];
}
}

View File

@ -24,7 +24,7 @@ export default class Dropdown extends Component {
props.buttonClassName = props.buttonClassName || '';
props.contentClassName = props.contentClassName || '';
props.icon = props.icon || 'ellipsis-v';
props.label = props.label || app.trans('controls');
props.label = props.label || app.trans('core.controls');
}
view() {

View File

@ -2,7 +2,6 @@ import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon';
/**
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses

View File

@ -65,9 +65,11 @@ export default class User extends mixin(Model, {
const items = new ItemList();
this.groups().forEach(group => {
const name = group.nameSingular();
items.add('group' + group.id(),
Badge.component({
label: group.nameSingular(),
label: app.trans('core.group_' + name.toLowerCase(), undefined, name),
icon: group.icon(),
style: {backgroundColor: group.color()}
})

View File

@ -1,2 +1,116 @@
core:
account: Account
activity: Activity
administration: Administration
alert: Alert
all_discussions: All Discussions
before_log_in_link: Already have an account?
before_sign_up_link: Don't have an account?
bio_placeholder: Write something about yourself
cannot_reply: Can't Reply
cannot_reply_help: You don't have permission to reply to this discussion.
change_email: Change Email
change_password: Change Password
change_password_help: Click the button below and check your email for a link to change your password.
close: Close
confirm_delete_discussion: Are you sure you want to delete this discussion?
confirm_discard_discussion: You have not posted your discussion. Do you wish to discard it?
confirm_discard_edit: You have not saved your changes. Do you wish to discard them?
confirm_discard_reply: You have not posted your reply. Do you wish to discard it?
confirmation_email_sent: "We've sent a confirmation email to <strong>{email}</strong>. If it doesn't arrive soon, check your spam folder."
controls: Controls
delete: Delete
delete: Delete
delete_account: Delete Account
delete_account_help: "Hold up! If you delete your account, there&#39;s no going back. Keep in mind:"
delete_forever: Delete Forever
deleted: "[deleted]"
disclose_online: Allow others to see when I am online
discussion_renamed: "changed the title from {old} to {new}"
discussion_renamed_notification: "{username} changed the title"
discussion_replied: "{username} replied {ago}"
discussion_started: "{username} started {ago}"
discussion_title: Discussion Title
discussions: Discussions
dismiss: Dismiss
edit: Edit
editing_post: "Post #{number} in {discussion}"
email: Email
email_confirmation_required: "You need to confirm your email before you can log in. We've sent a confirmation email to <strong>{email}</strong>. If it doesn't arrive soon, check your spam folder."
exit_full_screen: Exit Full Screen
forgot_password: Forgot Password
forgot_password_help: Enter your email address and we will send you a link to reset your password.
forgot_password_link: Forgot password?
full_screen: Full Screen
go_to: "Go to {location}"
group_admin: Admin
group_admins: Admins
group_guest: Guest
group_guests: Guests
group_member: Member
group_members: Members
group_mod: Mod
group_mods: Mods
invalid_login: Your login details were incorrect.
joined: "Joined {ago}"
joined_the_forum: Joined the forum
load_more: Load More
log_in: Log In
log_in_to_reply: Log In to Reply
log_out: Log Out
mark_all_as_read: Mark All as Read
mark_as_read: Mark as Read
minimize: Minimize
no_notifications: No Notifications
notifications: Notifications
notify_discussion_renamed: Someone renames a discussion I started
now: Now
online: Online
original_post: Original Post
password: Password
password_reset_email_sent: We've sent you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.
period_later: "{period} later"
post_discussion: Post Discussion
post_edited: "{username} edited {ago}"
post_number: "Post #{number}"
post_reply: Post Reply
posted_a_reply: Posted a reply
posts: Posts
posts_will_remain: "All of your posts will remain, but no longer associated with your account."
powered_by_flarum: Powered by Flarum
privacy: Privacy
profile: Profile
prompt_rename_discussion: Enter a new title for this discussion:
recover_password: Recover Password
refresh: Refresh
remove: Remove
rename: Rename
reply: Reply
reply_posted: Your reply was posted.
restore: Restore
save_changes: Save Changes
search_all_discussions: 'Search all discussions for "{query}"'
search_forum: Search Forum
send_password_reset_email: Send Password Reset Email
settings: Settings
settings: Settings
sign_up: Sign Up
sort_newest: Newest
sort_oldest: Oldest
sort_recent: Recent
sort_replies: Replies
start_a_discussion: Start a Discussion
started_a_discussion: Started a discussion
unread_posts: "{count} unread"
upload: Upload
username: Username
username_or_email: Username or Email
username_will_be_released: "Your username will be released, so someone else will be able to sign up with your name."
users: Users
view: View
viewing_posts:
one: "{index} of {count} post"
other: "{index} of {count} posts"
welcome_user: "Welcome, {username}!"
write_a_post: Write a Post...
write_a_reply: Write a Reply...

View File

@ -13,4 +13,123 @@ abstract class ClientAction extends BaseClientAction
* {@inheritdoc}
*/
protected $layout = 'flarum.forum::forum';
/**
* {@inheritdoc}
*/
protected $translationKeys = [
'core.account'
'core.activity'
'core.administration'
'core.alert'
'core.all_discussions'
'core.before_log_in_link'
'core.before_sign_up_link'
'core.bio_placeholder'
'core.cannot_reply'
'core.cannot_reply_help'
'core.change_email',
'core.change_password',
'core.change_password_help',
'core.close',
'core.confirm_delete_discussion'
'core.confirm_discard_discussion',
'core.confirm_discard_edit'
'core.confirm_discard_reply'
'core.confirmation_email_sent',
'core.controls',
'core.delete'
'core.delete'
'core.delete_account',
'core.delete_account_help',
'core.delete_forever'
'core.deleted',
'core.disclose_online'
'core.discussion_renamed'
'core.discussion_renamed_notification',
'core.discussion_replied',
'core.discussion_started',
'core.discussion_title',
'core.discussions'
'core.dismiss'
'core.edit'
'core.editing_post'
'core.email'
'core.email_confirmation_required'
'core.exit_full_screen',
'core.forgot_password'
'core.forgot_password_help'
'core.forgot_password_link'
'core.full_screen',
'core.go_to',
'core.group_admin'
'core.group_admins'
'core.group_guest'
'core.group_guests'
'core.group_member'
'core.group_members'
'core.group_mod'
'core.group_mods'
'core.invalid_login'
'core.joined'
'core.joined_the_forum'
'core.load_more',
'core.log_in'
'core.log_in_to_reply'
'core.log_out'
'core.mark_all_as_read'
'core.mark_as_read',
'core.minimize',
'core.no_notifications'
'core.notifications'
'core.notify_discussion_renamed'
'core.now'
'core.online'
'core.original_post'
'core.password'
'core.password_reset_email_sent'
'core.period_later'
'core.post_discussion',
'core.post_edited'
'core.post_number'
'core.post_reply'
'core.posted_a_reply'
'core.posts'
'core.posts_will_remain',
'core.powered_by_flarum'
'core.privacy'
'core.profile'
'core.prompt_rename_discussion'
'core.recover_password'
'core.refresh'
'core.remove',
'core.rename'
'core.reply'
'core.reply_posted'
'core.restore'
'core.save_changes',
'core.search_all_discussions'
'core.search_forum'
'core.send_password_reset_email',
'core.settings'
'core.settings'
'core.sign_up'
'core.sort_newest'
'core.sort_oldest'
'core.sort_recent'
'core.sort_replies'
'core.start_a_discussion'
'core.started_a_discussion'
'core.unread_posts'
'core.upload',
'core.username'
'core.username_or_email'
'core.username_will_be_released',
'core.users'
'core.view'
'core.viewing_posts'
'core.welcome_user'
'core.write_a_post',
'core.write_a_reply'
];
}