Add translation system (#85)

This commit is contained in:
Miguel Ribeiro 2024-01-23 22:46:26 +01:00 committed by GitHub
parent d287f303f0
commit d7366dcfb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 965 additions and 317 deletions

View File

@ -17,6 +17,7 @@ Wallos: Open-Source Personal Subscription Tracker
- [Docker-Compose](#docker-compose)
- [Usage](#usage)
- [Contributing](#contributing)
- [Translations](#translations)
- [Screenshots](#screenshots)
- [License](#license)
@ -139,6 +140,15 @@ Feel free to open Pull requests with bug fixes and features. I'll do my best to
Feel free to open issues with bug reports or feature requests. Bug fixes will take priority.
I welcome contributions from the community and look forward to working with you to improve this project.
### Translations
If you want to contribute with a translation of wallos:
- Add your language code to `includes/i18n/languages.php` in the format `"en" => "English"`. Please use the original language name and not the english translation.
- Create a copy of the file `includes/i18n/en.php` and rename it to the language code you used above. Example: pt.php for "pt" => "Português".
- Translate all the values on the language file to the new language. (Incomplete translations will not be accepted).
- Create a copy of the file `scripts/i18n/en.php` and rename it to the language code you used above.
- Translate all the values on the language file to the new language. (Incomplete translations will not be accepted).
## License
This project is licensed under the [GNU General Public License, Version 3](LICENSE.md) - see the [LICENSE.md](LICENSE.md) file for details.

View File

@ -6,50 +6,50 @@
<section class="account-section">
<header>
<h2>About and Credits</h2>
<h2><?= translate('about_and_credits', $i18n) ?></h2>
</header>
<div class="credits-list">
<p>Wallos v1.1.0</p>
<p>License:
<p>Wallos v1.2.0</p>
<p><?= translate('license', $i18n) ?>:
<span>
GPLv3
<a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank" title="Visit external url">
<a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
<p>
Issues and Request:
<?= translate('issues_and_requests', $i18n) ?>:
<span>
GitHub
<a href="https://github.com/ellite/Wallos/issues" target="_blank" title="Visit external url">
<a href="https://github.com/ellite/Wallos/issues" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
<p>
The author:
<?= translate('the_author', $i18n) ?>:
<span>
https://henrique.pt
<a href="https://henrique.pt/" target="_blank" title="Visit external url">
<a href="https://henrique.pt/" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
<p>
Icons:
<?= translate('icons', $i18n) ?>:
<span>
https://www.streamlinehq.com/freebies/plump-flat-free
<a href="https://www.streamlinehq.com/freebies/plump-flat-free" target="_blank" title="Visit external url">
<a href="https://www.streamlinehq.com/freebies/plump-flat-free" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
<p>
Payment Icons:
<?= translate('payment_icons', $i18n) ?>:
<span>
https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community)
<a href="https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community)" target="_blank" title="Visit external url">
<a href="https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community)" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
@ -58,7 +58,7 @@
Chart.js:
<span>
https://www.chartjs.org/
<a href="https://www.chartjs.org/" target="_blank" title="Visit external url">
<a href="https://www.chartjs.org/" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>

View File

@ -19,7 +19,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to add category"
"errorMessage" => translate('failed_add_category', $i18n)
];
echo json_encode($response);
}
@ -35,20 +35,21 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($result) {
$response = [
"success" => true
"success" => true,
"message" => translate('category_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to edit category"
"errorMessage" => translate('failed_edit_category', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"errorMessage" => "Please fill all the fields"
"errorMessage" => translate('fill_all_fields', $i18n)
];
echo json_encode($response);
}
@ -65,7 +66,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($count > 0) {
$response = [
"success" => false,
"errorMessage" => "Category is in use in subscriptions and can't be removed"
"errorMessage" => translate('category_in_use', $i18n)
];
echo json_encode($response);
} else {
@ -75,13 +76,14 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$result = $stmt->execute();
if ($result) {
$response = [
"success" => true
"success" => true,
"message" => translate('category_removed', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to remove category"
"errorMessage" => translate('failed_remove_category', $i18n)
];
echo json_encode($response);
}
@ -89,15 +91,15 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to remove category"
"errorMessage" => translate('failed_remove_category', $i18n)
];
echo json_encode($response);
}
} else {
echo "Error";
echo translate('error', $i18n);
}
} else {
echo "Error";
echo translate('error', $i18n);
}
?>

View File

@ -19,7 +19,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$currencyId = $db->lastInsertRowID();
echo $currencyId;
} else {
echo "Error adding currency entry.";
echo translate('error_adding_currency', $i18n);
}
} else if (isset($_GET['action']) && $_GET['action'] == "edit") {
if (isset($_GET['currencyId']) && $_GET['currencyId'] != "" && isset($_GET['name']) && $_GET['name'] != "" && isset($_GET['symbol']) && $_GET['symbol'] != "") {
@ -36,18 +36,22 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$result = $stmt->execute();
if ($result) {
echo json_encode(["success" => true]);
$response = [
"success" => true,
"message" => $name . " " . translate('currency_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"message" => "Failed to store Currency on the Database"
"message" => translate('failed_to_store_currency', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => "Some fields are missing"
"message" => translate('fields_missing', $i18n)
];
echo json_encode($response);
}
@ -70,7 +74,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($count > 0) {
$response = [
"success" => false,
"message" => "Currency is in use in subscriptions and can't be deleted."
"message" => translate('currency_in_use', $i18n)
];
echo json_encode($response);
exit;
@ -78,7 +82,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($currencyId == $mainCurrencyId) {
$response = [
"success" => false,
"message" => "Currency is set as main currency and can't be deleted."
"message" => translate('currency_is_main', $i18n)
];
echo json_encode($response);
exit;
@ -88,11 +92,11 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result) {
echo json_encode(["success" => true]);
echo json_encode(["success" => true, "message" => translate('currency_removed', $i18n)]);
} else {
$response = [
"success" => false,
"message" => "Failed to remove currency from the Database"
"message" => translate('failed_to_remove_currency', $i18n)
];
echo json_encode($response);
}
@ -101,7 +105,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
} else {
$response = [
"success" => false,
"message" => "Some fields are missing."
"message" => translate('fields_missing', $i18n)
];
echo json_encode($response);
}
@ -111,7 +115,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
} else {
$response = [
"success" => false,
"message" => "Your session expired. Please login again"
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}

View File

@ -17,21 +17,21 @@
$stmt->bindParam(":api_key", $newApiKey, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result) {
echo json_encode(["success" => true]);
echo json_encode(["success" => true, "message" => translate('api_key_saved', $i18n)]);
} else {
$response = [
"success" => false,
"message" => "Failed to store API Key on the Database"
"message" => translate('failed_to_store_api_key', $i18n)
];
echo json_encode($response);
}
} else {
echo json_encode(["success" => true]);
echo json_encode(["success" => true, "message" => translate('apy_key_saved', $i18n)]);
}
} else {
$response = [
"success" => false,
"message" => "Invalid API Key"
"message" => translate('invalid_api_key', $i18n)
];
echo json_encode($response);
}

View File

@ -13,13 +13,13 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$householdId = $db->lastInsertRowID();
$response = [
"success" => true,
"householdId" => $householdId
"householdId" => $householdId,
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to add household member"
"errorMessage" => translate('failed_add_household', $i18n)
];
echo json_encode($response);
}
@ -35,20 +35,21 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($result) {
$response = [
"success" => true
"success" => true,
"message" => translate('member_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to edit household member"
"errorMessage" => translate('failed_edit_household', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"errorMessage" => "Please fill all the fields"
"errorMessage" => translate('fill_all_fields', $i18n)
];
echo json_encode($response);
}
@ -65,7 +66,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($count > 0) {
$response = [
"success" => false,
"errorMessage" => "Household member is in use in subscriptions and can't be removed"
"errorMessage" => translate('household_in_use', $i18n)
];
echo json_encode($response);
} else {
@ -75,13 +76,14 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$result = $stmt->execute();
if ($result) {
$response = [
"success" => true
"success" => true,
"message" => translate('member_removed', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to remove household member"
"errorMessage" => translate('failed_remove_household', $i18n)
];
echo json_encode($response);
}
@ -89,15 +91,15 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
} else {
$response = [
"success" => false,
"errorMessage" => "Failed to remove household member"
"errorMessage" => translate('failed_remove_household', $i18n)
];
echo json_encode($response);
}
} else {
echo "Error";
echo translate('error', $i18n);
}
} else {
echo "Error";
echo translate('error', $i18n);
}
?>

View File

@ -15,7 +15,7 @@
) {
$response = [
"success" => false,
"errorMessage" => "Please fill all mandatory fields"
"errorMessage" => translate('fill_mandatory_fields', $i18n)
];
echo json_encode($response);
} else {
@ -33,7 +33,7 @@
if ($result === false) {
$response = [
"success" => false,
"errorMessage" => "Error saving notifications data"
"errorMessage" => translate('error_saving_notifications', $i18n)
];
echo json_encode($response);
} else {
@ -57,13 +57,14 @@
if ($stmt->execute()) {
$response = [
"success" => true
"success" => true,
"message" => translate('notifications_settings_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Error saving notification data"
"errorMessage" => translate('error_saving_notifications', $i18n)
];
echo json_encode($response);
}

View File

@ -19,7 +19,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
) {
$response = [
"success" => false,
"errorMessage" => "Please fill all fields"
"errorMessage" => translate('fill_all_fields', $i18n)
];
echo json_encode($response);
} else {
@ -34,6 +34,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$fromEmail = $data["fromemail"] ?? "wallos@wallosapp.com";
$mail = new PHPMailer(true);
$mail->CharSet="UTF-8";
$mail->isSMTP();
$mail->Host = $smtpAddress;
@ -51,18 +52,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$mail->setFrom($fromEmail, 'Wallos App');
$mail->addAddress($email, $name);
$mail->Subject = 'Wallos Notification';
$mail->Body = 'This is a test notification. If you\'re seeing this, the configuration is correct.';
$mail->Subject = translate('wallos_notification', $i18n);
$mail->Body = translate('test_notification', $i18n);
if ($mail->send()) {
$response = [
"success" => true,
"message" => translate('notification_sent_successfuly', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Error sending email." . $mail->ErrorInfo
"errorMessage" => translate('email_error', $i18n) . $mail->ErrorInfo
];
echo json_encode($response);
}

View File

@ -4,14 +4,14 @@ session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => "Your session expired. Please login again"
"message" => translate('session_expired', $i18n)
]));
}
if (!isset($_GET['paymentId']) || !isset($_GET['enabled'])) {
die(json_encode([
"success" => false,
"message" => "Some fields are missing."
"message" => translate('fields_missing', $i18n)
]));
}
@ -21,7 +21,7 @@ $inUse = $db->querySingle('SELECT COUNT(*) as count FROM subscriptions WHERE pay
if ($inUse) {
die(json_encode([
"success" => false,
"message" => "Can't delete used payment method"
"message" => translate('payment_in_use', $i18n)
]));
}
@ -33,13 +33,16 @@ $stmtUpdate->bindParam(':enabled', $enabled);
$stmtUpdate->bindParam(':id', $paymentId);
$resultUpdate = $stmtUpdate->execute();
$text = $enabled ? "enabled" : "disabled";
if ($resultUpdate) {
die(json_encode([
"success" => true
"success" => true,
"message" => translate($text, $i18n)
]));
}
die(json_encode([
"success" => false,
"message" => "Failed to update payment method in the database"
"message" => tranlate('failed_update_payment', $i18n)
]));

View File

@ -25,13 +25,13 @@
if (saveLogo($imageData, $uploadFile, $name)) {
return $fileName;
} else {
echo "Error fetching image: " . curl_error($ch);
echo translate('error_fetching_image', $i18n) . ": " . curl_error($ch);
return "";
}
curl_close($ch);
} else {
echo "Error fetching image: " . curl_error($ch);
echo translate('error_fetching_image', $i18n) . ": " . curl_error($ch);
return "";
}
}
@ -194,13 +194,13 @@
if ($stmt->execute()) {
$success['status'] = "Success";
$text = $isEdit ? "updated" : "added";
$success['message'] = "Subscription " . $text . " successfuly";
$success['message'] = translate('subscription_' . $text . '_successfuly', $i18n);
$json = json_encode($success);
header('Content-Type: application/json');
echo $json;
exit();
} else {
echo "Error: " . $db->lastErrorMsg();
echo translate('error', $i18n) . ": " . $db->lastErrorMsg();
}
}
}

View File

@ -12,11 +12,11 @@
http_response_code(204);
} else {
http_response_code(500);
echo json_encode(array("message" => "Error deleting the subscription."));
echo json_encode(array("message" => translate('error_deleting_subscription', $i18n)));
}
} else {
http_response_code(405);
echo json_encode(array("message" => "Invalid request method."));
echo json_encode(array("message" => translate('invalid_request_method', $i18n)));
}
}
$db->close();

View File

@ -31,10 +31,10 @@
header('Content-Type: application/json');
echo $subscriptionJson;
} else {
echo "Error";
echo translate('error', $i18n);
}
} else {
echo "Error";
echo translate('error', $i18n);
}
}
$db->close();

View File

@ -43,7 +43,7 @@
$print[$id]['name']= $subscription['name'];
$cycle = $subscription['cycle'];
$frequency = $subscription['frequency'];
$print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency);
$print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency, $i18n);
$paymentMethodId = $subscription['payment_method_id'];
$print[$id]['currency_code'] = $currencies[$subscription['currency_id']]['code'];
$currencyId = $subscription['currency_id'];
@ -66,19 +66,19 @@
}
if (isset($print)) {
printSubscriptions($print, $sort, $categories, $members);
printSubscriptions($print, $sort, $categories, $members, $i18n);
}
if (count($subscriptions) == 0) {
?>
<div class="empty-page">
<img src="images/siteimages/empty.png" alt="Empty page" />
<img src="images/siteimages/empty.png" alt="<?= translate('empty_page', $i18n) ?>" />
<p>
You don't have any subscriptions yet
<?= translate('no_subscriptions_yet', $i18n) ?>
</p>
<button class="button" onClick="addSubscription()">
<img class="button-icon" src="images/siteicons/plusicon.png">
Add First Subscription
<?= translate('add_first_subscription', $i18n) ?>
</button>
</div>
<?php

View File

@ -76,6 +76,7 @@
$email = $_POST['email'];
$avatar = $_POST['avatar'];
$main_currency = $_POST['main_currency'];
$language = $_POST['language'];
if (isset($_POST['password']) && $_POST['password'] != "") {
$password = $_POST['password'];
@ -84,7 +85,7 @@
if ($password != $confirm) {
$response = [
"success" => false,
"errorMessage" => "Passwords do not match"
"errorMessage" => translate('passwords_dont_match', $i18n)
];
echo json_encode($response);
exit();
@ -92,7 +93,7 @@
} else {
$response = [
"success" => false,
"errorMessage" => "Passwords do not match"
"errorMessage" => translate('passwords_dont_match', $i18n)
];
echo json_encode($response);
exit();
@ -100,9 +101,9 @@
}
if (isset($_POST['password']) && $_POST['password'] != "") {
$sql = "UPDATE user SET avatar = :avatar, username = :username, email = :email, password = :password, main_currency = :main_currency WHERE id = 1";
$sql = "UPDATE user SET avatar = :avatar, username = :username, email = :email, password = :password, main_currency = :main_currency, language = :language WHERE id = 1";
} else {
$sql = "UPDATE user SET avatar = :avatar, username = :username, email = :email, main_currency = :main_currency WHERE id = 1";
$sql = "UPDATE user SET avatar = :avatar, username = :username, email = :email, main_currency = :main_currency, language = :language WHERE id = 1";
}
$stmt = $db->prepare($sql);
@ -110,6 +111,7 @@
$stmt->bindParam(':username', $username, SQLITE3_TEXT);
$stmt->bindParam(':email', $email, SQLITE3_TEXT);
$stmt->bindParam(':main_currency', $main_currency, SQLITE3_INTEGER);
$stmt->bindParam(':language', $language, SQLITE3_TEXT);
if (isset($_POST['password']) && $_POST['password'] != "") {
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
@ -119,12 +121,13 @@
$result = $stmt->execute();
if ($result) {
$cookieExpire = time() + (30 * 24 * 60 * 60);
setcookie('language', $language, $cookieExpire, '/');
if ($username != $oldUsername) {
$_SESSION['username'] = $username;
if (isset($_COOKIE['wallos_login'])) {
$cookie = explode('|', $_COOKIE['wallos_login'], 2) ;
$token = $cookie[1];
$cookieExpire = time() + (30 * 24 * 60 * 60);
$cookieValue = $username . "|" . $token . "|" . $main_currency;
}
}
@ -137,12 +140,13 @@
$response = [
"success" => true,
"message" => translate('user_details_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => "Error updating user data"
"errorMessage" => translate('error_updating_user_data', $i18n)
];
echo json_encode($response);
}
@ -151,7 +155,7 @@
} else {
$response = [
"success" => false,
"errorMessage" => "Please fill all fields"
"errorMessage" => translate('fill_all_fields', $i18n)
];
echo json_encode($response);
exit();

View File

@ -8,4 +8,8 @@ if (!$db) {
die('Connection to the database failed.');
}
require_once 'i18n/languages.php';
require_once 'i18n/getlang.php';
require_once 'i18n/' . $lang . '.php';
?>

View File

@ -4,7 +4,7 @@
<div class="toast-content">
<i class="fas fa-solid fa-x toast-icon error"></i>
<div class="message">
<span class="text text-1">Error</span>
<span class="text text-1"><?= translate("error", $i18n) ?></span>
<span class="text text-2 errorMessage"></span>
</div>
</div>
@ -16,7 +16,7 @@
<div class="toast-content">
<i class="fas fa-solid fa-check toast-icon success"></i>
<div class="message">
<span class="text text-1">Success</span>
<span class="text text-1"><?= translate("success", $i18n) ?></span>
<span class="text text-2 successMessage"></span>
</div>
</div>

View File

@ -4,6 +4,10 @@
require_once 'checksession.php';
require_once 'currency_formatter.php';
require_once 'i18n/languages.php';
require_once 'i18n/getlang.php';
require_once 'i18n/' . $lang . '.php';
if ($userCount == 0) {
$db->close();
header("Location: registration.php");
@ -32,7 +36,10 @@
<script type="text/javascript" src="scripts/common.js"></script>
<script type="text/javascript">
window.theme = "<?= $theme ?>";
window.lang = "<?=$lang ?>";
</script>
<script type="text/javascript" src="scripts/i18n/<?= $lang ?>.js"></script>
<script type="text/javascript" src="scripts/i18n/getlang.js"></script>
</head>
<body>
<header>
@ -49,10 +56,11 @@
<span id="user"><?= $username ?></span>
</button>
<div class="dropdown-content">
<a href="stats.php"><i class="fa-solid fa-chart-simple"></i>Stats</a>
<a href="settings.php"><i class="fa-solid fa-gear"></i>Settings</a>
<a href="about.php"><i class="fa-solid fa-info-circle"></i>About</a>
<a href="logout.php"><i class="fa-solid fa-arrow-right-from-bracket"></i>Logout</a>
<a href="/"><i class="fa-solid fa-list"></i><?= translate('subscriptions', $i18n) ?></a>
<a href="stats.php"><i class="fa-solid fa-chart-simple"></i><?= translate('stats', $i18n) ?></a>
<a href="settings.php"><i class="fa-solid fa-gear"></i><?= translate('settings', $i18n) ?></a>
<a href="about.php"><i class="fa-solid fa-info-circle"></i><?= translate('about', $i18n) ?></a>
<a href="logout.php"><i class="fa-solid fa-arrow-right-from-bracket"></i><?= translate('logout', $i18n) ?></a>
</div>
</div>
</nav>

195
includes/i18n/en.php Normal file
View File

@ -0,0 +1,195 @@
<?php
$i18n = [
// Registration page
"create_account" => "You need to create an account before you're able to login",
'username' => "Username",
'password' => "Password",
"email" => "Email",
"confirm_password" => "Confirm Password",
"main_currency" => "Main Currency",
"language" => "Language",
"passwords_dont_match" => "Passwords do not match",
"registration_failed" => "Registration failed, please try again.",
"register" => "Register",
// Login Page
'please_login' => "Please login",
'stay_logged_in' => "Stay logged in (30 days)",
'login' => "Login",
'login_failed' => "Login details are incorrect",
// Header
'subscriptions' => "Subscriptions",
'stats' => "Statistics",
'settings' => "Settings",
'about' => "About",
'logout' => "Logout",
// Subscriptions page
"subscription" => "Subscription",
"no_subscriptions_yet" => "You don't have any subscriptions yet",
"add_first_subscription" => "Add first subscription",
'new_subscription' => "New Subscription",
'sort' => "Sort",
'name' => "Nome",
'last_added' => "Last Added",
'price' => "Price",
'next_payment' => "Next Payment",
'member' => "Member",
'category' => "Category",
'payment_method' => "Payment Method",
"Daily" => "Daily",
"Weekly" => "Weekly",
"Monthly" => "Monthly",
"Yearly" => "Yearly",
"days" => "days",
"weeks" => "weeks",
"months" => "months",
"years" => "years",
"external_url" => "Visit Externarl URL",
"empty_page" => "Empty Page",
// Subscription form
"add_subscription" => "Add subscription",
"edit_subscription" => "Edit subscription",
"subscription_name" => "Subscription name",
"logo_preview" => "Logo Preview",
"search_logo" => "Search logo on the web",
"web_search" => "Web search",
"currency" => "Currency",
"billing_cycle" => "Billing Cycle",
"frequency" => "Frequency",
"cycle" => "Cycle",
"next_payment" => "Next Payment",
"payment_method" => "Payment Method",
"no_category" => "No category",
"paid_by" => "Paid by",
"url" => "URL",
"notes" => "Notes",
"enable_notifications" => "Enable Notifications for this subscription",
"delete" => "Delete",
"cancel" => "Cancel",
"upload_logo" => "Upload Logo",
// Statistics page
'general_statistics' => "General Statistics",
'active_subscriptions' => "Active Subscriptions",
'monthly_cost' => "Monthly Cost",
'yearly_cost' => "Yearly Cost",
'average_monthly' => "Average Monthly Subscription Cost",
'most_expensive' => "Most Expensive Subscription Cost",
'amount_due' => "Amount due this month",
'split_views' => "Split Views",
'category_split' => "Category Split",
'household_split' => "Household Split",
// About page
'about_and_credits' => "About and Credits",
'license' => "License",
'issues_and_requests' => "Issues and Requests",
'the_author' => "The author",
'icons' => "Icons",
'payment_icons' => "Payment Icons",
// Settings page
'user_details' => "User Details",
"household" => "Household",
"save_member" => "Save Member",
"delete_member" => "Delete Member",
"cant_delete_member" => "Can't delete main member",
"cant_delete_member_in_use" => "Can't delete member in use in subscription",
"notifications" => "Notifications",
"enable_email_notifications" => "Enable email notifications",
"notify_me" => "Notify me",
"day_before" => "day before",
"days_before" => "days before",
"smtp_address" => "SMTP Address",
"port" => "Port",
"smtp_username" => "SMTP Username",
"smtp_password" => "SMTP Password",
"from_email" => "From email (Optional)",
"smtp_info" => "SMTP Password is transmitted and stored in plaintext. For security, please create an account just for this.",
"categories" => "Categories",
"save_category" => "Save Category",
"delete_category" => "Delete Category",
"cant_delete_category_in_use" => "Can't delete category in use in subscription",
"currencies" => "Currencies",
"save_currency" => "Save currency",
"delete_currency" => "Delete currency",
"cant_delete_main_currency" => "Can't delete main currency",
"cant_delete_currency_in_use" => "Can't delete currency in use in subscription",
"exchange_update" => "Exchange rates last updated on",
"currency_info" => "Find the supported currencies and correct currency codes on",
"currency_performance" => "For improved performance keep only the currencies you use.",
"fixer_api_key" => "Fixer API Key",
"api_key" => "API Key",
"fixer_info" => "If you use multiple currencies, and want accurate statistics and sorting on the subscriptions, a FREE API Key from Fixer is necessary.",
"get_key" => "Get your key at",
"display_settings" => "Display Settings",
"switch_theme" => "Switch Light / Dark Theme",
"calculate_monthly_price" => "Calculate and show monthly price for all subscriptions",
"convert_prices" => "Always convert and show prices on my main currency (slower)",
"experimental_settings" => "Experimental Settings",
"remove_background" => "Attempt to remove background of logos from image search (experimental)",
"experimental_info" => "Experimental settings will probably not work perfectly.",
"payment_methods" => "Payment Methods",
"payment_methods_info" => "Click a payment method to disable / enable it.",
"cant_delete_payment_method_in_use" => "Can't disable used payment method",
"disable" => "Disable",
"enable" => "Enable",
"test" => "Test",
"add" => "Add",
"save" => "Save",
// Toast
"success" => "Success",
// Endpoint responses
"session_expired" => "Your session expired. Please login again",
"fields_missing" => "Some fields are missing",
"fill_all_fields" => "Please fill all fields",
"fill_mandatory_fields" => "Please fill all mandatory fields",
"error" => "Error",
// Category
"failed_add_category" => "Failed to add category",
"failed_edit_category" => "Failed to edit category",
"category_in_use" => "Category is in use in subscriptions and can't be removed",
"failed_remove_category" => "Failed to remove category",
"category_saved" => "Category saved",
"category_removed" => "Category removed",
// Currency
"currency_saved" => "was saved.",
"error_adding_currency" => "Error adding currency entry.",
"failed_to_store_currency" => "Failed to store Currency on the Database.",
"currency_in_use" => "Currency is in use in subscriptions and can't be deleted.",
"currency_is_main" => "Currency is set as main currency and can't be deleted.",
"failed_to_remove_currency" => "Failed to remove currency from the Database.",
"failed_to_store_api_key" => "Failed to store API Key on the Database.",
"invalid_api_key" => "Invalid API Key.",
"api_key_saved" => "API key saved successfully",
"currency_removed" => "Currency removed",
// Household
"failed_add_household" => "Failed to add household member",
"failed_edit_household" => "Failed to edit household member",
"failed_remove_household" => "Failed to remove household member",
"household_in_use" => "Household member is in use in subscriptions and can't be removed",
"member_saved" => "Member saved",
"member_removed" => "Member removed",
// Notifications
"error_saving_notifications" => "Error saving notifications data.",
"wallos_notification" => "Wallos Notification",
"test_notification" => "This is a test notification. If you\'re seeing this, the configuration is correct.",
"email_error" => "Error sending email",
"notification_sent_successfuly" => "Notification sent successfuly",
"notifications_settings_saved" => "Notification settings saved successfully.",
// Payments
"payment_in_use" => "Can't disable used payment method",
"failed_update_payment" => "Failed to update payment method in the database",
"enabled" => "enabled",
"disabled" => "disabled",
// Subscription
"error_fetching_image" => "Error fetching image",
"subscription_updated_successfuly" => "Subscription updated successfuly",
"subscription_added_successfuly" => "Subscription added successfuly",
"error_deleting_subscription" => "Error deleting subscription.",
"invalid_request_method" => "Invalid request method.",
// User
"error_updating_user_data" => "Error updating user data.",
"user_details_saved" => "User details saved",
];
?>

25
includes/i18n/getlang.php Normal file
View File

@ -0,0 +1,25 @@
<?php
$lang = "en";
if (isset($_COOKIE['language'])) {
$selectedLanguage = $_COOKIE['language'];
if (array_key_exists($selectedLanguage, $languages)) {
$lang = $selectedLanguage;
}
}
function translate($text, $translations) {
if (array_key_exists($text, $translations)) {
return $translations[$text];
} else {
require_once 'en.php';
if (array_key_exists($text, $i18n)) {
return $i18n[$text];
} else {
return "[i18n String Missing]";
}
}
}
?>

View File

@ -0,0 +1,9 @@
<?php
// File Name => Language Name
$languages = [
"en" => "English",
"pt" => "Português",
]
?>

194
includes/i18n/pt.php Normal file
View File

@ -0,0 +1,194 @@
<?php
$i18n = [
// Registration page
"create_account" => "Tem que criar uma conta antes de poder iniciar sessão",
'username' => "Nome de utilizador",
'password' => "Password",
"email" => "Email",
"confirm_password" => "Confirmar Password",
"main_currency" => "Moeda Principal",
"language" => "Linguagem",
"passwords_dont_match" => "As passwords não coincidem",
"registration_failed" => "O registo falhou. Tente novamente",
"register" => "Registar",
// Login Page
'please_login' => "Por favor inicie sessão",
'stay_logged_in' => "Manter sessão (30 dias)",
'login' => "Iniciar Sessão",
'login_failed' => "Dados de autenticação incorrectos",
// Header
'subscriptions' => "Subscrições",
'stats' => "Estatísticas",
'settings' => "Definições",
'about' => "Sobre",
'logout' => "Terminar Sessão",
// Subscriptions page
"subscription" => "Subscrição",
"no_subscriptions_yet" => "Ainda não tem subscrições",
"add_first_subscription" => "Adicionar primeira subscrição",
'new_subscription' => "Nova Subscrição",
'sort' => "Ordenar",
'name' => "Nome",
'last_added' => "Última Adicionada",
'price' => "Preço",
'next_payment' => "Próximo Pagamento",
'member' => "Membro",
'category' => "Categoria",
'payment_method' => "Metodo de Pagamento",
"Daily" => "Diario",
"Weekly" => "Semanal",
"Monthly" => "Mensal",
"Yearly" => "Anual",
"days" => "dias",
"weeks" => "semanas",
"months" => "meses",
"years" => "anos",
"external_url" => "Visitar URL Externo",
"empty_page" => "Página Vazia",
// Subscription form
"add_subscription" => "Adicionar subscrição",
"edit_subscription" => "Modificar subscrição",
"subscription_name" => "Nome da subscrição",
"logo_preview" => "Pre-visualisação do logo",
"search_logo" => "Pesquisar logo na internet",
"web_search" => "Pesquisa online",
"currency" => "Moeda",
"billing_cycle" => "Ciclo de faturação",
"frequency" => "Frequencia",
"Cycle" => "Ciclo",
"next_payment" => "Próximo Pagamento",
"payment_method" => "Método de Pagamento",
"no_category" => "Sem categoria",
"paid_by" => "Pago por",
"url" => "URL",
"notes" => "Notas",
"enable_notifications" => "Activar notificações para esta subscrição",
"delete" => "Remover",
"cancel" => "Cancelar",
"upload_logo" => "Enviar Logo",
// Statistics page
'general_statistics' => "Estatísticas Gerais",
'active_subscriptions' => "Subscrições Activas",
'monthly_cost' => "Custo Mensal",
'yearly_cost' => "Custo Anual",
'average_monthly' => "Custo Mensal Médio das Subscrições",
'most_expensive' => "Custo da Subscrição Mais Cara",
'amount_due' => "Quantia em dívida este mês",
'split_views' => "Vistas Divididas",
'category_split' => "Por Categoria",
'household_split' => "Por Membro",
// About page
'about_and_credits' => "Sobre e Créditos",
'license' => "Licença",
'issues_and_requests' => "Problemas e Pedidos",
'the_author' => "O Autor",
'icons' => "Ícones",
'payment_icons' => "Ícones de Pagamentos",
// Settings page
'user_details' => "Detalhes do utilizador",
"household" => "Agregado",
"save_member" => "Guardar Membro",
"delete_member" => "Apagar Membro",
"cant_delete_member" => "Não pode apagar o membro principal",
"cant_delete_member_in_use" => "Não pode apagar membro em uso em subscrição",
"notifications" => "Notificações",
"enable_email_notifications" => "Activar notificações por email",
"notify_me" => "Notificar-me",
"day_before" => "dia antes",
"days_before" => "dias antes",
"smtp_address" => "Endereço SMTP",
"port" => "Porto",
"smtp_username" => "Utilizador SMTP",
"smtp_password" => "Password SMTP",
"from_email" => "Email de envio (Opcional)",
"smtp_info" => "A Password é armazenada e transmitida em texto. Por segurança, crie uma conta só para esta finalidade.",
"categories" => "Categorias",
"save_category" => "Guardar Categoria",
"delete_category" => "Apagar Categoria",
"cant_delete_category_in_use" => "Não pode apagar categoria em uso em subscrição",
"currencies" => "Moedas",
"save_currency" => "Guardar moeda",
"delete_currency" => "Apagar moeda",
"cant_delete_main_currency" => "Não pode apagar a moeda principal",
"cant_delete_currency_in_use" => "Não pode apagar moeda em uso em subscrição",
"exchange_update" => "Taxas de conversão actualizadas em",
"currency_info" => "Encontre a lista de moedas e os respectivos códigos em",
"currency_performance" => "Por motivos de desempenho mantenha apenas as moedas que usa.",
"fixer_api_key" => "Fixer API Key",
"api_key" => "API Key",
"fixer_info" => "Se usa multiplas moedas e deseja estatísticas correctas é necessário uma API Key grátis do Fixer.",
"get_key" => "Obtenha a sua API Key em",
"display_settings" => "Definições de visualização",
"switch_theme" => "Trocar Tema Claro / Escuro",
"calculate_monthly_price" => "Calcular e mostrar preço mensal para todas as subscrições",
"convert_prices" => "Converter e mostrar todas as subscrições na moeda principal (mais lento)",
"experimental_settings" => "Definições Experimentais",
"remove_background" => "Tentar remover o fundo dos logos na pesquisa de imagem (experimental)",
"experimental_info" => "Definições experimentais provavelmente não funcionarão correctamente.",
"payment_methods" => "Métodos de Pagamento",
"payment_methods_info" => "Clique num método de pagamento para o activar / desactivar.",
"cant_delete_payment_method_in_use" => "Não pode desactivar metodo de pagamento em uso",
"disable" => "Desactivar",
"enable" => "Activar",
"test" => "Testar",
"add" => "Adicionar",
"save" => "Guardar",
// Toast
"success" => "Sucesso",
// Endpoint responses
"session_expired" => "A sessão expirou. Por favor autentique-se.",
"fields_missing" => "Alguns campos em falta",
"fill_all_fields" => "Por favor preencha todos os campos",
"fill_mandatory_fields" => "Por favor preencha todos os campos obrigatórios",
"error" => "Erro",
// Category
"failed_add_category" => "Erro ao adicionar categoria",
"failed_edit_category" => "Erro ao modificar categoria",
"category_in_use" => "Categoria em uso em subscrição e não pode ser removida",
"failed_remove_category" => "Erro ao remover categoria",
"category_saved" => "Categoria guardada",
"category_removed" => "Categoria removida",
// Currency
"currency_saved" => "guardada.",
"error_adding_currency" => "Erro ao adicionar moeda.",
"failed_to_store_currency" => "Erro ao guardar a moeda na base de dados.",
"currency_in_use" => "Moeda em uso em subscrição e não pode ser removida.",
"currency_is_main" => "A Moeda principal não pode ser removida.",
"failed_to_remove_currency" => "Erro ao remover a moeda da base de dados.",
"failed_to_store_api_key" => "Erro ao guardar API Key na base de dados.",
"invalid_api_key" => "API Key inválida.",
"api_key_saved" => "API key guardada",
"currency_removed" => "Moeda removida",
// Household
"failed_add_household" => "Erro ao adicionar membro",
"failed_edit_household" => "Erro ao modificar membro",
"failed_remove_household" => "Erro ao remover membro",
"household_in_use" => "Membro está em uso em subscrição e não pode er removido",
"member_saved" => "Membro guardado",
"member_removed" => "Membro removido",
// Notifications
"error_saving_notifications" => "Erro ao guardar os dados das notificaçoes.",
"wallos_notification" => "Notificação Wallos",
"test_notification" => "Isto é uma notificação de teste. Se está a ver isto a configuração está correcta.",
"email_error" => "Erro ao enviar email",
"notification_sent_successfuly" => "Notificação enviada com sucesso",
"notifications_settings_saved" => "Configuração de notificações guardada.",
// Payments
"payment_in_use" => "Não pode desactivar método de pagamento em uso",
"failed_update_payment" => "Erro ao actualizar método de pagamento na base de dados",
"enabled" => "activado",
"disabled" => "descativado",
// Subscription
"error_fetching_image" => "Erro ao obter a imagem",
"subscription_updated_successfuly" => "Subscrição actualizada com sucesso",
"subscription_added_successfuly" => "Subscrição adicionada com sucesso",
"error_deleting_subscription" => "Erro ao remover subscrição.",
"invalid_request_method" => "Método invalido.",
// User
"error_updating_user_data" => "Erro ao actualizar dados do utilizador.",
"user_details_saved" => "Dados do utiliador actualizados.",
];
?>

View File

@ -1,18 +1,20 @@
<?php
function getBillingCycle($cycle, $frequency) {
require_once 'i18n/getlang.php';
function getBillingCycle($cycle, $frequency, $i18n) {
switch ($cycle) {
case 1:
return $frequency == 1 ? "Daily" : $frequency . " days";
return $frequency == 1 ? translate('Daily', $i18n) : $frequency . " " . translate('days', $i18n);
break;
case 2:
return $frequency == 1 ? "Weekly" : $frequency . " weeks";
return $frequency == 1 ? translate('Weekly', $i18n) : $frequency . " " . translate('weeks', $i18n);
break;
case 3:
return $frequency == 1 ? "Monthly" : $frequency . " months";
return $frequency == 1 ? translate('Monthly', $i18n) : $frequency . " " . translate('months', $i18n);
break;
case 4:
return $frequency == 1 ? "Yearly" : $frequency . " years";
return $frequency == 1 ? translate('YEarly', $i18n) : $frequency . " " . translate('years', $i18n);
break;
}
}
@ -54,7 +56,7 @@
}
}
function printSubscriptions($subscriptions, $sort, $categories, $members) {
function printSubscriptions($subscriptions, $sort, $categories, $members, $i18n) {
if ($sort === "price") {
usort($subscriptions, function($a, $b) {
return $a['price'] < $b['price'] ? 1 : -1;
@ -97,19 +99,19 @@
<span class="cycle"><?= $subscription['billing_cycle'] ?></span>
<span class="next"><?= $subscription['next_payment'] ?></span>
<span class="price">
<img src="<?= $subscription['payment_method_icon'] ?>" title="Payment Method: <?= $subscription['payment_method_name'] ?>"/>
<img src="<?= $subscription['payment_method_icon'] ?>" title="<?= translate('payment_method', $i18n) ?>: <?= $subscription['payment_method_name'] ?>"/>
<?= CurrencyFormatter::format($subscription['price'], $subscription['currency_code']) ?>
</span>
<span class="actions">
<button class="image-button medium" onClick="openEditSubscription(event, <?= $subscription['id'] ?>)" name="edit">
<img src="images/siteicons/edit.png" title="Edit subscription">
<img src="images/siteicons/edit.png" title="<?= translate('edit_subscription', $i18n) ?>">
</button>
</span>
</div>
<div class="subscription-secondary">
<span class="name"><img src="images/siteicons/subscription.png" alt="Subscription" /><?= $subscription['name'] ?></span>
<span class="payer_user" title="Paid By"><img src="images/siteicons/payment.png" alt="Paid By" /><?= $members[$subscription['payer_user_id']]['name'] ?></span>
<span class="category" title="Category" ><img src="images/siteicons/category.png" alt="Category" /><?= $categories[$subscription['category_id']]['name'] ?></span>
<span class="name"><img src="images/siteicons/subscription.png" alt="<?= translate('subscription', $i18n) ?>" /><?= $subscription['name'] ?></span>
<span class="payer_user" title="<?= translate('paid_by', $i18n) ?>"><img src="images/siteicons/payment.png" alt="<?= translate('paid_by', $i18n) ?>" /><?= $members[$subscription['payer_user_id']]['name'] ?></span>
<span class="category" title="<?= translate('category', $i18n) ?>" ><img src="images/siteicons/category.png" alt="<?= translate('category', $i18n) ?>" /><?= $categories[$subscription['category_id']]['name'] ?></span>
<?php
if ($subscription['url'] != "") {
$url = $subscription['url'];
@ -117,7 +119,7 @@
$url = "https://" . $url;
}
?>
<span class="url" title="External Url"><a href="<?= $url ?>" target="_blank"><img src="images/siteicons/web.png" alt="URL" /></a></span>
<span class="url" title="<?= translate('external_url', $i18n) ?>"><a href="<?= $url ?>" target="_blank"><img src="images/siteicons/web.png" alt="<?= translate('url', $i18n) ?>" /></a></span>
<?php
}
?>

View File

@ -39,25 +39,30 @@
$headerClass = count($subscriptions) > 0 ? "main-actions" : "main-actions hidden";
$defaultLogo = $theme == "light" ? "images/wallos.png" : "images/walloswhite.png";
?>
<style>
.logo-preview:after {
content: '<?= translate('upload_logo', $i18n) ?>';
}
</style>
<section class="contain">
<header class="<?= $headerClass ?>" id="main-actions">
<button class="button" onClick="addSubscription()">
<img class="button-icon" src="images/siteicons/plusicon.png">
New Subscription
<?= translate('new_subscription', $i18n) ?>
</button>
<div class="sort-container">
<button class="button" value="Sort" onClick="toggleSortOptions()" id="sort-button">
<img src="images/siteicons/sort.png" class="button-icon" /> Sort
<img src="images/siteicons/sort.png" class="button-icon" /> <?= translate('sort', $i18n) ?>
</button>
<div class="sort-options" id="sort-options">
<ul>
<li <?= $sort == "name" ? 'class="selected"' : "" ?> onClick="setSortOption('name')" id="sort-name">Name</li>
<li <?= $sort == "id" ? 'class="selected"' : "" ?> onClick="setSortOption('id')" id="sort-id">Last Added</li>
<li <?= $sort == "price" ? 'class="selected"' : "" ?> onClick="setSortOption('price')" id="sort-price">Price</li>
<li <?= $sort == "next_payment" ? 'class="selected"' : "" ?> onClick="setSortOption('next_payment')" id="sort-next_payment">Next payment</li>
<li <?= $sort == "payer_user_id" ? 'class="selected"' : "" ?> onClick="setSortOption('payer_user_id')" id="sort-payer_user_id">Member</li>
<li <?= $sort == "category_id" ? 'class="selected"' : "" ?> onClick="setSortOption('category_id')" id="sort-category_id">Category</li>
<li <?= $sort == "payment_method_id" ? 'class="selected"' : "" ?> onClick="setSortOption('payment_method_id')" id="sort-payment_method_id">Payment Method</li>
<li <?= $sort == "name" ? 'class="selected"' : "" ?> onClick="setSortOption('name')" id="sort-name"><?= translate('name', $i18n) ?></li>
<li <?= $sort == "id" ? 'class="selected"' : "" ?> onClick="setSortOption('id')" id="sort-id"><?= translate('last_added', $i18n) ?></li>
<li <?= $sort == "price" ? 'class="selected"' : "" ?> onClick="setSortOption('price')" id="sort-price"><?= translate('price', $i18n) ?></li>
<li <?= $sort == "next_payment" ? 'class="selected"' : "" ?> onClick="setSortOption('next_payment')" id="sort-next_payment"><?= translate('next_payment', $i18n) ?></li>
<li <?= $sort == "payer_user_id" ? 'class="selected"' : "" ?> onClick="setSortOption('payer_user_id')" id="sort-payer_user_id"><?= translate('member', $i18n) ?></li>
<li <?= $sort == "category_id" ? 'class="selected"' : "" ?> onClick="setSortOption('category_id')" id="sort-category_id"><?= translate('category', $i18n) ?></li>
<li <?= $sort == "payment_method_id" ? 'class="selected"' : "" ?> onClick="setSortOption('payment_method_id')" id="sort-payment_method_id"><?= translate('payment_method', $i18n) ?></li>
</ul>
</div>
</div>
@ -71,7 +76,7 @@
$print[$id]['name']= $subscription['name'];
$cycle = $subscription['cycle'];
$frequency = $subscription['frequency'];
$print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency);
$print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency, $i18n);
$paymentMethodId = $subscription['payment_method_id'];
$print[$id]['currency_code'] = $currencies[$subscription['currency_id']]['code'];
$currencyId = $subscription['currency_id'];
@ -94,20 +99,20 @@
}
if (isset($print)) {
printSubscriptions($print, $sort, $categories, $members);
printSubscriptions($print, $sort, $categories, $members, $i18n);
}
$db->close();
if (count($subscriptions) == 0) {
?>
<div class="empty-page">
<img src="images/siteimages/empty.png" alt="Empty page" />
<img src="images/siteimages/empty.png" alt="<?= translate('empty_page', $i18n) ?>" />
<p>
You don't have any subscriptions yet
<?= translate('no_subscriptions_yet', $i18n) ?>
</p>
<button class="button" onClick="addSubscription()">
<img class="button-icon" src="images/siteicons/plusicon.png">
Add First Subscription
<?= translate('add_first_subscription', $i18n) ?>
</button>
</div>
<?php
@ -117,25 +122,25 @@
<section class="subscription-form" id="subscription-form">
<header>
<h3 id="form-title">Add subscription</h3>
<h3 id="form-title"><?= translate('add_subscription', $i18n) ?></h3>
<span class="fa-solid fa-xmark close-form" onClick="closeAddSubscription()"></span>
</header>
<form action="endpoints/subscription/add.php" method="post" id="subs-form">
<div class="form-group-inline">
<input type="text" id="name" name="name" placeholder="Subscription name" onchange="setSearchButtonStatus()" onkeypress="this.onchange();" onpaste="this.onchange();" oninput="this.onchange();" required>
<input type="text" id="name" name="name" placeholder="<?= translate('subscription_name', $i18n) ?>" onchange="setSearchButtonStatus()" onkeypress="this.onchange();" onpaste="this.onchange();" oninput="this.onchange();" required>
<label for="logo" class="logo-preview">
<img src="" alt="Logo Preview" id="form-logo">
<img src="" alt="<?= translate('logo_preview', $i18n) ?>" id="form-logo">
</label>
<input type="file" id="logo" name="logo" accept="image/jpeg, image/png" onchange="handleFileSelect(event)" class="hidden-input">
<input type="hidden" id="logo-url" name="logo-url">
<div id="logo-search-button" class="image-button medium disabled" title="Search logo on the web" onClick="searchLogo()">
<div id="logo-search-button" class="image-button medium disabled" title="<?= translate('search_logo', $i18n) ?>" onClick="searchLogo()">
<img src="images/siteicons/websearch.png">
</div>
<input type="hidden" id="id" name="id">
<div id="logo-search-results" class="logo-search">
<header>
Web search
<?= translate('web_search', $i18n) ?>
<span class="fa-solid fa-xmark close-logo-search" onClick="closeLogoSearch()"></span>
</header>
<div id="logo-search-images"></div>
@ -143,8 +148,8 @@
</div>
<div class="form-group-inline">
<input type="number" step="0.01" id="price" name="price" placeholder="Price" required>
<select id="currency" name="currency_id" placeholder="Currency">
<input type="number" step="0.01" id="price" name="price" placeholder="<?= translate('price', $i18n) ?>" required>
<select id="currency" name="currency_id" placeholder="<?= translate('add_subscription', $i18n) ?>">
<?php
foreach ($currencies as $currency) {
$selected = ($currency['id'] == $main_currency) ? 'selected' : '';
@ -156,16 +161,12 @@
</select>
</div>
<div class="form-group">
</div>
<div class="form-group">
<div class="inline">
<div class="split66">
<label for="cycle">Billing Cycle</label>
<label for="cycle"><?= translate('billing_cycle', $i18n) ?></label>
<div class="inline">
<select id="frequency" name="frequency" placeholder="Frequency">
<select id="frequency" name="frequency" placeholder="<?= translate('frequency', $i18n) ?>">
<?php
foreach ($frequencies as $frequency) {
?>
@ -178,7 +179,7 @@
<?php
foreach ($cycles as $cycle) {
?>
<option value="<?= $cycle['id'] ?>" <?= $cycle['id'] == 3 ? "selected" : "" ?>><?= $cycle['name'] ?></option>
<option value="<?= $cycle['id'] ?>" <?= $cycle['id'] == 3 ? "selected" : "" ?>><?= translate($cycle['name'], $i18n) ?></option>
<?php
}
?>
@ -186,14 +187,14 @@
</div>
</div>
<div class="split33">
<label for="next_payment">Next Payment</label>
<label for="next_payment"><?= translate('next_payment', $i18n) ?></label>
<input type="date" id="next_payment" name="next_payment" required>
</div>
</div>
</div>
<div class="form-group">
<label for="payment_method">Payment Method</label>
<label for="payment_method"><?= translate('payment_method', $i18n) ?></label>
<select id="payment_method" name="payment_method_id">
<?php
foreach ($payment_methods as $payment) {
@ -208,7 +209,7 @@
</div>
<div class="form-group">
<label for="category">Category</label>
<label for="category"><?= translate('category', $i18n) ?></label>
<select id="category" name="category_id">
<?php
foreach ($categories as $category) {
@ -224,7 +225,7 @@
<div class="form-group">
<label for="payer_user">Paid by</label>
<label for="payer_user"><?= translate('paid_by', $i18n) ?></label>
<select id="payer_user" name="payer_user_id">
<?php
foreach ($members as $member) {
@ -237,11 +238,11 @@
</div>
<div class="form-group">
<input type="text" id="url" name="url" placeholder="URL">
<input type="text" id="url" name="url" placeholder="<?= translate('url', $i18n) ?>">
</div>
<div class="form-group">
<input type="text" id="notes" name="notes" placeholder="Notes">
<input type="text" id="notes" name="notes" placeholder="<?= translate('notes', $i18n) ?>">
</div>
<?php
@ -249,16 +250,16 @@
?>
<div class="form-group-inline">
<input type="checkbox" id="notifications" name="notifications">
<label for="notifications">Enable Notifications for this subscription</label>
<label for="notifications"><?= translate('enable_notifications', $i18n) ?></label>
</div>
<?php
}
?>
<div class="buttons">
<input type="button" value="Delete" class="warning-button left" id="deletesub" style="display: none">
<input type="button" value="Cancel" class="secondary-button" onClick="closeAddSubscription()">
<input type="submit" value="Save" id="save-button">
<input type="button" value="<?= translate('delete', $i18n) ?>" class="warning-button left" id="deletesub" style="display: none">
<input type="button" value="<?= translate('cancel', $i18n) ?>" class="secondary-button" onClick="closeAddSubscription()">
<input type="submit" value="<?= translate('save', $i18n) ?>" id="save-button">
</div>
</form>
</section>

View File

@ -2,6 +2,10 @@
require_once 'includes/connect.php';
require_once 'includes/checkuser.php';
require_once 'includes/i18n/languages.php';
require_once 'includes/i18n/getlang.php';
require_once 'includes/i18n/' . $lang . '.php';
if ($userCount == 0) {
header("Location: registration.php");
exit();
@ -25,7 +29,7 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
$password = $_POST['password'];
$rememberMe = isset($_POST['remember']) ? true : false;
$query = "SELECT id, password, main_currency FROM user WHERE username = :username";
$query = "SELECT id, password, main_currency, language FROM user WHERE username = :username";
$stmt = $db->prepare($query);
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$result = $stmt->execute();
@ -35,10 +39,13 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
$hashedPasswordFromDb = $row['password'];
$userId = $row['id'];
$main_currency = $row['main_currency'];
$language = $row['language'];
if (password_verify($password, $hashedPasswordFromDb)) {
$_SESSION['username'] = $username;
$_SESSION['loggedin'] = true;
$_SESSION['main_currency'] = $main_currency;
$cookieExpire = time() + (30 * 24 * 60 * 60);
setcookie('language', $language, $cookieExpire, '/');
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$addLoginTokens = "INSERT INTO login_tokens (user_id, token) VALUES (?, ?)";
@ -47,9 +54,8 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
$addLoginTokensStmt->bindValue(2, $token, SQLITE3_TEXT);
$addLoginTokensStmt->execute();
$_SESSION['token'] = $token;
$cookieExpire = time() + (30 * 24 * 60 * 60);
$cookieValue = $username . "|" . $token . "|" . $main_currency;
setcookie('wallos_login', $cookieValue , $cookieExpire, '/');
setcookie('wallos_login', $cookieValue, $cookieExpire, '/');
}
$db->close();
header("Location: /");
@ -87,33 +93,33 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
}
?>
<p>
Please login.
<?= translate('please_login', $i18n) ?>
</p>
</header>
<form action="login.php" method="post">
<div class="form-group">
<label for="username">Username:</label>
<label for="username"><?= translate('username', $i18n) ?>:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<label for="password"><?= translate('password', $i18n) ?>:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group-inline">
<input type="checkbox" id="remember" name="remember">
<label for="remember">Stay logged in (30 days)</label>
<label for="remember"><?= translate('stay_logged_in', $i18n) ?></label>
</div>
<?php
if ($loginFailed) {
?>
<sup class="error">
Login details are incorrect.
<?= translate('login_failed', $i18n) ?>.
</sup>
<?php
}
?>
<div class="form-group">
<input type="submit" value="Login">
<input type="submit" value="<?= translate('login', $i18n) ?>">
</div>
</form>
</section>

11
migrations/000005.php Normal file
View File

@ -0,0 +1,11 @@
<?php
// This migration adds a "language" column to the user table and sets all values to english.
/** @noinspection PhpUndefinedVariableInspection */
$columnQuery = $db->query("SELECT * FROM pragma_table_info('user') where name='language'");
$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;
if ($columnRequired) {
$db->exec('ALTER TABLE user ADD COLUMN language TEXT DEFAULT "en"');
$db->exec('UPDATE user SET language = "en"');
}

View File

@ -2,6 +2,10 @@
require_once 'includes/connect.php';
require_once 'includes/checkuser.php';
require_once 'includes/i18n/languages.php';
require_once 'includes/i18n/getlang.php';
require_once 'includes/i18n/' . $lang . '.php';
if ($userCount > 0) {
header("Location: login.php");
exit();
@ -28,12 +32,13 @@ if (isset($_POST['username'])) {
$password = $_POST['password'];
$confirm_password = $_POST['confirm_password'];
$main_currency = $_POST['main_currency'];
$language = $_POST['language'];
$avatar = "0";
if ($password != $confirm_password) {
$passwordMismatch = true;
} else {
$query = "INSERT INTO user (username, email, password, main_currency, avatar) VALUES (:username, :email, :password, :main_currency, :avatar)";
$query = "INSERT INTO user (username, email, password, main_currency, avatar, language) VALUES (:username, :email, :password, :main_currency, :avatar, :language)";
$stmt = $db->prepare($query);
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
@ -41,6 +46,7 @@ if (isset($_POST['username'])) {
$stmt->bindValue(':password', $hashedPassword, SQLITE3_TEXT);
$stmt->bindValue(':main_currency', $main_currency, SQLITE3_TEXT);
$stmt->bindValue(':avatar', $avatar, SQLITE3_TEXT);
$stmt->bindValue(':language', $language, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result) {
@ -80,6 +86,7 @@ if (isset($_POST['username'])) {
<link rel="manifest" href="images/icon/site.webmanifest">
<link rel="stylesheet" href="styles/login.css">
<link rel="stylesheet" href="styles/login-dark-theme.css" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>
<script type="text/javascript" src="scripts/registration.js"></script>
</head>
<body>
<div class="content">
@ -93,28 +100,28 @@ if (isset($_POST['username'])) {
}
?>
<p>
You need to create an account before you're able to login.
<?= translate('create_account', $i18n) ?>
</p>
</header>
<form action="registration.php" method="post">
<div class="form-group">
<label for="username">Username:</label>
<label for="username"><?= translate('username', $i18n) ?>:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<label for="email"><?= translate('email', $i18n) ?>:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<label for="password"><?= translate('password', $i18n) ?>:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password:</label>
<label for="confirm_password"><?= translate('confirm_password', $i18n) ?>:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<div class="form-group">
<label for="currency">Main Currency:</label>
<label for="currency"><?= translate('main_currency', $i18n) ?>:</label>
<select id="currency" name="main_currency" placeholder="Currency">
<?php
foreach ($currencies as $currency) {
@ -125,11 +132,24 @@ if (isset($_POST['username'])) {
?>
</select>
</div>
<div class="form-group">
<label for="language"><?= translate('language', $i18n) ?>:</label>
<select id="language" name="language" placeholder="Language" onchange="changeLanguage(this.value)">
<?php
foreach ($languages as $code => $name) {
$selected = ($code === $lang) ? 'selected' : '';
?>
<option value="<?= $code ?>" <?= $selected ?>><?= $name ?></option>
<?php
}
?>
</select>
</div>
<?php
if ($passwordMismatch) {
?>
<sup class="error">
Passwords do not match.
<?= translate('passwords_dont_match', $i18n) ?>
</sup>
<?php
}
@ -138,13 +158,13 @@ if (isset($_POST['username'])) {
if ($registrationFailed) {
?>
<sup class="error">
Registration failed, please try again.
<?= translate('registration_failed', $i18n) ?>
</sup>
<?php
}
?>
<div class="form-group">
<input type="submit" value="Register">
<input type="submit" value="<?= translate('register', $i18n) ?>">
</div>
</form>
</section>

View File

@ -15,7 +15,7 @@ function resetForm() {
const id = document.querySelector("#id");
id.value = "";
const formTitle = document.querySelector("#form-title");
formTitle.textContent = "Add subscription";
formTitle.textContent = translate('add_subscription');
const logo = document.querySelector("#form-logo");
logo.src = "";
logo.style = 'display: none';
@ -35,7 +35,7 @@ function resetForm() {
function fillEditFormFields(subscription) {
const formTitle = document.querySelector("#form-title");
formTitle.textContent = "Edit subscription";
formTitle.textContent = translate('edit_subscription');
const logo = document.querySelector("#form-logo");
const defaultLogo = window.theme && window.theme == "light" ? "images/wallos.png" : "images/walloswhite.png";
const logoFile = subscription.logo !== null ? "images/uploads/logos/" + subscription.logo : defaultLogo;
@ -91,19 +91,19 @@ function openEditSubscription(event, id) {
if (response.ok) {
return response.json();
} else {
showErrorMessage("Failed to load subscription");
showErrorMessage(translate('failed_to_load_subscription'));
}
})
.then((data) => {
if (data.error || data === "Error") {
showErrorMessage("Failed to load subscription");
showErrorMessage(translate('failed_to_load_subscription'));
} else {
const subscription = data;
fillEditFormFields(subscription);
}
})
.catch((error) => {
showErrorMessage("Failed to load subscription");
showErrorMessage(translate('failed_to_load_subscription'));
});
}
@ -145,11 +145,11 @@ function deleteSubscription(id) {
})
.then(response => {
if (response.ok) {
showSuccessMessage("Subscription deleted");
showSuccessMessage(translate('subscription_deleted'));
fetchSubscriptions();
closeAddSubscription();
} else {
alert("Error deleting the subscription");
alert(translate('error_deleting_subscription'));
}
})
.catch(error => {
@ -188,7 +188,7 @@ function searchLogo() {
}
})
.catch(error => {
console.error("Error fetching image results:", error);
console.error(translate('error_fetching_image_results'), error);
});
} else {
nameInput.focus();
@ -243,7 +243,7 @@ function fetchSubscriptions() {
}
})
.catch(error => {
console.error("Error reloading subscriptions:", error);
console.error(translate('error_reloading_subscription'), error);
});
}

34
scripts/i18n/en.js Normal file
View File

@ -0,0 +1,34 @@
let i18n = {
// Dashboard
'error_reloading_subscription': 'Error reloading subscription:',
'error_fetching_image_results': 'Error fetching image results:',
'subscription_deleted': 'Subscription deleted',
'error_deleting_subscription': "Error deleting subscription",
'failed_to_load_subscription': "Failed to load subscription",
'edit_subscription': "Edit subscription",
'add_subscription': "Add subscription",
// Settings
'network_response_error': "Network response was not ok",
'failed_add_member': 'Failed to add member',
'member': 'Member',
'save_member': 'Save member',
'delete_member': 'Delete member',
'failed_remove_member': 'Failed to remove member',
'failed_save_member': 'Failed to sabe member',
'failed_add_category': 'Failed to add categpry',
'category': 'Category',
'save_category': 'Save category',
'delete_category': 'Delete category',
'failed_remove_category': 'Failed to remove category',
'currency': 'Currency',
'currency_code': 'Currency code',
'save_currency': 'Save currency',
'delete_currency': 'Delete currency',
'failed_remove_currency': 'Failed to remove currency',
'failed_save_currency': 'Failed to save currency',
'cant_disable_payment_in_use': 'Can\'t disable payment in use',
'failed_save_payment_method': 'Failed to sabe payment method',
'unknown_error': 'Unknown error, please try again.',
'error_saving_notification_data': 'Error saving notification data',
'error_sending_notification': 'Error sending notification',
};

7
scripts/i18n/getlang.js Normal file
View File

@ -0,0 +1,7 @@
function translate(key) {
if (i18n[key]) {
return i18n[key];
} else {
return "[Translation Missing]";
}
}

34
scripts/i18n/pt.js Normal file
View File

@ -0,0 +1,34 @@
let i18n = {
// Dashboard
'error_reloading_subscription': 'Erro ao carregar a subscrição:',
'error_fetching_image_results': 'Erro ao obter imagens:',
'subscription_deleted': 'Subscrição eliminada',
'error_deleting_subscription': 'Erro ao eliminar a subscrição',
'failed_to_load_subscription': 'Falha ao carregar a subscrição',
'edit_subscription': 'Editar subscrição',
'add_subscription': 'Adicionar subscrição',
// Settings
'network_response_error': 'Erro de resposta de rede',
'failed_add_member': 'Falha ao adicionar membro',
'member': 'Membro',
'save_member': 'Guardar membro',
'delete_member': 'Remover membro',
'failed_remove_member': 'Erro ao remover membro',
'failed_save_member': 'Erro ao guardar membro',
'failed_add_category': 'Erro ao adicionar categoria',
'category': 'Categoria',
'save_category': 'Guardar categoria',
'delete_category': 'Remover categoria',
'failed_remove_category': 'Erro ao remover categoria',
'currency': 'Moeda',
'currency_code': 'Código de moeda',
'save_currency': 'Guardar moeda',
'delete_currency': 'Remover moeda',
'failed_remove_currency': 'Erro ao remover moeda',
'failed_save_currency': 'Erro ao guardar moeda',
'cant_disable_payment_in_use': 'Não é possível desativar pagamento em uso',
'failed_save_payment_method': 'Erro ao guardar método de pagamento',
'unknown_error': 'Erro desconhecido, por favor, tente novamente.',
'error_saving_notification_data': 'Erro ao guardar dados de notificação',
'error_sending_notification': 'Erro ao enviar notificação',
};

58
scripts/registration.js Normal file
View File

@ -0,0 +1,58 @@
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; path=/";
}
function storeFormFieldValue(fieldId) {
var fieldElement = document.getElementById(fieldId);
if (fieldElement) {
localStorage.setItem(fieldId, fieldElement.value);
}
}
function storeFormFields() {
storeFormFieldValue('username');
storeFormFieldValue('email');
storeFormFieldValue('password');
storeFormFieldValue('confirm_password');
storeFormFieldValue('currency');
}
function restoreFormFieldValue(fieldId) {
var fieldElement = document.getElementById(fieldId);
if (fieldElement) {
fieldElement.value = localStorage.getItem(fieldId) || '';
}
}
function restoreFormFields() {
restoreFormFieldValue('username');
restoreFormFieldValue('email');
restoreFormFieldValue('password');
restoreFormFieldValue('confirm_password');
restoreFormFieldValue('currency');
}
function removeFromStorage() {
localStorage.removeItem('username');
localStorage.removeItem('email');
localStorage.removeItem('password');
localStorage.removeItem('confirm_password');
localStorage.removeItem('currency');
}
function changeLanguage(selectedLanguage) {
storeFormFields();
setCookie("language", selectedLanguage, 365);
location.reload();
}
window.onload = function () {
restoreFormFields();
removeFromStorage();
};

View File

@ -24,8 +24,8 @@ function addMemberButton(memberId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
showErrorMessage("Failed to add member");
throw new Error(translate('network_response_error'));
showErrorMessage(translate('failed_add_member'));
}
return response.json();
})
@ -39,9 +39,9 @@ function addMemberButton(memberId) {
let input = document.createElement("input");
input.type = "text";
input.placeholder = "Member";
input.placeholder = translate('member');
input.name = "member";
input.value = "Member";
input.value = translate('member');
let editLink = document.createElement("button");
editLink.className = "image-button medium"
@ -52,7 +52,7 @@ function addMemberButton(memberId) {
let editImage = document.createElement("img");
editImage.src = "images/siteicons/save.png";
editImage.title = "Save Member";
editImage.title = translate('save_member');
editLink.appendChild(editImage);
@ -65,7 +65,7 @@ function addMemberButton(memberId) {
let deleteImage = document.createElement("img");
deleteImage.src = "images/siteicons/delete.png";
deleteImage.title = "Delete Member";
deleteImage.title = translate('delete_member');
deleteLink.appendChild(deleteImage);
@ -80,7 +80,7 @@ function addMemberButton(memberId) {
document.getElementById("addMember").disabled = false;
})
.catch(error => {
showErrorMessage("Failed to add member");
showErrorMessage(translate('failed_add_member'));
document.getElementById("addMember").disabled = false;
});
@ -91,7 +91,7 @@ function removeMember(memberId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
throw new Error(translate('network_response_error'));
}
return response.json();
})
@ -101,13 +101,13 @@ function removeMember(memberId) {
if (divToRemove) {
divToRemove.parentNode.removeChild(divToRemove);
}
showSuccessMessage("Member removed");
showSuccessMessage(responseData.message);
} else {
showErrorMessage(responseData.errorMessage || "Failed to remove member");
showErrorMessage(responseData.errorMessage || translate('failed_remove_member'));
}
})
.catch(error => {
showErrorMessage("Failed to remove member");
showErrorMessage(translate('failed_remove_member'));
});
}
@ -124,19 +124,19 @@ function editMember(memberId) {
.then(response => {
saveButton.classList.remove("disabled");
if (!response.ok) {
showErrorMessage("Failed to save member");
showErrorMessage(translate('failed_save_member'));
}
return response.json();
})
.then(responseData => {
if (responseData.success) {
showSuccessMessage("Member saved");
showSuccessMessage(responseData.message);
} else {
showErrorMessage(responseData.errorMessage || "Failed to save member");
showErrorMessage(responseData.errorMessage || translate('failed_save_member'));
}
})
.catch(error => {
showErrorMessage("Failed to save member");
showErrorMessage(translate('failed_save_member'));
});
}
}
@ -147,8 +147,8 @@ function addCategoryButton(categoryId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
showErrorMessage("Failed to add category");
throw new Error(translate('network_response_error'));
showErrorMessage(translate('failed_add_category'));
}
return response.json();
})
@ -162,9 +162,9 @@ function addCategoryButton(categoryId) {
let input = document.createElement("input");
input.type = "text";
input.placeholder = "Category";
input.placeholder = translate('category');
input.name = "category";
input.value = "Category";
input.value = translate('category');
let editLink = document.createElement("button");
editLink.className = "image-button medium"
@ -175,7 +175,7 @@ function addCategoryButton(categoryId) {
let editImage = document.createElement("img");
editImage.src = "images/siteicons/save.png";
editImage.title = "Save Category";
editImage.title = translate('save_category');
editLink.appendChild(editImage);
@ -188,7 +188,7 @@ function addCategoryButton(categoryId) {
let deleteImage = document.createElement("img");
deleteImage.src = "images/siteicons/delete.png";
deleteImage.title = "Delete Category";
deleteImage.title = translate('delete_category');
deleteLink.appendChild(deleteImage);
@ -203,7 +203,7 @@ function addCategoryButton(categoryId) {
document.getElementById("addCategory").disabled = false;
})
.catch(error => {
showErrorMessage("Failed to add category");
showErrorMessage(translate('failed_add_category'));
document.getElementById("addCategory").disabled = false;
});
@ -214,7 +214,7 @@ function removeCategory(categoryId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
throw new Error(translate('network_response_error'));
}
return response.json();
})
@ -224,13 +224,13 @@ function removeCategory(categoryId) {
if (divToRemove) {
divToRemove.parentNode.removeChild(divToRemove);
}
showSuccessMessage("Category removed");
showSuccessMessage(responseData.message);
} else {
showErrorMessage(responseData.errorMessage || "Failed to remove category");
showErrorMessage(responseData.errorMessage || translate('failed_remove_category'));
}
})
.catch(error => {
showErrorMessage("Failed to remove category");
showErrorMessage(translate('failed_remove_category'));
});
}
@ -247,19 +247,19 @@ function editCategory(categoryId) {
.then(response => {
saveButton.classList.remove("disabled");
if (!response.ok) {
showErrorMessage("Failed to save category");
showErrorMessage(translate('failed_save_category'));
}
return response.json();
})
.then(responseData => {
if (responseData.success) {
showSuccessMessage("Category saved");
showSuccessMessage(responseData.message);
} else {
showErrorMessage(responseData.errorMessage || "Failed to save category");
showErrorMessage(responseData.errorMessage || translate('failed_save_category'));
}
})
.catch(error => {
showErrorMessage("Failed to save category");
showErrorMessage(translate('failed_save_category'));
});
}
}
@ -270,7 +270,7 @@ function addCurrencyButton(currencyId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
throw new Error(translate('network_response_error'));
showErrorMessage(response.text());
}
return response.text();
@ -292,13 +292,13 @@ function addCurrencyButton(currencyId) {
let inputName = document.createElement("input");
inputName.type = "text";
inputName.placeholder = "Currency";
inputName.placeholder = translate('currency');
inputName.name = "currency";
inputName.value = "Currency";
inputName.value = translate('currency');
let inputCode = document.createElement("input");
inputCode.type = "text";
inputCode.placeholder = "Currency Code";
inputCode.placeholder = translate('currency_code');
inputCode.name = "code";
inputCode.value = "CODE";
@ -311,7 +311,7 @@ function addCurrencyButton(currencyId) {
let editImage = document.createElement("img");
editImage.src = "images/siteicons/save.png";
editImage.title = "Save Currency";
editImage.title = translate('save_currency');
editLink.appendChild(editImage);
@ -324,7 +324,7 @@ function addCurrencyButton(currencyId) {
let deleteImage = document.createElement("img");
deleteImage.src = "images/siteicons/delete.png";
deleteImage.title = "Delete Currency";
deleteImage.title = translate('delete_currency');
deleteLink.appendChild(deleteImage);
@ -352,23 +352,23 @@ function removeCurrency(currencyId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("There was an error removing the currency");
throw new Error(translate('network_response_error'));
}
return response.json();
})
.then(data => {
if (data.success) {
showSuccessMessage("Currency removed");
showSuccessMessage(data.message);
let divToRemove = document.querySelector(`[data-currencyid="${currencyId}"]`);
if (divToRemove) {
divToRemove.parentNode.removeChild(divToRemove);
}
} else {
showErrorMessage(data.message || "Failed to remove currency");
showErrorMessage(data.message || translate('failed_remove_currency'));
}
})
.catch(error => {
showErrorMessage(error.message || "There was an error removing the currency");
showErrorMessage(error.message || translate('failed_remove_currency'));
});
}
@ -388,7 +388,7 @@ function editCurrency(currencyId) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("There was an error saving the currency");
throw new Error(translate('network_response_error'));
}
return response.json();
})
@ -396,17 +396,17 @@ function editCurrency(currencyId) {
if (data.success) {
saveButton.classList.remove("disabled");
saveButton.disabled = false;
showSuccessMessage(currencyName + " was saved");
showSuccessMessage(decodeURI(data.message));
} else {
saveButton.classList.remove("disabled");
saveButton.disabled = false;
showErrorMessage(data.message || "Failed to save currency");
showErrorMessage(data.message || translate('failed_save_currency'));
}
})
.catch(error => {
saveButton.classList.remove("disabled");
saveButton.disabled = false;
showErrorMessage(error.message || "There was an error saving the currency");
showErrorMessage(error.message || translate('failed_save_currency'));
});
}
}
@ -415,7 +415,7 @@ function togglePayment(paymentId) {
const element = document.querySelector(`div[data-paymentid="${paymentId}"]`);
if (element.dataset.inUse === 'yes') {
return showErrorMessage('Can\'t delete used payment method');
return showErrorMessage(translate(cant_disable_payment_in_use));
}
const newEnabledState = element.dataset.enabled === '1' ? '0' : '1';
@ -425,18 +425,18 @@ function togglePayment(paymentId) {
fetch(url).then(response => {
if (!response.ok) {
throw new Error("There was an error saving the payments method");
throw new Error(translate('network_response_error'));
}
return response.json();
}).then(data => {
if (data.success) {
element.dataset.enabled = newEnabledState;
showSuccessMessage(`${paymentMethodName} was saved`);
showSuccessMessage(`${paymentMethodName} ${data.message}`);
} else {
showErrorMessage(data.message || "Failed to save payments method");
showErrorMessage(data.message || translate('failed_save_payment_method'));
}
}).catch(error => {
showErrorMessage(error.message || "There was an error saving the payments method");
showErrorMessage(error.message || translate('failed_save_payment_method'));
});
}
@ -457,14 +457,14 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById("avatar").src = "images/avatars/" + newAvatar + ".svg";
var newUsername = document.getElementById("username").value;
document.getElementById("user").textContent = newUsername;
showSuccessMessage("User details saved");
showSuccessMessage(data.message);
} else {
showErrorMessage(data.errorMessage);
}
document.getElementById("userSubmit").disabled = false;
})
.catch(error => {
showErrorMessage("Unknown error, please try again");
showErrorMessage(translate('unknown_error'));
});
});
@ -484,7 +484,7 @@ function addFixerKeyButton() {
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage("API key saved successfully");
showSuccessMessage(data.message);
document.getElementById("addFixerKey").disabled = false;
} else {
showErrorMessage(data.message);
@ -529,14 +529,14 @@ function saveNotificationsButton() {
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage("Notification settings saved successfully.");
showSuccessMessage(data.message);
} else {
showErrorMessage(data.errorMessage);
}
button.disabled = false;
})
.catch(error => {
showErrorMessage("Error saving notification data");
showErrorMessage(translate('error_saving_notification_data'));
button.disabled = false;
});
}
@ -569,14 +569,14 @@ function testNotificationButton() {
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage("Notification sent successfully.");
showSuccessMessage(data.message);
} else {
showErrorMessage(data.errorMessage);
}
button.disabled = false;
})
.catch(error => {
showErrorMessage("Error sending notification");
showErrorMessage(translate('error_sending_notification'));
button.disabled = false;
});
}

View File

@ -4,7 +4,7 @@
<section class="contain settings">
<section class="account-section">
<header>
<h2>User details</h2>
<h2><?= translate('user_details', $i18n) ?></h2>
</header>
<form action="endpoints/user/saveuser.php" method="post" id="userForm">
<div class="user-form">
@ -35,19 +35,19 @@
</div>
<div class="grow">
<div class="form-group">
<label for="username">Username:</label>
<label for="username"><?= translate('username', $i18n) ?>:</label>
<input type="text" id="username" name="username" value="<?= $userData['username'] ?>" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<label for="email"><?= translate('email', $i18n) ?>:</label>
<input type="email" id="email" name="email" value="<?= $userData['email'] ?>" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<label for="password"><?= translate('password', $i18n) ?>:</label>
<input type="password" id="password" name="password">
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password:</label>
<label for="confirm_password"><?= translate('confirm_password', $i18n) ?>:</label>
<input type="password" id="confirm_password" name="confirm_password">
</div>
<?php
@ -60,7 +60,7 @@
}
?>
<div class="form-group">
<label for="currency">Main Currency:</label>
<label for="currency"><?= translate('main_currency', $i18n) ?>:</label>
<select id="currency" name="main_currency" placeholder="Currency">
<?php
foreach ($currencies as $currency) {
@ -72,10 +72,23 @@
?>
</select>
</div>
<div class="form-group">
<label for="language"><?= translate('language', $i18n) ?>:</label>
<select id="language" name="language" placeholder="Language">
<?php
foreach ($languages as $code => $name) {
$selected = ($code === $lang) ? 'selected' : '';
?>
<option value="<?= $code ?>" <?= $selected ?>><?= $name ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="buttons">
<input type="submit" value="Save" id="userSubmit"/>
<input type="submit" value="<?= translate('save', $i18n) ?>" id="userSubmit"/>
</div>
</div>
</form>
@ -96,7 +109,7 @@
<section class="account-section">
<header>
<h2>Household</h2>
<h2><?= translate('household', $i18n) ?></h2>
</header>
<div class="account-members">
<div id="householdMembers">
@ -106,19 +119,19 @@
<div class="form-group-inline" data-memberid="<?= $member['id'] ?>">
<input type="text" name="member" value="<?= $member['name'] ?>" placeholder="Member">
<button class="image-button medium" onClick="editMember(<?= $member['id'] ?>)" name="save">
<img src="images/siteicons/save.png" title="Save Member">
<img src="images/siteicons/save.png" title="<?= translate('save_member', $i18n) ?>">
</button>
<?php
if ($member['id'] != 1) {
?>
<button class="image-button medium" onClick="removeMember(<?= $member['id'] ?>)">
<img src="images/siteicons/delete.png" title="Delete Member">
<img src="images/siteicons/delete.png" title="<?= translate('delete_member', $i18n) ?>">
</button>
<?php
} else {
?>
<button class="image-button medium disabled">
<img src="images/siteicons/delete.png" title="Can't delete main member">
<img src="images/siteicons/delete.png" title="<?= translate('cant_delete_member', $i18n) ?>">
</button>
<?php
}
@ -129,7 +142,7 @@
?>
</div>
<div class="buttons">
<input type="submit" value="Add" id="addMember" onClick="addMemberButton()"/>
<input type="submit" value="<?= translate('add', $i18n) ?>" id="addMember" onClick="addMemberButton()"/>
</div>
</div>
</section>
@ -158,23 +171,23 @@
<section class="account-section">
<header>
<h2>Notifications</h2>
<h2><?= translate('notifications', $i18n) ?></h2>
</header>
<div class="account-notifications">
<div class="form-group-inline">
<input type="checkbox" id="notifications" name="notifications" <?= $notifications['enabled'] ? "checked" : "" ?>>
<label for="notifications">Enable email notifications</label>
<label for="notifications"><?= translate('enable_email_notifications', $i18n) ?></label>
</div>
<div class="form-group">
<label for="days">Notify me: </label>
<label for="days"><?= translate('notify_me', $i18n) ?>:</label>
<select name="days" id="days">
<?php
for ($i = 1; $i <= 7; $i++) {
$dayText = $i > 1 ? "days" : "day";
$dayText = $i > 1 ? translate('days_before', $i18n) : translate('day_before', $i18n);
$selected = $i == $notifications['days'] ? "selected" : "";
?>
<option value="<?= $i ?>" <?= $selected ?>>
<?= $i ?> <?= $dayText ?> before
<?= $i ?> <?= $dayText ?>
</option>
<?php
}
@ -182,27 +195,26 @@
</select>
</div>
<div class="form-group-inline">
<input type="text" name="smtpaddress" id="smtpaddress" placeholder="SMTP Address" value="<?= $notifications['smtp_address'] ?>" />
<input type="text" name="smtpport" id="smtpport" placeholder="Port" class="one-third" value="<?= $notifications['smtp_port'] ?>" />
<input type="text" name="smtpaddress" id="smtpaddress" placeholder="<?= translate('smtp_address', $i18n) ?>" value="<?= $notifications['smtp_address'] ?>" />
<input type="text" name="smtpport" id="smtpport" placeholder="<?= translate('port', $i18n) ?>" class="one-third" value="<?= $notifications['smtp_port'] ?>" />
</div>
<div class="form-group-inline">
<input type="text" name="smtpusername" id="smtpusername" placeholder="SMTP Username" value="<?= $notifications['smtp_username'] ?>" />
<input type="text" name="smtpusername" id="smtpusername" placeholder="<?= translate('smtp_username', $i18n) ?>" value="<?= $notifications['smtp_username'] ?>" />
</div>
<div class="form-group-inline">
<input type="password" name="smtppassword" id="smtppassword" placeholder="SMTP Password" value="<?= $notifications['smtp_password'] ?>" />
<input type="password" name="smtppassword" id="smtppassword" placeholder="<?= translate('smtp_password', $i18n) ?>" value="<?= $notifications['smtp_password'] ?>" />
</div>
<div class="form-group-inline">
<input type="text" name="fromemail" id="fromemail" placeholder="From email (Optional)" value="<?= $notifications['from_email'] ?>" />
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i> SMTP Password is transmitted and stored in plaintext.
For security, please create an account just for this.</p>
<i class="fa-solid fa-circle-info"></i> <?= translate('smtp_info', $i18n) ?></p>
<p>
</div>
<div class="buttons">
<input type="button" class="secondary-button" value="Test" id="testNotifications" onClick="testNotificationButton()"/>
<input type="submit" value="Save" id="saveNotifications" onClick="saveNotificationsButton()"/>
<input type="button" class="secondary-button" value="<?= translate('test', $i18n) ?>" id="testNotifications" onClick="testNotificationButton()"/>
<input type="submit" value="<?= translate('save', $i18n) ?>" id="saveNotifications" onClick="saveNotificationsButton()"/>
</div>
</div>
</section>
@ -221,7 +233,7 @@
<section class="account-section">
<header>
<h2>Categories</h2>
<h2><?= translate('categories', $i18n) ?></h2>
</header>
<div class="account-categories">
<div id="categories">
@ -244,19 +256,19 @@
<div class="form-group-inline" data-categoryid="<?= $category['id'] ?>">
<input type="text" name="category" value="<?= $category['name'] ?>" placeholder="Category">
<button class="image-button medium" onClick="editCategory(<?= $category['id'] ?>)" name="save">
<img src="images/siteicons/save.png" title="Save Category">
<img src="images/siteicons/save.png" title="<?= translate('save_category', $i18n) ?>">
</button>
<?php
if ($canDelete) {
?>
<button class="image-button medium" onClick="removeCategory(<?= $category['id'] ?>)">
<img src="images/siteicons/delete.png" title="Delete Category">
<img src="images/siteicons/delete.png" title="<?= translate('delete_category', $i18n) ?>">
</button>
<?php
} else {
?>
<button class="image-button medium disabled">
<img src="images/siteicons/delete.png" title="Can't delete category in use in subscription">
<img src="images/siteicons/delete.png" title="<?= translate('cant_delete_category_in_use', $i18n) ?>">
</button>
<?php
}
@ -268,7 +280,7 @@
?>
</div>
<div class="buttons">
<input type="submit" value="Add" id="addCategory" onClick="addCategoryButton()"/>
<input type="submit" value="<?= translate('add', $i18n) ?>" id="addCategory" onClick="addCategoryButton()"/>
</div>
</div>
</section>
@ -309,7 +321,7 @@
<section class="account-section">
<header>
<h2>Currencies</h2>
<h2><?= translate('currencies', $i18n) ?></h2>
</header>
<div class="account-currencies">
<div id="currencies">
@ -339,20 +351,20 @@
<input type="text" name="currency" value="<?= $currency['name'] ?>" placeholder="Currency Name">
<input type="text" name="code" value="<?= $currency['code'] ?>" placeholder="Currency Code">
<button class="image-button medium" onClick="editCurrency(<?= $currency['id'] ?>)" name="save">
<img src="images/siteicons/save.png" title="Save Currency">
<img src="images/siteicons/save.png" title="<?= translate('save_currency', $i18n) ?>">
</button>
<?php
if ($canDelete) {
?>
<button class="image-button medium" onClick="removeCurrency(<?= $currency['id'] ?>)">
<img src="images/siteicons/delete.png" title="Delete Currency">
<img src="images/siteicons/delete.png" title="<?= translate('delete_currency', $i18n) ?>">
</button>
<?php
} else {
$cantDeleteMessage = $isMainCurrency ? "main currency" : "used currency";
$cantDeleteMessage = $isMainCurrency ? translate('cant_delete_main_currency', $i18n) : translate('cant_delete_currency_in_use', $i18n);
?>
<button class="image-button medium disabled">
<img src="images/siteicons/delete.png" title="Can't delete <?= $cantDeleteMessage ?>">
<img src="images/siteicons/delete.png" title="<?= $cantDeleteMessage ?>">
</button>
<?php
}
@ -364,19 +376,19 @@
?>
</div>
<div class="buttons">
<input type="submit" value="Add" id="addCurrency" onClick="addCurrencyButton()"/>
<input type="submit" value="<?= translate('add', $i18n) ?>" id="addCurrency" onClick="addCurrencyButton()"/>
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
Exchange rates last updated on
<?= translate('exchange_update', $i18n) ?>
<span>
<?= $exchange_rates_last_updated ?>
</span>
</p>
<p>
<i class="fa-solid fa-circle-info"></i>
Find the supported currencies and correct currency codes on
<?= translate('currency_info', $i18n) ?>
<span>
fixer.io
<a href="https://fixer.io/symbols" target="_blank" title="Currency codes">
@ -385,7 +397,7 @@
</span>
</p>
<p>
For improved performance keep only the currencies you use.
<?= translate('currency_performance', $i18n) ?>
</p>
</div>
</div>
@ -409,12 +421,11 @@
</header>
<div class="account-fixer">
<div class="form-group">
<input type="text" name="fixer-key" id="fixerKey" value="<?= $apiKey ?>" placeholder="ApiKey">
<input type="text" name="fixer-key" id="fixerKey" value="<?= $apiKey ?>" placeholder="<?= translate('api_key', $i18n) ?>">
</div>
<div class="settings-notes">
<p><i class="fa-solid fa-circle-info"></i> If you use multiple currencies, and want accurate statistics and sorting on the subscriptions,
a FREE API Key from Fixer is necessary.</p>
<p>Get your key at:
<p><i class="fa-solid fa-circle-info"></i><?= translate('fixer_info', $i18n) ?></p>
<p><?= translate('get_key', $i18n) ?>:
<span>
https://fixer.io/
<a href="https://fixer.io/#pricing_plan" title="Get free fixer api key" target="_blank">
@ -424,18 +435,18 @@
</p>
</div>
<div class="buttons">
<input type="submit" value="Save" id="addFixerKey" onClick="addFixerKeyButton()"/>
<input type="submit" value="<?= translate('save', $i18n) ?>" id="addFixerKey" onClick="addFixerKeyButton()"/>
</div>
</div>
</section>
<section class="account-section">
<header>
<h2>Display settings</h2>
<h2><?= translate('display_settings', $i18n) ?></h2>
</header>
<div class="account-settings-list">
<div>
<input type="button" value="Switch Light / Dark Theme" onClick="switchTheme()">
<input type="button" value="<?= translate('switch_theme', $i18n) ?>" onClick="switchTheme()">
</div>
<?php
$monthlyprice = isset($_COOKIE['showMonthlyPrice']) && $_COOKIE['showMonthlyPrice'] === 'true';
@ -445,13 +456,13 @@
<div>
<div class="form-group-inline">
<input type="checkbox" id="monthlyprice" name="monthlyprice" onChange="setShowMonthlyPriceCookie()" <?php if ($monthlyprice) echo 'checked'; ?>>
<label for="monthlyprice">Calculate and show monthly price for all subscriptions</label>
<label for="monthlyprice"><?= translate('calculate_monthly_price', $i18n) ?></label>
</div>
</div>
<div>
<div class="form-group-inline">
<input type="checkbox" id="convertcurrency" name="convertcurrency" onChange="setConvertCurrencyCookie()" <?php if ($convertcurrency) echo 'checked'; ?>>
<label for="convertcurrency">Always convert and show prices on my main currency (slower).</label>
<label for="convertcurrency"><?= translate('convert_prices', $i18n) ?></label>
</div>
</div>
</div>
@ -459,27 +470,27 @@
<section class="account-section">
<header>
<h2>Experimental settings</h2>
<h2><?= translate('experimental_settings', $i18n) ?></h2>
</header>
<div class="account-settings-list">
<div>
<div class="form-group-inline">
<input type="checkbox" id="removebackground" name="removebackground" onChange="setRemoveBackgroundCookie()" <?php if ($removebackground) echo 'checked'; ?>>
<label for="removebackground">Attempt to remove background of logos from image search (experimental).</label>
<label for="removebackground"><?= translate('remove_background', $i18n) ?></label>
</div>
</div>
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
Experimental settings will probably not work perfectly.
<?= translate('experimental_info', $i18n) ?>
</p>
</div>
</section>
<section class="account-section">
<header>
<h2>Payment Methods</h2>
<h2><?= translate('payment_methods', $i18n) ?></h2>
</header>
<div class="payments-list">
<?php
@ -496,7 +507,7 @@
data-enabled="<?= $payment['enabled']; ?>"
data-in-use="<?= $inUse ? 'yes' : 'no' ?>"
data-paymentid="<?= $payment['id'] ?>"
title="<?= $inUse ? 'Can\'t delete used payment method' : '' ?>"
title="<?= $inUse ? translate('cant_delete_payment_method_in_use', $i18n) : ($payment['enabled'] ? translate('disable', $i18n) : translate('enable', $i18n)) ?>"
onClick="togglePayment(<?= $payment['id'] ?>)">
<img src="images/uploads/icons/<?= $payment['icon'] ?>" alt="Logo" />
<span class="payment-name">
@ -510,7 +521,7 @@
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
Click a payment method to disable / enable it.
<?= translate('payment_methods_info', $i18n) ?>
</p>
</div>
</section>

View File

@ -144,31 +144,31 @@ if ($result) {
?>
<section class="contain">
<h2>General Statistics</h2>
<h2><?= translate('general_statistics', $i18n) ?></h2>
<div class="statistics">
<div class="statistic">
<span><?= $activeSubscriptions ?></span>
<div class="title">Active Subscriptions</div>
<div class="title"><?= translate('active_subscriptions', $i18n) ?></div>
</div>
<div class="statistic">
<span><?= CurrencyFormatter::format($totalCostPerMonth, $code) ?></span>
<div class="title">Monthly Cost</div>
<div class="title"><?= translate('monthly_cost', $i18n) ?></div>
</div>
<div class="statistic">
<span><?= CurrencyFormatter::format($totalCostPerYear, $code) ?></span>
<div class="title">Yearly Cost</div>
<div class="title"><?= translate('yearly_cost', $i18n) ?></div>
</div>
<div class="statistic">
<span><?= CurrencyFormatter::format($averageSubscriptionCost, $code) ?></span>
<div class="title">Average Monthly Subscription Cost</div>
<div class="title"><?= translate('average_monthly', $i18n) ?></div>
</div>
<div class="statistic">
<span><?= CurrencyFormatter::format($mostExpensiveSubscription, $code) ?></span>
<div class="title">Most Expensive Subscription Cost</div>
<div class="title"><?= translate('most_expensive', $i18n) ?></div>
</div>
<div class="statistic">
<span><?= CurrencyFormatter::format($amountDueThisMonth, $code) ?></span>
<div class="title">Amount due this month</div>
<div class="title"><?= translate('amount_due', $i18n) ?></div>
</div>
<?php
$numberOfElements = 6;
@ -179,7 +179,7 @@ if ($result) {
}
?>
</div>
<h2>Split Views</h2>
<h2><?= translate('split_views', $i18n) ?></h2>
<div class="graphs">
<?php
$categoryDataPoints = [];
@ -211,8 +211,8 @@ if ($result) {
?>
<section class="graph">
<header>
Household Split
<div class="sub-header">(Monthly cost)</div>
<?= translate('household_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="memberSplitChart"></canvas>
</section>
@ -223,8 +223,8 @@ if ($result) {
?>
<section class="graph">
<header>
Category Split
<div class="sub-header">(Monthly cost)</div>
<?= translate('category_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="categorySplitChart" style="height: 370px; width: 100%;"></canvas>
</section>

View File

@ -87,6 +87,7 @@ header .logo .logo-image {
min-width: 130px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 5;
width: max-content;
}
.dropdown-content a {