Skip to content

Commit

Permalink
Initial localization implementation (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
haomingli2020 authored Apr 11, 2024
1 parent 5111fc7 commit 7bd67c5
Show file tree
Hide file tree
Showing 32 changed files with 2,487 additions and 1,381 deletions.
2 changes: 1 addition & 1 deletion local-testing/amazon-connect-chat-interface.js

Large diffs are not rendered by default.

2,304 changes: 964 additions & 1,340 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
"postcss-preset-env": "^9.5.2",
"postcss-safe-parser": "^7.0.0",
"prop-types": "^15.8.1",
"react-intl": "^6.3.2",
"react-dev-utils": "^12.0.0",
"react-render-html": "^0.6.0",
"react-test-renderer": "^16.6.3",
Expand Down
16 changes: 15 additions & 1 deletion src/components/Chat/Chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import PT from "prop-types";
import { FormattedMessage } from "react-intl";
import { CONTACT_STATUS } from "../../constants/global";
import ChatTranscriptor from "./ChatTranscriptor";
import ChatComposer from "./ChatComposer";
Expand Down Expand Up @@ -73,7 +74,13 @@ const defaultHeaderConfig = {
render: () => {
return (
<HeaderWrapper>
<WelcomeText type={'h2'}>Hi there! </WelcomeText>
<WelcomeText type={'h2'}>
<FormattedMessage
id="header.headerText"
defaultMessage= "Hi there! "
/>
</WelcomeText>
{/*TODO: translate below texts*/}
<Text type={'p'}>This is an example of how customers experience chat on your website</Text>
</HeaderWrapper>
)
Expand Down Expand Up @@ -136,6 +143,9 @@ export default class Chat extends Component {
componentDidMount() {
this.init(this.props.chatSession);
this.resetChatHeight();
if (typeof this.props.changeLanguage === "function") {
this.props.changeLanguage(this.props.language);
}
this.logger && this.logger.info("Component mounted.")
}

Expand All @@ -144,6 +154,10 @@ export default class Chat extends Component {
this.cleanUp(prevProps.chatSession);
this.init(this.props.chatSession);
}
if (prevProps.language !== this.props.language &&
typeof this.props.changeLanguage === "function") {
this.props.changeLanguage(this.props.language);
}
}

componentWillUnmount() {
Expand Down
28 changes: 16 additions & 12 deletions src/components/Chat/Chat.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import Chat from './Chat';
import ThemeProvider from '../../theme/ThemeProvider';
import { render } from "@testing-library/react";
Expand Down Expand Up @@ -63,20 +64,22 @@ describe('<Chat />', () => {

test("should reset the header height", () => {
const mockResetHeightMethod = jest.spyOn(Chat.prototype, "resetChatHeight");
render(<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>);
render(<IntlProvider onError={jest.fn} locale="en">
<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>
</IntlProvider>);
expect(screen.getByTestId('amazon-connect-chat-wrapper')).not.toBe(null);
expect(mockResetHeightMethod).toBeCalled();
})

test("Should be able to jitter to fix iphone mobile scroll issue", () => {
const mockResetHeightMethod = jest.spyOn(Chat.prototype, "resetChatHeight");
const mockComposer = render(
<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>
);
const mockComposer = render(<IntlProvider onError={jest.fn} locale="en">
<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>
</IntlProvider>);
expect(screen.getByTestId('amazon-connect-chat-wrapper')).not.toBe(null);
expect(mockResetHeightMethod).toBeCalled();

Expand All @@ -88,10 +91,11 @@ describe('<Chat />', () => {

test("should send ReadReceipt when a message is visible in the viewport", () => {
ChatTranscriptor.mockClear();
render(
<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>)
render(<IntlProvider onError={jest.fn} locale="en">
<ThemeProvider>
<Chat {...mockProps} />
</ThemeProvider>
</IntlProvider>)
ChatTranscriptor.mock.calls[0][0].sendReadReceipt({});
expect(mockProps.chatSession.sendReadReceipt).toHaveBeenCalled();
})
Expand Down
15 changes: 13 additions & 2 deletions src/components/Chat/ChatActionBar/ChatActionBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import * as React from "react";
import { FormattedMessage } from "react-intl";
import styled from "styled-components";
import PT from "prop-types";
import { Button } from "connect-core";
Expand Down Expand Up @@ -99,7 +100,12 @@ export default class ChatActionBar extends React.Component {
type="default"
onClick={onEndChat}
>
<span>End chat</span>
<span>
<FormattedMessage
id="Chat.EndChat"
defaultMessage="End chat"
/>
</span>
</ActionButton>
</React.Fragment>
)}
Expand All @@ -111,7 +117,12 @@ export default class ChatActionBar extends React.Component {
type="default"
onClick={onClose}
>
<span>Close</span>
<span>
<FormattedMessage
id="Chat.Close"
defaultMessage="Close"
/>
</span>
</ActionButton>
</React.Fragment>

Expand Down
12 changes: 10 additions & 2 deletions src/components/Chat/ChatActionBar/ChatActionBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
// SPDX-License-Identifier: MIT-0

import React from 'react';
import ReactDOM from 'react-dom';
import { IntlProvider } from 'react-intl';
import ChatActionBar from './ChatActionBar';
import { ThemeProvider } from '../../../theme';

describe('<ChatActionBar />', () => {
test("Style should match the snapshot", () => {
const addMessageFn = jest.fn();
const tree = createTree(<ThemeProvider><ChatActionBar contactStatus="connected"/></ThemeProvider>);
const tree = createTree(<ThemeProvider>
<IntlProvider
locale="en"
onError={jest.fn}
key="en"
messages={{}}>
<ChatActionBar contactStatus="connected"/>
</IntlProvider>
</ThemeProvider>);
expect(tree).toMatchSnapshot();
});
let wrapper, instance;
Expand Down
7 changes: 6 additions & 1 deletion src/components/Chat/ChatComposer/ChatComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import React, { useState, useLayoutEffect, useEffect, useRef, useMemo } from "react";
import { useIntl } from 'react-intl'
import styled from "styled-components";
import throttle from "lodash/throttle";
import PT from "prop-types";
Expand Down Expand Up @@ -307,7 +308,11 @@ export default function ChatComposer({ addMessage, addAttachment, onTyping, cont
addAttachment(contactId, file);
}

const ariaLabel = "Type a message";
const intl = useIntl();
const ariaLabel = intl.formatMessage({
id: "chatComposer.placeholder",
defaultMessage: "Type a message"
});
const placeholder = attachment == null ? ariaLabel : "";

const richMessagingComposer = (
Expand Down
8 changes: 7 additions & 1 deletion src/components/Chat/ChatComposer/ChatComposer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import React from "react";
import { IntlProvider } from 'react-intl';
import ChatComposer from "./ChatComposer";
import { ThemeProvider } from "../../../theme";
import { render, fireEvent, screen, prettyDOM } from "@testing-library/react";
Expand All @@ -22,7 +23,12 @@ let mockProps;
function renderElement(props) {
mockComposer = render(
<ThemeProvider>
<ChatComposer {...props} />
<IntlProvider
locale="en"
key="en"
messages={{}}>
<ChatComposer {...props}/>
</IntlProvider>
</ThemeProvider>
);
}
Expand Down
20 changes: 19 additions & 1 deletion src/components/Chat/ChatContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { defaultTheme } from "connect-theme";
import { FlexRowContainer } from "connect-theme/Helpers";
import { CHAT_FEATURE_TYPES } from "./constants";
import { ContentType } from "./datamodel/Model";
import { LanguageProvider, LanguageContext } from "../../context/LanguageContext";

const ButtonWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -46,6 +47,7 @@ class ChatContainer extends Component {
chatSession: null,
composerConfig: {},
status: "NotInitiated",
language: 'en_US'
};

this.submitChatInitiationHandler = this.initiateChatSession.bind(this);
Expand Down Expand Up @@ -101,6 +103,7 @@ class ChatContainer extends Component {
(input.featurePermissions && input.featurePermissions[CHAT_FEATURE_TYPES.ATTACHMENTS]) ||
(chatDetails.featurePermissions && chatDetails.featurePermissions[CHAT_FEATURE_TYPES.ATTACHMENTS]);
const richMessagingEnabled = typeof input.supportedMessagingContentTypes === "string" ? input.supportedMessagingContentTypes.split(",").includes(ContentType.MESSAGE_CONTENT_TYPE.TEXT_MARKDOWN) : false;
const language = input.language || "en_US";

this.setState({
status: "Initiated",
Expand All @@ -109,6 +112,7 @@ class ChatContainer extends Component {
attachmentsEnabled,
richMessagingEnabled,
},
language
});
success && success(chatSession);
} catch (error) {
Expand Down Expand Up @@ -153,7 +157,21 @@ class ChatContainer extends Component {
</Wrapper>
);
}
return <Chat chatSession={this.state.chatSession} composerConfig={this.state.composerConfig} onEnded={this.resetState} {...this.props} />;
return (
<LanguageProvider>
<LanguageContext.Consumer>
{({changeLanguage}) => (<>
<Chat
chatSession={this.state.chatSession}
composerConfig={this.state.composerConfig}
onEnded={this.resetState}
changeLanguage={changeLanguage}
language={this.state.language}
{...this.props} />
</>)}
</LanguageContext.Consumer>
</LanguageProvider>
);
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/components/Chat/ChatContainer.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import ChatContainer from './ChatContainer';
import { render, waitFor } from '@testing-library/react';
import ThemeProvider from '../../theme/ThemeProvider';
import request from '../../utils/fetchRequest';
import EventBus from "./eventbus";
import { LanguageProvider } from "../../context/LanguageContext";

jest.mock('../../utils/fetchRequest');

Expand Down Expand Up @@ -107,9 +109,13 @@ describe("<ChatContainer />", () => {

const renderComponent = () =>
render(
<ThemeProvider>
<ChatContainer {...config} />
</ThemeProvider>
<LanguageProvider>
<IntlProvider locale="en">
<ThemeProvider>
<ChatContainer {...config} />
</ThemeProvider>
</IntlProvider>
</LanguageProvider>
);

it("should render component successfully", async () => {
Expand Down
35 changes: 30 additions & 5 deletions src/components/Chat/ChatTranscriptor/ChatMessages/ChatMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import React, { PureComponent } from "react";
import { FormattedMessage } from "react-intl";
import styled from "styled-components";
import PT from "prop-types";
import Linkify from "react-linkify";
Expand Down Expand Up @@ -174,14 +175,20 @@ export class ParticipantMessage extends PureComponent {
const isOutgoingMsg = this.props.messageDetails.transportDetails.direction === Direction.Outgoing;
const displayName = this.props.messageDetails.displayName || (isOutgoingMsg ? "Customer" : "Agent");
const transportDetails = this.props.messageDetails.transportDetails;
const statusStringPrefix = "connect-chat-transport-status-";

let transportStatusElement = <React.Fragment />;
switch (transportDetails.status) {
case Status.Sending:
transportStatusElement = (
<React.Fragment>
<StatusText>
<span>Sending</span>
<span>
<FormattedMessage
id={statusStringPrefix + "sending"}
defaultMessage="Sending"
/>
</span>
</StatusText>
</React.Fragment>
);
Expand All @@ -193,7 +200,12 @@ export class ParticipantMessage extends PureComponent {
transportStatusElement = (
<ErrorText>
<Icon />
<span>Failed to send!</span>
<span>
<FormattedMessage
id={statusStringPrefix + "sendFailed"}
defaultMessage="Failed to send! "
/>
</span>
</ErrorText>
);
break;
Expand All @@ -202,7 +214,12 @@ export class ParticipantMessage extends PureComponent {
}
return (
<React.Fragment>
<Header.Sender>{displayName}</Header.Sender>
<Header.Sender>
<FormattedMessage
id={displayName || "DISPLAY_NAME_MISSING"}
defaultMessage={displayName}
/>
</Header.Sender>
<Header.Status>{transportStatusElement}</Header.Status>
</React.Fragment>
);
Expand All @@ -222,8 +239,16 @@ export class ParticipantMessage extends PureComponent {
return (
<React.Fragment>
<Footer.MessageReceipt>
{lastReadReceipt && "Read"}
{lastDeliveredReceipt && "Delivered"}
{lastReadReceipt && <FormattedMessage
id="connect-chat-read-receipt"
defaultMessage="Read"
aria-live="polite"
/>}
{lastDeliveredReceipt && <FormattedMessage
id="connect-chat-delivered-receipt"
defaultMessage="Delivered"
aria-live="polite"
/>}
</Footer.MessageReceipt>
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT-0

import React from "react";
import { IntlProvider } from "react-intl";
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import { ParticipantMessage, ErrorFallback } from "./ChatMessage";
Expand Down Expand Up @@ -59,9 +60,11 @@ describe("ChatMessage", () => {
};

render(
<ThemeProvider>
<ParticipantMessage {...mockProps} />
</ThemeProvider>
<IntlProvider locale="en" onError={jest.fn} >
<ThemeProvider>
<ParticipantMessage {...mockProps} />
</ThemeProvider>
</IntlProvider>
);
}

Expand Down
Loading

0 comments on commit 7bd67c5

Please sign in to comment.