Migrate more strings to translation keys (#11574)
This commit is contained in:
parent
3201ae3534
commit
36a7a96e0e
72 changed files with 6593 additions and 6152 deletions
|
@ -51,7 +51,7 @@ export const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
|
|||
const privacyPolicyLink = privacyPolicyUrl ? (
|
||||
<span>
|
||||
{_t(
|
||||
"You can read all our terms <PrivacyPolicyUrl>here</PrivacyPolicyUrl>",
|
||||
"analytics|privacy_policy",
|
||||
{},
|
||||
{
|
||||
PrivacyPolicyUrl: (sub) => {
|
||||
|
@ -71,33 +71,18 @@ export const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
|
|||
<BaseDialog
|
||||
className="mx_AnalyticsLearnMoreDialog"
|
||||
contentId="mx_AnalyticsLearnMore"
|
||||
title={_t("Help improve %(analyticsOwner)s", { analyticsOwner })}
|
||||
title={_t("analytics|enable_prompt", { analyticsOwner })}
|
||||
onFinished={onFinished}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_AnalyticsLearnMore_image_holder" />
|
||||
<div className="mx_AnalyticsLearnMore_copy">
|
||||
{_t(
|
||||
"Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.",
|
||||
{ analyticsOwner },
|
||||
)}
|
||||
{_t("analytics|pseudonymous_usage_data", { analyticsOwner })}
|
||||
</div>
|
||||
<ul className="mx_AnalyticsLearnMore_bullets">
|
||||
<li>
|
||||
{_t(
|
||||
"We <Bold>don't</Bold> record or profile any account data",
|
||||
{},
|
||||
{ Bold: (sub) => <b>{sub}</b> },
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{_t(
|
||||
"We <Bold>don't</Bold> share information with third parties",
|
||||
{},
|
||||
{ Bold: (sub) => <b>{sub}</b> },
|
||||
)}
|
||||
</li>
|
||||
<li>{_t("You can turn this off anytime in settings")}</li>
|
||||
<li>{_t("analytics|bullet_1", {}, { Bold: (sub) => <b>{sub}</b> })}</li>
|
||||
<li>{_t("analytics|bullet_2", {}, { Bold: (sub) => <b>{sub}</b> })}</li>
|
||||
<li>{_t("analytics|disable_prompt")}</li>
|
||||
</ul>
|
||||
{privacyPolicyLink}
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class SessionRestoreErrorDialog extends React.Component<IProps> {
|
|||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
dialogButtons = (
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send Logs")}
|
||||
primaryButton={_t("bug_reporting|send_logs")}
|
||||
onPrimaryButtonClick={this.sendBugReport}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
|
|
|
@ -92,14 +92,14 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
case SERVICE_TYPES.IS:
|
||||
return (
|
||||
<div>
|
||||
{_t("Identity server")}
|
||||
{_t("common|identity_server")}
|
||||
<br />({host})
|
||||
</div>
|
||||
);
|
||||
case SERVICE_TYPES.IM:
|
||||
return (
|
||||
<div>
|
||||
{_t("Integration manager")}
|
||||
{_t("common|integration_manager")}
|
||||
<br />({host})
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> {
|
|||
<React.Fragment>
|
||||
<p>
|
||||
{_t(
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
|
||||
"bug_reporting|create_new_issue",
|
||||
{},
|
||||
{
|
||||
newIssueLink: (sub) => {
|
||||
|
|
|
@ -133,7 +133,7 @@ export default class EventListSummary extends React.Component<
|
|||
|
||||
const desc = formatCommaSeparatedList(descs);
|
||||
|
||||
return _t("%(nameList)s %(transitionList)s", { nameList, transitionList: desc });
|
||||
return _t("timeline|summary|format", { nameList, transitionList: desc });
|
||||
});
|
||||
|
||||
if (!summaries) {
|
||||
|
@ -250,101 +250,101 @@ export default class EventListSummary extends React.Component<
|
|||
case TransitionType.Joined:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|joined_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|joined", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.Left:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|left_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|left", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.JoinedAndLeft:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|joined_and_left_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|joined_and_left", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.LeftAndJoined:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|rejoined_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|rejoined", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.InviteReject:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)srejected their invitations %(count)s times", {
|
||||
? _t("timeline|summary|rejected_invite_multiple", {
|
||||
severalUsers: "",
|
||||
count,
|
||||
})
|
||||
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count });
|
||||
: _t("timeline|summary|rejected_invite", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.InviteWithdrawal:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
|
||||
? _t("timeline|summary|invite_withdrawn_multiple", {
|
||||
severalUsers: "",
|
||||
count,
|
||||
})
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count });
|
||||
: _t("timeline|summary|invite_withdrawn", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.Invited:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("were invited %(count)s times", { count })
|
||||
: _t("was invited %(count)s times", { count });
|
||||
? _t("timeline|summary|invited_multiple", { count })
|
||||
: _t("timeline|summary|invited", { count });
|
||||
break;
|
||||
case TransitionType.Banned:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("were banned %(count)s times", { count })
|
||||
: _t("was banned %(count)s times", { count });
|
||||
? _t("timeline|summary|banned_multiple", { count })
|
||||
: _t("timeline|summary|banned", { count });
|
||||
break;
|
||||
case TransitionType.Unbanned:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("were unbanned %(count)s times", { count })
|
||||
: _t("was unbanned %(count)s times", { count });
|
||||
? _t("timeline|summary|unbanned_multiple", { count })
|
||||
: _t("timeline|summary|unbanned", { count });
|
||||
break;
|
||||
case TransitionType.Kicked:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("were removed %(count)s times", { count })
|
||||
: _t("was removed %(count)s times", { count });
|
||||
? _t("timeline|summary|kicked_multiple", { count })
|
||||
: _t("timeline|summary|kicked", { count });
|
||||
break;
|
||||
case TransitionType.ChangedName:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|changed_name_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|changed_name", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.ChangedAvatar:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)schanged their profile picture %(count)s times", {
|
||||
? _t("timeline|summary|changed_avatar_multiple", {
|
||||
severalUsers: "",
|
||||
count,
|
||||
})
|
||||
: _t("%(oneUser)schanged their profile picture %(count)s times", { oneUser: "", count });
|
||||
: _t("timeline|summary|changed_avatar", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.NoChange:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|no_change_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|no_change", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.ServerAcl:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)schanged the server ACLs %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|server_acls_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|server_acls", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.ChangedPins:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t(
|
||||
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times",
|
||||
"timeline|summary|pinned_events_multiple",
|
||||
{ severalUsers: "", count },
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -355,7 +355,7 @@ export default class EventListSummary extends React.Component<
|
|||
},
|
||||
)
|
||||
: _t(
|
||||
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times",
|
||||
"timeline|summary|pinned_events",
|
||||
{ oneUser: "", count },
|
||||
{
|
||||
a: (sub) => (
|
||||
|
@ -369,14 +369,14 @@ export default class EventListSummary extends React.Component<
|
|||
case TransitionType.MessageRemoved:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)sremoved a message %(count)s times", { severalUsers: "", count })
|
||||
: _t("%(oneUser)sremoved a message %(count)s times", { oneUser: "", count });
|
||||
? _t("timeline|summary|redacted_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|redacted", { oneUser: "", count });
|
||||
break;
|
||||
case TransitionType.HiddenEvent:
|
||||
res =
|
||||
userCount > 1
|
||||
? _t("%(severalUsers)ssent %(count)s hidden messages", { severalUsers: "", count })
|
||||
: _t("%(oneUser)ssent %(count)s hidden messages", { oneUser: "", count });
|
||||
? _t("timeline|summary|hidden_event_multiple", { severalUsers: "", count })
|
||||
: _t("timeline|summary|hidden_event", { oneUser: "", count });
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,63 +87,63 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
this.categories = [
|
||||
{
|
||||
id: "recent",
|
||||
name: _t("Frequently Used"),
|
||||
name: _t("emoji|category_frequently_used"),
|
||||
enabled: this.recentlyUsed.length > 0,
|
||||
visible: this.recentlyUsed.length > 0,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "people",
|
||||
name: _t("Smileys & People"),
|
||||
name: _t("emoji|category_smileys_people"),
|
||||
enabled: true,
|
||||
visible: true,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "nature",
|
||||
name: _t("Animals & Nature"),
|
||||
name: _t("emoji|category_animals_nature"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "foods",
|
||||
name: _t("Food & Drink"),
|
||||
name: _t("emoji|category_food_drink"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "activity",
|
||||
name: _t("Activities"),
|
||||
name: _t("emoji|category_activities"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "places",
|
||||
name: _t("Travel & Places"),
|
||||
name: _t("emoji|category_travel_places"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "objects",
|
||||
name: _t("Objects"),
|
||||
name: _t("emoji|category_objects"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "symbols",
|
||||
name: _t("Symbols"),
|
||||
name: _t("emoji|category_symbols"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
},
|
||||
{
|
||||
id: "flags",
|
||||
name: _t("Flags"),
|
||||
name: _t("emoji|category_flags"),
|
||||
enabled: true,
|
||||
visible: false,
|
||||
ref: React.createRef(),
|
||||
|
|
|
@ -95,7 +95,7 @@ class Header extends React.PureComponent<IProps> {
|
|||
<nav
|
||||
className="mx_EmojiPicker_header"
|
||||
role="tablist"
|
||||
aria-label={_t("Categories")}
|
||||
aria-label={_t("emoji|categories")}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{this.props.categories.map((category) => {
|
||||
|
|
|
@ -64,7 +64,7 @@ class QuickReactions extends React.Component<IProps, IState> {
|
|||
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
|
||||
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
|
||||
{!this.state.hover ? (
|
||||
_t("Quick Reactions")
|
||||
_t("emoji|quick_reactions")
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<span className="mx_EmojiPicker_name">{this.state.hover.label}</span>
|
||||
|
@ -72,7 +72,7 @@ class QuickReactions extends React.Component<IProps, IState> {
|
|||
</React.Fragment>
|
||||
)}
|
||||
</h2>
|
||||
<Toolbar className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
|
||||
<Toolbar className="mx_EmojiPicker_list" aria-label={_t("emoji|quick_reactions")}>
|
||||
{QUICK_REACTIONS.map((emoji) => (
|
||||
<Emoji
|
||||
key={emoji.hexcode}
|
||||
|
|
|
@ -117,7 +117,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
"For extra security, verify this user by checking a one-time code on both of your devices.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("To be secure, do this in person or use a trusted way to communicate.")}</p>
|
||||
<p>{_t("encryption|verification|in_person")}</p>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -90,14 +90,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const noCommonMethodError: JSX.Element | null =
|
||||
!showSAS && !showQR ? (
|
||||
<p>
|
||||
{_t(
|
||||
"The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||
{ brand },
|
||||
)}
|
||||
</p>
|
||||
) : null;
|
||||
!showSAS && !showQR ? <p>{_t("encryption|verification|no_support_qr_emoji", { brand })}</p> : null;
|
||||
|
||||
if (this.props.layout === "dialog") {
|
||||
// HACK: This is a terrible idea.
|
||||
|
@ -106,7 +99,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
if (showQR) {
|
||||
qrBlockDialog = (
|
||||
<div className="mx_VerificationPanel_QRPhase_startOption">
|
||||
<p>{_t("Scan this unique code")}</p>
|
||||
<p>{_t("encryption|verification|qr_prompt")}</p>
|
||||
<VerificationQRCode qrCodeBytes={this.state.qrCodeBytes} />
|
||||
</div>
|
||||
);
|
||||
|
@ -114,9 +107,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
if (showSAS) {
|
||||
sasBlockDialog = (
|
||||
<div className="mx_VerificationPanel_QRPhase_startOption">
|
||||
<p>{_t("Compare unique emoji")}</p>
|
||||
<p>{_t("encryption|verification|sas_prompt")}</p>
|
||||
<span className="mx_VerificationPanel_QRPhase_helpText">
|
||||
{_t("Compare a unique set of emoji if you don't have a camera on either device")}
|
||||
{_t("encryption|verification|sas_description")}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
disabled={this.state.emojiButtonClicked}
|
||||
|
@ -131,7 +124,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const or =
|
||||
qrBlockDialog && sasBlockDialog ? (
|
||||
<div className="mx_VerificationPanel_QRPhase_betweenText">
|
||||
{_t("%(qrCode)s or %(emojiCompare)s", {
|
||||
{_t("encryption|verification|qr_or_sas", {
|
||||
emojiCompare: "",
|
||||
qrCode: "",
|
||||
})}
|
||||
|
@ -139,7 +132,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
) : null;
|
||||
return (
|
||||
<div>
|
||||
{_t("Verify this device by completing one of the following:")}
|
||||
{_t("encryption|verification|qr_or_sas_header")}
|
||||
<div className="mx_VerificationPanel_QRPhase_startOptions">
|
||||
{qrBlockDialog}
|
||||
{or}
|
||||
|
|
|
@ -104,6 +104,6 @@ export default class IntegrationManager extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
return <iframe title={_t("Integration manager")} src={this.props.url} onError={this.onError} />;
|
||||
return <iframe title={_t("common|integration_manager")} src={this.props.url} onError={this.onError} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
|||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("IRC (Experimental)")}
|
||||
{_t("settings|appearance|layout_irc")}
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={groupClasses}>
|
||||
|
@ -121,7 +121,7 @@ export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
|||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Message bubbles")}
|
||||
{_t("settings|appearance|layout_bubbles")}
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -396,7 +396,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
sectionTitle = _t("Identity server");
|
||||
sectionTitle = _t("common|identity_server");
|
||||
bodyText = _t(
|
||||
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.",
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class VerificationCancelled extends React.Component<IProps> {
|
|||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("The other party cancelled the verification.")}</p>
|
||||
<p>{_t("encryption|verification|other_party_cancelled")}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|ok")}
|
||||
hasCancel={false}
|
||||
|
|
|
@ -27,8 +27,8 @@ export default class VerificationComplete extends React.Component<IProps> {
|
|||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<h2>{_t("Verified!")}</h2>
|
||||
<p>{_t("You've successfully verified this user.")}</p>
|
||||
<h2>{_t("encryption|verification|complete_title")}</h2>
|
||||
<p>{_t("encryption|verification|complete_description")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
|
|
|
@ -180,10 +180,10 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
confirm = (
|
||||
<div className="mx_VerificationShowSas_buttonRow">
|
||||
<AccessibleButton onClick={this.onDontMatchClick} kind="danger">
|
||||
{_t("They don't match")}
|
||||
{_t("encryption|verification|sas_no_match")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onMatchClick} kind="primary">
|
||||
{_t("They match")}
|
||||
{_t("encryption|verification|sas_match")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -193,11 +193,7 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
<div className="mx_VerificationShowSas">
|
||||
<p>{sasCaption}</p>
|
||||
{sasDisplay}
|
||||
<p>
|
||||
{this.props.isSelf
|
||||
? ""
|
||||
: _t("To be secure, do this in person or use a trusted way to communicate.")}
|
||||
</p>
|
||||
<p>{this.props.isSelf ? "" : _t("encryption|verification|in_person")}</p>
|
||||
{confirm}
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue