Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions public/lang/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"buttons.add": "Add",
"buttons.queue": "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.newPlaylist": "New Playlist",
"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",
"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.",
"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",
"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.search.top": "Top Results",
"pages.search.playlists": "Playlists",
"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"
}
11 changes: 11 additions & 0 deletions public/lang/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"languages": {
"en-US": {
"displayName": "English (United States)"
}
},
"primaryDialects": {
"en": "en-US"
},
"default": "en-US"
}
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { initialiseLanguageAdapter } from "./logic/LanguageAdapter";

const theme = Theme.getTheme(getSetting("theme", "Classic"));
initialiseLanguageAdapter();

const App = React.memo(function App() {
const navigate = useNavigate();
Expand Down
40 changes: 27 additions & 13 deletions src/components/AddToPlaylist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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';
import { localise } from '../logic/LanguageAdapter';

let openModal = () => {};
let addToPlaylist = (playlistID: string) => {};
Expand All @@ -25,21 +27,28 @@ export function addTrack(playlist: Playlist) {

interface lastButton {
playlistID: string,
value: string | JSX.Element
value: React.ReactNode,
isAdded: boolean
}

const AddToPlaylist = React.memo(function AddToPlaylist() {
const [visible, setVisible] = useState(false);
const [playlists, setPlaylists] = useState(PlaylistIndex.getInstance().getPlaylists());
const [lastTrackButton, setLastTrackButton] = useState<lastButton>({
playlistID: "",
value: ""
value: "",
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: "",
value: ""
value: "",
isAdded: false
});
setVisible(true);
}
Expand All @@ -53,13 +62,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: <Loading type="points"></Loading>
value: <Loading type="points"></Loading>,
isAdded: false
});

if (!selectedTrack) return;
Expand All @@ -71,11 +81,12 @@ const AddToPlaylist = React.memo(function AddToPlaylist() {
trackName = (await selectedTrack.loadMetadata()).title;
}
createNotification({
text: `Added ${trackName} to ${playlist.getName()}`
text: localise("components.addToPlaylist.notifications.added", trackName, playlist.getName())
});
setLastTrackButton({
playlistID: playlist.collectionID,
value: "Added"
value: lastTrackButtonAdded,
isAdded: true
});
}).catch(async (error: any) => {
console.error(error);
Expand All @@ -84,34 +95,37 @@ const AddToPlaylist = React.memo(function AddToPlaylist() {
trackName = (await selectedTrack.loadMetadata()).title;
}
createNotification({
text: `Failed to add ${trackName} to ${playlist.getName()}`
text: localise("components.addToPlaylist.notifications.failed", trackName, playlist.getName())
});
setLastTrackButton({
playlistID: playlist.collectionID,
value: "Error"
value: lastTrackButtonError,
isAdded: false
});
});
})
}

const playlistsLoaderText = useTranslation("common.loader.playlists");

function generatePlaylistHTML() {
if (!playlists) return <Loader text="Loading Playlists" />;
if (!playlists) return <Loader text={playlistsLoaderText as string} />;

return playlists.map(playlist => (
<div key={playlist.collectionID} className={styles.playlist}>
<Text className={styles.name} h3>{playlist.getName()}</Text>
<Button className={styles.add} color="secondary" auto onPress={() => addToPlaylist(playlist.collectionID)} disabled={lastTrackButton.playlistID == playlist.collectionID}>{lastTrackButton.playlistID == playlist.collectionID ? lastTrackButton.value : "Add"}</Button>
<Button className={styles.add} color="secondary" auto onPress={() => addToPlaylist(playlist.collectionID)} disabled={lastTrackButton.playlistID == playlist.collectionID}>{lastTrackButton.playlistID == playlist.collectionID ? lastTrackButton.value : lastTrackButtonAdd}</Button>
</div>
));
}

return (
<>
<CustomModal visible={visible} onClose={() => setVisible(false)} title="Add to Playlist">
<CustomModal visible={visible} onClose={() => setVisible(false)} title={useTranslation("components.addToPlaylist.title") as string}>
{ generatePlaylistHTML() }
<Grid.Container justify="flex-end">
<Grid>
<Button onPress={() => openCreatePlaylist(selectedTrack || undefined)} bordered auto>New Playlist</Button>
<Button onPress={() => openCreatePlaylist(selectedTrack || undefined)} bordered auto>{useTranslation("buttons.newPlaylist")}</Button>
</Grid>
</Grid.Container>
</CustomModal>
Expand Down
3 changes: 2 additions & 1 deletion src/components/DeletePlaylist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {};
Expand Down Expand Up @@ -86,7 +87,7 @@ const AddToPlaylist = React.memo(function AddToPlaylist() {
))}
<Grid.Container justify="flex-end">
<Grid>
<Button onPress={() => openCreatePlaylist(selectedTrack || undefined)} className={styles.newPlaylist} bordered auto>New Playlist</Button>
<Button onPress={() => openCreatePlaylist(selectedTrack || undefined)} className={styles.newPlaylist} bordered auto>{useTranslation("button.newPlaylist")}</Button>
</Grid>
</Grid.Container>
</Modal>
Expand Down
19 changes: 10 additions & 9 deletions src/components/Lyrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -76,26 +77,26 @@ const Lyrics = React.memo(function Lyrics() {
if (!track) {
return (
<>
<h1>Lyrics</h1>
<h3>Play a track to view its lyrics.</h3>
<h1>{useTranslation("lyrics.title")}</h1>
<h3>{useTranslation("lyrics.description")}</h3>
</>
)
}

if (lyrics === null) {
return (
<>
<h1>Lyrics</h1>
<Loader text="Loading lyrics" />
<h1>{useTranslation("lyrics.title")}</h1>
<Loader text={useTranslation("common.loader.lyrics") as string} />
</>
)
}

if (!lyrics) {
return (
<>
<h1>Lyrics</h1>
<h3>We couldn't find any lyrics for this track.</h3>
<h1>{useTranslation("lyrics.title")}</h1>
<h3>{useTranslation("lyrics.error")}</h3>
</>
)
}
Expand All @@ -120,11 +121,11 @@ const Lyrics = React.memo(function Lyrics() {

return (
<div className={styles.container}>
<h1>Lyrics</h1>
<h1>{useTranslation("lyrics.title")}</h1>
{!lyrics.synced && (
<h4>These lyrics aren't synced with the track.</h4>
<h4>{useTranslation("lyrics.notSynced")}</h4>
)}
<h5>Lyrics by { lyrics.provider }</h5>
<h5>{useTranslation("lyrics.title", lyrics.provider)}</h5>

<div className={styles.fadeContainer + (lyrics.synced ? ` ${styles.synced}` : "")}>
<div className={styles.mainContainer}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/NotificationManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let notificationDupeCount = 0;
let mouseInTime: number;

export interface NotificationInfo {
text: string,
text: React.ReactNode,
status?: "normal" | "warning" | "error"
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/PublicServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,7 +51,7 @@ const PublicServer = React.memo(function PublicServer({ server, connectCallback
function generatePingHTML() {
if (ping == false) {
return (
<Button onClick={getPing} auto bordered size="sm">Get Ping</Button>
<Button onClick={getPing} auto bordered size="sm">{useTranslation("buttons.connect.getPing")}</Button>
)
}

Expand All @@ -76,7 +77,7 @@ const PublicServer = React.memo(function PublicServer({ server, connectCallback
<Text h5>{server.address}</Text>
</Grid>
<Grid className={styles.details}>
<Text h5>Uptime: <span>{formatTimeWords(server.uptime)}</span></Text>
<Text h5>{useTranslation("pages.connect.uptime", <span className={styles.value}>{formatTimeWords(server.uptime)}</span>)}</Text>
<div className={styles.ping}>
<Text h5>Ping: </Text>
{ generatePingHTML() }
Expand Down
25 changes: 25 additions & 0 deletions src/hooks/TranslationHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 {0}, {1}, etc... in the localised string.
*/
export default function useTranslation(translationKey: string, ...args: (string | ReactNode)[]) : ReactNode {
const [translation, setTranslation] = useState(localise(translationKey, args));

function onLanguageChange() {
setTranslation(localise(translationKey, args));
}

useEffect(() => {
registerLanguageChangeListener(onLanguageChange);

return () => {
unregisterLanguageChangeListener(onLanguageChange);
};
}, []);

return translation;
}
42 changes: 42 additions & 0 deletions src/logic/Language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from 'axios';

export default class Language {
private languageId: string;
private displayName: string;
private translations?: Map<string, string>;

public constructor(languageId: string, displayName: string) {
this.languageId = languageId;
this.displayName = displayName;
}

/**
* Resolve the language data, if not already loaded.
*/
public async resolve() : Promise<void> {
// 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));
}

/**
* 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, without applying any formatting. 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;
}
}
Loading