Added basis for internationlisation
Currently supported: el_GR, en_US, ar_SA, zh_TW
This commit is contained in:
parent
24eb8e8067
commit
e85f778774
8 changed files with 241 additions and 111 deletions
55
src/App.tsx
55
src/App.tsx
|
@ -1,31 +1,56 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import Chat from "./Chat/Chat";
|
||||
import "./App.css";
|
||||
import { Message } from "./Chat/types";
|
||||
import { LangType, Message } from "./Chat/types";
|
||||
import { MessageContainer } from "./Chat/MessageContainer";
|
||||
const App = (): React.ReactElement => {
|
||||
import strings from "./Intl/strings.json";
|
||||
import { LangContext } from "./context";
|
||||
|
||||
// what we call "in the business" type gymnastics
|
||||
const Wrapper = (): React.ReactElement => {
|
||||
const [lang, setLang] = useState<LangType>("en_US");
|
||||
|
||||
return (
|
||||
<LangContext.Provider value={lang}>
|
||||
<App
|
||||
changeLang={(value: string) => {
|
||||
setLang(value as LangType);
|
||||
}}
|
||||
/>
|
||||
</LangContext.Provider>
|
||||
);
|
||||
};
|
||||
const App = ({
|
||||
changeLang,
|
||||
}: {
|
||||
changeLang: (value: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
// const [lang, setLang] = useState<LangType>("en_US");
|
||||
const lang = useContext(LangContext);
|
||||
const home = strings[lang].homepage;
|
||||
if (!username) {
|
||||
const newName = prompt("Username:") as string;
|
||||
const newName = prompt(home.userNamePrompt) as string;
|
||||
setUsername(newName);
|
||||
}
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Local Area Network Chat Application</h1>
|
||||
<pre>
|
||||
This web application was built for the purposes of an EPQ
|
||||
project.
|
||||
</pre>
|
||||
<h1>{home.title}</h1>
|
||||
<pre>{home.description}</pre>
|
||||
<button
|
||||
onClick={(ev) => {
|
||||
const selection = prompt(home.newLangPrompt);
|
||||
changeLang(selection ? (selection as LangType) : lang);
|
||||
}}
|
||||
>
|
||||
{home.switchLang}
|
||||
</button>
|
||||
{messages.map((message) => {
|
||||
return <MessageContainer {...message} />;
|
||||
})}
|
||||
{
|
||||
<Chat
|
||||
user={username as string}
|
||||
/>
|
||||
}
|
||||
{<Chat user={username as string} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default App;
|
||||
export default Wrapper;
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import React, { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
ReactElement,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { MessageContainer } from "./MessageContainer";
|
||||
import { Client, Stomp, StompHeaders } from "@stomp/stompjs";
|
||||
import { Message, MessageType } from "./types";
|
||||
import { LangType, Message, MessageType } from "./types";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import "./Chat.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { LangContext } from "../context";
|
||||
// The last bit of magic sauce to make this work
|
||||
// EXPLANATION
|
||||
//
|
||||
|
@ -16,6 +24,8 @@ const endpoints = {
|
|||
history: "/api/v1/msg/",
|
||||
};
|
||||
const Chat = ({ user }: { user: string }): React.ReactElement => {
|
||||
const lang = useContext(LangContext);
|
||||
const chatPage = strings[lang].chat;
|
||||
const [messages, setMessages] = useState<ReactElement[]>([]);
|
||||
let stompClientRef = useRef(
|
||||
new Client({
|
||||
|
@ -61,7 +71,6 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
|
|||
|
||||
// Button press event handler.
|
||||
const sendData = () => {
|
||||
|
||||
const entryElement: HTMLInputElement = document.getElementById(
|
||||
"data-entry"
|
||||
) as HTMLInputElement;
|
||||
|
@ -103,12 +112,12 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
|
|||
});
|
||||
return (
|
||||
<div className="chat">
|
||||
<div className="chat-inner-wrapper">
|
||||
{messages}
|
||||
</div>
|
||||
<div className="chat-inner-wrapper">{messages}</div>
|
||||
<span className="entry-box">
|
||||
<input id="data-entry"></input>
|
||||
<button onClick={() => sendData()}>Send</button>
|
||||
<button onClick={() => sendData()}>
|
||||
{chatPage.sendButtonPrompt}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,24 +1,55 @@
|
|||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { Message, MessageType } from "./types";
|
||||
import { LangContext } from "../context";
|
||||
import strings from "../Intl/strings.json";
|
||||
export const MessageContainer = ({
|
||||
type,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
content,
|
||||
timeMillis,
|
||||
}: Message): React.ReactElement<Message> => {
|
||||
const dateTime: Date = new Date(timeMillis);
|
||||
const lang = useContext(LangContext);
|
||||
const msgPage = strings[lang].chat;
|
||||
/* FIXED funny error
|
||||
* DESCRIPTION
|
||||
* The line below was
|
||||
* return (<p>[{dateTime.toLocaleString(Intl.DateTimeFormat().resolvedOptions().timeZone)}]...</p>)
|
||||
* The line incorrectly generated a value of "UTC" as the parameter to toLocaleString()
|
||||
* While "UTC" is an accepted string value, in EEST, aka. "Europe/Athens" timezone string is not an acceptable parameter.
|
||||
* This caused the return statement to fail, and the message fails to render, despite it being correctly committed to the db.
|
||||
* Funny clown moment 🤡
|
||||
*/
|
||||
|
||||
export const MessageContainer = (
|
||||
{
|
||||
type,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
content,
|
||||
timeMillis,
|
||||
}: Message
|
||||
): React.ReactElement<Message> => {
|
||||
const dateTime: Date = new Date(timeMillis);
|
||||
/* FIXED funny error
|
||||
* DESCRIPTION
|
||||
* The line below was
|
||||
* return (<p>[{dateTime.toLocaleString(Intl.DateTimeFormat().resolvedOptions().timeZone)}]...</p>)
|
||||
* The line incorrectly generated a value of "UTC" as the parameter to toLocaleString()
|
||||
* While "UTC" is an accepted string value, in EEST, aka. "Europe/Athens" timezone string is not an acceptable parameter.
|
||||
* This caused the return statement to fail, and the message fails to render, despite it being correctly committed to the db.
|
||||
* Funny clown moment 🤡
|
||||
*/
|
||||
return (<p>[{dateTime.toLocaleString()}] Message from {fromUserId}: {content}</p>);
|
||||
switch (type) {
|
||||
case MessageType.HELLO as MessageType:
|
||||
return (
|
||||
<p>
|
||||
[{dateTime.toLocaleString()}]{" "}
|
||||
{msgPage.joinMessage.replace("$userName", fromUserId)}
|
||||
</p>
|
||||
);
|
||||
case MessageType.MESSAGE as MessageType:
|
||||
return (
|
||||
<p>
|
||||
[{dateTime.toLocaleString()}]{" "}
|
||||
{msgPage.serverMessage
|
||||
.replace("$userName", fromUserId)
|
||||
.replace("$content", content)}
|
||||
</p>
|
||||
);
|
||||
case MessageType.DATA as MessageType:
|
||||
return <></>;
|
||||
case MessageType.SYSTEM as MessageType:
|
||||
return <></>;
|
||||
default:
|
||||
console.error("Illegal MessageType reported!");
|
||||
return (
|
||||
<p>
|
||||
[{dateTime.toLocaleString()}] **THIS MESSAGE CANNOT BE
|
||||
CORRECTLY SHOWN BECAUSE THE CLIENT ENCOUNTERED AN ERROR**
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
export enum MessageType {
|
||||
MESSAGE,
|
||||
SYSTEM,
|
||||
HELLO,
|
||||
DATA,
|
||||
export const enum MessageType {
|
||||
MESSAGE = "MESSAGE",
|
||||
SYSTEM = "SYSTEM",
|
||||
HELLO = "HELLO",
|
||||
DATA = "DATA",
|
||||
}
|
||||
export enum SystemMessageCode {
|
||||
REQ,
|
||||
RES,
|
||||
ERR,
|
||||
REQ,
|
||||
RES,
|
||||
ERR,
|
||||
}
|
||||
export type HistoryFetchResult = {
|
||||
count: number,
|
||||
items: Array<ChatMessage>,
|
||||
}
|
||||
count: number;
|
||||
items: Array<ChatMessage>;
|
||||
};
|
||||
export type ErrorResult = {
|
||||
text: string,
|
||||
}
|
||||
text: string;
|
||||
};
|
||||
export type TimestampSendRequest = {
|
||||
ts: number,
|
||||
}
|
||||
ts: number;
|
||||
};
|
||||
export type SystemMessage = {
|
||||
code: SystemMessageCode
|
||||
data: HistoryFetchResult | ErrorResult | TimestampSendRequest
|
||||
}
|
||||
code: SystemMessageCode;
|
||||
data: HistoryFetchResult | ErrorResult | TimestampSendRequest;
|
||||
};
|
||||
export type ChatMessage = {
|
||||
fromUserId: string,
|
||||
toUserId: string,
|
||||
content: string,
|
||||
timeMillis: number
|
||||
}
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type HelloMessage = {
|
||||
fromUserId: string,
|
||||
timeMillis: number,
|
||||
}
|
||||
export type DataMessage = {
|
||||
|
||||
}
|
||||
fromUserId: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type DataMessage = {};
|
||||
export type Message = {
|
||||
type: MessageType,
|
||||
// data: SystemMessage | ChatMessage | HelloMessage
|
||||
fromUserId: string,
|
||||
toUserId: string,
|
||||
content: string,
|
||||
timeMillis: number
|
||||
}
|
||||
type: MessageType;
|
||||
// data: SystemMessage | ChatMessage | HelloMessage
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export const acceptedLangs = ["en_US", "zh_TW", "el_GR"] as const;
|
||||
export type LangType = (typeof acceptedLangs)[number];
|
||||
|
|
64
src/Intl/strings.json
Normal file
64
src/Intl/strings.json
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"variableNames": ["userName", "content"],
|
||||
"acceptedLangs": ["en_US", "zh_TW", "el_GR"],
|
||||
"en_US": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "Your username: ",
|
||||
"title": "LAN Chat Server",
|
||||
"description": "This web application was built for the purposes of an EPQ project.",
|
||||
"copyrightText": "Copyright 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Switch Language",
|
||||
"newLangPrompt": "Input your ISO-639_ISO-3166 language-contry code below - for example: \"en_US\": "
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "Send",
|
||||
"serverMessage": "Message from $userName: $content",
|
||||
"joinMessage": "$userName joined the server"
|
||||
}
|
||||
},
|
||||
"zh_TW": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "您的用戶名: ",
|
||||
"title": "本地聊天伺服器",
|
||||
"description": "該網絡伺服器應用程式專爲Edexcel Level 3 EPQ而製作",
|
||||
"copyrightText": "版權所有 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "切換語言",
|
||||
"newLangPrompt": "在下方輸入您想使用的語言的ISO-639_ISO-3166組合語言代碼 - 例如:\"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "發送",
|
||||
"serverMessage": "來自 $userName 的訊息:$content",
|
||||
"joinMessage": "$userName 加入了伺服器!"
|
||||
}
|
||||
},
|
||||
"el_GR": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "το όνομα χρήστη σας: ",
|
||||
"title": "Διακομιστής τοπικού δικτύου συνομιλίας",
|
||||
"description": "Αυτή η διαδικτυακή εφαρμογή δημιουργήθηκε για τους σκοπούς ενός έργου EPQ.",
|
||||
"copyrightText": "Πνευματικά δικαιώματα 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Αλλαγή γλώσσας",
|
||||
"newLangPrompt": "Εισαγάγετε τον κωδικό γλώσσας-χώρας ISO-639 ISO-3166 παρακάτω - για παράδειγμα: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "Στείλετε",
|
||||
"joinMessage": "$userName έγινε μέλος του διακομιστή",
|
||||
"serverMessage": "μήνυμα από $userName: $content"
|
||||
}
|
||||
},
|
||||
"ar_SA": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "اسم المستخدم الخاص بك:",
|
||||
"title": "خادم الدردشة LAN",
|
||||
"description": "تم إنشاء تطبيق الويب هذا لأغراض مشروع EPQ.",
|
||||
"copyrightText": "حقوق الطبع والنشر 2024 - 2025 Zhongheng Liu @ كلية بايرون",
|
||||
"switchLang": "تبديل اللغة",
|
||||
"newLangPrompt": "أدخل رمز اللغة ISO-639_ISO-3166 أدناه - على سبيل المثال: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "يرسل",
|
||||
"joinMessage": "$userName انضم إلى الخادم",
|
||||
"serverMessage": "رسالة من $userName: $content"
|
||||
}
|
||||
}
|
||||
}
|
4
src/context.ts
Normal file
4
src/context.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { createContext } from "react";
|
||||
import { LangType } from "./Chat/types";
|
||||
|
||||
export const LangContext = createContext<LangType>("en_US");
|
|
@ -1,11 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
import React, { createContext } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import Wrapper from "./App";
|
||||
const LangContext = createContext("en_US");
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<App />
|
||||
);
|
||||
<LangContext.Provider value="en_US">
|
||||
<Wrapper />
|
||||
</LangContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue