diff --git a/resources/original_messages.json b/resources/original_messages.json
index 9bb2656801..fad156b86b 100644
--- a/resources/original_messages.json
+++ b/resources/original_messages.json
@@ -37,7 +37,7 @@
},
"cancel": {
"description": "The text on a button to cancel the current operation.",
- "message": "cancel"
+ "message": "Cancel"
},
"change_language_page_title": {
"description": "The menu item text and title of a page for a user to set the app language.",
@@ -59,6 +59,90 @@
"description": "A status message on a particular server that indicates the application is currently trying to connect to the labeled server.",
"message": "Connecting..."
},
+ "contact_page_title": {
+ "description": "The menu item text and title of a page for a user to contact the support team.",
+ "message": "Contact us"
+ },
+ "contact_view_exit_cannot_add_server": {
+ "description": "Message shown to users who are trying to contact support about an unsupported issue.",
+ "message": "The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed $START_OF_LINK$here$END_OF_LINK$ and then contact the person who gave you the access key to troubleshoot this issue.",
+ "placeholders": {
+ "END_OF_LINK": {
+ "content": "{closeLink}"
+ },
+ "START_OF_LINK": {
+ "content": "{openLink}"
+ }
+ }
+ },
+ "contact_view_exit_connection": {
+ "description": "Message shown to users who are trying to contact support about an unsupported issue.",
+ "message": "The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps listed $START_OF_LINK$hereEND_OF_LINK and then contact the person who gave you the access key to troubleshoot this issue.",
+ "placeholders": {
+ "END_OF_LINK": {
+ "content": "{closeLink}"
+ },
+ "START_OF_LINK": {
+ "content": "{openLink}"
+ }
+ }
+ },
+ "contact_view_exit_no_server": {
+ "description": "Message shown to users who are trying to contact support about an unsupported issue.",
+ "message": "The Outline team does not distribute free or paid access keys. $START_OF_LINK$Learn more about how to get an access key.END_OF_LINK",
+ "placeholders": {
+ "END_OF_LINK": {
+ "content": "{closeLink}"
+ },
+ "START_OF_LINK": {
+ "content": "{openLink}"
+ }
+ }
+ },
+ "contact_view_exit_open_ticket": {
+ "description": "Message shown to users who are trying to contact support again while having a pending support ticket open.",
+ "message": "We are currently experiencing high support volume and appreciate your patience. Please do not submit a new request for this concern. If you have additional information to provide, please reply to the initial email about this request."
+ },
+ "contact_view_intro": {
+ "description": "Introduction message to users who are looking to contact support.",
+ "message": "Tell us how we can help. Please explain your issue in detail and do not enter personal information that is not requested below."
+ },
+ "contact_view_issue_cannot_add_server": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "I am having trouble adding a server using my access key"
+ },
+ "contact_view_issue_connection": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "I am having trouble connecting to my Outline VPN server"
+ },
+ "contact_view_issue_general": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "General feedback & suggestions"
+ },
+ "contact_view_issue_installation": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "I am having trouble installing Outline"
+ },
+ "contact_view_issue_managing": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "I need assistance managing my Outline VPN server or helping others connect to it"
+ },
+ "contact_view_issue_no_server": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "I need an access key"
+ },
+ "contact_view_issue_performance": {
+ "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.",
+ "message": "My internet access is slow while connected to my Outline VPN server"
+ },
+ "contact_view_issue": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Outline issue"
+ },
+ "contact_view_open_ticket": {
+ "description": "Label for a Contact Support form asking if the user already has a pending ticket open with support.",
+ "message": "Do you have an open ticket for this issue?"
+ },
"data_collection": {
"description": "The text for a main menu item that takes the user to a webpage explaining the application's data collection policies.",
"message": "Data collection"
@@ -215,6 +299,10 @@
"description": "The menu item text to navigate to the page showing all the software licenses used in building the app.",
"message": "Licenses"
},
+ "no": {
+ "description": "Negative answer to a form question.",
+ "message": "No"
+ },
"non_system_vpn_warning_detail": {
"description": "Further explanation message that the user needs to verify that their browser is connected through Outline. Outline is the product name and can be found in the translation glossary.",
"message": "Most browsers connect automatically with Outline, some do not."
@@ -469,6 +557,50 @@
"description": "Status text showing that a form is progress of being submitted.",
"message": "Submitting..."
},
+ "support_form_access_key_source": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Where did you get your access key?"
+ },
+ "support_form_cloud_provider_aws": {
+ "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.",
+ "message": "Amazon Web Services"
+ },
+ "support_form_cloud_provider_digitalocean": {
+ "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.",
+ "message": "DigitalOcean"
+ },
+ "support_form_cloud_provider_gcloud": {
+ "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.",
+ "message": "Google Cloud"
+ },
+ "support_form_cloud_provider_other": {
+ "description": "Item in a dropdown menu to select an unknown cloud provider that the user is contacting support about.",
+ "message": "Other"
+ },
+ "support_form_cloud_provider": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Cloud Provider"
+ },
+ "support_form_description": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Description of the issue"
+ },
+ "support_form_email_invalid": {
+ "description": "Error message shown to a user supplying an invalid email address on the Contact Support form.",
+ "message": "You have entered an invalid format."
+ },
+ "support_form_email": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Email"
+ },
+ "support_form_required_field": {
+ "description": "Text on a Contact Support form indicating that a field is required.",
+ "message": "Required field"
+ },
+ "support_form_subject": {
+ "description": "Label of a Contact Support form input field.",
+ "message": "Subject"
+ },
"terms": {
"description": "The text for a main menu item that takes the user to the applications Terms of Service.",
"message": "Terms"
@@ -502,5 +634,9 @@
"example": "1.0.2"
}
}
+ },
+ "yes": {
+ "description": "Affirmative answer to a form question.",
+ "message": "Yes"
}
}
\ No newline at end of file
diff --git a/src/infrastructure/i18n.ts b/src/infrastructure/i18n.ts
new file mode 100644
index 0000000000..52011620df
--- /dev/null
+++ b/src/infrastructure/i18n.ts
@@ -0,0 +1,12 @@
+import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
+
+export type FormattableMessage =
+ | string
+ | symbol
+ | object
+ | PrimitiveType
+ | FormatXMLElementFn;
+
+export interface Localizer {
+ (messageID: string, ...formatKeyValueList: FormattableMessage[]): string;
+}
diff --git a/src/www/app/app.ts b/src/www/app/app.ts
index baaef9b096..23006b0913 100644
--- a/src/www/app/app.ts
+++ b/src/www/app/app.ts
@@ -27,6 +27,7 @@ import {Settings, SettingsKey} from './settings';
import {Updater} from './updater';
import {UrlInterceptor} from './url_interceptor';
import {VpnInstaller} from './vpn_installer';
+import {Localizer} from 'src/infrastructure/i18n';
enum OUTLINE_ACCESS_KEY_SCHEME {
STATIC = 'ss',
@@ -79,7 +80,7 @@ const DEFAULT_SERVER_CONNECTION_STATUS_CHANGE_TIMEOUT = 600;
export class App {
private feedbackViewEl: polymer.Base;
- private localize: (...args: string[]) => string;
+ private localize: Localizer;
private ignoredAccessKeys: {[accessKey: string]: boolean} = {};
private serverConnectionChangeTimeouts: {[serverId: string]: boolean} = {};
diff --git a/src/www/app/main.ts b/src/www/app/main.ts
index 18ec3d8308..bd8afb11b4 100644
--- a/src/www/app/main.ts
+++ b/src/www/app/main.ts
@@ -23,6 +23,7 @@ import {makeConfig, SIP002_URI} from 'ShadowsocksConfig';
import {OutlinePlatform} from './platform';
import {Settings} from './settings';
import {TunnelFactory} from './tunnel';
+import {Localizer} from 'src/infrastructure/i18n.js';
// Used to determine whether to use Polymer functionality on app initialization failure.
let webComponentsAreReady = false;
@@ -44,7 +45,7 @@ const oncePolymerIsReady = new Promise(resolve => {
// Do not call until WebComponentsReady has fired!
function getRootEl() {
- return (document.querySelector('app-root') as {}) as polymer.Base;
+ return document.querySelector('app-root') as {} as polymer.Base;
}
function createServerRepo(
@@ -143,7 +144,7 @@ function onUnexpectedError(error: Error) {
}
// Returns Polymer's localization function. Must be called after WebComponentsReady has fired.
-export function getLocalizationFunction() {
+export function getLocalizationFunction(): Localizer {
const rootEl = getRootEl();
if (!rootEl) {
return null;
diff --git a/src/www/messages/en.json b/src/www/messages/en.json
index 8d174c6a2d..823cf34aff 100644
--- a/src/www/messages/en.json
+++ b/src/www/messages/en.json
@@ -3,13 +3,27 @@
"about-page-title": "About",
"auto-connect-dialog-detail": "Unless you disconnect from your Outline server, Outline will automatically connect next time you restart your device.",
"auto-connect-dialog-title": "Stay protected, always",
- "cancel": "cancel",
+ "cancel": "Cancel",
"change-language-page-title": "Change Language",
"close": "Close",
"connect-button-label": "Connect",
"connected-server-state": "Connected",
"connecting-server-state": "Connecting...",
"contact-page-title": "Contact us",
+ "contact-view-exit-cannot-add-server": "The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed {openLink}here{closeLink} and then contact the person who gave you the access key to troubleshoot this issue.",
+ "contact-view-exit-connection": "The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps listed {openLink}here{closeLink} and then contact the person who gave you the access key to troubleshoot this issue.",
+ "contact-view-exit-no-server": "The Outline team does not distribute free or paid access keys. {openLink}Learn more about how to get an access key.{closeLink}",
+ "contact-view-exit-open-ticket": "We are currently experiencing high support volume and appreciate your patience. Please do not submit a new request for this concern. If you have additional information to provide, please reply to the initial email about this request.",
+ "contact-view-intro": "Tell us how we can help. Please explain your issue in detail and do not enter personal information that is not requested below.",
+ "contact-view-issue-cannot-add-server": "I am having trouble adding a server using my access key",
+ "contact-view-issue-connection": "I am having trouble connecting to my Outline VPN server",
+ "contact-view-issue-general": "General feedback & suggestions",
+ "contact-view-issue-installation": "I am having trouble installing Outline",
+ "contact-view-issue-managing": "I need assistance managing my Outline VPN server or helping others connect to it",
+ "contact-view-issue-no-server": "I need an access key",
+ "contact-view-issue-performance": "My internet access is slow while connected to my Outline VPN server",
+ "contact-view-issue": "Outline issue",
+ "contact-view-open-ticket": "Do you have an open ticket for this issue?",
"data-collection": "Data collection",
"disconnect-button-label": "Disconnect",
"disconnected-server-state": "Disconnected",
@@ -44,6 +58,7 @@
"language-page-title": "Select a language",
"learn-more": "Learn More",
"licenses-page-title": "Licenses",
+ "no": "No",
"non-system-vpn-warning-detail": "Most browsers connect automatically with Outline, some do not.",
"non-system-vpn-warning-title": "Verify your browser connection",
"tray-open-window": "Open",
@@ -93,11 +108,23 @@
"servers-page-title": "Outline",
"submit": "Submit",
"submitting": "Submitting...",
+ "support-form-access-key-source": "Where did you get your access key?",
+ "support-form-cloud-provider-aws": "Amazon Web Services",
+ "support-form-cloud-provider-digitalocean": "DigitalOcean",
+ "support-form-cloud-provider-gcloud": "Google Cloud",
+ "support-form-cloud-provider-other": "Other",
+ "support-form-cloud-provider": "Cloud Provider",
+ "support-form-description": "Description of the issue",
+ "support-form-email-invalid": "You have entered an invalid format.",
+ "support-form-email": "Email",
+ "support-form-required-field": "Required field",
+ "support-form-subject": "Subject",
"terms": "Terms",
"tips": "Tips",
"undo-button-label": "Undo",
"unreachable-server-state": "Unreachable",
"unsupported-cipher": "Unsupported cipher",
"update-downloaded": "An updated version of Outline has been downloaded. It will be installed when you restart Outline.",
- "version": "Version {appVersion}"
+ "version": "Version {appVersion}",
+ "yes": "Yes"
}
diff --git a/src/www/.storybook/localize.ts b/src/www/testing/localize.ts
similarity index 71%
rename from src/www/.storybook/localize.ts
rename to src/www/testing/localize.ts
index ad96da8336..00a736a8c0 100644
--- a/src/www/.storybook/localize.ts
+++ b/src/www/testing/localize.ts
@@ -14,17 +14,11 @@
limitations under the License.
*/
+import type {FormattableMessage, Localizer} from 'src/infrastructure/i18n';
import englishMessages from '../messages/en.json';
-import IntlMessageFormat, {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
+import IntlMessageFormat from 'intl-messageformat';
-type FormattableMessage =
- | string
- | symbol
- | object
- | PrimitiveType
- | FormatXMLElementFn;
-
-export const localize = (messageID: string, ...formatKeyValueList: FormattableMessage[]): string => {
+export const localize: Localizer = (messageID: string, ...formatKeyValueList: FormattableMessage[]): string => {
const message = (englishMessages as {[messageID: string]: string})[messageID];
const formatConfigObject: Record = {};
@@ -38,8 +32,8 @@ export const localize = (messageID: string, ...formatKeyValueList: FormattableMe
return `${messageID}(${JSON.stringify(formatConfigObject)})`;
}
- // we support only english messages in the storybook, for now.
- // blocked on modern-web.dev adding support for addons:
+ // We support only english messages for now.
+ // Blocked on modern-web.dev adding support for addons:
// https://github.com/modernweb-dev/web/issues/1341
return String(new IntlMessageFormat(message, 'en').format(formatConfigObject));
};
diff --git a/src/www/ui_components/app-root.js b/src/www/ui_components/app-root.js
index 7f54ce48c1..c465fb11e4 100644
--- a/src/www/ui_components/app-root.js
+++ b/src/www/ui_components/app-root.js
@@ -298,6 +298,8 @@ export class AppRoot extends mixinBehaviors([AppLocalizeBehavior], PolymerElemen
{
let el: ContactView;
@@ -29,7 +30,7 @@ describe('ContactView', () => {
'SentryErrorReporter',
Object.getOwnPropertyNames(SentryErrorReporter.prototype)
);
- el = await fixture(html` `);
+ el = await fixture(html` `);
});
it('is defined', async () => {
@@ -37,7 +38,7 @@ describe('ContactView', () => {
});
it('hides issue selector by default', async () => {
- const issueSelector = el.shadowRoot?.querySelector('mwc-select[label="Outline issue"]');
+ const issueSelector = el.shadowRoot?.querySelector('mwc-select');
expect(issueSelector?.hasAttribute('hidden')).toBeTrue();
});
@@ -47,7 +48,7 @@ describe('ContactView', () => {
});
it('shows exit message if the user selects that they have an open ticket', async () => {
- const radioButton = el.shadowRoot!.querySelector('mwc-formfield[label="Yes"] mwc-radio')! as HTMLElement;
+ const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[0] as HTMLElement;
radioButton.click();
await nextFrame();
@@ -56,11 +57,11 @@ describe('ContactView', () => {
});
it('shows issue selector if the user selects that they have no open tickets', async () => {
- const radioButton = el.shadowRoot!.querySelector('mwc-formfield[label="No"] mwc-radio')! as HTMLElement;
+ const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[1] as HTMLElement;
radioButton.click();
await nextFrame();
- const issueSelector = el.shadowRoot!.querySelector('mwc-select[label="Outline issue"]');
+ const issueSelector = el.shadowRoot!.querySelector('mwc-select');
expect(issueSelector?.hasAttribute('hidden')).toBeFalse();
});
@@ -68,8 +69,8 @@ describe('ContactView', () => {
let issueSelector: HTMLElement;
beforeEach(async () => {
- issueSelector = el.shadowRoot!.querySelector('mwc-select[label="Outline issue"]')!;
- const radioButton: HTMLElement = el.shadowRoot!.querySelector('mwc-formfield[label="No"] mwc-radio')!;
+ issueSelector = el.shadowRoot!.querySelector('mwc-select')!;
+ const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[1] as HTMLElement;
radioButton.click();
await nextFrame();
});
diff --git a/src/www/views/contact_view/index.ts b/src/www/views/contact_view/index.ts
index 64abbe0f88..c7286f5b56 100644
--- a/src/www/views/contact_view/index.ts
+++ b/src/www/views/contact_view/index.ts
@@ -17,6 +17,7 @@
import {html, css, LitElement, TemplateResult, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {Ref, createRef, ref} from 'lit/directives/ref.js';
+import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import '@material/mwc-circular-progress';
import '@material/mwc-radio';
import '@material/mwc-select';
@@ -26,10 +27,11 @@ import {SingleSelectedEvent} from '@material/mwc-list/mwc-list';
import './support_form';
import {CardType} from '../shared/card';
-import {IssueType} from './issue_type';
+import {IssueType, UNSUPPORTED_ISSUE_TYPE_HELPPAGES} from './issue_type';
import {AppType} from './app_type';
import {FormValues, SupportForm, ValidFormValues} from './support_form';
import {OutlineErrorReporter} from '../../shared/error_reporter';
+import {Localizer} from 'src/infrastructure/i18n';
/** The possible steps in the stepper. Only one step is shown at a time. */
enum Step {
@@ -88,16 +90,9 @@ export class ContactView extends LitElement {
`,
];
- private static readonly Issues = new Map([
- [IssueType.INSTALLATION, 'I am having trouble installing Outline'],
- [IssueType.NO_SERVER, 'I need an access key'],
- [IssueType.ADDING_SERVER, 'I am having trouble adding a server using my access key'],
- [IssueType.CONNECTION, 'I am having trouble connecting to my Outline VPN server'],
- [IssueType.MANAGING, 'I need assistance managing my Outline VPN server or helping others connect to it'],
- [IssueType.INTERNET_SPEED, 'My internet access is slow while connected to my Outline VPN server'],
- [IssueType.GENERAL, 'General feedback & suggestions'],
- ]);
+ private static readonly ISSUES = Object.values(IssueType);
+ @property({type: Function}) localize: Localizer = msg => msg;
@property({type: String}) variant: AppType = AppType.CLIENT;
@property({type: Object, attribute: 'error-reporter'}) errorReporter: OutlineErrorReporter;
@@ -108,17 +103,17 @@ export class ContactView extends LitElement {
private readonly openTicketSelectionOptions: Array<{
ref: Ref;
value: boolean;
- label: string;
+ labelMsg: string;
}> = [
{
ref: createRef(),
value: true,
- label: 'Yes',
+ labelMsg: 'yes',
},
{
ref: createRef(),
value: false,
- label: 'No',
+ labelMsg: 'no',
},
];
@@ -131,11 +126,7 @@ export class ContactView extends LitElement {
const radio = e.target as Radio;
const hasOpenTicket = radio.value;
if (hasOpenTicket) {
- this.exitTemplate = html`
- We are currently experiencing high support volume and appreciate your patience. Please do not submit a new
- request for this concern. If you have additional information to provide, please reply to the initial email about
- this request.
- `;
+ this.exitTemplate = html`${this.localize('contact-view-exit-open-ticket')}`;
this.step = Step.EXIT;
return;
}
@@ -146,48 +137,19 @@ export class ContactView extends LitElement {
}
private selectIssue(e: SingleSelectedEvent) {
- this.selectedIssueType = Array.from(ContactView.Issues.keys())[e.detail.index];
- switch (this.selectedIssueType) {
- case IssueType.INSTALLATION:
- case IssueType.MANAGING:
- case IssueType.INTERNET_SPEED:
- case IssueType.GENERAL:
- this.step = Step.FORM;
- break;
- case IssueType.NO_SERVER:
- // TODO: Send users to localized support pages based on chosen language.
- this.exitTemplate = html`
- The Outline team does not distribute free or paid access keys.
-
- Learn more about how to get an access key.
-
- `;
- this.step = Step.EXIT;
- break;
- case IssueType.ADDING_SERVER:
- this.exitTemplate = html`
- The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed
-
- here
-
- and then contact the person who gave you the access key to troubleshoot this issue.
- `;
- this.step = Step.EXIT;
- break;
- case IssueType.CONNECTION:
- this.exitTemplate = html`
- The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps
- listed
-
- here
-
- and then contact the person who gave you the access key to troubleshoot this issue.
- `;
- this.step = Step.EXIT;
- break;
- default:
- throw Error('Unexpected issue found');
+ this.selectedIssueType = ContactView.ISSUES[e.detail.index];
+
+ if (UNSUPPORTED_ISSUE_TYPE_HELPPAGES.has(this.selectedIssueType)) {
+ // TODO: Send users to localized support pages based on chosen language.
+ this.exitTemplate = this.localizeWithUrl(
+ `contact-view-exit-${this.selectedIssueType}`,
+ UNSUPPORTED_ISSUE_TYPE_HELPPAGES.get(this.selectedIssueType)
+ );
+ this.step = Step.EXIT;
+ return;
}
+
+ this.step = Step.FORM;
}
private reset() {
@@ -219,10 +181,15 @@ export class ContactView extends LitElement {
this.dispatchEvent(new CustomEvent('success'));
}
+ // TODO: Consider moving this functionality to a more centralized place for re-use.
+ private localizeWithUrl(messageID: string, url: string): TemplateResult {
+ const openLink = ``;
+ const closeLink = '';
+ return html` ${unsafeHTML(this.localize(messageID, 'openLink', openLink, 'closeLink', closeLink))} `;
+ }
+
private get renderIntroTemplate(): TemplateResult {
- return html`
-
Tell us how we can help. Please do not enter personal information that is not requested below.
- `;
+ return html`
${this.localize('contact-view-intro')}
`;
}
private get renderForm(): TemplateResult | typeof nothing {
@@ -234,6 +201,7 @@ export class ContactView extends LitElement {
return html`
Do you have an open ticket for this issue?
+
${this.localize('contact-view-open-ticket')}
${this.openTicketSelectionOptions.map(
element => html`