Skip to content

Commit f764afd

Browse files
committed
WIP(4): Implement language switching for File Upload Feature
1 parent ed8c1e9 commit f764afd

File tree

14 files changed

+196
-109
lines changed

14 files changed

+196
-109
lines changed

app/frontend/src/components/Answer/Answer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const Answer = ({
7878
{!!parsedAnswer.citations.length && (
7979
<Stack.Item>
8080
<Stack horizontal wrap tokens={{ childrenGap: 5 }}>
81-
<span className={styles.citationLearnMore}>Citations:</span>
81+
<span className={styles.citationLearnMore}>{t("headerTexts.citation")}:</span>
8282
{parsedAnswer.citations.map((x, i) => {
8383
const path = getCitationFilePath(x);
8484
return (

app/frontend/src/components/Answer/SpeechOutputBrowser.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from "react";
22
import { IconButton } from "@fluentui/react";
33
import { useTranslation } from "react-i18next";
4+
import { supportedLngs } from "../../i18n/config";
45

56
interface Props {
67
answer: string;
@@ -16,20 +17,25 @@ try {
1617
console.error("SpeechSynthesis is not supported");
1718
}
1819

19-
const getUtterance = function (text: string) {
20+
const getUtterance = function (text: string, lngCode: string) {
2021
if (synth) {
2122
const utterance = new SpeechSynthesisUtterance(text);
22-
utterance.lang = "en-US";
23+
utterance.lang = lngCode;
2324
utterance.volume = 1;
2425
utterance.rate = 1;
2526
utterance.pitch = 1;
26-
utterance.voice = synth.getVoices().filter((voice: SpeechSynthesisVoice) => voice.lang === "en-US")[0];
27+
utterance.voice = synth.getVoices().filter((voice: SpeechSynthesisVoice) => voice.lang === lngCode)[0];
2728
return utterance;
2829
}
2930
};
3031

3132
export const SpeechOutputBrowser = ({ answer }: Props) => {
32-
const { t } = useTranslation();
33+
const { t, i18n } = useTranslation();
34+
const currentLng = i18n.language;
35+
let lngCode = supportedLngs[currentLng]?.locale;
36+
if (!lngCode) {
37+
lngCode = "en-US";
38+
}
3339
const [isPlaying, setIsPlaying] = useState<boolean>(false);
3440

3541
const startOrStopSpeech = (answer: string) => {
@@ -39,7 +45,7 @@ export const SpeechOutputBrowser = ({ answer }: Props) => {
3945
setIsPlaying(false);
4046
return;
4147
}
42-
const utterance: SpeechSynthesisUtterance | undefined = getUtterance(answer);
48+
const utterance: SpeechSynthesisUtterance | undefined = getUtterance(answer, lngCode);
4349

4450
if (!utterance) {
4551
return;
@@ -64,8 +70,8 @@ export const SpeechOutputBrowser = ({ answer }: Props) => {
6470
<IconButton
6571
style={{ color: color }}
6672
iconProps={{ iconName: "Volume3" }}
67-
title="Speak answer"
68-
ariaLabel={t("tooltips.speakAnswer")}
73+
title={t("tooltips.speakAnswer")}
74+
ariaLabel="Speak answer"
6975
onClick={() => startOrStopSpeech(answer)}
7076
disabled={!synth}
7177
/>

app/frontend/src/components/LoginButton/LoginButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const LoginButton = () => {
5757
};
5858
return (
5959
<DefaultButton
60-
text={loggedIn ? `t("logout")\n${username}` : t("login")}
60+
text={loggedIn ? `${t("logout")}\n${username}` : `${t("login")}`}
6161
className={styles.loginButton}
6262
onClick={loggedIn ? handleLogoutPopup : handleLoginPopup}
6363
></DefaultButton>

app/frontend/src/components/MarkdownViewer/MarkdownViewer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ src }) => {
6767
className={styles.downloadButton}
6868
style={{ color: "black" }}
6969
iconProps={{ iconName: "Save" }}
70-
title="Save"
71-
ariaLabel={t("tooltips.save")}
70+
title={t("tooltips.save")}
71+
ariaLabel="Save"
7272
href={src}
7373
download
7474
/>

app/frontend/src/components/QuestionInput/SpeechInput.tsx

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,49 @@ import { Button, Tooltip } from "@fluentui/react-components";
33
import { Mic28Filled } from "@fluentui/react-icons";
44
import { useTranslation } from "react-i18next";
55
import styles from "./QuestionInput.module.css";
6+
import { supportedLngs } from "../../i18n/config";
67

78
interface Props {
89
updateQuestion: (question: string) => void;
910
}
1011

11-
const SpeechRecognition = (window as any).speechRecognition || (window as any).webkitSpeechRecognition;
12-
let speechRecognition: {
13-
continuous: boolean;
14-
lang: string;
15-
interimResults: boolean;
16-
maxAlternatives: number;
17-
start: () => void;
18-
onresult: (event: { results: { transcript: SetStateAction<string> }[][] }) => void;
19-
onend: () => void;
20-
onerror: (event: { error: string }) => void;
21-
stop: () => void;
22-
} | null = null;
23-
try {
24-
speechRecognition = new SpeechRecognition();
25-
if (speechRecognition != null) {
26-
speechRecognition.lang = "en-US";
27-
speechRecognition.interimResults = true;
12+
const useCustomSpeechRecognition = () => {
13+
const { i18n } = useTranslation();
14+
const currentLng = i18n.language;
15+
let lngCode = supportedLngs[currentLng]?.locale;
16+
if (!lngCode) {
17+
lngCode = "en-US";
2818
}
29-
} catch (err) {
30-
console.error("SpeechRecognition not supported");
31-
speechRecognition = null;
32-
}
19+
20+
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
21+
let speechRecognition: {
22+
continuous: boolean;
23+
lang: string;
24+
interimResults: boolean;
25+
maxAlternatives: number;
26+
start: () => void;
27+
onresult: (event: { results: { transcript: SetStateAction<string> }[][] }) => void;
28+
onend: () => void;
29+
onerror: (event: { error: string }) => void;
30+
stop: () => void;
31+
} | null = null;
32+
33+
try {
34+
speechRecognition = new SpeechRecognition();
35+
if (speechRecognition != null) {
36+
speechRecognition.lang = lngCode;
37+
speechRecognition.interimResults = true;
38+
}
39+
} catch (err) {
40+
console.error("SpeechRecognition not supported");
41+
speechRecognition = null;
42+
}
43+
44+
return speechRecognition;
45+
};
3346

3447
export const SpeechInput = ({ updateQuestion }: Props) => {
48+
let speechRecognition = useCustomSpeechRecognition();
3549
const { t } = useTranslation();
3650
const [isRecording, setIsRecording] = useState<boolean>(false);
3751
const startRecording = () => {

app/frontend/src/components/UploadFile/UploadFile.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Callout, Label, Text } from "@fluentui/react";
33
import { Button } from "@fluentui/react-components";
44
import { Add24Regular, Delete24Regular } from "@fluentui/react-icons";
55
import { useMsal } from "@azure/msal-react";
6+
import { useTranslation } from "react-i18next";
67

78
import { SimpleAPIResponse, uploadFileApi, deleteUploadedFileApi, listUploadedFilesApi } from "../../api";
89
import { useLogin, getToken } from "../../authConfig";
@@ -22,6 +23,7 @@ export const UploadFile: React.FC<Props> = ({ className, disabled }: Props) => {
2223
const [uploadedFile, setUploadedFile] = useState<SimpleAPIResponse>();
2324
const [uploadedFileError, setUploadedFileError] = useState<string>();
2425
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
26+
const { t } = useTranslation();
2527

2628
if (!useLogin) {
2729
throw new Error("The UploadFile component requires useLogin to be true");
@@ -96,15 +98,15 @@ export const UploadFile: React.FC<Props> = ({ className, disabled }: Props) => {
9698
} catch (error) {
9799
console.error(error);
98100
setIsUploading(false);
99-
setUploadedFileError(`Error uploading file - please try again or contact admin.`);
101+
setUploadedFileError(t("upload.uploadedFileError"));
100102
}
101103
};
102104

103105
return (
104106
<div className={`${styles.container} ${className ?? ""}`}>
105107
<div>
106108
<Button id="calloutButton" icon={<Add24Regular />} disabled={disabled} onClick={handleButtonClick}>
107-
Manage file uploads
109+
{t("upload.manageFileUploads")}
108110
</Button>
109111

110112
{isCalloutVisible && (
@@ -118,7 +120,7 @@ export const UploadFile: React.FC<Props> = ({ className, disabled }: Props) => {
118120
>
119121
<form encType="multipart/form-data">
120122
<div>
121-
<Label>Upload file:</Label>
123+
<Label>{t("upload.fileLabel")}</Label>
122124
<input
123125
accept=".txt, .md, .json, .png, .jpg, .jpeg, .bmp, .heic, .tiff, .pdf, .docx, .xlsx, .pptx, .html"
124126
className={styles.chooseFiles}
@@ -129,15 +131,15 @@ export const UploadFile: React.FC<Props> = ({ className, disabled }: Props) => {
129131
</form>
130132

131133
{/* Show a loading message while files are being uploaded */}
132-
{isUploading && <Text>{"Uploading files..."}</Text>}
134+
{isUploading && <Text>{t("upload.uploadingFiles")}</Text>}
133135
{!isUploading && uploadedFileError && <Text>{uploadedFileError}</Text>}
134136
{!isUploading && uploadedFile && <Text>{uploadedFile.message}</Text>}
135137

136138
{/* Display the list of already uploaded */}
137-
<h3>Previously uploaded files:</h3>
139+
<h3>{t("upload.uploadedFilesLabel")}</h3>
138140

139-
{isLoading && <Text>Loading...</Text>}
140-
{!isLoading && uploadedFiles.length === 0 && <Text>No files uploaded yet</Text>}
141+
{isLoading && <Text>{t("upload.loading")}</Text>}
142+
{!isLoading && uploadedFiles.length === 0 && <Text>{t("upload.noFilesUploaded")}</Text>}
141143
{uploadedFiles.map((filename, index) => {
142144
return (
143145
<div key={index} className={styles.list}>
@@ -148,10 +150,10 @@ export const UploadFile: React.FC<Props> = ({ className, disabled }: Props) => {
148150
onClick={() => handleRemoveFile(filename)}
149151
disabled={deletionStatus[filename] === "pending" || deletionStatus[filename] === "success"}
150152
>
151-
{!deletionStatus[filename] && "Delete file"}
152-
{deletionStatus[filename] == "pending" && "Deleting file..."}
153-
{deletionStatus[filename] == "error" && "Error deleting."}
154-
{deletionStatus[filename] == "success" && "File deleted"}
153+
{!deletionStatus[filename] && t("upload.deleteFile")}
154+
{deletionStatus[filename] == "pending" && t("upload.deletingFile")}
155+
{deletionStatus[filename] == "error" && t("upload.errorDeleting")}
156+
{deletionStatus[filename] == "success" && t("upload.fileDeleted")}
155157
</Button>
156158
</div>
157159
);

app/frontend/src/i18n/LocaleSwitcher.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ export const LocaleSwitcher = ({ onLanguageChange }: Props) => {
1212

1313
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
1414
onLanguageChange(event.target.value);
15-
}
15+
};
1616

1717
return (
1818
<div className={styles.localeSwitcher}>
1919
<LocalLanguage24Regular className={styles.localeSwitcherIcon} />
2020
<select value={i18n.language} onChange={handleLanguageChange} className={styles.localeSwitcherText}>
21-
{Object.entries(supportedLngs).map(([code, name]) => (
21+
{Object.entries(supportedLngs).map(([code, details]) => (
2222
<option value={code} key={code}>
23-
{name}
23+
{details.name}
2424
</option>
2525
))}
26-
</select>
26+
</select>
2727
</div>
2828
);
29-
}
29+
};

app/frontend/src/i18n/config.ts

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,53 @@ import LanguageDetector from "i18next-browser-languagedetector";
33
import HttpApi from "i18next-http-backend";
44
import { initReactI18next } from "react-i18next";
55
import formatters from "./formatters";
6-
import enTranslation from '../locales/en/translation.json';
7-
import esTranslation from '../locales/es/translation.json';
8-
import jaTranslation from '../locales/ja/translation.json';
9-
import frTranslation from '../locales/fr/translation.json';
6+
import enTranslation from "../locales/en/translation.json";
7+
import esTranslation from "../locales/es/translation.json";
8+
import jaTranslation from "../locales/ja/translation.json";
9+
import frTranslation from "../locales/fr/translation.json";
1010

11-
export const supportedLngs = {
12-
en: "English",
13-
es: "Español",
14-
fr: "Français",
15-
ja: "日本語",
11+
export const supportedLngs: { [key: string]: { name: string; locale: string } } = {
12+
en: {
13+
name: "English",
14+
locale: "en-US"
15+
},
16+
es: {
17+
name: "Español",
18+
locale: "es-ES"
19+
},
20+
fr: {
21+
name: "Français",
22+
locale: "fr-FR"
23+
},
24+
ja: {
25+
name: "日本語",
26+
locale: "ja-JP"
27+
}
1628
};
1729

1830
i18next
19-
.use(HttpApi)
20-
.use(LanguageDetector)
21-
.use(initReactI18next)
22-
// init i18next
23-
// for all options read: https://www.i18next.com/overview/configuration-options
24-
.init({
25-
resources: {
26-
en: { translation: enTranslation },
27-
es: { translation: esTranslation },
28-
fr: { translation: frTranslation },
29-
ja: { translation: jaTranslation },
30-
},
31-
fallbackLng: "en",
32-
supportedLngs: Object.keys(supportedLngs),
33-
debug: import.meta.env.DEV,
34-
interpolation: {
35-
escapeValue: false, // not needed for react as it escapes by default
36-
},
37-
});
31+
.use(HttpApi)
32+
.use(LanguageDetector)
33+
.use(initReactI18next)
34+
// init i18next
35+
// for all options read: https://www.i18next.com/overview/configuration-options
36+
.init({
37+
resources: {
38+
en: { translation: enTranslation },
39+
es: { translation: esTranslation },
40+
fr: { translation: frTranslation },
41+
ja: { translation: jaTranslation }
42+
},
43+
fallbackLng: "en",
44+
supportedLngs: Object.keys(supportedLngs),
45+
debug: import.meta.env.DEV,
46+
interpolation: {
47+
escapeValue: false // not needed for react as it escapes by default
48+
}
49+
});
3850

3951
Object.entries(formatters).forEach(([key, resolver]) => {
40-
i18next.services.formatter?.add(key, resolver);
52+
i18next.services.formatter?.add(key, resolver);
4153
});
4254

4355
export default i18next;

app/frontend/src/locales/en/translation.json

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
{
2-
"language_name": "English",
3-
"pageTitle": "GPT + Enterprise data | Sample",
4-
"headerTitle": "GPT + Enterprise data | Sample",
2+
"pageTitle": "Azure OpenAI + AI Search",
3+
"headerTitle": "Azure OpenAI + AI Search",
54
"chat": "Chat",
65
"qa": "Ask a question",
76
"login": "Login",
87
"logout": "Logout",
98
"clearChat": "Clear chat",
9+
"upload": {
10+
"fileLabel": "Upload file:",
11+
"uploadedFilesLabel": "Previously uploaded files:",
12+
"noFilesUploaded": "No files uploaded yet",
13+
"loading": "Loading...",
14+
"manageFileUploads": "Manage file uploads",
15+
"uploadingFiles": "Uploading files...",
16+
"uploadedFileError": "Error uploading file - please try again or contact admin.",
17+
"deleteFile": "Delete file",
18+
"deletingFile": "Deleting file...",
19+
"errorDeleting": "Error deleting.",
20+
"fileDeleted": "File deleted"
21+
},
1022
"developerSettings": "Developer settings",
1123

1224
"chatEmptyStateTitle": "Chat with your data",
@@ -96,8 +108,10 @@
96108
"Sets a minimum score for search results coming back from the semantic reranker. The score always ranges between 0-4. The higher the score, the more semantically relevant the result is to the question.",
97109
"retrieveNumber":
98110
"Sets the number of search results to retrieve from Azure AI search. More results may increase the likelihood of finding the correct answer, but may lead to the model getting 'lost in the middle'.",
99-
"excludeCategory": "Specifies a category to exclude from the search results. There are no categories used in the default data set.",
100-
"useSemanticReranker": "Enables the Azure AI Search semantic ranker, a model that re-ranks search results based on semantic similarity to the user's query.",
111+
"excludeCategory":
112+
"Specifies a category to exclude from the search results. There are no categories used in the default data set.",
113+
"useSemanticReranker":
114+
"Enables the Azure AI Search semantic ranker, a model that re-ranks search results based on semantic similarity to the user's query.",
101115
"useSemanticCaptions":
102116
"Sends semantic captions to the LLM instead of the full search result. A semantic caption is extracted from a search result during the process of semantic ranking.",
103117
"suggestFollowupQuestions": "Asks the LLM to suggest follow-up questions based on the user's query.",
@@ -111,5 +125,5 @@
111125
"streamChat": "Continuously streams the response to the chat UI as it is generated.",
112126
"useOidSecurityFilter": "Filter search results based on the authenticated user's OID.",
113127
"useGroupsSecurityFilter": "Filter search results based on the authenticated user's groups."
114-
}
115-
}
128+
}
129+
}

0 commit comments

Comments
 (0)