diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..116d6852 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "inlang.vs-code-extension" + ] +} \ No newline at end of file diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index edce7790..82a7d555 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -9,18 +9,20 @@ type UserDto struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` IsAdmin bool `json:"isAdmin"` + Locale *string `json:"locale"` CustomClaims []CustomClaimDto `json:"customClaims"` UserGroups []UserGroupDto `json:"userGroups"` LdapID *string `json:"ldapId"` } type UserCreateDto struct { - Username string `json:"username" binding:"required,username,min=2,max=50"` - Email string `json:"email" binding:"required,email"` - FirstName string `json:"firstName" binding:"required,min=1,max=50"` - LastName string `json:"lastName" binding:"required,min=1,max=50"` - IsAdmin bool `json:"isAdmin"` - LdapID string `json:"-"` + Username string `json:"username" binding:"required,username,min=2,max=50"` + Email string `json:"email" binding:"required,email"` + FirstName string `json:"firstName" binding:"required,min=1,max=50"` + LastName string `json:"lastName" binding:"required,min=1,max=50"` + IsAdmin bool `json:"isAdmin"` + Locale *string `json:"locale"` + LdapID string `json:"-"` } type OneTimeAccessTokenCreateDto struct { diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 92043891..a2251b66 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -14,6 +14,7 @@ type User struct { FirstName string `sortable:"true"` LastName string `sortable:"true"` IsAdmin bool `sortable:"true"` + Locale *string LdapID *string CustomClaims []CustomClaim diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 42507bba..8c63c260 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -153,6 +153,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) { Email: input.Email, Username: input.Username, IsAdmin: input.IsAdmin, + Locale: input.Locale, } if input.LdapID != "" { user.LdapID = &input.LdapID @@ -182,6 +183,7 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u user.LastName = updatedUser.LastName user.Email = updatedUser.Email user.Username = updatedUser.Username + user.Locale = updatedUser.Locale if !updateOwnUser { user.IsAdmin = updatedUser.IsAdmin } diff --git a/backend/resources/migrations/postgres/20250320171311_user_locale.down.sql b/backend/resources/migrations/postgres/20250320171311_user_locale.down.sql new file mode 100644 index 00000000..6686c139 --- /dev/null +++ b/backend/resources/migrations/postgres/20250320171311_user_locale.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN locale; \ No newline at end of file diff --git a/backend/resources/migrations/postgres/20250320171311_user_locale.up.sql b/backend/resources/migrations/postgres/20250320171311_user_locale.up.sql new file mode 100644 index 00000000..99644e0c --- /dev/null +++ b/backend/resources/migrations/postgres/20250320171311_user_locale.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN locale TEXT; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20250320171311_user_locale.down.sql b/backend/resources/migrations/sqlite/20250320171311_user_locale.down.sql new file mode 100644 index 00000000..6686c139 --- /dev/null +++ b/backend/resources/migrations/sqlite/20250320171311_user_locale.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN locale; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20250320171311_user_locale.up.sql b/backend/resources/migrations/sqlite/20250320171311_user_locale.up.sql new file mode 100644 index 00000000..99644e0c --- /dev/null +++ b/backend/resources/migrations/sqlite/20250320171311_user_locale.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN locale TEXT; \ No newline at end of file diff --git a/frontend/messages/en.json b/frontend/messages/en.json new file mode 100644 index 00000000..4cd4e4ab --- /dev/null +++ b/frontend/messages/en.json @@ -0,0 +1,318 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "my_account": "My Account", + "logout": "Logout", + "english": "English", + "dutch": "Nederlands", + "confirm": "Confirm", + "key": "Key", + "value": "Value", + "remove_custom_claim": "Remove custom claim", + "add_custom_claim": "Add custom claim", + "add_another": "Add another", + "select_a_date": "Select a date", + "select_file": "Select File", + "profile_picture": "Profile Picture", + "profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.", + "click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.", + "image_should_be_in_format": "The image should be in PNG or JPEG format.", + "items_per_page": "Items per page", + "no_items_found": "No items found", + "search": "Search...", + "expand_card": "Expand card", + "copied": "Copied", + "click_to_copy": "Click to copy", + "something_went_wrong": "Something went wrong", + "go_back_to_home": "Go back to home", + "dont_have_access_to_your_passkey": "Don't have access to your passkey?", + "login_background": "Login background", + "logo": "Logo", + "login_code": "Login Code", + "create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.", + "one_hour": "1 hour", + "twelve_hours": "12 hours", + "one_day": "1 day", + "one_week": "1 week", + "one_month": "1 month", + "expiration": "Expiration", + "generate_code": "Generate Code", + "name": "Name", + "browser_unsupported": "Browser unsupported", + "this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please or use a alternative sign in method.", + "an_unknown_error_occurred": "An unknown error occurred", + "authentication_process_was_aborted": "The authentication process was aborted", + "error_occurred_with_authenticator": "An error occurred with the authenticator", + "authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials", + "authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys", + "passkey_was_previously_registered": "This passkey was previously registered", + "authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms", + "authenticator_timed_out": "The authenticator timed out", + "critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.", + "sign_in_to": "Sign in to {name}", + "client_not_found": "Client not found", + "client_wants_to_access_the_following_information": "{client} wants to access the following information:", + "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to {client} with your {appName} account?", + "email": "Email", + "view_your_email_address": "View your email address", + "profile": "Profile", + "view_your_profile_information": "View your profile information", + "groups": "Groups", + "view_the_groups_you_are_a_member_of": "View the groups you are a member of", + "cancel": "Cancel", + "sign_in": "Sign in", + "try_again": "Try again", + "client_logo": "Client Logo", + "sign_out": "Sign out", + "do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account {username}?", + "sign_in_to_appname": "Sign in to {appName}", + "please_try_to_sign_in_again": "Please try to sign in again.", + "authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.", + "authenticate": "Authenticate", + "appname_setup": "{appName} Setup", + "please_try_again": "Please try again.", + "you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.", + "continue": "Continue", + "alternative_sign_in": "Alternative Sign In", + "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you dont't have access to your passkey, you can sign in using one of the following methods.", + "use_your_passkey_instead": "Use your passkey instead?", + "email_login": "Email Login", + "enter_a_login_code_to_sign_in": "Enter a login code to sign in.", + "request_a_login_code_via_email": "Request a login code via email.", + "go_back": "Go back", + "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.", + "enter_code": "Enter code", + "enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.", + "your_email": "Your email", + "submit": "Submit", + "enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.", + "code": "Code", + "invalid_redirect_url": "Invalid redirect URL", + "audit_log": "Audit Log", + "users": "Users", + "user_groups": "User Groups", + "oidc_clients": "OIDC Clients", + "api_keys": "API Keys", + "application_configuration": "Application Configuration", + "settings": "Settings", + "update_pocket_id": "Update Pocket ID", + "powered_by": "Powered by", + "see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.", + "time": "Time", + "event": "Event", + "approximate_location": "Approximate Location", + "ip_address": "IP Address", + "device": "Device", + "client": "Client", + "unknown": "Unknown", + "account_details_updated_successfully": "Account details updated successfully", + "profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.", + "account_settings": "Account Settings", + "passkey_missing": "Passkey missing", + "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.", + "single_passkey_configured": "Single Passkey Configured", + "it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.", + "account_details": "Account Details", + "passkeys": "Passkeys", + "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.", + "add_passkey": "Add Passkey", + "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.", + "create": "Create", + "first_name": "First name", + "last_name": "Last name", + "username": "Username", + "save": "Save", + "username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols", + "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.", + "or_visit": "or visit", + "added_on": "Added on", + "rename": "Rename", + "delete": "Delete", + "are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?", + "passkey_deleted_successfully": "Passkey deleted successfully", + "delete_passkey_name": "Delete {passkeyName}", + "passkey_name_updated_successfully": "Passkey name updated successfully", + "name_passkey": "Name Passkey", + "name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.", + "create_api_key": "Create API Key", + "add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access.", + "add_api_key": "Add API Key", + "manage_api_keys": "Manage API Keys", + "api_key_created": "API Key Created", + "for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.", + "description": "Description", + "api_key": "API Key", + "close": "Close", + "name_to_identify_this_api_key": "Name to identify this API key.", + "expires_at": "Expires At", + "when_this_api_key_will_expire": "When this API key will expire.", + "optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.", + "name_must_be_at_least_3_characters": "Name must be at least 3 characters", + "name_cannot_exceed_50_characters": "Name cannot exceed 50 characters", + "expiration_date_must_be_in_the_future": "Expiration date must be in the future", + "revoke_api_key": "Revoke API Key", + "never": "Never", + "revoke": "Revoke", + "api_key_revoked_successfully": "API key revoked successfully", + "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.", + "last_used": "Last Used", + "actions": "Actions", + "images_updated_successfully": "Images updated successfully", + "general": "General", + "enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.", + "ldap": "LDAP", + "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.", + "images": "Images", + "update": "Update", + "email_configuration_updated_successfully": "Email configuration updated successfully", + "save_changes_question": "Save changes?", + "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?", + "save_and_send": "Save and send", + "test_email_sent_successfully": "Test email sent successfully to your email address.", + "failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.", + "smtp_configuration": "SMTP Configuration", + "smtp_host": "SMTP Host", + "smtp_port": "SMTP Port", + "smtp_user": "SMTP User", + "smtp_password": "SMTP Password", + "smtp_from": "SMTP From", + "smtp_tls_option": "SMTP TLS Option", + "email_tls_option": "Email TLS Option", + "skip_certificate_verification": "Skip Certificate Verification", + "this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.", + "enabled_emails": "Enabled Emails", + "email_login_notification": "Email Login Notification", + "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.", + "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.", + "send_test_email": "Send test email", + "application_configuration_updated_successfully": "Application configuration updated successfully", + "application_name": "Application Name", + "session_duration": "Session Duration", + "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.", + "enable_self_account_editing": "Enable Self-Account Editing", + "whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.", + "emails_verified": "Emails Verified", + "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.", + "ldap_configuration_updated_successfully": "LDAP configuration updated successfully", + "ldap_disabled_successfully": "LDAP disabled successfully", + "ldap_sync_finished": "LDAP sync finished", + "client_configuration": "Client Configuration", + "ldap_url": "LDAP URL", + "ldap_bind_dn": "LDAP Bind DN", + "ldap_bind_password": "LDAP Bind Password", + "ldap_base_dn": "LDAP Base DN", + "user_search_filter": "User Search Filter", + "the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.", + "groups_search_filter": "Groups Search Filter", + "the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.", + "attribute_mapping": "Attribute Mapping", + "user_unique_identifier_attribute": "User Unique Identifier Attribute", + "the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.", + "username_attribute": "Username Attribute", + "user_mail_attribute": "User Mail Attribute", + "user_first_name_attribute": "User First Name Attribute", + "user_last_name_attribute": "User Last Name Attribute", + "user_profile_picture_attribute": "User Profile Picture Attribute", + "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.", + "group_members_attribute": "Group Members Attribute", + "the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.", + "group_unique_identifier_attribute": "Group Unique Identifier Attribute", + "group_name_attribute": "Group Name Attribute", + "admin_group_name": "Admin Group Name", + "members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.", + "disable": "Disable", + "sync_now": "Sync now", + "enable": "Enable", + "user_created_successfully": "User created successfully", + "create_user": "Create User", + "add_a_new_user_to_appname": "Add a new user to {appName}", + "add_user": "Add User", + "manage_users": "Manage Users", + "admin_privileges": "Admin Privileges", + "admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.", + "delete_firstname_lastname": "Delete {firstName} {lastName}", + "are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?", + "user_deleted_successfully": "User deleted successfully", + "role": "Role", + "source": "Source", + "admin": "Admin", + "user": "User", + "local": "Local", + "toggle_menu": "Toggle menu", + "edit": "Edit", + "user_groups_updated_successfully": "User groups updated successfully", + "user_updated_successfully": "User updated successfully", + "custom_claims_updated_successfully": "Custom claims updated successfully", + "back": "Back", + "user_details_firstname_lastname": "User Details {firstName} {lastName}", + "manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.", + "custom_claims": "Custom Claims", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.", + "user_group_created_successfully": "User group created successfully", + "create_user_group": "Create User Group", + "create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.", + "add_group": "Add Group", + "manage_user_groups": "Manage User Groups", + "friendly_name": "Friendly Name", + "name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI", + "name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim", + "delete_name": "Delete {name}", + "are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?", + "user_group_deleted_successfully": "User group deleted successfully", + "user_count": "User Count", + "user_group_updated_successfully": "User group updated successfully", + "users_updated_successfully": "Users updated successfully", + "user_group_details_name": "User Group Details {name}", + "assign_users_to_this_group": "Assign users to this group.", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.", + "oidc_client_created_successfully": "OIDC client created successfully", + "create_oidc_client": "Create OIDC Client", + "add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.", + "add_oidc_client": "Add OIDC Client", + "manage_oidc_clients": "Manage OIDC Clients", + "one_time_link": "One Time Link", + "use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or\n\t\t\t\thave lost it.", + "add": "Add", + "callback_urls": "Callback URLs", + "logout_callback_urls": "Logout Callback URLs", + "public_client": "Public Client", + "public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.", + "pkce": "PKCE", + "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.", + "name_logo": "{name} logo", + "change_logo": "Change Logo", + "upload_logo": "Upload Logo", + "remove_logo": "Remove Logo", + "are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?", + "oidc_client_deleted_successfully": "OIDC client deleted successfully", + "authorization_url": "Authorization URL", + "oidc_discovery_url": "OIDC Discovery URL", + "token_url": "Token URL", + "userinfo_url": "Userinfo URL", + "logout_url": "Logout URL", + "certificate_url": "Certificate URL", + "enabled": "Enabled", + "disabled": "Disabled", + "oidc_client_updated_successfully": "OIDC client updated successfully", + "create_new_client_secret": "Create new client secret", + "are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.", + "generate": "Generate", + "new_client_secret_created_successfully": "New client secret created successfully", + "allowed_user_groups_updated_successfully": "Allowed user groups updated successfully", + "oidc_client_name": "OIDC Client {name}", + "client_id": "Client ID", + "client_secret": "Client secret", + "show_more_details": "Show more details", + "allowed_user_groups": "Allowed User Groups", + "add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.", + "favicon": "Favicon", + "light_mode_logo": "Light Mode Logo", + "dark_mode_logo": "Dark Mode Logo", + "background_image": "Background Image", + "language": "Language", + "reset_profile_picture_question": "Reset profile picture?", + "this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?", + "reset": "Reset", + "reset_to_default": "Reset to default", + "profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.", + "select_the_language_you_want_to_use": "Select the language you want to use. Some languages may not be fully translated." +} diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json new file mode 100644 index 00000000..3ddb3dbc --- /dev/null +++ b/frontend/messages/nl.json @@ -0,0 +1,313 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "my_account": "Mijn Account", + "logout": "Uitloggen", + "english": "English", + "dutch": "Nederlands", + "confirm": "Bevestigen", + "key": "Sleutel", + "value": "Waarde", + "remove_custom_claim": "Aangepaste claim verwijderen", + "add_custom_claim": "Aangepaste claim toevoegen", + "add_another": "Voeg nog een toe", + "select_a_date": "Selecteer een datum", + "select_file": "Selecteer bestand", + "profile_picture": "Profielfoto", + "profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.", + "click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit uw bestanden te uploaden.", + "image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.", + "items_per_page": "Aantal per pagina", + "no_items_found": "Geen items gevonden", + "search": "Zoekopdracht...", + "expand_card": "Kaart uitbreiden", + "copied": "Gekopieerd", + "click_to_copy": "Klik om te kopiëren", + "something_went_wrong": "Er is iets misgegaan", + "go_back_to_home": "Ga terug naar huis", + "dont_have_access_to_your_passkey": "Hebt u geen toegang tot uw toegangscode?", + "login_background": "Inlogachtergrond", + "logo": "Logo", + "login_code": "Inlogcode", + "create_a_login_code_to_sign_in_without_a_passkey_once": "Maak een inlogcode aan waarmee de gebruiker zich eenmalig kan aanmelden zonder passkey.", + "one_hour": "1 uur", + "twelve_hours": "12 uur", + "one_day": "1 dag", + "one_week": "1 week", + "one_month": "1 maand", + "expiration": "Vervaldatum", + "generate_code": "Genereer code", + "name": "Naam", + "browser_unsupported": "Browser niet ondersteund", + "this_browser_does_not_support_passkeys": "Deze browser ondersteunt geen passkeys. Gebruik een alternatieve aanmeldmethode.", + "an_unknown_error_occurred": "Er is een onbekende fout opgetreden", + "authentication_process_was_aborted": "Het authenticatieproces is afgebroken", + "error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator", + "authenticator_does_not_support_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties", + "authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen residente sleutels", + "passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd", + "authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen", + "authenticator_timed_out": "De authenticator is verlopen", + "critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.", + "sign_in_to": "Meld u aan bij {name}", + "client_not_found": "Client niet gevonden", + "client_wants_to_access_the_following_information": "{client} wil toegang tot de volgende informatie:", + "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wilt u zich aanmelden bij {client} met uw {appName} account?", + "email": "E-mail", + "view_your_email_address": "Bekijk uw e-mailadres", + "profile": "Profiel", + "view_your_profile_information": "Bekijk uw profielgegevens", + "groups": "Groepen", + "view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan u lid bent", + "cancel": "Annuleren", + "sign_in": "Aanmelden", + "try_again": "Probeer het opnieuw", + "client_logo": "Client logo", + "sign_out": "Afmelden", + "do_you_want_to_sign_out_of_pocketid_with_the_account": "Wilt u zich afmelden bij Pocket ID met het account {username} ?", + "sign_in_to_appname": "Meld u aan bij {appName}", + "please_try_to_sign_in_again": "Probeer opnieuw in te loggen.", + "authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Verifieer uzelf met uw toegangscode om toegang te krijgen tot het beheerderspaneel.", + "authenticate": "Authenticeren", + "appname_setup": "{appName} Instellen", + "please_try_again": "Probeer het opnieuw.", + "you_are_about_to_sign_in_to_the_initial_admin_account": "U staat op het punt om in te loggen op het oorspronkelijke beheerdersaccount. Iedereen met deze link heeft toegang tot het account totdat er een passkey is toegevoegd. Stel zo snel mogelijk een passkey in om ongeautoriseerde toegang te voorkomen.", + "continue": "Doorgaan", + "alternative_sign_in": "Alternatieve aanmelding", + "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als u geen toegang hebt tot uw toegangscode, kunt u zich op een van de volgende manieren aanmelden.", + "use_your_passkey_instead": "Wilt u in plaats daarvan uw toegangscode gebruiken?", + "email_login": "E-mail inloggen", + "enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.", + "request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.", + "go_back": "Ga terug", + "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien dit in het systeem voorkomt.", + "enter_code": "Voer code in", + "enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer uw e-mailadres in om een e-mail met een inlogcode te ontvangen.", + "your_email": "Uw e-mail", + "submit": "Indienen", + "enter_the_code_you_received_to_sign_in": "Voer de code in die u hebt ontvangen om in te loggen.", + "code": "Code", + "invalid_redirect_url": "Ongeldige omleidings-URL", + "audit_log": "Audit logboek", + "users": "Gebruikers", + "user_groups": "Gebruikersgroepen", + "oidc_clients": "OIDC-clients", + "api_keys": "API-sleutels", + "application_configuration": "Toepassingsconfiguratie", + "settings": "Instellingen", + "update_pocket_id": "Pocket-ID bijwerken", + "powered_by": "Aangedreven door", + "see_your_account_activities_from_the_last_3_months": "Bekijk uw accountactiviteiten van de afgelopen 3 maanden.", + "time": "Tijd", + "event": "Evenement", + "approximate_location": "Geschatte locatie", + "ip_address": "IP-adres", + "device": "Apparaat", + "client": "Cliënt", + "unknown": "Onbekend", + "account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt", + "profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.", + "account_settings": "Accountinstellingen", + "passkey_missing": "Passkey ontbreekt", + "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat u de toegang tot uw account verliest.", + "single_passkey_configured": "Eén enkele toegangscode geconfigureerd", + "it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat u de toegang tot uw account verliest.", + "account_details": "Accountgegevens", + "passkeys": "Toegangscodes", + "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.", + "add_passkey": "Passkey toevoegen", + "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.", + "create": "Creëren", + "first_name": "Voornaam", + "last_name": "Achternaam", + "username": "Gebruikersnaam", + "save": "Opslaan", + "username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten", + "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld u aan met de volgende code. De code verloopt over 15 minuten.", + "or_visit": "of bezoek", + "added_on": "Toegevoegd op", + "rename": "Hernoemen", + "delete": "Verwijderen", + "are_you_sure_you_want_to_delete_this_passkey": "Weet u zeker dat u deze toegangscode wilt verwijderen?", + "passkey_deleted_successfully": "Passkey succesvol verwijderd", + "delete_passkey_name": "Verwijder {passkeyName}", + "passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt", + "name_passkey": "Naam Passkey", + "name_your_passkey_to_easily_identify_it_later": "Geef uw toegangscode een naam, zodat u deze later gemakkelijk kunt terugvinden.", + "create_api_key": "API-sleutel aanmaken", + "add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.", + "add_api_key": "API-sleutel toevoegen", + "manage_api_keys": "API-sleutels beheren", + "api_key_created": "API-sleutel gemaakt", + "for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar hem veilig.", + "description": "Beschrijving", + "api_key": "API-sleutel", + "close": "Dichtbij", + "name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.", + "expires_at": "Verloopt op", + "when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.", + "optional_description_to_help_identify_this_keys_purpose": "Optionele beschrijving om het doel van deze sleutel te helpen identificeren.", + "name_must_be_at_least_3_characters": "Naam moet minimaal 3 tekens lang zijn", + "name_cannot_exceed_50_characters": "Naam mag niet langer zijn dan 50 tekens", + "expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen", + "revoke_api_key": "API-sleutel intrekken", + "never": "Nooit", + "revoke": "Herroepen", + "api_key_revoked_successfully": "API-sleutel succesvol ingetrokken", + "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet u zeker dat u de API-sleutel \" {apiKeyName} \" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.", + "last_used": "Laatst gebruikt", + "actions": "Acties", + "images_updated_successfully": "Afbeeldingen succesvol bijgewerkt", + "general": "Algemeen", + "enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Schakel e-mailmeldingen in om gebruikers te waarschuwen wanneer er wordt ingelogd vanaf een nieuw apparaat of een nieuwe locatie.", + "ldap": "LDAP", + "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.", + "images": "Afbeeldingen", + "update": "Update", + "email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt", + "save_changes_question": "Wijzigingen opslaan?", + "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "U moet de wijzigingen opslaan voordat u een test-e-mail verzendt. Wilt u nu opslaan?", + "save_and_send": "Opslaan en verzenden", + "test_email_sent_successfully": "Test-e-mail succesvol verzonden naar uw e-mailadres.", + "failed_to_send_test_email": "Het is niet gelukt om een test-e-mail te versturen. Controleer de serverlogs voor meer informatie.", + "smtp_configuration": "SMTP-configuratie", + "smtp_host": "SMTP-host", + "smtp_port": "SMTP-poort", + "smtp_user": "SMTP-gebruiker", + "smtp_password": "SMTP-wachtwoord", + "smtp_from": "SMTP van", + "smtp_tls_option": "SMTP TLS-optie", + "email_tls_option": "E-mail TLS-optie", + "skip_certificate_verification": "Certificaatverificatie overslaan", + "this_can_be_useful_for_selfsigned_certificates": "Dit kan handig zijn voor zelfondertekende certificaten.", + "enabled_emails": "Ingeschakelde e-mails", + "email_login_notification": "E-mail-inlogmelding", + "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.", + "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers inloggen met een inlogcode die naar hun e-mail is gestuurd. Dit vermindert de beveiliging aanzienlijk, omdat iedereen met toegang tot de e-mail van de gebruiker toegang kan krijgen.", + "send_test_email": "Test-e-mail verzenden", + "application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt", + "application_name": "Toepassingsnaam", + "session_duration": "Sessieduur", + "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "De duur van een sessie in minuten voordat de gebruiker zich opnieuw moet aanmelden.", + "enable_self_account_editing": "Zelf-accountbewerking inschakelen", + "whether_the_users_should_be_able_to_edit_their_own_account_details": "Of gebruikers hun eigen accountgegevens moeten kunnen bewerken.", + "emails_verified": "E-mails geverifieerd", + "whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Of het e-mailadres van de gebruiker als geverifieerd moet worden gemarkeerd voor de OIDC-clients.", + "ldap_configuration_updated_successfully": "LDAP-configuratie succesvol bijgewerkt", + "ldap_disabled_successfully": "LDAP succesvol uitgeschakeld", + "ldap_sync_finished": "LDAP-synchronisatie voltooid", + "client_configuration": "Clientconfiguratie", + "ldap_url": "LDAP-URL", + "ldap_bind_dn": "LDAP Bind-DN", + "ldap_bind_password": "LDAP Bind-wachtwoord", + "ldap_base_dn": "LDAP-basis-DN", + "user_search_filter": "Gebruikerszoekfilter", + "the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.", + "groups_search_filter": "Groepen Zoekfilter", + "the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.", + "attribute_mapping": "Attribuuttoewijzing", + "user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk", + "the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.", + "username_attribute": "Gebruikersnaam Attribuut", + "user_mail_attribute": "Gebruikersmailkenmerk", + "user_first_name_attribute": "Gebruikersvoornaam Attribuut", + "user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut", + "user_profile_picture_attribute": "Gebruikersprofielfoto-attribuut", + "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit kenmerk kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.", + "group_members_attribute": "Groepsleden Attribuut", + "the_attribute_to_use_for_querying_members_of_a_group": "Het kenmerk dat gebruikt moet worden om leden van een groep te bevragen.", + "group_unique_identifier_attribute": "Groeps uniek identificatiekenmerk", + "group_name_attribute": "Groepsnaam Attribuut", + "admin_group_name": "Naam van beheerdersgroep", + "members_of_this_group_will_have_admin_privileges_in_pocketid": "Leden van deze groep hebben beheerdersrechten in Pocket ID.", + "disable": "Uitzetten", + "sync_now": "Nu synchroniseren", + "enable": "Inschakelen", + "user_created_successfully": "Gebruiker succesvol aangemaakt", + "create_user": "Gebruiker aanmaken", + "add_a_new_user_to_appname": "Voeg een nieuwe gebruiker toe aan {appName}", + "add_user": "Gebruiker toevoegen", + "manage_users": "Gebruikers beheren", + "admin_privileges": "Beheerdersrechten", + "admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.", + "delete_firstname_lastname": "Verwijderen {firstName} {lastName}", + "are_you_sure_you_want_to_delete_this_user": "Weet u zeker dat u deze gebruiker wilt verwijderen?", + "user_deleted_successfully": "Gebruiker succesvol verwijderd", + "role": "Rol", + "source": "Bron", + "admin": "Beheerder", + "user": "Gebruiker", + "local": "Lokaal", + "toggle_menu": "Menu wisselen", + "edit": "Bewerking", + "user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt", + "user_updated_successfully": "Gebruiker succesvol bijgewerkt", + "custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt", + "back": "Terug", + "user_details_firstname_lastname": "Gebruikersgegevens {firstName} {lastName}", + "manage_which_groups_this_user_belongs_to": "Beheer tot welke groepen deze gebruiker behoort.", + "custom_claims": "Aangepaste claims", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Aangepaste claims zijn sleutel-waardeparen die kunnen worden gebruikt om aanvullende informatie over een gebruiker op te slaan. Deze claims worden opgenomen in het ID-token als de scope 'profile' wordt aangevraagd.", + "user_group_created_successfully": "Gebruikersgroep succesvol aangemaakt", + "create_user_group": "Gebruikersgroep aanmaken", + "create_a_new_group_that_can_be_assigned_to_users": "Maak een nieuwe groep aan die aan gebruikers kan worden toegewezen.", + "add_group": "Groep toevoegen", + "manage_user_groups": "Gebruikersgroepen beheren", + "friendly_name": "Vriendelijke naam", + "name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven", + "name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groepen' zal staan", + "delete_name": "Verwijder {name}", + "are_you_sure_you_want_to_delete_this_user_group": "Weet u zeker dat u deze gebruikersgroep wilt verwijderen?", + "user_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd", + "user_count": "Gebruikersaantal", + "user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt", + "users_updated_successfully": "Gebruikers succesvol bijgewerkt", + "user_group_details_name": "Gebruikersgroepdetails {name}", + "assign_users_to_this_group": "Gebruikers aan deze groep toewijzen.", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Aangepaste claims zijn sleutel-waardeparen die kunnen worden gebruikt om aanvullende informatie over een gebruiker op te slaan. Deze claims worden opgenomen in het ID-token als de scope 'profile' wordt opgevraagd. Aangepaste claims die zijn gedefinieerd voor de gebruiker, krijgen prioriteit als er conflicten zijn.", + "oidc_client_created_successfully": "OIDC-client succesvol aangemaakt", + "create_oidc_client": "Maak een OIDC-client", + "add_a_new_oidc_client_to_appname": "Voeg een nieuwe OIDC-client toe aan {appName} .", + "add_oidc_client": "OIDC-client toevoegen", + "manage_oidc_clients": "OIDC-clients beheren", + "one_time_link": "Eenmalige link", + "use_this_link_to_sign_in_once": "Gebruik deze link om u eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of\nben het kwijt.", + "add": "Toevoegen", + "callback_urls": "Callback-URL's", + "logout_callback_urls": "Callback-URL's voor afmelden", + "public_client": "Publieke client", + "public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.", + "pkce": "PKCE", + "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.", + "name_logo": "{name} logo", + "change_logo": "Logo wijzigen", + "upload_logo": "Logo uploaden", + "remove_logo": "Logo verwijderen", + "are_you_sure_you_want_to_delete_this_oidc_client": "Weet u zeker dat u deze OIDC-client wilt verwijderen?", + "oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd", + "authorization_url": "Autorisatie-URL", + "oidc_discovery_url": "OIDC-ontdekkings-URL", + "token_url": "Token-URL", + "userinfo_url": "Gebruikersinfo-URL", + "logout_url": "Uitlog-URL", + "certificate_url": "Certificaat-URL", + "enabled": "Ingeschakeld", + "disabled": "Uitgeschakeld", + "oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt", + "create_new_client_secret": "Nieuw clientgeheim aanmaken", + "are_you_sure_you_want_to_create_a_new_client_secret": "Weet u zeker dat u een nieuw client secret wilt aanmaken? De oude wordt ongeldig.", + "generate": "Genereren", + "new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt", + "allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt", + "oidc_client_name": "OIDC-client {name}", + "client_id": "Client id", + "client_secret": "Client geheim", + "show_more_details": "Meer details weergeven", + "allowed_user_groups": "Toegestane gebruikersgroepen", + "add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Voeg gebruikersgroepen toe aan deze client om de toegang tot gebruikers in deze groepen te beperken. Als er geen gebruikersgroepen zijn geselecteerd, hebben alle gebruikers toegang tot deze client.", + "favicon": "Favicon", + "light_mode_logo": "Lichte modus logo", + "dark_mode_logo": "Donkere modus logo", + "background_image": "Achtergrondfoto", + "language": "Taal", + "profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn." +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8cdae40a..f4151c73 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "pocket-id-frontend", - "version": "0.39.0", + "version": "0.42.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pocket-id-frontend", - "version": "0.39.0", + "version": "0.42.1", "dependencies": { "@simplewebauthn/browser": "^13.1.0", "@tailwindcss/vite": "^4.0.0", @@ -25,6 +25,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@inlang/paraglide-js": "^2.0.0", "@internationalized/date": "^3.7.0", "@playwright/test": "^1.50.0", "@sveltejs/adapter-auto": "^4.0.0", @@ -747,6 +748,78 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inlang/paraglide-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.0.0.tgz", + "integrity": "sha512-ufe/k4tfBIQrJf6X1L+KGtvHYRhvDPX53m7vVe+IOYs0DkyR7RkBgwPBQb3kbXKpr5atCD+D2BDh/I7EpK5Clg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inlang/recommend-sherlock": "0.2.1", + "@inlang/sdk": "2.4.2", + "commander": "11.1.0", + "consola": "3.4.0", + "json5": "2.2.3", + "unplugin": "^2.1.2", + "urlpattern-polyfill": "^10.0.0" + }, + "bin": { + "paraglide-js": "bin/run.js" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/@inlang/sdk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.4.2.tgz", + "integrity": "sha512-EqL32PcFHOlXWEg2o0nftSBZ376tSxuAhV8uTZoaq521AKSRMEvjTpVsJ9eS6ZJDCRiIXx7avtsdVNwkUntf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lix-js/sdk": "0.4.2", + "@sinclair/typebox": "^0.31.17", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/@lix-js/sdk": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.2.tgz", + "integrity": "sha512-wrQQMAZzOxQEAssxUnajn7Djua98MlIzs+V6GdX51VN6b7iA3qvZJY4L9xEEMh0nRFvpAO3wOt7uBth9580pog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lix-js/server-api-schema": "0.1.1", + "dedent": "1.5.1", + "human-id": "^4.1.1", + "js-sha256": "^0.11.0", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=21" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/@sinclair/typebox": { + "version": "0.31.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", + "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz", + "integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3" + } + }, "node_modules/@internationalized/date": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz", @@ -798,6 +871,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lix-js/server-api-schema": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@lix-js/server-api-schema/-/server-api-schema-0.1.1.tgz", + "integrity": "sha512-W1Z7KKOxAQ4Dag9V2wrDevHPh5rPk+icBUsxNfNCNB2tlPrKpba99562vcTCPoT03KXpihEbWutZNujCRtMA+g==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1214,6 +1294,16 @@ "integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==", "optional": true }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.48.0-build4", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz", + "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@sveltejs/adapter-auto": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz", @@ -1909,6 +1999,13 @@ "@ark/util": "0.38.0" } }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2099,6 +2196,33 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -2111,6 +2235,16 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -2119,6 +2253,13 @@ "node": ">= 0.6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2173,6 +2314,21 @@ } } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2490,6 +2646,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2814,6 +2984,16 @@ "node": ">=8" } }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2826,6 +3006,16 @@ "node": ">= 0.4" } }, + "node_modules/human-id": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", + "integrity": "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2964,6 +3154,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3007,6 +3204,19 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3030,6 +3240,16 @@ "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", "dev": true }, + "node_modules/kysely": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3906,6 +4126,16 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "optional": true }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4094,6 +4324,18 @@ "source-map": "^0.6.0" } }, + "node_modules/sqlite-wasm-kysely": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz", + "integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==", + "dev": true, + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.48.0-build2" + }, + "peerDependencies": { + "kysely": "*" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4548,6 +4790,20 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "devOptional": true }, + "node_modules/unplugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.2.0.tgz", + "integrity": "sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4557,12 +4813,33 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/valibot": { "version": "1.0.0-beta.11", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.11.tgz", @@ -4687,6 +4964,13 @@ } } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 557a52f6..eba0aff0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@inlang/paraglide-js": "^2.0.0", "@internationalized/date": "^3.7.0", "@playwright/test": "^1.50.0", "@sveltejs/adapter-auto": "^4.0.0", diff --git a/frontend/project.inlang/.gitignore b/frontend/project.inlang/.gitignore new file mode 100644 index 00000000..5e465967 --- /dev/null +++ b/frontend/project.inlang/.gitignore @@ -0,0 +1 @@ +cache \ No newline at end of file diff --git a/frontend/project.inlang/project_id b/frontend/project.inlang/project_id new file mode 100644 index 00000000..1ff8aadf --- /dev/null +++ b/frontend/project.inlang/project_id @@ -0,0 +1 @@ +O2jvFph6P4Jpehf2BT \ No newline at end of file diff --git a/frontend/project.inlang/settings.json b/frontend/project.inlang/settings.json new file mode 100644 index 00000000..bf60cc91 --- /dev/null +++ b/frontend/project.inlang/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": [ + "en", + "nl" + ], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} \ No newline at end of file diff --git a/frontend/src/app.html b/frontend/src/app.html index befb926b..83bacbc4 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 2ff787dc..cead04c5 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,6 +1,8 @@ import { env } from '$env/dynamic/private'; import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants'; +import { paraglideMiddleware } from '$lib/paraglide/server'; import type { Handle, HandleServerError } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; import { AxiosError } from 'axios'; import { decodeJwt } from 'jose'; @@ -9,7 +11,16 @@ import { decodeJwt } from 'jose'; // this is still secure as process will just be undefined in the browser process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080'; -export const handle: Handle = async ({ event, resolve }) => { +// Handle to use the paraglide middleware +const paraglideHandle: Handle = ({ event, resolve }) => { + return paraglideMiddleware(event.request, ({ locale }) => { + return resolve(event, { + transformPageChunk: ({ html }) => html.replace('%lang%', locale) + }); + }); +}; + +const authenticationHandle: Handle = async ({ event, resolve }) => { const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const isUnauthenticatedOnlyPath = event.url.pathname.startsWith('/login') || event.url.pathname.startsWith('/lc') @@ -43,6 +54,8 @@ export const handle: Handle = async ({ event, resolve }) => { return response; }; +export const handle: Handle = sequence(paraglideHandle, authenticationHandle); + export const handleError: HandleServerError = async ({ error, message, status }) => { if (error instanceof AxiosError) { message = error.response?.data.error || message; diff --git a/frontend/src/lib/components/advanced-table.svelte b/frontend/src/lib/components/advanced-table.svelte index 28113e6d..4a076469 100644 --- a/frontend/src/lib/components/advanced-table.svelte +++ b/frontend/src/lib/components/advanced-table.svelte @@ -11,6 +11,7 @@ import { ChevronDown } from 'lucide-svelte'; import type { Snippet } from 'svelte'; import Button from './ui/button/button.svelte'; + import { m } from '$lib/paraglide/messages'; let { items, @@ -93,7 +94,7 @@ 'relative z-50 mb-4 max-w-sm', items.data.length == 0 && searchValue == '' && 'hidden' )} - placeholder={'Search...'} + placeholder={m.search()} type="text" oninput={(e) => onSearch((e.target as HTMLInputElement).value)} /> @@ -102,7 +103,7 @@ {#if items.data.length === 0 && searchValue === ''}
-

No items found

+

{m.no_items_found()}

{:else} @@ -166,7 +167,7 @@
-

Items per page

+

{m.items_per_page()}

{description} {/if}
- diff --git a/frontend/src/lib/components/form/file-input.svelte b/frontend/src/lib/components/form/file-input.svelte index 56f74b82..c82a663c 100644 --- a/frontend/src/lib/components/form/file-input.svelte +++ b/frontend/src/lib/components/form/file-input.svelte @@ -3,6 +3,7 @@ import type { HTMLInputAttributes } from 'svelte/elements'; import type { VariantProps } from 'tailwind-variants'; import type { buttonVariants } from '$lib/components/ui/button'; + import { m } from '$lib/paraglide/messages'; let { id, @@ -21,7 +22,7 @@ {#if restProps.children} {@render restProps.children()} {:else} - Select File + {m.select_file()} {/if} diff --git a/frontend/src/lib/components/form/profile-picture-settings.svelte b/frontend/src/lib/components/form/profile-picture-settings.svelte index c49e6f74..88fc7005 100644 --- a/frontend/src/lib/components/form/profile-picture-settings.svelte +++ b/frontend/src/lib/components/form/profile-picture-settings.svelte @@ -4,6 +4,7 @@ import Button from '$lib/components/ui/button/button.svelte'; import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte'; import { openConfirmDialog } from '../confirm-dialog'; + import { m } from '$lib/paraglide/messages'; let { userId, @@ -40,11 +41,10 @@ function onReset() { openConfirmDialog({ - title: 'Reset profile picture?', - message: - 'This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?', + title: m.reset_profile_picture_question(), + message: m.this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default(), confirm: { - label: 'Reset', + label: m.reset(), action: async () => { isLoading = true; await resetCallback().catch(); @@ -58,16 +58,16 @@
-

Profile Picture

+

{m.profile_picture()}

{#if isLdapUser}

- The profile picture is managed by the LDAP server and cannot be changed here. + {m.profile_picture_is_managed_by_ldap_server()}

{:else}

- Click on the profile picture to upload a custom one from your files. + {m.click_profile_picture_to_upload_custom()}

-

The image should be in PNG or JPEG format.

+

{m.image_should_be_in_format()}

{/if}
{#if isLdapUser} diff --git a/frontend/src/lib/components/header/header-avatar.svelte b/frontend/src/lib/components/header/header-avatar.svelte index b0d3d63c..3670f9a2 100644 --- a/frontend/src/lib/components/header/header-avatar.svelte +++ b/frontend/src/lib/components/header/header-avatar.svelte @@ -1,6 +1,7 @@ @@ -26,8 +26,10 @@ {/if}
- {#if $userStore?.id} - - {/if} +
+ {#if $userStore?.id} + + {/if} +
diff --git a/frontend/src/lib/components/login-wrapper.svelte b/frontend/src/lib/components/login-wrapper.svelte index 10b6735d..ed66852d 100644 --- a/frontend/src/lib/components/login-wrapper.svelte +++ b/frontend/src/lib/components/login-wrapper.svelte @@ -2,6 +2,7 @@ import { page } from '$app/state'; import type { Snippet } from 'svelte'; import * as Card from './ui/card'; + import { m } from '$lib/paraglide/messages'; let { children, @@ -29,7 +30,7 @@ )}`} class="text-muted-foreground text-xs" > - Don't have access to your passkey? + {m.dont_have_access_to_your_passkey()} {/if} @@ -38,7 +39,7 @@ Login background @@ -60,7 +61,7 @@ )}`} class="text-muted-foreground mt-7 flex justify-center text-xs" > - Don't have access to your passkey? + {m.dont_have_access_to_your_passkey()} {/if} diff --git a/frontend/src/lib/components/logo.svelte b/frontend/src/lib/components/logo.svelte index 7e6fc2dc..cd720a15 100644 --- a/frontend/src/lib/components/logo.svelte +++ b/frontend/src/lib/components/logo.svelte @@ -1,4 +1,5 @@ -Logo +{m.logo()} diff --git a/frontend/src/lib/components/one-time-link-modal.svelte b/frontend/src/lib/components/one-time-link-modal.svelte index 0dc1e0cf..c6e11882 100644 --- a/frontend/src/lib/components/one-time-link-modal.svelte +++ b/frontend/src/lib/components/one-time-link-modal.svelte @@ -5,6 +5,7 @@ import Input from '$lib/components/ui/input/input.svelte'; import Label from '$lib/components/ui/label/label.svelte'; import * as Select from '$lib/components/ui/select/index.js'; + import { m } from '$lib/paraglide/messages'; import UserService from '$lib/services/user-service'; import { axiosErrorToast } from '$lib/utils/error-util'; @@ -17,14 +18,14 @@ const userService = new UserService(); let oneTimeLink: string | null = $state(null); - let selectedExpiration: keyof typeof availableExpirations = $state('1 hour'); + let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour()); let availableExpirations = { - '1 hour': 60 * 60, - '12 hours': 60 * 60 * 12, - '1 day': 60 * 60 * 24, - '1 week': 60 * 60 * 24 * 7, - '1 month': 60 * 60 * 24 * 30 + [m.one_hour()]: 60 * 60, + [m.twelve_hours()]: 60 * 60 * 12, + [m.one_day()]: 60 * 60 * 24, + [m.one_week()]: 60 * 60 * 24 * 7, + [m.one_month()]: 60 * 60 * 24 * 30 }; async function createOneTimeAccessToken() { @@ -48,14 +49,14 @@ - Login Code + {m.login_code()} Create a login code that the user can use to sign in without a passkey once.{m.create_a_login_code_to_sign_in_without_a_passkey_once()} {#if oneTimeLink === null}
- +
{:else} - + {/if}
diff --git a/frontend/src/lib/components/user-group-selection.svelte b/frontend/src/lib/components/user-group-selection.svelte index 6befbb3d..8c1bf755 100644 --- a/frontend/src/lib/components/user-group-selection.svelte +++ b/frontend/src/lib/components/user-group-selection.svelte @@ -1,6 +1,7 @@ @@ -6,8 +7,8 @@
-

Browser unsupported

+

{m.browser_unsupported()}

- This browser doesn't support passkeys. Please or use a alternative sign in method. + {m.this_browser_does_not_support_passkeys()}

diff --git a/frontend/src/lib/stores/user-store.ts b/frontend/src/lib/stores/user-store.ts index 19147c50..5f241f79 100644 --- a/frontend/src/lib/stores/user-store.ts +++ b/frontend/src/lib/stores/user-store.ts @@ -1,9 +1,13 @@ +import { setLocale } from '$lib/paraglide/runtime'; import type { User } from '$lib/types/user.type'; import { writable } from 'svelte/store'; const userStore = writable(null); const setUser = (user: User) => { + if (user.locale) { + setLocale(user.locale, { reload: false }); + } userStore.set(user); }; diff --git a/frontend/src/lib/types/user.type.ts b/frontend/src/lib/types/user.type.ts index e44c7e5d..39b3da86 100644 --- a/frontend/src/lib/types/user.type.ts +++ b/frontend/src/lib/types/user.type.ts @@ -1,3 +1,4 @@ +import type { Locale } from '$lib/paraglide/runtime'; import type { CustomClaim } from './custom-claim.type'; import type { UserGroup } from './user-group.type'; @@ -10,6 +11,7 @@ export type User = { isAdmin: boolean; userGroups: UserGroup[]; customClaims: CustomClaim[]; + locale?: Locale; ldapId?: string; }; diff --git a/frontend/src/lib/utils/error-util.ts b/frontend/src/lib/utils/error-util.ts index ffd93cdf..09f1d372 100644 --- a/frontend/src/lib/utils/error-util.ts +++ b/frontend/src/lib/utils/error-util.ts @@ -1,10 +1,11 @@ +import { m } from '$lib/paraglide/messages'; import { WebAuthnError } from '@simplewebauthn/browser'; import { AxiosError } from 'axios'; import { toast } from 'svelte-sonner'; export function getAxiosErrorMessage( e: unknown, - defaultMessage: string = 'An unknown error occurred' + defaultMessage: string = m.an_unknown_error_occurred() ) { let message = defaultMessage; if (e instanceof AxiosError) { @@ -13,29 +14,29 @@ export function getAxiosErrorMessage( return message; } -export function axiosErrorToast(e: unknown, defaultMessage: string = 'An unknown error occurred') { +export function axiosErrorToast(e: unknown, defaultMessage: string = m.an_unknown_error_occurred()) { const message = getAxiosErrorMessage(e, defaultMessage); toast.error(message); } export function getWebauthnErrorMessage(e: unknown) { const errors = { - ERROR_CEREMONY_ABORTED: 'The authentication process was aborted', - ERROR_AUTHENTICATOR_GENERAL_ERROR: 'An error occurred with the authenticator', + ERROR_CEREMONY_ABORTED: m.authentication_process_was_aborted(), + ERROR_AUTHENTICATOR_GENERAL_ERROR: m.error_occurred_with_authenticator(), ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT: - 'The authenticator does not support discoverable credentials', + m.authenticator_does_not_support_discoverable_credentials(), ERROR_AUTHENTICATOR_MISSING_RESIDENT_KEY_SUPPORT: - 'The authenticator does not support resident keys', - ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: 'This passkey was previously registered', + m.authenticator_does_not_support_resident_keys(), + ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: m.passkey_was_previously_registered(), ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG: - 'The authenticator does not support any of the requested algorithms' + m.authenticator_does_not_support_any_of_the_requested_algorithms() }; - let message = 'An unknown error occurred'; + let message = m.an_unknown_error_occurred(); if (e instanceof WebAuthnError && e.code in errors) { message = errors[e.code as keyof typeof errors]; } else if (e instanceof WebAuthnError && e?.message.includes('timed out')) { - message = 'The authenticator timed out'; + message = m.authenticator_timed_out(); } else if (e instanceof AxiosError && e.response?.data.error) { message = e.response?.data.error; } else { diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 0523ab3b..c7ae447c 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -4,6 +4,7 @@ import Error from '$lib/components/error.svelte'; import Header from '$lib/components/header/header.svelte'; import { Toaster } from '$lib/components/ui/sonner'; + import { m } from '$lib/paraglide/messages'; import appConfigStore from '$lib/stores/application-configuration-store'; import userStore from '$lib/stores/user-store'; import { ModeWatcher } from 'mode-watcher'; @@ -30,10 +31,7 @@ {#if !appConfig} - + {:else}
{@render children()} diff --git a/frontend/src/routes/authorize/+page.svelte b/frontend/src/routes/authorize/+page.svelte index 8e643595..db020bad 100644 --- a/frontend/src/routes/authorize/+page.svelte +++ b/frontend/src/routes/authorize/+page.svelte @@ -14,6 +14,7 @@ import type { PageData } from './$types'; import ClientProviderImages from './components/client-provider-images.svelte'; import ScopeItem from './components/scope-item.svelte'; + import { m } from '$lib/paraglide/messages'; const webauthnService = new WebAuthnService(); const oidService = new OidcService(); @@ -77,15 +78,15 @@ - Sign in to {client.name} + {m.sign_in_to({name: client.name})} {#if client == null} -

Client not found

+

{m.client_not_found()}

{:else} -

Sign in to {client.name}

+

{m.sign_in_to({name: client.name})}

{#if errorMessage}

{errorMessage}. @@ -93,34 +94,36 @@ {/if} {#if !authorizationRequired && !errorMessage}

- Do you want to sign in to {client.name} with your - {$appConfigStore.appName} account? + {@html m.do_you_want_to_sign_in_to_client_with_your_app_name_account({ + client: client.name, + appName: $appConfigStore.appName + })}

{:else if authorizationRequired}

- {client.name} wants to access the following information: + {@html m.client_wants_to_access_the_following_information({ client: client.name })}

{#if scope!.includes('email')} - + {/if} {#if scope!.includes('profile')} {/if} {#if scope!.includes('groups')} {/if}
@@ -129,11 +132,11 @@
{/if}
- + {#if !errorMessage} - + {:else} - + {/if}
diff --git a/frontend/src/routes/authorize/components/client-provider-images.svelte b/frontend/src/routes/authorize/components/client-provider-images.svelte index 66e7d446..a7ed7a85 100644 --- a/frontend/src/routes/authorize/components/client-provider-images.svelte +++ b/frontend/src/routes/authorize/components/client-provider-images.svelte @@ -3,6 +3,7 @@ import CheckmarkAnimated from '$lib/icons/checkmark-animated.svelte'; import ConnectArrow from '$lib/icons/connect-arrow.svelte'; import CrossAnimated from '$lib/icons/cross-animated.svelte'; + import { m } from '$lib/paraglide/messages'; import type { OidcClientMetaData } from '$lib/types/oidc.type'; const { @@ -61,7 +62,7 @@ class="h-10 w-10" src="/api/oidc/clients/{client.id}/logo" draggable={false} - alt="Client Logo" + alt={m.client_logo()} /> {:else}
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index c1a9a085..24e9e515 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -9,6 +9,7 @@ import { startAuthentication } from '@simplewebauthn/browser'; import { fade } from 'svelte/transition'; import LoginLogoErrorSuccessIndicator from './components/login-logo-error-success-indicator.svelte'; + import { m } from '$lib/paraglide/messages'; const webauthnService = new WebAuthnService(); let isLoading = $state(false); @@ -32,7 +33,7 @@ - Sign In + {m.sign_in()} @@ -40,18 +41,18 @@

- Sign in to {$appConfigStore.appName} + {m.sign_in_to_appname({ appName: $appConfigStore.appName})}

{#if error}

- {error}. Please try to sign in again. + {error}. {m.please_try_to_sign_in_again()}

{:else}

- Authenticate yourself with your passkey to access the admin panel. + {m.authenticate_yourself_with_your_passkey_to_access_the_admin_panel()}

{/if} {error ? m.try_again() : m.authenticate()} diff --git a/frontend/src/routes/login/alternative/+page.svelte b/frontend/src/routes/login/alternative/+page.svelte index ad361286..e20e030d 100644 --- a/frontend/src/routes/login/alternative/+page.svelte +++ b/frontend/src/routes/login/alternative/+page.svelte @@ -4,14 +4,15 @@ import Logo from '$lib/components/logo.svelte'; import { Button } from '$lib/components/ui/button'; import * as Card from '$lib/components/ui/card'; + import { m } from '$lib/paraglide/messages'; import appConfigStore from '$lib/stores/application-configuration-store'; import { LucideChevronRight, LucideMail, LucideRectangleEllipsis } from 'lucide-svelte'; const methods = [ { icon: LucideRectangleEllipsis, - title: 'Login Code', - description: 'Enter a login code to sign in.', + title: m.login_code(), + description: m.enter_a_login_code_to_sign_in(), href: '/login/alternative/code' } ]; @@ -19,15 +20,15 @@ if ($appConfigStore.emailOneTimeAccessEnabled) { methods.push({ icon: LucideMail, - title: 'Email Login', - description: 'Request a login code via email.', + title: m.email_login(), + description: m.request_a_login_code_via_email(), href: '/login/alternative/email' }); } - Sign In + {m.sign_in()} @@ -35,9 +36,9 @@
-

Alternative Sign In

+

{m.alternative_sign_in()}

- If you dont't have access to your passkey, you can sign in using one of the following methods. + {m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}

{#each methods as method} @@ -59,7 +60,7 @@
Use your passkey instead?{m.use_your_passkey_instead()}
diff --git a/frontend/src/routes/login/alternative/code/+page.svelte b/frontend/src/routes/login/alternative/code/+page.svelte index 24edfc09..3ed40306 100644 --- a/frontend/src/routes/login/alternative/code/+page.svelte +++ b/frontend/src/routes/login/alternative/code/+page.svelte @@ -9,6 +9,7 @@ import { onMount } from 'svelte'; import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte'; import { page } from '$app/state'; + import { m } from '$lib/paraglide/messages'; let { data } = $props(); let code = $state(data.code ?? ''); @@ -26,7 +27,7 @@ try { goto(data.redirect); } catch (e) { - error = 'Invalid redirect URL'; + error = m.invalid_redirect_url(); } } catch (e) { error = getAxiosErrorMessage(e); @@ -43,20 +44,20 @@ - Login Code + {m.login_code()}
-

Login Code

+

{m.login_code()}

{#if error}

- {error}. Please try again. + {error}. {m.please_try_again()}

{:else} -

Enter the code you received to sign in.

+

{m.enter_the_code_you_received_to_sign_in()}

{/if}
{ @@ -65,10 +66,10 @@ }} class="w-full max-w-[450px]" > - +
- - + +
diff --git a/frontend/src/routes/login/alternative/email/+page.svelte b/frontend/src/routes/login/alternative/email/+page.svelte index 8483eef7..a7cc9ee2 100644 --- a/frontend/src/routes/login/alternative/email/+page.svelte +++ b/frontend/src/routes/login/alternative/email/+page.svelte @@ -6,6 +6,7 @@ import UserService from '$lib/services/user-service'; import { fade } from 'svelte/transition'; import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte'; + import { m } from '$lib/paraglide/messages'; const { data } = $props(); @@ -21,38 +22,38 @@ await userService .requestOneTimeAccessEmail(email, data.redirect) .then(() => (success = true)) - .catch((e) => (error = e.response?.data.error || 'An unknown error occurred')); + .catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred())); isLoading = false; } - Email Login + {m.email_login()}
-

Email Login

+

{m.email_login()}

{#if error}

- {error}. Please try again. + {error}. {m.please_try_again()}

- - + +
{:else if success}

- An email has been sent to the provided email, if it exists in the system. + {m.an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system()}

{m.go_back()} - +
{:else}

- Enter your email address to receive an email with a login code. + {m.enter_your_email_address_to_receive_an_email_with_a_login_code()}

- +
{m.go_back()} - +
{/if} diff --git a/frontend/src/routes/login/setup/+page.svelte b/frontend/src/routes/login/setup/+page.svelte index 5617e7fc..749fe24f 100644 --- a/frontend/src/routes/login/setup/+page.svelte +++ b/frontend/src/routes/login/setup/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import SignInWrapper from '$lib/components/login-wrapper.svelte'; import { Button } from '$lib/components/ui/button'; + import { m } from '$lib/paraglide/messages'; import UserService from '$lib/services/user-service'; import appConfigStore from '$lib/stores/application-configuration-store.js'; import userStore from '$lib/stores/user-store.js'; @@ -33,18 +34,16 @@

- {`${$appConfigStore.appName} Setup`} + {m.appname_setup({ appName: $appConfigStore.appName })}

{#if error}

- {error}. Please try again. + {error}. {m.please_try_again()}

{:else}

- You're about to sign in to the initial admin account. Anyone with this link can access the - account until a passkey is added. Please set up a passkey as soon as possible to prevent - unauthorized access. + {m.you_are_about_to_sign_in_to_the_initial_admin_account()}

- + {/if}
diff --git a/frontend/src/routes/logout/+page.svelte b/frontend/src/routes/logout/+page.svelte index 8d2ba8b4..3b87328e 100644 --- a/frontend/src/routes/logout/+page.svelte +++ b/frontend/src/routes/logout/+page.svelte @@ -3,6 +3,7 @@ import SignInWrapper from '$lib/components/login-wrapper.svelte'; import Logo from '$lib/components/logo.svelte'; import { Button } from '$lib/components/ui/button'; + import { m } from '$lib/paraglide/messages'; import WebAuthnService from '$lib/services/webauthn-service'; import userStore from '$lib/stores/user-store.js'; import { axiosErrorToast } from '$lib/utils/error-util.js'; @@ -22,7 +23,7 @@ - Logout + {m.logout()} @@ -31,13 +32,13 @@ -

Sign out

+

{m.sign_out()}

- Do you want to sign out of Pocket ID with the account {$userStore?.username}? + {@html m.do_you_want_to_sign_out_of_pocketid_with_the_account({ username: $userStore?.username ?? '' })}

- - + +
diff --git a/frontend/src/routes/settings/+layout.svelte b/frontend/src/routes/settings/+layout.svelte index f6cdc944..adafd04b 100644 --- a/frontend/src/routes/settings/+layout.svelte +++ b/frontend/src/routes/settings/+layout.svelte @@ -4,6 +4,7 @@ import { LucideExternalLink } from 'lucide-svelte'; import type { Snippet } from 'svelte'; import type { LayoutData } from './$types'; + import { m } from '$lib/paraglide/messages'; let { children, @@ -16,19 +17,19 @@ const { versionInformation } = data; let links = $state([ - { href: '/settings/account', label: 'My Account' }, - { href: '/settings/audit-log', label: 'Audit Log' } + { href: '/settings/account', label: m.my_account() }, + { href: '/settings/audit-log', label: m.audit_log() } ]); if ($userStore?.isAdmin) { links = [ // svelte-ignore state_referenced_locally ...links, - { href: '/settings/admin/users', label: 'Users' }, - { href: '/settings/admin/user-groups', label: 'User Groups' }, - { href: '/settings/admin/oidc-clients', label: 'OIDC Clients' }, - { href: '/settings/admin/api-keys', label: 'API Keys' }, - { href: '/settings/admin/application-configuration', label: 'Application Configuration' } + { href: '/settings/admin/users', label: m.users() }, + { href: '/settings/admin/user-groups', label: m.user_groups() }, + { href: '/settings/admin/oidc-clients', label: m.oidc_clients() }, + { href: '/settings/admin/api-keys', label: m.api_keys() }, + { href: '/settings/admin/application-configuration', label: m.application_configuration() } ]; } @@ -40,7 +41,7 @@ >
-

Settings

+

{m.settings()}

@@ -65,7 +66,7 @@

- Powered by Pocket ID toast.success('Account details updated successfully')) + .then(() => toast.success(m.account_details_updated_successfully())) .catch((e) => { axiosErrorToast(e); success = false; @@ -51,9 +53,7 @@ async function updateProfilePicture(image: File) { await userService .updateCurrentUsersProfilePicture(image) - .then(() => - toast.success('Profile picture updated successfully. It may take a few minutes to update.') - ) + .then(() => toast.success(m.profile_picture_updated_successfully())) .catch(axiosErrorToast); } @@ -72,24 +72,22 @@ - Account Settings + {m.account_settings()} {#if passkeys.length == 0} - Passkey missing + {m.passkey_missing()} Please add a passkey to prevent losing access to your account.{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()} {:else if passkeys.length == 1} - Single Passkey Configured - It is recommended to add more than one passkey to avoid losing access to your account. + {m.single_passkey_configured()} + {m.it_is_recommended_to_add_more_than_one_passkey()} {/if} @@ -99,7 +97,7 @@ > - Account Details + {m.account_details()} @@ -122,12 +120,12 @@

- Passkeys + {m.passkeys()} - Manage your passkeys that you can use to authenticate yourself. + {m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
- +
{#if passkeys.length != 0} @@ -141,12 +139,28 @@
- Login Code + {m.login_code()} - Create a one-time login code to sign in from a different device without a passkey. + {m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
- + +
+
+ + + + +
+
+ {m.language()} + + {m.select_the_language_you_want_to_use()} + +
+
diff --git a/frontend/src/routes/settings/account/account-form.svelte b/frontend/src/routes/settings/account/account-form.svelte index fcfe9d15..ad847667 100644 --- a/frontend/src/routes/settings/account/account-form.svelte +++ b/frontend/src/routes/settings/account/account-form.svelte @@ -1,6 +1,7 @@ + + updateLocale(v!.value)} +> + + {locales[currentLocale]} + + + {#each Object.entries(locales) as [value, label]} + {label} + {/each} + + diff --git a/frontend/src/routes/settings/account/login-code-modal.svelte b/frontend/src/routes/settings/account/login-code-modal.svelte index ab0a7911..9a1a459b 100644 --- a/frontend/src/routes/settings/account/login-code-modal.svelte +++ b/frontend/src/routes/settings/account/login-code-modal.svelte @@ -3,6 +3,7 @@ import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte'; import * as Dialog from '$lib/components/ui/dialog'; import { Separator } from '$lib/components/ui/separator'; + import { m } from '$lib/paraglide/messages'; import UserService from '$lib/services/user-service'; import { axiosErrorToast } from '$lib/utils/error-util'; @@ -37,9 +38,9 @@ - Login Code + {m.login_code()} Sign in using the following code. The code will expire in 15 minutes. + >{m.sign_in_using_the_following_code_the_code_will_expire_in_minutes()} @@ -49,7 +50,7 @@
-

or visit

+

{m.or_visit()}

diff --git a/frontend/src/routes/settings/account/passkey-list.svelte b/frontend/src/routes/settings/account/passkey-list.svelte index 9cf31a60..ad00b0e7 100644 --- a/frontend/src/routes/settings/account/passkey-list.svelte +++ b/frontend/src/routes/settings/account/passkey-list.svelte @@ -8,6 +8,7 @@ import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import RenamePasskeyModal from './rename-passkey-modal.svelte'; + import { m } from '$lib/paraglide/messages'; let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props(); @@ -17,16 +18,16 @@ async function deletePasskey(passkey: Passkey) { openConfirmDialog({ - title: `Delete ${passkey.name}`, - message: 'Are you sure you want to delete this passkey?', + title: m.delete_passkey_name({ passkeyName: passkey.name }), + message: m.are_you_sure_you_want_to_delete_this_passkey(), confirm: { - label: 'Delete', + label: m.delete(), destructive: true, action: async () => { try { await webauthnService.removeCredential(passkey.id); passkeys = await webauthnService.listCredentials(); - toast.success('Passkey deleted successfully'); + toast.success(m.passkey_deleted_successfully()); } catch (e) { axiosErrorToast(e); } @@ -44,7 +45,7 @@

{passkey.name}

- Added on {new Date(passkey.createdAt).toLocaleDateString()} + {m.added_on()} {new Date(passkey.createdAt).toLocaleDateString()}

@@ -53,13 +54,13 @@ on:click={() => (passkeyToRename = passkey)} size="sm" variant="outline" - aria-label="Rename">
diff --git a/frontend/src/routes/settings/account/rename-passkey-modal.svelte b/frontend/src/routes/settings/account/rename-passkey-modal.svelte index 4854be22..49caf7aa 100644 --- a/frontend/src/routes/settings/account/rename-passkey-modal.svelte +++ b/frontend/src/routes/settings/account/rename-passkey-modal.svelte @@ -3,6 +3,7 @@ import * as Dialog from '$lib/components/ui/dialog'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; + import { m } from '$lib/paraglide/messages'; import WebAuthnService from '$lib/services/webauthn-service'; import type { Passkey } from '$lib/types/passkey.type'; import { axiosErrorToast } from '$lib/utils/error-util'; @@ -35,7 +36,7 @@ .updateCredentialName(passkey!.id, name) .then(() => { passkey = null; - toast.success('Passkey name updated successfully'); + toast.success(m.passkey_name_updated_successfully()); callback?.(); }) .catch(axiosErrorToast); @@ -45,16 +46,16 @@ - Name Passkey - Name your passkey to easily identify it later. + {m.name_passkey()} + {m.name_your_passkey_to_easily_identify_it_later()}
- +
- +
diff --git a/frontend/src/routes/settings/admin/api-keys/+page.svelte b/frontend/src/routes/settings/admin/api-keys/+page.svelte index 785ac804..98afeef7 100644 --- a/frontend/src/routes/settings/admin/api-keys/+page.svelte +++ b/frontend/src/routes/settings/admin/api-keys/+page.svelte @@ -9,6 +9,7 @@ import ApiKeyDialog from './api-key-dialog.svelte'; import ApiKeyForm from './api-key-form.svelte'; import ApiKeyList from './api-key-list.svelte'; + import { m } from '$lib/paraglide/messages'; let { data } = $props(); let apiKeys = $state(data.apiKeys); @@ -35,18 +36,18 @@ - API Keys + {m.api_keys()}
- Create API Key - Add a new API key for programmatic access. + {m.create_api_key()} + {m.add_a_new_api_key_for_programmatic_access()}
{#if !expandAddApiKey} - + {:else} + diff --git a/frontend/src/routes/settings/admin/api-keys/api-key-form.svelte b/frontend/src/routes/settings/admin/api-keys/api-key-form.svelte index 88f8ad5d..333c447f 100644 --- a/frontend/src/routes/settings/admin/api-keys/api-key-form.svelte +++ b/frontend/src/routes/settings/admin/api-keys/api-key-form.svelte @@ -1,6 +1,7 @@ - Application Configuration + {m.application_configuration()} - + - + diff --git a/frontend/src/routes/settings/admin/application-configuration/application-image.svelte b/frontend/src/routes/settings/admin/application-configuration/application-image.svelte index 5e5c424f..a6e11ddc 100644 --- a/frontend/src/routes/settings/admin/application-configuration/application-image.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/application-image.svelte @@ -1,6 +1,7 @@
- +
- +
diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte index 26b6a02e..58c8303d 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte @@ -3,6 +3,7 @@ import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte'; import FormInput from '$lib/components/form/form-input.svelte'; import { Button } from '$lib/components/ui/button'; + import { m } from '$lib/paraglide/messages'; import AppConfigService from '$lib/services/app-config-service'; import type { AllAppConfig } from '$lib/types/application-configuration'; import { axiosErrorToast } from '$lib/utils/error-util'; @@ -74,14 +75,14 @@ ...data, ldapEnabled: true }); - toast.success('LDAP configuration updated successfully'); + toast.success(m.ldap_configuration_updated_successfully()); return true; } async function onDisable() { ldapEnabled = false; await callback({ ldapEnabled }); - toast.success('LDAP disabled successfully'); + toast.success(m.ldap_disabled_successfully()); } async function onEnable() { @@ -94,7 +95,7 @@ ldapSyncing = true; await appConfigService .syncLdap() - .then(() => toast.success('LDAP sync finished')) + .then(() => toast.success(m.ldap_sync_finished())) .catch(axiosErrorToast); ldapSyncing = false; @@ -102,98 +103,98 @@
-

Client Configuration

+

{m.client_configuration()}

- +
-

Attribute Mapping

+

{m.attribute_mapping()}

@@ -202,11 +203,11 @@
{#if ldapEnabled} - - - + + + {:else} - + {/if}
diff --git a/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte b/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte index 7f64e341..e69d1cc7 100644 --- a/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte @@ -1,5 +1,6 @@ - OIDC Clients + {m.oidc_clients()}
- Create OIDC Client - Add a new OIDC client to {$appConfigStore.appName}. + {m.create_oidc_client()} + {m.add_a_new_oidc_client_to_appname({ appName: $appConfigStore.appName})}
{#if !expandAddClient} - + {:else} {m.show_more_details()}
{/if} @@ -172,11 +173,11 @@
- +
diff --git a/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte b/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte index e639a4b0..ad192641 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte @@ -2,6 +2,7 @@ import * as Dialog from '$lib/components/ui/dialog'; import Input from '$lib/components/ui/input/input.svelte'; import Label from '$lib/components/ui/label/label.svelte'; + import { m } from '$lib/paraglide/messages'; let { oneTimeLink = $bindable() @@ -19,13 +20,12 @@ - One Time Link + {m.one_time_link()} Use this link to sign in once. This is needed for users who haven't added a passkey yet or - have lost it.{m.use_this_link_to_sign_in_once()} - + diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-callback-url-input.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-callback-url-input.svelte index 279e572d..db6aa968 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-callback-url-input.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-callback-url-input.svelte @@ -2,6 +2,7 @@ import FormInput from '$lib/components/form/form-input.svelte'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; + import { m } from '$lib/paraglide/messages'; import { LucideMinus, LucidePlus } from 'lucide-svelte'; import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; @@ -53,7 +54,7 @@ on:click={() => (callbackURLs = [...callbackURLs, ''])} > - {callbackURLs.length === 0 ? 'Add' : 'Add another'} + {callbackURLs.length === 0 ? m.add() : m.add_another()} {/if}
diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte index cdb19b7b..0fe0276f 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte @@ -12,6 +12,7 @@ import { createForm } from '$lib/utils/form-util'; import { z } from 'zod'; import OidcCallbackUrlInput from './oidc-callback-url-input.svelte'; + import { m } from '$lib/paraglide/messages'; let { callback, @@ -79,16 +80,16 @@
- +
{ if (v == true) form.setValue('pkceEnabled', true); }} @@ -105,21 +106,21 @@ />
- +
{#if logoDataURL}
{`${$inputs.name.value}
{/if} @@ -131,17 +132,17 @@ onchange={onLogoChange} > {#if logoDataURL} - + {/if}
- +
diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte index d637a78f..fe171a0a 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte @@ -10,6 +10,7 @@ import { LucidePencil, LucideTrash } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import OneTimeLinkModal from './client-secret.svelte'; + import { m } from '$lib/paraglide/messages'; let { clients = $bindable(), @@ -25,16 +26,16 @@ async function deleteClient(client: OidcClient) { openConfirmDialog({ - title: `Delete ${client.name}`, - message: 'Are you sure you want to delete this OIDC client?', + title: m.delete_name({name: client.name}), + message: m.are_you_sure_you_want_to_delete_this_oidc_client(), confirm: { - label: 'Delete', + label: m.delete(), destructive: true, action: async () => { try { await oidcService.removeClient(client.id); clients = await oidcService.listClients(requestOptions!); - toast.success('OIDC client deleted successfully'); + toast.success(m.oidc_client_deleted_successfully()); } catch (e) { axiosErrorToast(e); } @@ -49,9 +50,9 @@ {requestOptions} onRefresh={async (o) => (clients = await oidcService.listClients(o))} columns={[ - { label: 'Logo' }, - { label: 'Name', sortColumn: 'name' }, - { label: 'Actions', hidden: true } + { label: m.logo() }, + { label: m.name(), sortColumn: 'name' }, + { label: m.actions(), hidden: true } ]} > {#snippet rows({ item })} @@ -61,7 +62,7 @@ {item.name} logo {/if} @@ -72,9 +73,9 @@ href="/settings/admin/oidc-clients/{item.id}" size="sm" variant="outline" - aria-label="Edit"> - diff --git a/frontend/src/routes/settings/admin/user-groups/+page.svelte b/frontend/src/routes/settings/admin/user-groups/+page.svelte index 797ed21d..caf76405 100644 --- a/frontend/src/routes/settings/admin/user-groups/+page.svelte +++ b/frontend/src/routes/settings/admin/user-groups/+page.svelte @@ -11,6 +11,7 @@ import { slide } from 'svelte/transition'; import UserGroupForm from './user-group-form.svelte'; import UserGroupList from './user-group-list.svelte'; + import { m } from '$lib/paraglide/messages'; let { data } = $props(); let userGroups = $state(data.userGroups); @@ -24,7 +25,7 @@ await userGroupService .create(userGroup) .then((createdUserGroup) => { - toast.success('User group created successfully'); + toast.success(m.user_group_created_successfully()); goto(`/settings/admin/user-groups/${createdUserGroup.id}`); }) .catch((e) => { @@ -36,18 +37,18 @@ - User Groups + {m.user_groups()}
- Create User Group - Create a new group that can be assigned to users. + {m.create_user_group()} + {m.create_a_new_group_that_can_be_assigned_to_users()}
{#if !expandAddUserGroup} - + {:else} updateUserGroupUsers(userGroup.userIds)}>{m.save()}
@@ -99,11 +100,11 @@
- +
diff --git a/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte b/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte index 4f4ca532..a379b0fd 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte @@ -1,6 +1,7 @@ - Users + {m.users()}
- Create User - Add a new user to {$appConfigStore.appName}. + {m.create_user()} + {m.add_a_new_user_to_appname({ appName: $appConfigStore.appName })}.
{#if !expandAddUser} - + {:else} {m.save()}
- +
diff --git a/frontend/src/routes/settings/admin/users/user-form.svelte b/frontend/src/routes/settings/admin/users/user-form.svelte index b15f49a6..48d3dba2 100644 --- a/frontend/src/routes/settings/admin/users/user-form.svelte +++ b/frontend/src/routes/settings/admin/users/user-form.svelte @@ -2,6 +2,7 @@ import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte'; import FormInput from '$lib/components/form/form-input.svelte'; import { Button } from '$lib/components/ui/button'; + import { m } from '$lib/paraglide/messages'; import appConfigStore from '$lib/stores/application-configuration-store'; import type { User, UserCreate } from '$lib/types/user.type'; import { createForm } from '$lib/utils/form-util'; @@ -35,7 +36,7 @@ .max(30) .regex( /^[a-z0-9_@.-]+$/, - "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols" + m.username_can_only_contain() ), email: z.string().email(), isAdmin: z.boolean() @@ -57,19 +58,19 @@
- - - - + + + +
- +
diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index e48e189d..820e28a8 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -15,6 +15,7 @@ import Ellipsis from 'lucide-svelte/icons/ellipsis'; import { toast } from 'svelte-sonner'; import OneTimeLinkModal from '$lib/components/one-time-link-modal.svelte'; + import { m } from '$lib/paraglide/messages'; let { users = $bindable(), @@ -27,10 +28,10 @@ async function deleteUser(user: User) { openConfirmDialog({ - title: `Delete ${user.firstName} ${user.lastName}`, - message: 'Are you sure you want to delete this user?', + title: m.delete_firstname_lastname({firstName: user.firstName, lastName: user.lastName}), + message: m.are_you_sure_you_want_to_delete_this_user(), confirm: { - label: 'Delete', + label: m.delete(), destructive: true, action: async () => { try { @@ -39,7 +40,7 @@ } catch (e) { axiosErrorToast(e); } - toast.success('User deleted successfully'); + toast.success(m.user_deleted_successfully()); } } }); @@ -51,13 +52,13 @@ {requestOptions} onRefresh={async (options) => (users = await userService.list(options))} columns={[ - { label: 'First name', sortColumn: 'firstName' }, - { label: 'Last name', sortColumn: 'lastName' }, - { label: 'Email', sortColumn: 'email' }, - { label: 'Username', sortColumn: 'username' }, - { label: 'Role', sortColumn: 'isAdmin' }, - ...($appConfigStore.ldapEnabled ? [{ label: 'Source' }] : []), - { label: 'Actions', hidden: true } + { label: m.first_name(), sortColumn: 'firstName' }, + { label: m.last_name(), sortColumn: 'lastName' }, + { label: m.email(), sortColumn: 'email' }, + { label: m.username(), sortColumn: 'username' }, + { label: m.role(), sortColumn: 'isAdmin' }, + ...($appConfigStore.ldapEnabled ? [{ label: m.source()}] : []), + { label: m.actions(), hidden: true } ]} > {#snippet rows({ item })} @@ -66,11 +67,11 @@ {item.email} {item.username} - {item.isAdmin ? 'Admin' : 'User'} + {item.isAdmin ? m.admin() : m.user()} {#if $appConfigStore.ldapEnabled} - {item.ldapId ? 'LDAP' : 'Local'}{item.ldapId ? m.ldap() : m.local()} {/if} @@ -78,20 +79,20 @@ - Toggle menu + {m.toggle_menu()} (userIdToCreateOneTimeLink = item.id)} - >Login Code{m.login_code()} goto(`/settings/admin/users/${item.id}`)} - > Edit {m.edit()} {#if !item.ldapId || !$appConfigStore.ldapEnabled} deleteUser(item)} - >Delete{m.delete()} {/if} diff --git a/frontend/src/routes/settings/audit-log/+page.svelte b/frontend/src/routes/settings/audit-log/+page.svelte index 6f33b3f6..b66142e6 100644 --- a/frontend/src/routes/settings/audit-log/+page.svelte +++ b/frontend/src/routes/settings/audit-log/+page.svelte @@ -1,5 +1,6 @@ - Audit Log + {m.audit_log()} - Audit Log + {m.audit_log()} See your account activities from the last 3 months.{m.see_your_account_activities_from_the_last_3_months()} diff --git a/frontend/src/routes/settings/audit-log/audit-log-list.svelte b/frontend/src/routes/settings/audit-log/audit-log-list.svelte index 2256c3f5..eb3cda96 100644 --- a/frontend/src/routes/settings/audit-log/audit-log-list.svelte +++ b/frontend/src/routes/settings/audit-log/audit-log-list.svelte @@ -2,6 +2,7 @@ import AdvancedTable from '$lib/components/advanced-table.svelte'; import { Badge } from '$lib/components/ui/badge'; import * as Table from '$lib/components/ui/table'; + import { m } from '$lib/paraglide/messages'; import AuditLogService from '$lib/services/audit-log-service'; import type { AuditLog } from '$lib/types/audit-log.type'; import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; @@ -27,12 +28,12 @@ {requestOptions} onRefresh={async (options) => (auditLogs = await auditLogService.list(options))} columns={[ - { label: 'Time', sortColumn: 'createdAt' }, - { label: 'Event', sortColumn: 'event' }, - { label: 'Approximate Location', sortColumn: 'city' }, - { label: 'IP Address', sortColumn: 'ipAddress' }, - { label: 'Device', sortColumn: 'device' }, - { label: 'Client' } + { label: m.time(), sortColumn: 'createdAt' }, + { label: m.event(), sortColumn: 'event' }, + { label: m.approximate_location(), sortColumn: 'city' }, + { label: m.ip_address(), sortColumn: 'ipAddress' }, + { label: m.device(), sortColumn: 'device' }, + { label: m.client() } ]} withoutSearch > @@ -42,7 +43,7 @@ {toFriendlyEventString(item.event)} {item.city && item.country ? `${item.city}, ${item.country}` : 'Unknown'}{item.city && item.country ? `${item.city}, ${item.country}` : m.unknown()} {item.ipAddress} {item.device} diff --git a/frontend/tests/account-settings.spec.ts b/frontend/tests/account-settings.spec.ts index 1abfba57..d61ebb2d 100644 --- a/frontend/tests/account-settings.spec.ts +++ b/frontend/tests/account-settings.spec.ts @@ -2,6 +2,7 @@ import test, { expect } from '@playwright/test'; import { users } from './data'; import { cleanupBackend } from './utils/cleanup.util'; import passkeyUtil from './utils/passkey.util'; +import authUtil from './utils/auth.util'; test.beforeEach(cleanupBackend); @@ -37,6 +38,22 @@ test('Update account details fails with already taken username', async ({ page } await expect(page.getByRole('status')).toHaveText('Username is already in use'); }); +test('Change Locale', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Select Locale').click(); + await page.getByRole('option', { name: 'Nederlands' }).click(); + + // Check if th language heading now says 'Taal' instead of 'Language' + await expect(page.getByRole('heading', { name: 'Taal' })).toBeVisible(); + + // Clear all cookies and sign in again to check if the language is still set to Dutch + await page.context().clearCookies(); + await authUtil.authenticate(page); + + await expect(page.getByRole('heading', { name: 'Taal' })).toBeVisible(); +}); + test('Add passkey to an account', async ({ page }) => { await page.goto('/settings/account'); diff --git a/frontend/tests/auth.setup.ts b/frontend/tests/auth.setup.ts index 1f298907..ce2a999b 100644 --- a/frontend/tests/auth.setup.ts +++ b/frontend/tests/auth.setup.ts @@ -1,16 +1,13 @@ import { test as setup } from '@playwright/test'; -import passkeyUtil from './utils/passkey.util'; +import authUtil from './utils/auth.util'; import { cleanupBackend } from './utils/cleanup.util'; const authFile = 'tests/.auth/user.json'; setup('authenticate', async ({ page }) => { await cleanupBackend(); - await page.goto('/login'); - await (await passkeyUtil.init(page)).addPasskey(); - - await page.getByRole('button', { name: 'Authenticate' }).click(); + await authUtil.authenticate(page); await page.waitForURL('/settings/account'); await page.context().storageState({ path: authFile }); diff --git a/frontend/tests/utils/auth.util.ts b/frontend/tests/utils/auth.util.ts new file mode 100644 index 00000000..5ace564a --- /dev/null +++ b/frontend/tests/utils/auth.util.ts @@ -0,0 +1,12 @@ +import type { Page } from '@playwright/test'; +import passkeyUtil from './passkey.util'; + +async function authenticate(page: Page) { + await page.goto('/login'); + + await (await passkeyUtil.init(page)).addPasskey(); + + await page.getByRole('button', { name: 'Authenticate' }).click(); +} + +export default { authenticate }; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ea3034a8..0b472696 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,17 @@ +import { paraglideVitePlugin } from '@inlang/paraglide-js'; import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit(), tailwindcss()] + plugins: [ + sveltekit(), + tailwindcss(), + paraglideVitePlugin({ + project: './project.inlang', + outdir: './src/lib/paraglide', + cookieName: "locale", + strategy: ['cookie', 'preferredLanguage', 'baseLocale'] + }), + ] });