Skip to content

Commit

Permalink
feat(www): localize new contact view (#1735)
Browse files Browse the repository at this point in the history
* Localize the `SupportForm` component.

* Remove duplicate required fields helper.

* Localize the `ContactView` component.

* Localize the intro messages.

* Use existing "Submit" and "Cancel" messages.

* Fix import for `LocalizeFunc`.

* Move issue type constant into `issue_type.ts`.

* Rename `LocalizeFunc` to `Localizer`.

* Import types only.

* Add a TODO comment to consider moving `localizeWithUrl` to some centralized place.

* Actually pass through the `localize` property.

* Fix tests that rely on localization.

* Update the original message files.

* Add "Yes" and "No" to original messages file.
  • Loading branch information
sbruens authored Oct 4, 2023
1 parent 08b09ee commit 17fdb52
Show file tree
Hide file tree
Showing 19 changed files with 298 additions and 144 deletions.
138 changes: 137 additions & 1 deletion resources/original_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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"
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -502,5 +634,9 @@
"example": "1.0.2"
}
}
},
"yes": {
"description": "Affirmative answer to a form question.",
"message": "Yes"
}
}
12 changes: 12 additions & 0 deletions src/infrastructure/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';

export type FormattableMessage =
| string
| symbol
| object
| PrimitiveType
| FormatXMLElementFn<symbol | object, string | symbol | object | (string | symbol | object)[]>;

export interface Localizer {
(messageID: string, ...formatKeyValueList: FormattableMessage[]): string;
}
3 changes: 2 additions & 1 deletion src/www/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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} = {};

Expand Down
5 changes: 3 additions & 2 deletions src/www/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,7 +45,7 @@ const oncePolymerIsReady = new Promise<void>(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(
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 29 additions & 2 deletions src/www/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
16 changes: 5 additions & 11 deletions src/www/.storybook/localize.ts → src/www/testing/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<symbol | object, string | symbol | object | (string | symbol | object)[]>;

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<string, FormattableMessage> = {};

Expand All @@ -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));
};
2 changes: 2 additions & 0 deletions src/www/ui_components/app-root.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ export class AppRoot extends mixinBehaviors([AppLocalizeBehavior], PolymerElemen
<contact-view
name="contact"
id="contactView"
localize="[[localize]]"
variant="client"
error-reporter="[[errorReporter]]"
on-success="showContactSuccessToast"
on-error="showContactErrorToast"
Expand Down
15 changes: 8 additions & 7 deletions src/www/views/contact_view/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {ContactView} from './index';
import {fixture, html, nextFrame, oneEvent} from '@open-wc/testing';
import {SupportForm} from './support_form';
import {OutlineErrorReporter, SentryErrorReporter} from '../../shared/error_reporter';
import {localize} from '../../testing/localize';

describe('ContactView', () => {
let el: ContactView;
Expand All @@ -29,15 +30,15 @@ describe('ContactView', () => {
'SentryErrorReporter',
Object.getOwnPropertyNames(SentryErrorReporter.prototype)
);
el = await fixture(html` <contact-view .errorReporter=${mockErrorReporter}></contact-view> `);
el = await fixture(html` <contact-view .localize=${localize} .errorReporter=${mockErrorReporter}></contact-view> `);
});

it('is defined', async () => {
expect(el).toBeInstanceOf(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();
});

Expand All @@ -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();

Expand All @@ -56,20 +57,20 @@ 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();
});

describe('when the user selects issue', () => {
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();
});
Expand Down
Loading

0 comments on commit 17fdb52

Please sign in to comment.