Added basis for internationlisation

Currently supported: el_GR, en_US, ar_SA, zh_TW
This commit is contained in:
Zhongheng Liu 2024-01-14 22:42:56 +02:00
commit e85f778774
No known key found for this signature in database
8 changed files with 241 additions and 111 deletions

View file

@ -1,31 +1,56 @@
import React, { useState } from "react"; import React, { createContext, useContext, useState } from "react";
import Chat from "./Chat/Chat"; import Chat from "./Chat/Chat";
import "./App.css"; import "./App.css";
import { Message } from "./Chat/types"; import { LangType, Message } from "./Chat/types";
import { MessageContainer } from "./Chat/MessageContainer"; 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 [username, setUsername] = useState<string>();
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
// const [lang, setLang] = useState<LangType>("en_US");
const lang = useContext(LangContext);
const home = strings[lang].homepage;
if (!username) { if (!username) {
const newName = prompt("Username:") as string; const newName = prompt(home.userNamePrompt) as string;
setUsername(newName); setUsername(newName);
} }
return ( return (
<div className="App"> <div className="App">
<h1>Local Area Network Chat Application</h1> <h1>{home.title}</h1>
<pre> <pre>{home.description}</pre>
This web application was built for the purposes of an EPQ <button
project. onClick={(ev) => {
</pre> const selection = prompt(home.newLangPrompt);
changeLang(selection ? (selection as LangType) : lang);
}}
>
{home.switchLang}
</button>
{messages.map((message) => { {messages.map((message) => {
return <MessageContainer {...message} />; return <MessageContainer {...message} />;
})} })}
{ {<Chat user={username as string} />}
<Chat
user={username as string}
/>
}
</div> </div>
); );
}; };
export default App; export default Wrapper;

View file

@ -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 { MessageContainer } from "./MessageContainer";
import { Client, Stomp, StompHeaders } from "@stomp/stompjs"; 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 { renderToStaticMarkup } from "react-dom/server";
import "./Chat.css"; import "./Chat.css";
import strings from "../Intl/strings.json";
import { LangContext } from "../context";
// The last bit of magic sauce to make this work // The last bit of magic sauce to make this work
// EXPLANATION // EXPLANATION
// //
@ -16,6 +24,8 @@ const endpoints = {
history: "/api/v1/msg/", history: "/api/v1/msg/",
}; };
const Chat = ({ user }: { user: string }): React.ReactElement => { const Chat = ({ user }: { user: string }): React.ReactElement => {
const lang = useContext(LangContext);
const chatPage = strings[lang].chat;
const [messages, setMessages] = useState<ReactElement[]>([]); const [messages, setMessages] = useState<ReactElement[]>([]);
let stompClientRef = useRef( let stompClientRef = useRef(
new Client({ new Client({
@ -61,7 +71,6 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
// Button press event handler. // Button press event handler.
const sendData = () => { const sendData = () => {
const entryElement: HTMLInputElement = document.getElementById( const entryElement: HTMLInputElement = document.getElementById(
"data-entry" "data-entry"
) as HTMLInputElement; ) as HTMLInputElement;
@ -103,12 +112,12 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
}); });
return ( return (
<div className="chat"> <div className="chat">
<div className="chat-inner-wrapper"> <div className="chat-inner-wrapper">{messages}</div>
{messages}
</div>
<span className="entry-box"> <span className="entry-box">
<input id="data-entry"></input> <input id="data-entry"></input>
<button onClick={() => sendData()}>Send</button> <button onClick={() => sendData()}>
{chatPage.sendButtonPrompt}
</button>
</span> </span>
</div> </div>
); );

View file

@ -1,24 +1,55 @@
import React from "react"; import React, { useContext } from "react";
import { Message, MessageType } from "./types"; 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 = ( switch (type) {
{ case MessageType.HELLO as MessageType:
type, return (
fromUserId, <p>
toUserId, [{dateTime.toLocaleString()}]{" "}
content, {msgPage.joinMessage.replace("$userName", fromUserId)}
timeMillis, </p>
}: Message );
): React.ReactElement<Message> => { case MessageType.MESSAGE as MessageType:
const dateTime: Date = new Date(timeMillis); return (
/* FIXED funny error <p>
* DESCRIPTION [{dateTime.toLocaleString()}]{" "}
* The line below was {msgPage.serverMessage
* return (<p>[{dateTime.toLocaleString(Intl.DateTimeFormat().resolvedOptions().timeZone)}]...</p>) .replace("$userName", fromUserId)
* The line incorrectly generated a value of "UTC" as the parameter to toLocaleString() .replace("$content", content)}
* While "UTC" is an accepted string value, in EEST, aka. "Europe/Athens" timezone string is not an acceptable parameter. </p>
* This caused the return statement to fail, and the message fails to render, despite it being correctly committed to the db. );
* Funny clown moment 🤡 case MessageType.DATA as MessageType:
*/ return <></>;
return (<p>[{dateTime.toLocaleString()}] Message from {fromUserId}: {content}</p>); 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>
);
}
}; };

View file

@ -1,46 +1,46 @@
export enum MessageType { export const enum MessageType {
MESSAGE, MESSAGE = "MESSAGE",
SYSTEM, SYSTEM = "SYSTEM",
HELLO, HELLO = "HELLO",
DATA, DATA = "DATA",
} }
export enum SystemMessageCode { export enum SystemMessageCode {
REQ, REQ,
RES, RES,
ERR, ERR,
} }
export type HistoryFetchResult = { export type HistoryFetchResult = {
count: number, count: number;
items: Array<ChatMessage>, items: Array<ChatMessage>;
} };
export type ErrorResult = { export type ErrorResult = {
text: string, text: string;
} };
export type TimestampSendRequest = { export type TimestampSendRequest = {
ts: number, ts: number;
} };
export type SystemMessage = { export type SystemMessage = {
code: SystemMessageCode code: SystemMessageCode;
data: HistoryFetchResult | ErrorResult | TimestampSendRequest data: HistoryFetchResult | ErrorResult | TimestampSendRequest;
} };
export type ChatMessage = { export type ChatMessage = {
fromUserId: string, fromUserId: string;
toUserId: string, toUserId: string;
content: string, content: string;
timeMillis: number timeMillis: number;
} };
export type HelloMessage = { export type HelloMessage = {
fromUserId: string, fromUserId: string;
timeMillis: number, timeMillis: number;
} };
export type DataMessage = { export type DataMessage = {};
}
export type Message = { export type Message = {
type: MessageType, type: MessageType;
// data: SystemMessage | ChatMessage | HelloMessage // data: SystemMessage | ChatMessage | HelloMessage
fromUserId: string, fromUserId: string;
toUserId: string, toUserId: string;
content: string, content: string;
timeMillis: number 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
View 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
View file

@ -0,0 +1,4 @@
import { createContext } from "react";
import { LangType } from "./Chat/types";
export const LangContext = createContext<LangType>("en_US");

View file

@ -1,11 +1,14 @@
import React from 'react'; import React, { createContext } from "react";
import ReactDOM from 'react-dom/client'; import ReactDOM from "react-dom/client";
import './index.css'; import "./index.css";
import App from './App'; import App from "./App";
import Wrapper from "./App";
const LangContext = createContext("en_US");
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById("root") as HTMLElement
); );
root.render( root.render(
<App /> <LangContext.Provider value="en_US">
); <Wrapper />
</LangContext.Provider>
);

View file

@ -1,26 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "allowJs": true,
"dom.iterable", "skipLibCheck": true,
"esnext" "esModuleInterop": true,
], "allowSyntheticDefaultImports": true,
"allowJs": true, "strict": true,
"skipLibCheck": true, "forceConsistentCasingInFileNames": true,
"esModuleInterop": true, "noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true, "module": "esnext",
"strict": true, "moduleResolution": "node",
"forceConsistentCasingInFileNames": true, "resolveJsonModule": true,
"noFallthroughCasesInSwitch": true, "isolatedModules": true,
"module": "esnext", "noEmit": true,
"moduleResolution": "node", "jsx": "react-jsx"
"resolveJsonModule": true, },
"isolatedModules": true, "include": ["src"]
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
} }