diff --git a/README.md b/README.md index 4ee514a..d82f301 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/about.php b/about.php index 74c0c5a..c92c64f 100644 --- a/about.php +++ b/about.php @@ -6,50 +6,50 @@
-

About and Credits

+

-

Wallos v1.1.0

-

License: +

Wallos v1.2.0

+

: GPLv3 - +

- Issues and Request: + : GitHub - +

- The author: + : https://henrique.pt - +

- Icons: + : https://www.streamlinehq.com/freebies/plump-flat-free - +

- Payment Icons: + : https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community) - + @@ -58,7 +58,7 @@ Chart.js: https://www.chartjs.org/ - + diff --git a/endpoints/categories/category.php b/endpoints/categories/category.php index 3b73b8d..9b47b44 100644 --- a/endpoints/categories/category.php +++ b/endpoints/categories/category.php @@ -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); } ?> \ No newline at end of file diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index f0179c4..b9aa2e4 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -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); } diff --git a/endpoints/currency/fixer_api_key.php b/endpoints/currency/fixer_api_key.php index e4b93af..bac1b39 100644 --- a/endpoints/currency/fixer_api_key.php +++ b/endpoints/currency/fixer_api_key.php @@ -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); } diff --git a/endpoints/household/household.php b/endpoints/household/household.php index 218c691..b685176 100644 --- a/endpoints/household/household.php +++ b/endpoints/household/household.php @@ -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); } ?> \ No newline at end of file diff --git a/endpoints/notifications/save.php b/endpoints/notifications/save.php index 8995205..4d09e9f 100644 --- a/endpoints/notifications/save.php +++ b/endpoints/notifications/save.php @@ -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); } diff --git a/endpoints/notifications/sendtestmail.php b/endpoints/notifications/sendtestmail.php index 80072cf..60a1b7d 100644 --- a/endpoints/notifications/sendtestmail.php +++ b/endpoints/notifications/sendtestmail.php @@ -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); } diff --git a/endpoints/payments/payment.php b/endpoints/payments/payment.php index 05be2cc..7242ad6 100644 --- a/endpoints/payments/payment.php +++ b/endpoints/payments/payment.php @@ -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) ])); diff --git a/endpoints/subscription/add.php b/endpoints/subscription/add.php index ed6f92f..bab1d81 100644 --- a/endpoints/subscription/add.php +++ b/endpoints/subscription/add.php @@ -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(); } } } diff --git a/endpoints/subscription/delete.php b/endpoints/subscription/delete.php index 640fc20..a63af2b 100644 --- a/endpoints/subscription/delete.php +++ b/endpoints/subscription/delete.php @@ -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(); diff --git a/endpoints/subscription/get.php b/endpoints/subscription/get.php index 1b9b112..38ad3ce 100644 --- a/endpoints/subscription/get.php +++ b/endpoints/subscription/get.php @@ -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(); diff --git a/endpoints/subscriptions/get.php b/endpoints/subscriptions/get.php index ca19ac7..96d710d 100644 --- a/endpoints/subscriptions/get.php +++ b/endpoints/subscriptions/get.php @@ -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) { ?>

- Empty page + <?= translate('empty_page', $i18n) ?>

- You don't have any subscriptions yet +

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(); diff --git a/includes/connect_endpoint.php b/includes/connect_endpoint.php index c67ca45..80f4cf1 100644 --- a/includes/connect_endpoint.php +++ b/includes/connect_endpoint.php @@ -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'; + ?> \ No newline at end of file diff --git a/includes/footer.php b/includes/footer.php index fb82302..b15b618 100644 --- a/includes/footer.php +++ b/includes/footer.php @@ -4,7 +4,7 @@
- Error +
@@ -16,7 +16,7 @@
- Success +
diff --git a/includes/header.php b/includes/header.php index 35641e8..85a54f1 100644 --- a/includes/header.php +++ b/includes/header.php @@ -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 @@ + +
@@ -49,10 +56,11 @@
diff --git a/includes/i18n/en.php b/includes/i18n/en.php new file mode 100644 index 0000000..8cc100d --- /dev/null +++ b/includes/i18n/en.php @@ -0,0 +1,195 @@ + "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", + +]; + + +?> \ No newline at end of file diff --git a/includes/i18n/getlang.php b/includes/i18n/getlang.php new file mode 100644 index 0000000..345c74d --- /dev/null +++ b/includes/i18n/getlang.php @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/includes/i18n/languages.php b/includes/i18n/languages.php new file mode 100644 index 0000000..6277c0e --- /dev/null +++ b/includes/i18n/languages.php @@ -0,0 +1,9 @@ + Language Name + + $languages = [ + "en" => "English", + "pt" => "Português", + ] + +?> \ No newline at end of file diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php new file mode 100644 index 0000000..1069fe9 --- /dev/null +++ b/includes/i18n/pt.php @@ -0,0 +1,194 @@ + "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.", + +]; + +?> \ No newline at end of file diff --git a/includes/list_subscriptions.php b/includes/list_subscriptions.php index 37b3767..b4390e5 100644 --- a/includes/list_subscriptions.php +++ b/includes/list_subscriptions.php @@ -1,18 +1,20 @@ - +
- Subscription - Paid By - Category + <?= translate('subscription', $i18n) ?> + <?= translate('paid_by', $i18n) ?> + <?= translate('category', $i18n) ?> - URL + <?= translate('url', $i18n) ?> diff --git a/index.php b/index.php index dc97b31..cfc9860 100644 --- a/index.php +++ b/index.php @@ -39,25 +39,30 @@ $headerClass = count($subscriptions) > 0 ? "main-actions" : "main-actions hidden"; $defaultLogo = $theme == "light" ? "images/wallos.png" : "images/walloswhite.png"; ?> +
    -
  • onClick="setSortOption('name')" id="sort-name">Name
  • -
  • onClick="setSortOption('id')" id="sort-id">Last Added
  • -
  • onClick="setSortOption('price')" id="sort-price">Price
  • -
  • onClick="setSortOption('next_payment')" id="sort-next_payment">Next payment
  • -
  • onClick="setSortOption('payer_user_id')" id="sort-payer_user_id">Member
  • -
  • onClick="setSortOption('category_id')" id="sort-category_id">Category
  • -
  • onClick="setSortOption('payment_method_id')" id="sort-payment_method_id">Payment Method
  • +
  • onClick="setSortOption('name')" id="sort-name">
  • +
  • onClick="setSortOption('id')" id="sort-id">
  • +
  • onClick="setSortOption('price')" id="sort-price">
  • +
  • onClick="setSortOption('next_payment')" id="sort-next_payment">
  • +
  • onClick="setSortOption('payer_user_id')" id="sort-payer_user_id">
  • +
  • onClick="setSortOption('category_id')" id="sort-category_id">
  • +
  • onClick="setSortOption('payment_method_id')" id="sort-payment_method_id">
@@ -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) { ?>
- Empty page + <?= translate('empty_page', $i18n) ?>

- You don't have any subscriptions yet +

-

Add subscription

+

- + -
+
- - + +
- + - + +
- +
- +
- - - + + +
diff --git a/login.php b/login.php index cf99a06..29b1e94 100644 --- a/login.php +++ b/login.php @@ -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'])) { } ?>

- Please login. +

- +
- +
- +
- Login details are incorrect. + .
- +
diff --git a/migrations/000005.php b/migrations/000005.php new file mode 100644 index 0000000..b71bb79 --- /dev/null +++ b/migrations/000005.php @@ -0,0 +1,11 @@ +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"'); +} diff --git a/registration.php b/registration.php index 60c0581..979be2c 100644 --- a/registration.php +++ b/registration.php @@ -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'])) { > +
@@ -93,33 +100,46 @@ if (isset($_POST['username'])) { } ?>

- You need to create an account before you're able to login. +

- +
- +
- +
- +
- + +
+
+ + +
diff --git a/scripts/dashboard.js b/scripts/dashboard.js index b17e896..8472b14 100644 --- a/scripts/dashboard.js +++ b/scripts/dashboard.js @@ -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); }); } diff --git a/scripts/i18n/en.js b/scripts/i18n/en.js new file mode 100644 index 0000000..9821598 --- /dev/null +++ b/scripts/i18n/en.js @@ -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', +}; \ No newline at end of file diff --git a/scripts/i18n/getlang.js b/scripts/i18n/getlang.js new file mode 100644 index 0000000..3008140 --- /dev/null +++ b/scripts/i18n/getlang.js @@ -0,0 +1,7 @@ +function translate(key) { + if (i18n[key]) { + return i18n[key]; + } else { + return "[Translation Missing]"; + } +} \ No newline at end of file diff --git a/scripts/i18n/pt.js b/scripts/i18n/pt.js new file mode 100644 index 0000000..4e4f337 --- /dev/null +++ b/scripts/i18n/pt.js @@ -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', +}; diff --git a/scripts/registration.js b/scripts/registration.js new file mode 100644 index 0000000..645560e --- /dev/null +++ b/scripts/registration.js @@ -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(); +}; \ No newline at end of file diff --git a/scripts/settings.js b/scripts/settings.js index ae30258..2eede3d 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -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; }); } diff --git a/settings.php b/settings.php index e88a4ff..aaa23f2 100644 --- a/settings.php +++ b/settings.php @@ -4,7 +4,7 @@
-
- - -
-
- - -
-
- - -
-
- - -
- query($query); - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $currencyId = $row['id']; - $currencies[$currencyId] = $row; - } - ?> -
- - +
+
+ + +
+
+ + +
+
+ + +
query($query); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $currencyId = $row['id']; + $currencies[$currencyId] = $row; } ?> - -
+
+ + +
+
+ + +
- +
@@ -96,7 +109,7 @@
-

Household

+

@@ -106,19 +119,19 @@
- +
@@ -158,23 +171,23 @@
-

Notifications

+

> - +
- + - + +
- +
- +

- SMTP Password is transmitted and stored in plaintext. - For security, please create an account just for this.

+

- - + +
@@ -221,7 +233,7 @@
-

Categories

+

@@ -244,19 +256,19 @@
- +
@@ -309,7 +321,7 @@
-

Currencies

+

@@ -339,20 +351,20 @@
- +

- Exchange rates last updated on +

- Find the supported currencies and correct currency codes on + fixer.io @@ -385,7 +397,7 @@

- For improved performance keep only the currencies you use. +

@@ -409,12 +421,11 @@
- +
- +
-

Display settings

+

> - +
@@ -459,27 +470,27 @@ diff --git a/stats.php b/stats.php index 68047ba..71584f3 100644 --- a/stats.php +++ b/stats.php @@ -144,31 +144,31 @@ if ($result) { ?>
-

General Statistics

+

-
Active Subscriptions
+
-
Monthly Cost
+
-
Yearly Cost
+
-
Average Monthly Subscription Cost
+
-
Most Expensive Subscription Cost
+
-
Amount due this month
+
-

Split Views

+

- Household Split -
(Monthly cost)
+ +
()
@@ -223,8 +223,8 @@ if ($result) { ?>
- Category Split -
(Monthly cost)
+ +
()
diff --git a/styles/styles.css b/styles/styles.css index b07d126..da61058 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -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 {