From 342079f4c0ceb5d4336a660cc7e40ba0af7294da Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Sun, 16 Jul 2023 11:30:23 +1200 Subject: [PATCH 01/14] start language system --- public/lang/en-US.json | 1 + public/lang/index.json | 8 +++++ src/App.tsx | 2 ++ src/logic/Language.ts | 25 ++++++++++++++ src/logic/LanguageAdapter.ts | 66 ++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 public/lang/en-US.json create mode 100644 public/lang/index.json create mode 100644 src/logic/Language.ts create mode 100644 src/logic/LanguageAdapter.ts diff --git a/public/lang/en-US.json b/public/lang/en-US.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/public/lang/en-US.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/public/lang/index.json b/public/lang/index.json new file mode 100644 index 0000000..d74d65a --- /dev/null +++ b/public/lang/index.json @@ -0,0 +1,8 @@ +{ + "languages": { + "en-US": { + "displayName": "English (United States)" + } + }, + "default": "en-US" +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index dc2da19..52e0286 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,8 +31,10 @@ import { getSetting, setSetting } from "./logic/SettingsIndex"; import Theme from "./logic/ThemeIndex"; import React from "react"; import RenamePlaylist from "./components/RenamePlaylist"; +import LanguageAdapter from "./logic/LanguageAdapter"; const theme = Theme.getTheme(getSetting("theme", "Classic")); +const language = new LanguageAdapter(); const App = React.memo(function App() { const navigate = useNavigate(); diff --git a/src/logic/Language.ts b/src/logic/Language.ts new file mode 100644 index 0000000..6333e1c --- /dev/null +++ b/src/logic/Language.ts @@ -0,0 +1,25 @@ +export default class Language { + private i18n: string; + private displayName: string; + private translations?: Map; + + public constructor(i18n: string, displayName: string) { + this.i18n = i18n; + this.displayName = displayName; + } + + /** + * Resolve the language data, if not already loaded. + */ + public async resolve() : Promise { + if (!this.translations) { + const response = await fetch(`lang/${this.i18n}.json`); + const json = await response.json(); + this.translations = new Map(Object.entries(json)); + } + } + + public getDisplayName(): string { + return this.displayName; + } +} \ No newline at end of file diff --git a/src/logic/LanguageAdapter.ts b/src/logic/LanguageAdapter.ts new file mode 100644 index 0000000..c87675f --- /dev/null +++ b/src/logic/LanguageAdapter.ts @@ -0,0 +1,66 @@ +import Language from "./Language"; + +interface LanguageMeta { + displayName: string; +} + +interface LanguagesIndex { + languages: { + [key: string]: LanguageMeta + }, + default: string +} + +export default class LanguageAdapter { + private languages : Map; + private defaultLanguage : string; + + public constructor() { + // default values + this.languages = new Map(); + this.defaultLanguage = "en-GB"; + + // load language index + fetch("lang/index.json") + .then(response => response.json()) + .then((data: LanguagesIndex) => { + // read index data + for (const [key, value] of Object.entries(data.languages)) { + this.languages.set(key, new Language(key, value.displayName)); + } + + this.defaultLanguage = data.default; + + // load preferred language + this.getLanguage(); + }) + .catch(error => { + // TODO how should errors be handled? Build in the default en-US translation, or just show translation keys? Show error page? + console.log(error); + }); + } + + private getPreferredLanguage() : string { + if (navigator.languages) { + // Find first language we support + for (let language of navigator.languages) { + if (this.languages.has(language)) { + return language; + } + } + } else { + // old browsers + if (this.languages.has(navigator.language)) { + return navigator.language; + } + } + + return this.defaultLanguage; + } + + public getLanguage(): Language { + const language: Language = this.languages.get(this.getPreferredLanguage()); + language.resolve(); + return language; + } +} \ No newline at end of file From f91c1fc9729c2a9a965962fa95152634f614dd5c Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Mon, 17 Jul 2023 23:18:00 +1200 Subject: [PATCH 02/14] localisation framework --- src/App.tsx | 4 +- src/hooks/TranslationHook.ts | 24 +++++++ src/logic/Language.ts | 27 +++++-- src/logic/LanguageAdapter.ts | 132 +++++++++++++++++++++++------------ 4 files changed, 137 insertions(+), 50 deletions(-) create mode 100644 src/hooks/TranslationHook.ts diff --git a/src/App.tsx b/src/App.tsx index 52e0286..e3589e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,10 +31,10 @@ import { getSetting, setSetting } from "./logic/SettingsIndex"; import Theme from "./logic/ThemeIndex"; import React from "react"; import RenamePlaylist from "./components/RenamePlaylist"; -import LanguageAdapter from "./logic/LanguageAdapter"; +import { initialiseLanguageAdapter } from "./logic/LanguageAdapter"; const theme = Theme.getTheme(getSetting("theme", "Classic")); -const language = new LanguageAdapter(); +initialiseLanguageAdapter(); const App = React.memo(function App() { const navigate = useNavigate(); diff --git a/src/hooks/TranslationHook.ts b/src/hooks/TranslationHook.ts new file mode 100644 index 0000000..d744f41 --- /dev/null +++ b/src/hooks/TranslationHook.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; +import { localise, registerLanguageChangeListener, unregisterLanguageChangeListener } from "../logic/LanguageAdapter"; + +/** + * Get the localised string from the provided translation key. + * @param translationKey the translation key to look up in the current language. + */ +export default function useTranslation(translationKey: string) : string { + const [translation, setTranslation] = useState(localise(translationKey)); + + function onLanguageChange() { + setTranslation(localise(translationKey)); + } + + useEffect(() => { + registerLanguageChangeListener(onLanguageChange); + + return () => { + unregisterLanguageChangeListener(onLanguageChange); + }; + }, []); + + return translation; +} \ No newline at end of file diff --git a/src/logic/Language.ts b/src/logic/Language.ts index 6333e1c..305002a 100644 --- a/src/logic/Language.ts +++ b/src/logic/Language.ts @@ -1,10 +1,12 @@ +import { Dispatch, SetStateAction } from "react"; + export default class Language { - private i18n: string; + private languageId: string; private displayName: string; private translations?: Map; - public constructor(i18n: string, displayName: string) { - this.i18n = i18n; + public constructor(languageId: string, displayName: string) { + this.languageId = languageId; this.displayName = displayName; } @@ -13,13 +15,30 @@ export default class Language { */ public async resolve() : Promise { if (!this.translations) { - const response = await fetch(`lang/${this.i18n}.json`); + const response = await fetch(`lang/${this.languageId}.json`); const json = await response.json(); this.translations = new Map(Object.entries(json)); } } + /** + * Localise the given translation key to the localised string to use. + * @param key the translation key to find the localised translation for. + * @returns the localised string to display. If there is no translation, will return undefined. + */ + public localise(key: string): string | undefined { + return this.translations?.get(key); + } + public getDisplayName(): string { return this.displayName; } + + /** + * Get the ID of the translation. + * @returns the standard translation id for this translation, such as en-US for English (United States) + */ + public getId(): string { + return this.languageId; + } } \ No newline at end of file diff --git a/src/logic/LanguageAdapter.ts b/src/logic/LanguageAdapter.ts index c87675f..29e2940 100644 --- a/src/logic/LanguageAdapter.ts +++ b/src/logic/LanguageAdapter.ts @@ -1,5 +1,8 @@ +import { Dispatch, SetStateAction, useState } from "react"; import Language from "./Language"; +import axios from 'axios'; +// Interface for structure of the language metadata file interface LanguageMeta { displayName: string; } @@ -11,56 +14,97 @@ interface LanguagesIndex { default: string } -export default class LanguageAdapter { - private languages : Map; - private defaultLanguage : string; - - public constructor() { - // default values - this.languages = new Map(); - this.defaultLanguage = "en-GB"; - - // load language index - fetch("lang/index.json") - .then(response => response.json()) - .then((data: LanguagesIndex) => { - // read index data - for (const [key, value] of Object.entries(data.languages)) { - this.languages.set(key, new Language(key, value.displayName)); - } +// Interface for Listeners +type LanguageChangeListener = () => void; - this.defaultLanguage = data.default; +let languages : Map; +let defaultLanguage : Language; +let currentLanguage : Language; - // load preferred language - this.getLanguage(); - }) - .catch(error => { - // TODO how should errors be handled? Build in the default en-US translation, or just show translation keys? Show error page? - console.log(error); - }); - } +let listeners : LanguageChangeListener[]; - private getPreferredLanguage() : string { - if (navigator.languages) { - // Find first language we support - for (let language of navigator.languages) { - if (this.languages.has(language)) { - return language; - } - } - } else { - // old browsers - if (this.languages.has(navigator.language)) { - return navigator.language; +/** + * Get first preferred language by the browser that we also support. + * In the case of a generic language such as "en" being supported, any particular localisation + * ("en-US", "en-GB") will match. If the user has a particular localisation preferred for that language, + * at another point in the list, that will be prioritised. + * @returns the first language the user's browser prefers that we support. If there are no languages in common, returns + * the default language. + */ +function getPreferredLanguage() : string { + if (navigator.languages) { + // Find first language we support + for (let language of navigator.languages) { + if (this.languages.has(language)) { + return language; } } - - return this.defaultLanguage; + } else { + // old browsers + if (this.languages.has(navigator.language)) { + return navigator.language; + } } - public getLanguage(): Language { - const language: Language = this.languages.get(this.getPreferredLanguage()); - language.resolve(); - return language; + return this.defaultLanguage; +} + +export function initialiseLanguageAdapter() { + // initialise + languages = new Map(); + + // set dummy value for current and default language to begin with. + defaultLanguage = currentLanguage = new Language("xx-XX", "No Translation"); + + // load language index for metadata such as supported languages + axios.get("lang/index.json") + .then(response => response.data) + .then((data: LanguagesIndex) => { + // read index data + for (const [key, value] of Object.entries(data.languages)) { + languages.set(key, new Language(key, value.displayName)); + } + + // load default language + defaultLanguage = loadLanguage(data.default); + + // load preferred language + loadLanguage(getPreferredLanguage()); + }) + .catch(error => { + // TODO how should errors be handled? Build in the default en-US translation, or just show translation keys? Show error page? + console.log(error); + }); +} + +export function loadLanguage(language: string) : Language { + currentLanguage = this.languages.get(language); + + currentLanguage.resolve().then(() => { + // update listeners + }); + + return currentLanguage; +} + +/** + * Localise the given translation key to the current language. + * @param key the translation key to localise. + * @returns the localisation for the given translation key, searching first in the current language, falling back on the default language, and finally using the translation key + * if no translation is found. + */ +export function localise(key: string): string { + return currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; +} + +export function registerLanguageChangeListener(listener: LanguageChangeListener): void { + listeners.push(listener); +} + +export function unregisterLanguageChangeListener(listener: LanguageChangeListener): void { + let i : number = listeners.indexOf(listener); + + if (i > -1) { + listeners.splice(i, 1); } } \ No newline at end of file From 4671aafcc95bda230dde8727cac2cb489e44cd03 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Mon, 17 Jul 2023 23:50:00 +1200 Subject: [PATCH 03/14] fix errors --- public/lang/index.json | 3 ++ src/logic/Language.ts | 7 ++- src/logic/LanguageAdapter.ts | 86 +++++++++++++++++++++++++++++------- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/public/lang/index.json b/public/lang/index.json index d74d65a..70f923c 100644 --- a/public/lang/index.json +++ b/public/lang/index.json @@ -4,5 +4,8 @@ "displayName": "English (United States)" } }, + "primaryDialects": { + "en": "en-US" + }, "default": "en-US" } \ No newline at end of file diff --git a/src/logic/Language.ts b/src/logic/Language.ts index 305002a..b0892ac 100644 --- a/src/logic/Language.ts +++ b/src/logic/Language.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from "react"; +import axios from 'axios'; export default class Language { private languageId: string; @@ -15,9 +15,8 @@ export default class Language { */ public async resolve() : Promise { if (!this.translations) { - const response = await fetch(`lang/${this.languageId}.json`); - const json = await response.json(); - this.translations = new Map(Object.entries(json)); + const response = await axios.get(`lang/${this.languageId}.json`); + this.translations = new Map(Object.entries(response.data)); } } diff --git a/src/logic/LanguageAdapter.ts b/src/logic/LanguageAdapter.ts index 29e2940..47077b7 100644 --- a/src/logic/LanguageAdapter.ts +++ b/src/logic/LanguageAdapter.ts @@ -1,4 +1,3 @@ -import { Dispatch, SetStateAction, useState } from "react"; import Language from "./Language"; import axios from 'axios'; @@ -11,17 +10,21 @@ interface LanguagesIndex { languages: { [key: string]: LanguageMeta }, + primaryDialects: { + [key: string]: string + }, default: string } // Interface for Listeners type LanguageChangeListener = () => void; -let languages : Map; +let languages : Map = new Map(); +let primaryDialects : Map = new Map(); let defaultLanguage : Language; let currentLanguage : Language; -let listeners : LanguageChangeListener[]; +let listeners : LanguageChangeListener[] = []; /** * Get first preferred language by the browser that we also support. @@ -35,24 +38,67 @@ function getPreferredLanguage() : string { if (navigator.languages) { // Find first language we support for (let language of navigator.languages) { - if (this.languages.has(language)) { - return language; + let supportedLanguage = getSupportedLanguage(language, navigator.languages); + + if (supportedLanguage) { + return supportedLanguage; } } } else { // old browsers - if (this.languages.has(navigator.language)) { - return navigator.language; + let supportedLanguage = getSupportedLanguage(navigator.language, [navigator.language]); + + if (supportedLanguage) { + return supportedLanguage; } } - return this.defaultLanguage; + return defaultLanguage.getId(); } -export function initialiseLanguageAdapter() { - // initialise - languages = new Map(); +/** + * Finds a language code for a supported language that best suits the given language requested. + * @param language the language to find the supported language for. + * @param allLanguages the list of all languages requested, to assist with finding the most suitable language. + * That is, if a generic language code like "en" is requested, this will be checked to see if a more specific version is requested + * later. + * @returns the best supported language for the given requested language, or undefined if none match. + */ +function getSupportedLanguage(language: string, allLanguages: readonly string[]): string | undefined { + // first, prioritise exact matches + if (languages.has(language)) { + return language; + } + + // if a 2-letter code (generic language), find best localisation + if (language.length === 2) { + // check allLanguages to see if more specific localisation is preferred. + for (let requestedLanguage of allLanguages) { + if (requestedLanguage.startsWith(language) && languages.has(requestedLanguage)) { + return requestedLanguage; + } + } + + // check primary dialect + let primaryDialect = primaryDialects.get(language); + + // ensure the primary dialect actually exists before using it + if (primaryDialect && languages.has(primaryDialect)) { + return primaryDialect; + } + // check for any version of the language supported + for (let supportedLanguage of languages.keys()) { + if (supportedLanguage.startsWith(language)) { + return supportedLanguage; + } + } + } + + return undefined; +} + +export function initialiseLanguageAdapter() { // set dummy value for current and default language to begin with. defaultLanguage = currentLanguage = new Language("xx-XX", "No Translation"); @@ -65,6 +111,10 @@ export function initialiseLanguageAdapter() { languages.set(key, new Language(key, value.displayName)); } + for (const [key, value] of Object.entries(data.primaryDialects)) { + primaryDialects.set(key, value); + } + // load default language defaultLanguage = loadLanguage(data.default); @@ -78,11 +128,17 @@ export function initialiseLanguageAdapter() { } export function loadLanguage(language: string) : Language { - currentLanguage = this.languages.get(language); + console.log("Loading Language: " + language); - currentLanguage.resolve().then(() => { - // update listeners - }); + // only update if language not current + if (language !== currentLanguage.getId()) { + currentLanguage = languages.get(language); + + // notify all listeners + currentLanguage.resolve().then(() => { + listeners.forEach(listener => listener()); + }); + } return currentLanguage; } From 5fd3313568dc7fe8804768b78371778f8353fa84 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Mon, 17 Jul 2023 23:56:16 +1200 Subject: [PATCH 04/14] first few translations --- public/lang/en-US.json | 6 +++++- src/pages/Connect.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index 9e26dfe..f77a5f5 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1 +1,5 @@ -{} \ No newline at end of file +{ + "buttons.add": "Add", + "pages.connect.publicServers": "Public Servers", + "pages.connect.title": "Connect to Server" +} \ No newline at end of file diff --git a/src/pages/Connect.tsx b/src/pages/Connect.tsx index 418489a..d2d2614 100644 --- a/src/pages/Connect.tsx +++ b/src/pages/Connect.tsx @@ -8,6 +8,7 @@ import ServerInfo from "pipebomb.js/dist/ServerInfo"; import Loader from "../components/Loader"; import PublicServer from "../components/PublicServer"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const ConnectPage = React.memo(function ConnectPage() { const serverIndex = ServerIndex.getInstance(); @@ -69,13 +70,13 @@ const ConnectPage = React.memo(function ConnectPage() { return (
-

Connect to Server

+

{useTranslation("pages.connect.title")}

setValue(e.target.value)} initialValue={value} /> - +
@@ -83,7 +84,7 @@ const ConnectPage = React.memo(function ConnectPage() { ))}
- Public Servers + {useTranslation("pages.connect.publicServers")} { generateRegistryHTML() }
) From 7f752574aa45cc1875ebb48e3d98fb46624efd90 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Tue, 18 Jul 2023 11:47:31 +1200 Subject: [PATCH 05/14] always update language data; more translations. --- public/lang/en-US.json | 2 ++ src/components/PublicServer.tsx | 3 ++- src/logic/Language.ts | 7 +++---- src/logic/LanguageAdapter.ts | 15 ++++++--------- src/pages/Connect.tsx | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index f77a5f5..621e282 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1,5 +1,7 @@ { "buttons.add": "Add", + "buttons.connect.getPing": "Get Ping", "pages.connect.publicServers": "Public Servers", + "pages.connect.publicServers.error": "Couldn't contact public server registry", "pages.connect.title": "Connect to Server" } \ No newline at end of file diff --git a/src/components/PublicServer.tsx b/src/components/PublicServer.tsx index e631e88..030630d 100644 --- a/src/components/PublicServer.tsx +++ b/src/components/PublicServer.tsx @@ -7,6 +7,7 @@ import { formatTimeWords } from "../logic/Utils"; import { useState } from "react"; import ServerIndex from "../logic/ServerIndex"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; export interface PublicServerProps { server: ServerInfo, @@ -50,7 +51,7 @@ const PublicServer = React.memo(function PublicServer({ server, connectCallback function generatePingHTML() { if (ping == false) { return ( - + ) } diff --git a/src/logic/Language.ts b/src/logic/Language.ts index b0892ac..732f687 100644 --- a/src/logic/Language.ts +++ b/src/logic/Language.ts @@ -14,10 +14,9 @@ export default class Language { * Resolve the language data, if not already loaded. */ public async resolve() : Promise { - if (!this.translations) { - const response = await axios.get(`lang/${this.languageId}.json`); - this.translations = new Map(Object.entries(response.data)); - } + // always update language to ensure the latest translations are present + const response = await axios.get(`lang/${this.languageId}.json`); + this.translations = new Map(Object.entries(response.data)); } /** diff --git a/src/logic/LanguageAdapter.ts b/src/logic/LanguageAdapter.ts index 47077b7..342b06b 100644 --- a/src/logic/LanguageAdapter.ts +++ b/src/logic/LanguageAdapter.ts @@ -130,15 +130,12 @@ export function initialiseLanguageAdapter() { export function loadLanguage(language: string) : Language { console.log("Loading Language: " + language); - // only update if language not current - if (language !== currentLanguage.getId()) { - currentLanguage = languages.get(language); - - // notify all listeners - currentLanguage.resolve().then(() => { - listeners.forEach(listener => listener()); - }); - } + currentLanguage = languages.get(language); + + // notify all listeners after load + currentLanguage.resolve().then(() => { + listeners.forEach(listener => listener()); + }); return currentLanguage; } diff --git a/src/pages/Connect.tsx b/src/pages/Connect.tsx index d2d2614..ca68f85 100644 --- a/src/pages/Connect.tsx +++ b/src/pages/Connect.tsx @@ -47,7 +47,7 @@ const ConnectPage = React.memo(function ConnectPage() { function generateRegistryHTML() { if (registryServers === false) { return ( - Couldn't contact public server registry + {useTranslation("pages.connect.publicServers.error")} ) } From 55f2884c9c99836aa29a488f06013631719791d9 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Tue, 18 Jul 2023 12:58:13 +1200 Subject: [PATCH 06/14] support string formatting but there are some issues with the library that may lead to me needing to implement my own version --- package-lock.json | 14 ++++++++++++++ package.json | 1 + public/lang/en-US.json | 4 +++- src/components/PublicServer.tsx | 2 +- src/hooks/TranslationHook.ts | 9 +++++---- src/logic/Language.ts | 2 +- .../{LanguageAdapter.ts => LanguageAdapter.tsx} | 17 +++++++++++++++-- 7 files changed, 40 insertions(+), 9 deletions(-) rename src/logic/{LanguageAdapter.ts => LanguageAdapter.tsx} (88%) diff --git a/package-lock.json b/package-lock.json index 6c12c37..4b7f9be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react-resize-detector": "^9.1.0", "react-router-dom": "^6.8.1", "react-sortablejs": "^6.1.4", + "react-string-replace": "^1.1.1", "react-viewport-list": "^7.1.1", "react-window-sortable": "^1.4.12", "sass": "^1.58.3", @@ -3299,6 +3300,14 @@ "sortablejs": "1" } }, + "node_modules/react-string-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", + "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/react-viewport-list": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-viewport-list/-/react-viewport-list-7.1.1.tgz", @@ -6085,6 +6094,11 @@ "tiny-invariant": "1.2.0" } }, + "react-string-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", + "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==" + }, "react-viewport-list": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-viewport-list/-/react-viewport-list-7.1.1.tgz", diff --git a/package.json b/package.json index f088434..4f38e60 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react-resize-detector": "^9.1.0", "react-router-dom": "^6.8.1", "react-sortablejs": "^6.1.4", + "react-string-replace": "^1.1.1", "react-viewport-list": "^7.1.1", "react-window-sortable": "^1.4.12", "sass": "^1.58.3", diff --git a/public/lang/en-US.json b/public/lang/en-US.json index 621e282..a3a96a8 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -3,5 +3,7 @@ "buttons.connect.getPing": "Get Ping", "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", - "pages.connect.title": "Connect to Server" + "pages.connect.title": "Connect to Server", + "pages.connect.uptime": "Uptime: {}", + "pages.connect.ping": "Ping: {}" } \ No newline at end of file diff --git a/src/components/PublicServer.tsx b/src/components/PublicServer.tsx index 030630d..dc109b8 100644 --- a/src/components/PublicServer.tsx +++ b/src/components/PublicServer.tsx @@ -77,7 +77,7 @@ const PublicServer = React.memo(function PublicServer({ server, connectCallback {server.address} - Uptime: {formatTimeWords(server.uptime)} + {useTranslation("pages.connect.uptime", {formatTimeWords(server.uptime)})}
Ping: { generatePingHTML() } diff --git a/src/hooks/TranslationHook.ts b/src/hooks/TranslationHook.ts index d744f41..afe25d1 100644 --- a/src/hooks/TranslationHook.ts +++ b/src/hooks/TranslationHook.ts @@ -1,15 +1,16 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, ReactNode } from "react"; import { localise, registerLanguageChangeListener, unregisterLanguageChangeListener } from "../logic/LanguageAdapter"; /** * Get the localised string from the provided translation key. * @param translationKey the translation key to look up in the current language. + * @param args arguments to substitute for '{}' instances in the localised string. */ -export default function useTranslation(translationKey: string) : string { - const [translation, setTranslation] = useState(localise(translationKey)); +export default function useTranslation(translationKey: string, ...args: (string | ReactNode)[]) : ReactNode { + const [translation, setTranslation] = useState(localise(translationKey, args)); function onLanguageChange() { - setTranslation(localise(translationKey)); + setTranslation(localise(translationKey, args)); } useEffect(() => { diff --git a/src/logic/Language.ts b/src/logic/Language.ts index 732f687..34d75b9 100644 --- a/src/logic/Language.ts +++ b/src/logic/Language.ts @@ -22,7 +22,7 @@ export default class Language { /** * Localise the given translation key to the localised string to use. * @param key the translation key to find the localised translation for. - * @returns the localised string to display. If there is no translation, will return undefined. + * @returns the localised string to display, without applying any formatting. If there is no translation, will return undefined. */ public localise(key: string): string | undefined { return this.translations?.get(key); diff --git a/src/logic/LanguageAdapter.ts b/src/logic/LanguageAdapter.tsx similarity index 88% rename from src/logic/LanguageAdapter.ts rename to src/logic/LanguageAdapter.tsx index 342b06b..6da38e2 100644 --- a/src/logic/LanguageAdapter.ts +++ b/src/logic/LanguageAdapter.tsx @@ -1,5 +1,7 @@ +import reactStringReplace from "react-string-replace"; import Language from "./Language"; import axios from 'axios'; +import { ReactNode } from "react"; // Interface for structure of the language metadata file interface LanguageMeta { @@ -143,11 +145,22 @@ export function loadLanguage(language: string) : Language { /** * Localise the given translation key to the current language. * @param key the translation key to localise. + * @param args the args to substitute for {%d} instances in the localisation. * @returns the localisation for the given translation key, searching first in the current language, falling back on the default language, and finally using the translation key * if no translation is found. */ -export function localise(key: string): string { - return currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; +export function localise(key: string, args: (ReactNode | string)[]): ReactNode { + let localised = currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; + // This library is stupid. It provides odd numbers rather than 0,1,2. Might need to implement own function. + return reactStringReplace(localised, "{}", (match, i) => { + let arg : ReactNode | string = args[Math.floor(i/2)]; + + if (typeof arg === 'string') { + return arg; + } else { + return arg; + } + }); } export function registerLanguageChangeListener(listener: LanguageChangeListener): void { From 61a50b08dce7c99d3e078b1d0410dea58aa34769 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Tue, 18 Jul 2023 23:38:20 +1200 Subject: [PATCH 07/14] fully implement localise system, allowing for custom ordering of args as defined by the localisation --- package-lock.json | 14 ---------- package.json | 1 - public/lang/en-US.json | 4 +-- src/logic/LanguageAdapter.tsx | 50 ++++++++++++++++++++++++++--------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b7f9be..6c12c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "react-resize-detector": "^9.1.0", "react-router-dom": "^6.8.1", "react-sortablejs": "^6.1.4", - "react-string-replace": "^1.1.1", "react-viewport-list": "^7.1.1", "react-window-sortable": "^1.4.12", "sass": "^1.58.3", @@ -3300,14 +3299,6 @@ "sortablejs": "1" } }, - "node_modules/react-string-replace": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", - "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/react-viewport-list": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-viewport-list/-/react-viewport-list-7.1.1.tgz", @@ -6094,11 +6085,6 @@ "tiny-invariant": "1.2.0" } }, - "react-string-replace": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", - "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==" - }, "react-viewport-list": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-viewport-list/-/react-viewport-list-7.1.1.tgz", diff --git a/package.json b/package.json index 4f38e60..f088434 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "react-resize-detector": "^9.1.0", "react-router-dom": "^6.8.1", "react-sortablejs": "^6.1.4", - "react-string-replace": "^1.1.1", "react-viewport-list": "^7.1.1", "react-window-sortable": "^1.4.12", "sass": "^1.58.3", diff --git a/public/lang/en-US.json b/public/lang/en-US.json index a3a96a8..1727145 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -4,6 +4,6 @@ "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", "pages.connect.title": "Connect to Server", - "pages.connect.uptime": "Uptime: {}", - "pages.connect.ping": "Ping: {}" + "pages.connect.uptime": "Uptime: {0}", + "pages.connect.ping": "Ping: {0}" } \ No newline at end of file diff --git a/src/logic/LanguageAdapter.tsx b/src/logic/LanguageAdapter.tsx index 6da38e2..ffafe24 100644 --- a/src/logic/LanguageAdapter.tsx +++ b/src/logic/LanguageAdapter.tsx @@ -1,4 +1,3 @@ -import reactStringReplace from "react-string-replace"; import Language from "./Language"; import axios from 'axios'; import { ReactNode } from "react"; @@ -149,18 +148,45 @@ export function loadLanguage(language: string) : Language { * @returns the localisation for the given translation key, searching first in the current language, falling back on the default language, and finally using the translation key * if no translation is found. */ -export function localise(key: string, args: (ReactNode | string)[]): ReactNode { - let localised = currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; - // This library is stupid. It provides odd numbers rather than 0,1,2. Might need to implement own function. - return reactStringReplace(localised, "{}", (match, i) => { - let arg : ReactNode | string = args[Math.floor(i/2)]; - - if (typeof arg === 'string') { - return arg; - } else { - return arg; +export function localise(key: string, args: (ReactNode | string)[]): ReactNode[] { + let localisedTemplate : string = currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; + + // Insert Args + // First, get the args in order that they are present in the template. + let indicesToArg : Map = new Map(); + + for (let i = 0; i < args.length; i++) { + let index = localisedTemplate.indexOf(`{${i}}`); + + if (index > -1) { + indicesToArg.set(index, args[i]); } - }); + } + + let orderedIndices = [...indicesToArg.keys()].sort(); + let split = localisedTemplate.split(/{\d+}/); + let components : ReactNode[] = []; + + // add parts of the split with the args added between components + for (let i = 0; i < split.length; i++) { + // append literal, filtering out blank strings that would just become empty spans + let literal = split[i]; + + if (literal.length > 0) { + components.push({literal}); + } + + // append arg + if (i < orderedIndices.length) { + let index = orderedIndices[i]; + let arg : ReactNode | string = indicesToArg.get(index); + components.push({arg}); + + + } + } + + return components; } export function registerLanguageChangeListener(listener: LanguageChangeListener): void { From db37cfd50d252b92118430603e91031213ebbeea Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Wed, 19 Jul 2023 00:49:33 +1200 Subject: [PATCH 08/14] fix up classes to fix styling; clone component instead of wrapping elements to add the key prop --- public/lang/en-US.json | 4 +++- src/components/PublicServer.tsx | 2 +- src/logic/LanguageAdapter.tsx | 21 ++++++++++++--------- src/pages/LoginPage.tsx | 5 +++-- src/styles/PublicServer.module.scss | 2 +- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index 1727145..0e85c37 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1,9 +1,11 @@ { "buttons.add": "Add", "buttons.connect.getPing": "Get Ping", + "pages.connect.ping": "Ping: {0}", "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", "pages.connect.title": "Connect to Server", "pages.connect.uptime": "Uptime: {0}", - "pages.connect.ping": "Ping: {0}" + "pages.login.notice": "Enter the credentials to a new or existing account", + "pages.login.title": "Login" } \ No newline at end of file diff --git a/src/components/PublicServer.tsx b/src/components/PublicServer.tsx index dc109b8..12d09f7 100644 --- a/src/components/PublicServer.tsx +++ b/src/components/PublicServer.tsx @@ -77,7 +77,7 @@ const PublicServer = React.memo(function PublicServer({ server, connectCallback {server.address} - {useTranslation("pages.connect.uptime", {formatTimeWords(server.uptime)})} + {useTranslation("pages.connect.uptime", {formatTimeWords(server.uptime)})}
Ping: { generatePingHTML() } diff --git a/src/logic/LanguageAdapter.tsx b/src/logic/LanguageAdapter.tsx index ffafe24..3642a45 100644 --- a/src/logic/LanguageAdapter.tsx +++ b/src/logic/LanguageAdapter.tsx @@ -1,6 +1,6 @@ import Language from "./Language"; import axios from 'axios'; -import { ReactNode } from "react"; +import { ReactNode, isValidElement, cloneElement } from "react"; // Interface for structure of the language metadata file interface LanguageMeta { @@ -71,9 +71,9 @@ function getSupportedLanguage(language: string, allLanguages: readonly string[]) return language; } - // if a 2-letter code (generic language), find best localisation + // if a 2-letter code (generic language), find best specific localisation if (language.length === 2) { - // check allLanguages to see if more specific localisation is preferred. + // check allLanguages to see if more specific localisation is preferred by the user. for (let requestedLanguage of allLanguages) { if (requestedLanguage.startsWith(language) && languages.has(requestedLanguage)) { return requestedLanguage; @@ -148,12 +148,12 @@ export function loadLanguage(language: string) : Language { * @returns the localisation for the given translation key, searching first in the current language, falling back on the default language, and finally using the translation key * if no translation is found. */ -export function localise(key: string, args: (ReactNode | string)[]): ReactNode[] { +export function localise(key: string, args: ReactNode[]): ReactNode[] { let localisedTemplate : string = currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; // Insert Args // First, get the args in order that they are present in the template. - let indicesToArg : Map = new Map(); + let indicesToArg : Map = new Map(); for (let i = 0; i < args.length; i++) { let index = localisedTemplate.indexOf(`{${i}}`); @@ -179,10 +179,13 @@ export function localise(key: string, args: (ReactNode | string)[]): ReactNode[] // append arg if (i < orderedIndices.length) { let index = orderedIndices[i]; - let arg : ReactNode | string = indicesToArg.get(index); - components.push({arg}); - - + let arg : ReactNode = indicesToArg.get(index); + + if (isValidElement(arg)) { + components.push(cloneElement(arg, { key: "arg_" + i })); + } else { + components.push({arg}); + } } } diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 8e9a347..9d5984f 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -5,6 +5,7 @@ import { useRef, useState } from "react"; import PipeBombConnection from "../logic/PipeBombConnection"; import CustomModal from "../components/CustomModal"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const LoginPage = React.memo(function LoginPage() { const authStatus = useAuthenticationStatus(); @@ -65,9 +66,9 @@ const LoginPage = React.memo(function LoginPage() { return ( <>
-

Login

+

{useTranslation("pages.login.title")}

-

Enter the credentials to a new or existing account

+

{useTranslation("pages.login.notice")}

setUsername(e.currentTarget.value)} helperColor={usernameStatus} helperText={usernameMessage} />
diff --git a/src/styles/PublicServer.module.scss b/src/styles/PublicServer.module.scss index 018a457..ac24e47 100644 --- a/src/styles/PublicServer.module.scss +++ b/src/styles/PublicServer.module.scss @@ -62,7 +62,7 @@ h5 { font-weight: var(--nextui-fontWeights-normal); - & > span { + & > span.value { font-weight: var(--nextui-fontWeights-semibold); } } From 5ce5ac360dc275f698e7a01e8899076ff3b44ea0 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Wed, 19 Jul 2023 11:36:38 +1200 Subject: [PATCH 09/14] simplify LanguageAdapter --- public/lang/en-US.json | 7 +++++++ src/logic/LanguageAdapter.tsx | 4 ++-- src/pages/LoginPage.tsx | 14 +++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index 0e85c37..f8cc3df 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1,11 +1,18 @@ { "buttons.add": "Add", + "buttons.close": "Close", "buttons.connect.getPing": "Get Ping", "pages.connect.ping": "Ping: {0}", "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", "pages.connect.title": "Connect to Server", "pages.connect.uptime": "Uptime: {0}", + "pages.login.modal.0": "Because Pipe Bomb accounts can be used on any Pipe Bomb server without a centralized database, accounts don't use the standard username + password model.", + "pages.login.modal.1": "Your username and password are used to create a public and private key pair, which is also used to generate your user ID.", + "pages.login.modal.2": "If you can prove to a Pipe Bomb server that you have both the public and private keys used to generate your user ID, the server considers you the owner of the account.", + "pages.login.modal.3": "Because of this architecture, there is no \"registration\" of accounts. You can just enter any username and password, and your keys will be generated behind the scenes for you.", + "pages.login.modal.4": "However there is a caveat. You cannot change your username or password, as the new combination would generate completely different keys and a different user ID, effectively logging you into a different account.", + "pages.login.modal.5": "Because your account keys are generated using both your username and password, usernames are not unique. Two accounts with the same username but different passwords will generate different keys and a differrent user ID.", "pages.login.notice": "Enter the credentials to a new or existing account", "pages.login.title": "Login" } \ No newline at end of file diff --git a/src/logic/LanguageAdapter.tsx b/src/logic/LanguageAdapter.tsx index 3642a45..ac227fc 100644 --- a/src/logic/LanguageAdapter.tsx +++ b/src/logic/LanguageAdapter.tsx @@ -173,7 +173,7 @@ export function localise(key: string, args: ReactNode[]): ReactNode[] { let literal = split[i]; if (literal.length > 0) { - components.push({literal}); + components.push(literal); } // append arg @@ -184,7 +184,7 @@ export function localise(key: string, args: ReactNode[]): ReactNode[] { if (isValidElement(arg)) { components.push(cloneElement(arg, { key: "arg_" + i })); } else { - components.push({arg}); + components.push(arg); } } } diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 9d5984f..d2cbc2e 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -88,13 +88,13 @@ const LoginPage = React.memo(function LoginPage() {
setModalOpen(false)}> -

Because Pipe Bomb accounts can be used on any Pipe Bomb server without a centralized database, accounts don't use the standard username + password model.

-

Your username and password are used to create a public and private key pair, which is also used to generate your user ID.

-

If you can prove to a Pipe Bomb server that you have both the public and private keys used to generate your user ID, the server considers you the owner of the account.

-

Because of this architecture, there is no "registration" of accounts. You can just enter any username and password, and your keys will be generated behind the scenes for you.

-

However there is a caveat. You cannot change your username or password, as the new combination would generate completely different keys and a different user ID, effectively logging you into a different account.

-

Because your account keys are generated using both your username and password, usernames are not unique. Two accounts with the same username but different passwords will generate different keys and a differrent user ID.

- +

{useTranslation("pages.login.modal.0")}

+

{useTranslation("pages.login.modal.1")}

+

{useTranslation("pages.login.modal.2")}

+

{useTranslation("pages.login.modal.3")}

+

{useTranslation("pages.login.modal.4")}

+

{useTranslation("pages.login.modal.5")}

+
) From 7f016a4cb73a1fa9a487545f138cbd993d7413e0 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Wed, 19 Jul 2023 12:14:08 +1200 Subject: [PATCH 10/14] more translations --- public/lang/en-US.json | 21 ++++++++++++++++++++- src/pages/Chart.tsx | 7 ++++--- src/pages/ExternalPlaylistPage.tsx | 13 +++++++------ src/pages/Home.tsx | 9 +++++---- src/pages/LoginPage.tsx | 2 +- src/pages/Playlist.tsx | 27 ++++++++++++++------------- src/pages/Search.tsx | 13 +++++++------ 7 files changed, 58 insertions(+), 34 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index f8cc3df..e7a4c75 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1,18 +1,37 @@ { "buttons.add": "Add", + "buttons.addToQueue": "Add to Queue", "buttons.close": "Close", "buttons.connect.getPing": "Get Ping", + "buttons.deletePlaylist": "Delete Playlist", + "buttons.likePlaylist": "Like Playlist", + "buttons.m3u": "Download as M3U", + "buttons.renamePlaylist": "Rename Playlist", + "buttons.search": "Search", + "buttons.share": "Copy Link", + "error.404": "Error 404", + "error.404.playlist": "Playlist Not Found.", + "error.500": "Error 500", + "error.500.message": "Something went wrong!", "pages.connect.ping": "Ping: {0}", "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", "pages.connect.title": "Connect to Server", "pages.connect.uptime": "Uptime: {0}", + "pages.home.charts": "Charts", + "pages.home.playlists": "Playlists", + "pages.home.welcome": "Welcome!", + "pages.home.welcomeUser": "Welcome, {0}!", "pages.login.modal.0": "Because Pipe Bomb accounts can be used on any Pipe Bomb server without a centralized database, accounts don't use the standard username + password model.", "pages.login.modal.1": "Your username and password are used to create a public and private key pair, which is also used to generate your user ID.", "pages.login.modal.2": "If you can prove to a Pipe Bomb server that you have both the public and private keys used to generate your user ID, the server considers you the owner of the account.", "pages.login.modal.3": "Because of this architecture, there is no \"registration\" of accounts. You can just enter any username and password, and your keys will be generated behind the scenes for you.", "pages.login.modal.4": "However there is a caveat. You cannot change your username or password, as the new combination would generate completely different keys and a different user ID, effectively logging you into a different account.", "pages.login.modal.5": "Because your account keys are generated using both your username and password, usernames are not unique. Two accounts with the same username but different passwords will generate different keys and a differrent user ID.", + "pages.login.modal.prompt": "How does this work?", "pages.login.notice": "Enter the credentials to a new or existing account", - "pages.login.title": "Login" + "pages.login.title": "Login", + "pages.search.top": "Top Results", + "pages.search.playlists": "Playlists", + "pages.search.loadMore": "More Results" } \ No newline at end of file diff --git a/src/pages/Chart.tsx b/src/pages/Chart.tsx index bac759c..ee0b466 100644 --- a/src/pages/Chart.tsx +++ b/src/pages/Chart.tsx @@ -14,6 +14,7 @@ import Track from "pipebomb.js/dist/music/Track"; import PipeBombConnection from "../logic/PipeBombConnection"; import { ViewportList } from "react-viewport-list"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const ChartPage = React.memo(function ChartPage() { let paramID: any = useParams().chartID; @@ -90,9 +91,9 @@ const ChartPage = React.memo(function ChartPage() { - Add to Queue - Copy Link - Download as M3U + {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.share")} + {useTranslation("buttons.m3u")} diff --git a/src/pages/ExternalPlaylistPage.tsx b/src/pages/ExternalPlaylistPage.tsx index 7a6a3e5..bb1c12f 100644 --- a/src/pages/ExternalPlaylistPage.tsx +++ b/src/pages/ExternalPlaylistPage.tsx @@ -14,6 +14,7 @@ import { convertTracklistToM3u } from "../logic/Utils"; import PlaylistTop from "../components/PlaylistTop"; import { ViewportList } from "react-viewport-list"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { const collectionID = useParams().collectionID; @@ -49,8 +50,8 @@ const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { if (collection === false) { return ( <> - Error 404 - Playlist Not Found. + {useTranslation("error.404")} + {useTranslation("error.404.playlist")} ) } @@ -99,10 +100,10 @@ const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { <> - Add to Queue - Copy Link - Like Playlist - Download as M3U + {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.share")} + {useTranslation("buttons.likePlaylist")} + {useTranslation("buttons.m3u")} } /> {tracklist && ( diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 9461d8b..6d0f2a6 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -10,6 +10,7 @@ import ChartIndex from "../logic/ChartIndex"; import SquareChart from "../components/SquareChart"; import PlaylistCollection from "../components/PlaylistCollection"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const HomePage = React.memo(function HomePage() { const [playlists, setPlaylists] = useState(PlaylistIndex.getInstance().getPlaylists()); @@ -45,7 +46,7 @@ const HomePage = React.memo(function HomePage() { if (playlists.length) { return ( <> - Playlists + {useTranslation("pages.home.playlists")} ) @@ -66,7 +67,7 @@ const HomePage = React.memo(function HomePage() { if (charts.length) { return ( <> - Charts + {useTranslation("pages.home.charts")}
{charts.map((chart, index) => (
@@ -86,9 +87,9 @@ const HomePage = React.memo(function HomePage() { {userData ? ( -

Welcome, {userData.username}!

+

{useTranslation("pages.home.welcomeUser", userData.username)}

) : ( -

Welcome!

+

{useTranslation("pages.home.welcome")}

)}
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index d2cbc2e..3c3fee0 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -83,7 +83,7 @@ const LoginPage = React.memo(function LoginPage() { ) }
- +
diff --git a/src/pages/Playlist.tsx b/src/pages/Playlist.tsx index a1a4a63..1f81101 100644 --- a/src/pages/Playlist.tsx +++ b/src/pages/Playlist.tsx @@ -18,6 +18,7 @@ import PlaylistTop from "../components/PlaylistTop"; import useIsSelf from "../hooks/IsSelfHook"; import { ViewportList } from "react-viewport-list"; import { openRenamePlaylist } from "../components/RenamePlaylist" +import useTranslation from "../hooks/TranslationHook"; let lastPlaylistID = ""; @@ -90,8 +91,8 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (errorCode == 400) { return ( <> - Error 404 - Playlist Not Found. + {useTranslation("error.404")} + {useTranslation("error.404.playlist")} ) } @@ -99,8 +100,8 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (paramID === undefined || errorCode != 0) { return ( <> - Error 500 - Something went wrong! + {useTranslation("error.500")} + {useTranslation("error.500.message")} ) } @@ -180,20 +181,20 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (self) { return ( - Add to Queue - Copy Link - Rename Playlist - Download as M3U - Delete Playlist + {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.share")} + {useTranslation("buttons.renamePlaylist")} + {useTranslation("buttons.m3u")} + {useTranslation("buttons.deletePlaylist")} ); } else { return ( - Add to Queue - Copy Link - Like Playlist - Download as M3U + {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.share")} + {useTranslation("buttons.likePlaylist")} + {useTranslation("buttons.m3u")} ) } diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index c664cc9..faad79d 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -13,6 +13,7 @@ import { TbMoodEmpty } from "react-icons/tb"; import CenterIcon from "../components/CenterIcon"; import PlaylistCollection from "../components/PlaylistCollection"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; interface storedResults { tracks: Track[], @@ -125,11 +126,11 @@ const SearchPage = React.memo(function SearchPage() { const firstTracks = newTracklist.splice(0, 5); if (playlists.length) { return <> - Top Results + {useTranslation("pages.search.top")} {firstTracks.map((item, index) => ( ))} - Playlists + {useTranslation("pages.search.playlists")}
{playlists.map((item, index) => ( @@ -138,7 +139,7 @@ const SearchPage = React.memo(function SearchPage() {
{!!newTracklist.length && ( - More Results + {useTranslation("pages.search.loadMore")} )} {newTracklist.map((item, index) => ( @@ -146,12 +147,12 @@ const SearchPage = React.memo(function SearchPage() { } else { return <> - Top Results + {useTranslation("pages.search.top")} {firstTracks.map((item, index) => ( ))} {!!newTracklist.length && ( - More Results + {useTranslation("pages.search.loadMore")} )} {newTracklist.map((item, index) => ( @@ -183,7 +184,7 @@ const SearchPage = React.memo(function SearchPage() { className={styles.button} auto > - Search + {useTranslation("buttons.search")}
{ generateServices() } From 8d0db72be94c4445c01cae4fa1253d9399a3715a Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Thu, 20 Jul 2023 01:14:35 +1200 Subject: [PATCH 11/14] move most text on page files to translations; mostly widgets to go --- public/lang/en-US.json | 27 ++++++++++++++++++++++++++- src/components/AddToPlaylist.tsx | 3 ++- src/components/Lyrics.tsx | 19 ++++++++++--------- src/hooks/TranslationHook.ts | 2 +- src/pages/SettingsPage.tsx | 11 ++++++----- src/pages/SuggestionsPlaylist.tsx | 13 +++++++------ src/pages/TrackPage.tsx | 23 ++++++++++++----------- src/pages/UserPage.tsx | 9 +++++---- 8 files changed, 69 insertions(+), 38 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index e7a4c75..1242f31 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -1,18 +1,33 @@ { "buttons.add": "Add", "buttons.addToQueue": "Add to Queue", + "buttons.addToPlaylist": "Add to Playlist", "buttons.close": "Close", + "buttons.download": "Download as MP3", "buttons.connect.getPing": "Get Ping", "buttons.deletePlaylist": "Delete Playlist", "buttons.likePlaylist": "Like Playlist", + "buttons.logout": "Logout", "buttons.m3u": "Download as M3U", + "buttons.playNext": "Play Next", "buttons.renamePlaylist": "Rename Playlist", "buttons.search": "Search", "buttons.share": "Copy Link", + "common.loader.lyrics": "Loading Lyrics", + "common.loader.playlists": "Loading Playlists", + "common.loader.tracks": "Loading Tracks", + "common.loader.user": "Loading User", "error.404": "Error 404", "error.404.playlist": "Playlist Not Found.", + "error.404.track": "Track Not Found.", + "error.404.user": "User Not Found.", "error.500": "Error 500", "error.500.message": "Something went wrong!", + "lyrics.attribution": "Lyrics by {0}", + "lyrics.description": "Play a track to view its lyrics.", + "lyrics.error": "We couldn't find any lyrics for this track.", + "lyrics.notSynced": "These lyrics aren't synced with the track.", + "lyrics.title": "Lyrics", "pages.connect.ping": "Ping: {0}", "pages.connect.publicServers": "Public Servers", "pages.connect.publicServers.error": "Couldn't contact public server registry", @@ -33,5 +48,15 @@ "pages.login.title": "Login", "pages.search.top": "Top Results", "pages.search.playlists": "Playlists", - "pages.search.loadMore": "More Results" + "pages.search.loadMore": "More Results", + "pages.settings.title": "Settings", + "pages.settings.reloadNotice": "Reload for changes to take effect", + "pages.settings.equalizer": "Equalizer", + "pages.settings.equalizer.notice": "Play a track to load the EQ", + "pages.suggestions.description": "A collection of tracks similar to {0}", + "pages.suggestions.error": "Couldn't find any tracks like {0}", + "pages.suggestions.title": "Suggestions", + "pages.track.error": "Couldn't find any tracks like {0}", + "pages.track.similar": "Similar Tracks", + "pages.user.playlists": "Playlists" } \ No newline at end of file diff --git a/src/components/AddToPlaylist.tsx b/src/components/AddToPlaylist.tsx index 1efd407..78f107e 100644 --- a/src/components/AddToPlaylist.tsx +++ b/src/components/AddToPlaylist.tsx @@ -9,6 +9,7 @@ import Loader from './Loader'; import Playlist from 'pipebomb.js/dist/collection/Playlist'; import { createNotification } from './NotificationManager'; import React from 'react'; +import useTranslation from '../hooks/TranslationHook'; let openModal = () => {}; let addToPlaylist = (playlistID: string) => {}; @@ -95,7 +96,7 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { } function generatePlaylistHTML() { - if (!playlists) return ; + if (!playlists) return ; return playlists.map(playlist => (
diff --git a/src/components/Lyrics.tsx b/src/components/Lyrics.tsx index 91c6d1a..ae6af53 100644 --- a/src/components/Lyrics.tsx +++ b/src/components/Lyrics.tsx @@ -7,6 +7,7 @@ import AudioPlayer from "../logic/AudioPlayer"; import Loader from "./Loader"; import useWindowSize from "../hooks/WindowSizeHook"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const Lyrics = React.memo(function Lyrics() { const track = useCurrentTrack(); @@ -76,8 +77,8 @@ const Lyrics = React.memo(function Lyrics() { if (!track) { return ( <> -

Lyrics

-

Play a track to view its lyrics.

+

{useTranslation("lyrics.title")}

+

{useTranslation("lyrics.description")}

) } @@ -85,8 +86,8 @@ const Lyrics = React.memo(function Lyrics() { if (lyrics === null) { return ( <> -

Lyrics

- +

{useTranslation("lyrics.title")}

+ ) } @@ -94,8 +95,8 @@ const Lyrics = React.memo(function Lyrics() { if (!lyrics) { return ( <> -

Lyrics

-

We couldn't find any lyrics for this track.

+

{useTranslation("lyrics.title")}

+

{useTranslation("lyrics.error")}

) } @@ -120,11 +121,11 @@ const Lyrics = React.memo(function Lyrics() { return (
-

Lyrics

+

{useTranslation("lyrics.title")}

{!lyrics.synced && ( -

These lyrics aren't synced with the track.

+

{useTranslation("lyrics.notSynced")}

)} -
Lyrics by { lyrics.provider }
+
{useTranslation("lyrics.title", lyrics.provider)}
diff --git a/src/hooks/TranslationHook.ts b/src/hooks/TranslationHook.ts index afe25d1..f37f842 100644 --- a/src/hooks/TranslationHook.ts +++ b/src/hooks/TranslationHook.ts @@ -4,7 +4,7 @@ import { localise, registerLanguageChangeListener, unregisterLanguageChangeListe /** * Get the localised string from the provided translation key. * @param translationKey the translation key to look up in the current language. - * @param args arguments to substitute for '{}' instances in the localised string. + * @param args arguments to substitute for {0}, {1}, etc... in the localised string. */ export default function useTranslation(translationKey: string, ...args: (string | ReactNode)[]) : ReactNode { const [translation, setTranslation] = useState(localise(translationKey, args)); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 3b7cf54..a74f43d 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -10,6 +10,7 @@ import useSetting from "../hooks/SettingHook"; import styles from "../styles/SettingsPage.module.scss" import { useResizeDetector } from "react-resize-detector"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const themeObject = Theme.getTheme(getSetting("theme", "Classic")); @@ -96,12 +97,12 @@ const SettingsPage = React.memo(function SettingsPage() { return ( <> -

Settings

+

{useTranslation("pages.settings.title")}

Theme {themeChanged && ( -

Reload for changes to take effect

+

{useTranslation("pages.settings.reloadNotice")}

)} @@ -116,9 +117,9 @@ const SettingsPage = React.memo(function SettingsPage() { - Equalizer + {useTranslation("pages.settings.equalizer")} {!nodes.length ? ( -

Play a track to load the EQ

+

{useTranslation("pages.settings.equalizer.notice")}

) : ( <>
@@ -139,7 +140,7 @@ const SettingsPage = React.memo(function SettingsPage() { - + ) }); diff --git a/src/pages/SuggestionsPlaylist.tsx b/src/pages/SuggestionsPlaylist.tsx index 3511fa5..a4b5236 100644 --- a/src/pages/SuggestionsPlaylist.tsx +++ b/src/pages/SuggestionsPlaylist.tsx @@ -12,6 +12,7 @@ import TrackList from "pipebomb.js/dist/collection/TrackList"; import useTrack from "../hooks/TrackHook"; import useTrackMeta from "../hooks/TrackMetaHook"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { let paramID: any = useParams().ID; @@ -42,8 +43,8 @@ const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { if (!trackMeta) { return <> - Error 404 - Track Not Found. + {useTranslation("error.404")} + {useTranslation("error.404.track")} } @@ -51,7 +52,7 @@ const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { if (suggestions === null) { return <> {trackMeta && ( - A collection of tracks similar to {trackMeta.title} + {useTranslation("pages.suggestions.description", {trackMeta.title})} )} @@ -59,13 +60,13 @@ const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { if (!suggestions || !suggestions.getTrackList().length) { const title = trackMeta ? trackMeta.title : "Unknown Track"; - return Couldn't find any tracks like {title} + return {useTranslation("pages.suggestions.error", {title})} } return ( <> {trackMeta && ( - A collection of tracks similar to {trackMeta.title} + {useTranslation("pages.suggestions.description", {trackMeta.title})} )} @@ -98,7 +99,7 @@ const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { return ( <> - Suggestions + {useTranslation("pages.suggestions.title")} { generateListHTML() } ) diff --git a/src/pages/TrackPage.tsx b/src/pages/TrackPage.tsx index 808c072..7239eb3 100644 --- a/src/pages/TrackPage.tsx +++ b/src/pages/TrackPage.tsx @@ -20,6 +20,7 @@ import { useResizeDetector } from "react-resize-detector"; import { BiPlus } from "react-icons/bi"; import PipeBombConnection from "../logic/PipeBombConnection"; import React from "react"; +import useTranslation from "../hooks/TranslationHook"; const TrackPage = React.memo(function TrackPage() { let paramID: any = useParams().ID; @@ -65,8 +66,8 @@ const TrackPage = React.memo(function TrackPage() { if (!trackMeta || !track) { return <> - Error 404 - Track Not Found. + {useTranslation("error.404")} + {useTranslation("error.404.track")} } @@ -131,7 +132,7 @@ const TrackPage = React.memo(function TrackPage() { if (suggestions === null) { return ( <> - Similar Tracks + {useTranslation("pages.track.similar")}
@@ -144,8 +145,8 @@ const TrackPage = React.memo(function TrackPage() { const title = trackMeta ? trackMeta.title : "this"; return ( <> - Similar Tracks - Couldn't find any tracks like {title} + {useTranslation("pages.track.similar")} + {useTranslation("pages.track.error", {title})} ) } @@ -154,7 +155,7 @@ const TrackPage = React.memo(function TrackPage() { <> - Similar Tracks + {useTranslation("pages.track.similar")} @@ -219,11 +220,11 @@ const TrackPage = React.memo(function TrackPage() { - Play Next - Add to Queue - Copy Link - Add to Playlist - Download as MP3 + {useTranslation("buttons.playNext")} + {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.share")} + {useTranslation("buttons.addToPlaylist")} + {useTranslation("buttons.download")} - {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.queue")} {useTranslation("buttons.share")} {useTranslation("buttons.m3u")} diff --git a/src/pages/ExternalPlaylistPage.tsx b/src/pages/ExternalPlaylistPage.tsx index bb1c12f..3b6c961 100644 --- a/src/pages/ExternalPlaylistPage.tsx +++ b/src/pages/ExternalPlaylistPage.tsx @@ -100,7 +100,7 @@ const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { <> - {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.queue")} {useTranslation("buttons.share")} {useTranslation("buttons.likePlaylist")} {useTranslation("buttons.m3u")} diff --git a/src/pages/Playlist.tsx b/src/pages/Playlist.tsx index 1f81101..79f8771 100644 --- a/src/pages/Playlist.tsx +++ b/src/pages/Playlist.tsx @@ -181,7 +181,7 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (self) { return ( - {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.queue")} {useTranslation("buttons.share")} {useTranslation("buttons.renamePlaylist")} {useTranslation("buttons.m3u")} @@ -191,7 +191,7 @@ const PlaylistPage = React.memo(function PlaylistPage() { } else { return ( - {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.queue")} {useTranslation("buttons.share")} {useTranslation("buttons.likePlaylist")} {useTranslation("buttons.m3u")} diff --git a/src/pages/TrackPage.tsx b/src/pages/TrackPage.tsx index 7239eb3..421c817 100644 --- a/src/pages/TrackPage.tsx +++ b/src/pages/TrackPage.tsx @@ -221,7 +221,7 @@ const TrackPage = React.memo(function TrackPage() { {useTranslation("buttons.playNext")} - {useTranslation("buttons.addToQueue")} + {useTranslation("buttons.queue")} {useTranslation("buttons.share")} {useTranslation("buttons.addToPlaylist")} {useTranslation("buttons.download")} From f62cbcf0ca0c850ea77ca400d9f38b9a2997f17c Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Sun, 6 Aug 2023 17:53:39 +1200 Subject: [PATCH 13/14] translate AddToPlaylist --- public/lang/en-US.json | 7 ++++++ src/components/AddToPlaylist.tsx | 30 +++++++++++++++----------- src/components/DeletePlaylist.tsx | 3 ++- src/components/NotificationManager.tsx | 2 +- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/public/lang/en-US.json b/public/lang/en-US.json index 1875ccf..4ce2391 100644 --- a/public/lang/en-US.json +++ b/public/lang/en-US.json @@ -9,6 +9,7 @@ "buttons.likePlaylist": "Like Playlist", "buttons.logout": "Logout", "buttons.m3u": "Download as M3U", + "buttons.newPlaylist": "New Playlist", "buttons.playNext": "Play Next", "buttons.renamePlaylist": "Rename Playlist", "buttons.search": "Search", @@ -17,6 +18,12 @@ "common.loader.playlists": "Loading Playlists", "common.loader.tracks": "Loading Tracks", "common.loader.user": "Loading User", + "components.addToPlaylist.title": "Add to Playlist", + "components.addToPlaylist.lastTrackButton.add": "Add", + "components.addToPlaylist.lastTrackButton.added": "Added", + "components.addToPlaylist.lastTrackButton.error": "Error", + "components.addToPlaylist.notifications.added": "Added {0} to {1}", + "components.addToPlaylist.notifications.failed": "Failed to add {0} to {1}", "error.404": "Error 404", "error.404.playlist": "Playlist Not Found.", "error.404.track": "Track Not Found.", diff --git a/src/components/AddToPlaylist.tsx b/src/components/AddToPlaylist.tsx index 78f107e..dffbf39 100644 --- a/src/components/AddToPlaylist.tsx +++ b/src/components/AddToPlaylist.tsx @@ -26,7 +26,8 @@ export function addTrack(playlist: Playlist) { interface lastButton { playlistID: string, - value: string | JSX.Element + value: React.ReactNode, + isAdded: boolean } const AddToPlaylist = React.memo(function AddToPlaylist() { @@ -34,13 +35,15 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { const [playlists, setPlaylists] = useState(PlaylistIndex.getInstance().getPlaylists()); const [lastTrackButton, setLastTrackButton] = useState({ playlistID: "", - value: "" + value: "", + isAdded: false }); openModal = () => { setLastTrackButton({ playlistID: "", - value: "" + value: "", + isAdded: false }); setVisible(true); } @@ -54,13 +57,14 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { }, []); addToPlaylist = (playlistID: string) => { - if (!selectedTrack || (lastTrackButton.playlistID == playlistID && lastTrackButton.value == "Added")) return; + if (!selectedTrack || (lastTrackButton.playlistID == playlistID && lastTrackButton.isAdded)) return; PlaylistIndex.getInstance().getPlaylist(playlistID) .then(playlist => { setLastTrackButton({ playlistID: playlist.collectionID, - value: + value: , + isAdded: false }); if (!selectedTrack) return; @@ -72,11 +76,12 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { trackName = (await selectedTrack.loadMetadata()).title; } createNotification({ - text: `Added ${trackName} to ${playlist.getName()}` + text: useTranslation("components.addToPlaylist.notifications.added", trackName, playlist.getName()) }); setLastTrackButton({ playlistID: playlist.collectionID, - value: "Added" + value: useTranslation("components.addToPlaylist.lastTrackButton.added"), + isAdded: true }); }).catch(async (error: any) => { console.error(error); @@ -85,11 +90,12 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { trackName = (await selectedTrack.loadMetadata()).title; } createNotification({ - text: `Failed to add ${trackName} to ${playlist.getName()}` + text: useTranslation("components.addToPlaylist.notifications.failed", trackName, playlist.getName()) }); setLastTrackButton({ playlistID: playlist.collectionID, - value: "Error" + value: useTranslation("components.addToPlaylist.lastTrackButton.error"), + isAdded: false }); }); }) @@ -101,18 +107,18 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { return playlists.map(playlist => (
{playlist.getName()} - +
)); } return ( <> - setVisible(false)} title="Add to Playlist"> + setVisible(false)} title={useTranslation("components.addToPlaylist.title") as string}> { generatePlaylistHTML() } - + diff --git a/src/components/DeletePlaylist.tsx b/src/components/DeletePlaylist.tsx index 74acd3e..723ebd7 100644 --- a/src/components/DeletePlaylist.tsx +++ b/src/components/DeletePlaylist.tsx @@ -6,6 +6,7 @@ import styles from "../styles/AddToPlaylist.module.scss"; import { openCreatePlaylist } from "./CreatePlaylist"; import Playlist from 'pipebomb.js/dist/collection/Playlist'; import React from 'react'; +import useTranslation from '../hooks/TranslationHook'; let openModal = () => {}; let addToPlaylist = (playlist: Playlist) => {}; @@ -86,7 +87,7 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { ))} - + diff --git a/src/components/NotificationManager.tsx b/src/components/NotificationManager.tsx index 0dc69c7..9d1ddc5 100644 --- a/src/components/NotificationManager.tsx +++ b/src/components/NotificationManager.tsx @@ -9,7 +9,7 @@ let notificationDupeCount = 0; let mouseInTime: number; export interface NotificationInfo { - text: string, + text: React.ReactNode, status?: "normal" | "warning" | "error" } From df1faee4da607bbbc80abbcb838ec5ed6d7b0a64 Mon Sep 17 00:00:00 2001 From: Valoeghese Date: Thu, 10 Aug 2023 00:37:02 +1200 Subject: [PATCH 14/14] fix order stuff that prevented some pages loading and refactor&fix some code --- src/components/AddToPlaylist.tsx | 19 +++++++++++------ src/logic/Language.ts | 2 +- src/logic/LanguageAdapter.tsx | 4 ++-- src/pages/ErrorPage.tsx | 17 +++++++++++++++ src/pages/ExternalPlaylistPage.tsx | 4 ++-- src/pages/Home.tsx | 8 ++++++-- src/pages/Playlist.tsx | 33 +++++++++++++++++------------- src/pages/SuggestionsPlaylist.tsx | 4 ++-- src/pages/TrackPage.tsx | 4 ++-- src/pages/UserPage.tsx | 4 ++-- 10 files changed, 66 insertions(+), 33 deletions(-) create mode 100644 src/pages/ErrorPage.tsx diff --git a/src/components/AddToPlaylist.tsx b/src/components/AddToPlaylist.tsx index dffbf39..0fa13e8 100644 --- a/src/components/AddToPlaylist.tsx +++ b/src/components/AddToPlaylist.tsx @@ -10,6 +10,7 @@ import Playlist from 'pipebomb.js/dist/collection/Playlist'; import { createNotification } from './NotificationManager'; import React from 'react'; import useTranslation from '../hooks/TranslationHook'; +import { localise } from '../logic/LanguageAdapter'; let openModal = () => {}; let addToPlaylist = (playlistID: string) => {}; @@ -39,6 +40,10 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { isAdded: false }); + const lastTrackButtonAdd = useTranslation("components.addToPlaylist.lastTrackButton.add"); + const lastTrackButtonAdded = useTranslation("components.addToPlaylist.lastTrackButton.added"); + const lastTrackButtonError = useTranslation("components.addToPlaylist.lastTrackButton.error"); + openModal = () => { setLastTrackButton({ playlistID: "", @@ -76,11 +81,11 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { trackName = (await selectedTrack.loadMetadata()).title; } createNotification({ - text: useTranslation("components.addToPlaylist.notifications.added", trackName, playlist.getName()) + text: localise("components.addToPlaylist.notifications.added", trackName, playlist.getName()) }); setLastTrackButton({ playlistID: playlist.collectionID, - value: useTranslation("components.addToPlaylist.lastTrackButton.added"), + value: lastTrackButtonAdded, isAdded: true }); }).catch(async (error: any) => { @@ -90,24 +95,26 @@ const AddToPlaylist = React.memo(function AddToPlaylist() { trackName = (await selectedTrack.loadMetadata()).title; } createNotification({ - text: useTranslation("components.addToPlaylist.notifications.failed", trackName, playlist.getName()) + text: localise("components.addToPlaylist.notifications.failed", trackName, playlist.getName()) }); setLastTrackButton({ playlistID: playlist.collectionID, - value: useTranslation("components.addToPlaylist.lastTrackButton.error"), + value: lastTrackButtonError, isAdded: false }); }); }) } + const playlistsLoaderText = useTranslation("common.loader.playlists"); + function generatePlaylistHTML() { - if (!playlists) return ; + if (!playlists) return ; return playlists.map(playlist => (
{playlist.getName()} - +
)); } diff --git a/src/logic/Language.ts b/src/logic/Language.ts index 34d75b9..05eb5bb 100644 --- a/src/logic/Language.ts +++ b/src/logic/Language.ts @@ -15,7 +15,7 @@ export default class Language { */ public async resolve() : Promise { // always update language to ensure the latest translations are present - const response = await axios.get(`lang/${this.languageId}.json`); + const response = await axios.get(`/lang/${this.languageId}.json`); this.translations = new Map(Object.entries(response.data)); } diff --git a/src/logic/LanguageAdapter.tsx b/src/logic/LanguageAdapter.tsx index ac227fc..6c5a231 100644 --- a/src/logic/LanguageAdapter.tsx +++ b/src/logic/LanguageAdapter.tsx @@ -104,7 +104,7 @@ export function initialiseLanguageAdapter() { defaultLanguage = currentLanguage = new Language("xx-XX", "No Translation"); // load language index for metadata such as supported languages - axios.get("lang/index.json") + axios.get("/lang/index.json") .then(response => response.data) .then((data: LanguagesIndex) => { // read index data @@ -148,7 +148,7 @@ export function loadLanguage(language: string) : Language { * @returns the localisation for the given translation key, searching first in the current language, falling back on the default language, and finally using the translation key * if no translation is found. */ -export function localise(key: string, args: ReactNode[]): ReactNode[] { +export function localise(key: string, ...args: ReactNode[]): ReactNode[] { let localisedTemplate : string = currentLanguage.localise(key) ?? defaultLanguage.localise(key) ?? key; // Insert Args diff --git a/src/pages/ErrorPage.tsx b/src/pages/ErrorPage.tsx new file mode 100644 index 0000000..99f710d --- /dev/null +++ b/src/pages/ErrorPage.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Text } from "@nextui-org/react" +import useTranslation from "../hooks/TranslationHook"; + +interface ErrorProps { + type: string, + cause?: string +} + +const ErrorPage = ({type, cause}: ErrorProps) => { + return <> + {useTranslation(`error.${type}`)} + {useTranslation(`error.${type}.${cause ?? "message"}`)} + +} + +export default ErrorPage; \ No newline at end of file diff --git a/src/pages/ExternalPlaylistPage.tsx b/src/pages/ExternalPlaylistPage.tsx index 3b6c961..74a7b43 100644 --- a/src/pages/ExternalPlaylistPage.tsx +++ b/src/pages/ExternalPlaylistPage.tsx @@ -15,6 +15,7 @@ import PlaylistTop from "../components/PlaylistTop"; import { ViewportList } from "react-viewport-list"; import React from "react"; import useTranslation from "../hooks/TranslationHook"; +import ErrorPage from "./ErrorPage"; const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { const collectionID = useParams().collectionID; @@ -50,8 +51,7 @@ const ExternalPlaylistPage = React.memo(function ExternalPlaylistPage() { if (collection === false) { return ( <> - {useTranslation("error.404")} - {useTranslation("error.404.playlist")} + ) } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 6d0f2a6..049828a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -34,6 +34,8 @@ const HomePage = React.memo(function HomePage() { } }, []); + const playlistsText = useTranslation("pages.home.playlists"); + function generatePlaylistHTML() { if (playlists === null) { return ( @@ -46,7 +48,7 @@ const HomePage = React.memo(function HomePage() { if (playlists.length) { return ( <> - {useTranslation("pages.home.playlists")} + {playlistsText} ) @@ -55,6 +57,8 @@ const HomePage = React.memo(function HomePage() { return null; } + const chartsText = useTranslation("pages.home.charts"); + function generateChartHTML() { if (charts === null) { return ( @@ -67,7 +71,7 @@ const HomePage = React.memo(function HomePage() { if (charts.length) { return ( <> - {useTranslation("pages.home.charts")} + {chartsText}
{charts.map((chart, index) => (
diff --git a/src/pages/Playlist.tsx b/src/pages/Playlist.tsx index 79f8771..2304d2a 100644 --- a/src/pages/Playlist.tsx +++ b/src/pages/Playlist.tsx @@ -19,6 +19,7 @@ import useIsSelf from "../hooks/IsSelfHook"; import { ViewportList } from "react-viewport-list"; import { openRenamePlaylist } from "../components/RenamePlaylist" import useTranslation from "../hooks/TranslationHook"; +import ErrorPage from "./ErrorPage"; let lastPlaylistID = ""; @@ -32,6 +33,13 @@ const PlaylistPage = React.memo(function PlaylistPage() { const navigate = useNavigate(); const self = useIsSelf(playlist?.owner); + // context menu + const queueTranslation = useTranslation("buttons.queue"); + const shareTranslation = useTranslation("buttons.share"); + const renamePlaylistTranslation = useTranslation("buttons.renamePlaylist"); + const likeTranslation = useTranslation("buttons.likePlaylist"); + const m3uTranslation = useTranslation("buttons.m3u"); + const deletePlaylistTranslation = useTranslation("buttons.deletePlaylist"); const playlistID: string = paramID; @@ -91,8 +99,7 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (errorCode == 400) { return ( <> - {useTranslation("error.404")} - {useTranslation("error.404.playlist")} + ) } @@ -100,8 +107,7 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (paramID === undefined || errorCode != 0) { return ( <> - {useTranslation("error.500")} - {useTranslation("error.500.message")} + ) } @@ -119,7 +125,6 @@ const PlaylistPage = React.memo(function PlaylistPage() { ) } - const newTrackList: Track[] = trackList || []; function playPlaylist() { @@ -181,20 +186,20 @@ const PlaylistPage = React.memo(function PlaylistPage() { if (self) { return ( - {useTranslation("buttons.queue")} - {useTranslation("buttons.share")} - {useTranslation("buttons.renamePlaylist")} - {useTranslation("buttons.m3u")} - {useTranslation("buttons.deletePlaylist")} + {queueTranslation} + {shareTranslation} + {renamePlaylistTranslation} + {m3uTranslation} + {deletePlaylistTranslation} ); } else { return ( - {useTranslation("buttons.queue")} - {useTranslation("buttons.share")} - {useTranslation("buttons.likePlaylist")} - {useTranslation("buttons.m3u")} + {queueTranslation} + {shareTranslation} + {likeTranslation} + {m3uTranslation} ) } diff --git a/src/pages/SuggestionsPlaylist.tsx b/src/pages/SuggestionsPlaylist.tsx index a4b5236..d973929 100644 --- a/src/pages/SuggestionsPlaylist.tsx +++ b/src/pages/SuggestionsPlaylist.tsx @@ -13,6 +13,7 @@ import useTrack from "../hooks/TrackHook"; import useTrackMeta from "../hooks/TrackMetaHook"; import React from "react"; import useTranslation from "../hooks/TranslationHook"; +import ErrorPage from "./ErrorPage"; const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { let paramID: any = useParams().ID; @@ -43,8 +44,7 @@ const SuggestionsPlaylist = React.memo(function SuggestionsPlaylist() { if (!trackMeta) { return <> - {useTranslation("error.404")} - {useTranslation("error.404.track")} + } diff --git a/src/pages/TrackPage.tsx b/src/pages/TrackPage.tsx index 421c817..6f4f59b 100644 --- a/src/pages/TrackPage.tsx +++ b/src/pages/TrackPage.tsx @@ -21,6 +21,7 @@ import { BiPlus } from "react-icons/bi"; import PipeBombConnection from "../logic/PipeBombConnection"; import React from "react"; import useTranslation from "../hooks/TranslationHook"; +import ErrorPage from "./ErrorPage"; const TrackPage = React.memo(function TrackPage() { let paramID: any = useParams().ID; @@ -66,8 +67,7 @@ const TrackPage = React.memo(function TrackPage() { if (!trackMeta || !track) { return <> - {useTranslation("error.404")} - {useTranslation("error.404.track")} + } diff --git a/src/pages/UserPage.tsx b/src/pages/UserPage.tsx index 83e7d47..e9023b8 100644 --- a/src/pages/UserPage.tsx +++ b/src/pages/UserPage.tsx @@ -10,6 +10,7 @@ import { Text } from "@nextui-org/react" import PlaylistCollection from "../components/PlaylistCollection"; import React from "react"; import useTranslation from "../hooks/TranslationHook"; +import ErrorPage from "./ErrorPage"; const UserPage = React.memo(function UserPage() { const userID = useParams().userID; @@ -33,8 +34,7 @@ const UserPage = React.memo(function UserPage() { if (user === false) { return ( <> - {useTranslation("error.404")} - {useTranslation("error.404.user")} + ) }