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 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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
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 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>
|
||||||
|
);
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue