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 "./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;

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 { 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>
);

View file

@ -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>
);
}
};

View file

@ -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
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 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>
);

View file

@ -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"]
}