diff --git a/framework/SimpleModule.DevTools/ViteDevWatchService.cs b/framework/SimpleModule.DevTools/ViteDevWatchService.cs index ce3c5d40..dc133de3 100644 --- a/framework/SimpleModule.DevTools/ViteDevWatchService.cs +++ b/framework/SimpleModule.DevTools/ViteDevWatchService.cs @@ -61,7 +61,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) moduleDir, FrontendExtensions, ShouldIgnoreModulePath, - () => RunBuild(moduleName, "npx vite build", moduleDir) + () => RunBuild(moduleName, "npx vite build --configLoader runner", moduleDir) ); LogWatchingModule(logger, moduleName, moduleDir); } @@ -74,7 +74,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) clientAppDir, FrontendExtensions, ShouldIgnoreClientAppPath, - () => RunBuild("ClientApp", "npx vite build", clientAppDir) + () => RunBuild("ClientApp", "npx vite build --configLoader runner", clientAppDir) ); LogWatchingClientApp(logger, clientAppDir); } diff --git a/modules/Admin/src/SimpleModule.Admin/Locales/en.json b/modules/Admin/src/SimpleModule.Admin/Locales/en.json new file mode 100644 index 00000000..a4922a70 --- /dev/null +++ b/modules/Admin/src/SimpleModule.Admin/Locales/en.json @@ -0,0 +1,152 @@ +{ + "Users.Title": "Users", + "Users.TotalCount": "{count} total users", + "Users.CreateButton": "Create User", + "Users.SearchPlaceholder": "Search by name or email...", + "Users.SearchButton": "Search", + "Users.AllStatuses": "All statuses", + "Users.StatusActive": "Active", + "Users.StatusLocked": "Locked", + "Users.StatusDeactivated": "Deactivated", + "Users.AllRoles": "All roles", + "Users.ColName": "Name", + "Users.ColEmail": "Email", + "Users.ColRoles": "Roles", + "Users.ColStatus": "Status", + "Users.ColCreated": "Created", + "Users.EditButton": "Edit", + "Users.Unverified": "unverified", + "Users.EmptyTitle": "No users found", + "Users.EmptyDescription": "Get started by creating your first user.", + "Users.EmptySearch": "No users matching \"{search}\".", + "Users.Pagination": "Page {page} of {totalPages}", + "Users.PreviousButton": "Previous", + "Users.NextButton": "Next", + "UsersCreate.BreadcrumbUsers": "Users", + "UsersCreate.BreadcrumbCreate": "Create User", + "UsersCreate.Title": "Create User", + "UsersCreate.ErrorPasswordMismatch": "Passwords do not match.", + "UsersCreate.ErrorCreateFailed": "Failed to create user. The email may already be in use or the password does not meet requirements.", + "UsersCreate.FieldDisplayName": "Display Name", + "UsersCreate.FieldEmail": "Email", + "UsersCreate.FieldPassword": "Password", + "UsersCreate.FieldConfirmPassword": "Confirm Password", + "UsersCreate.FieldEmailConfirmed": "Email confirmed", + "UsersCreate.FieldRoles": "Roles", + "UsersCreate.SubmitButton": "Create User", + "UsersEdit.BreadcrumbUsers": "Users", + "UsersEdit.BreadcrumbEdit": "Edit User", + "UsersEdit.Title": "Edit User", + "UsersEdit.BadgeYou": "You", + "UsersEdit.BadgeDeactivated": "Deactivated", + "UsersEdit.BadgeLocked": "Locked", + "UsersEdit.TabDetails": "Details", + "UsersEdit.TabRoles": "Roles & Permissions", + "UsersEdit.TabSecurity": "Security", + "UsersEdit.TabSessions": "Sessions", + "UsersEdit.DetailsTitle": "Details", + "UsersEdit.FieldDisplayName": "Display Name", + "UsersEdit.FieldEmail": "Email", + "UsersEdit.FieldEmailConfirmed": "Email confirmed", + "UsersEdit.SaveDetailsButton": "Save Details", + "UsersEdit.AccountStatusTitle": "Account Status", + "UsersEdit.AccountDeactivatedMessage": "This account has been deactivated.", + "UsersEdit.ReactivateButton": "Reactivate Account", + "UsersEdit.CannotDeactivateSelf": "You cannot deactivate your own account.", + "UsersEdit.DeactivateWarning": "Deactivating will lock the account and prevent login.", + "UsersEdit.DeactivateButton": "Deactivate Account", + "UsersEdit.RolesTitle": "Roles", + "UsersEdit.NoRolesDefined": "No roles defined.", + "UsersEdit.SaveRolesButton": "Save Roles", + "UsersEdit.DirectPermissionsTitle": "Direct Permissions", + "UsersEdit.DirectPermissionsDescription": "These permissions are granted directly to this user, bypassing role assignments.", + "UsersEdit.SavePermissionsButton": "Save Permissions", + "UsersEdit.ResetPasswordTitle": "Reset Password", + "UsersEdit.ErrorPasswordMismatch": "Passwords do not match.", + "UsersEdit.FieldNewPassword": "New Password", + "UsersEdit.FieldConfirmPassword": "Confirm Password", + "UsersEdit.ResetPasswordButton": "Reset Password", + "UsersEdit.AccountLockTitle": "Account Lock", + "UsersEdit.AccountLockedMessage": "This account is locked.", + "UsersEdit.UnlockButton": "Unlock Account", + "UsersEdit.CannotLockSelf": "You cannot lock your own account.", + "UsersEdit.AccountActiveMessage": "This account is active.", + "UsersEdit.LockButton": "Lock Account", + "UsersEdit.EmailVerificationTitle": "Email Verification", + "UsersEdit.EmailVerificationStatus": "Status: {status}", + "UsersEdit.EmailVerified": "Verified", + "UsersEdit.EmailNotVerified": "Not verified", + "UsersEdit.ForceReverifyButton": "Force Re-verification", + "UsersEdit.TwoFactorTitle": "Two-Factor Authentication", + "UsersEdit.TwoFactorStatus": "Status: {status}", + "UsersEdit.TwoFactorEnabled": "Enabled", + "UsersEdit.TwoFactorNotEnabled": "Not enabled", + "UsersEdit.Disable2faButton": "Disable 2FA", + "UsersEdit.LoginInfoTitle": "Login Info", + "UsersEdit.FailedLoginAttempts": "Failed login attempts:", + "UsersEdit.LastLogin": "Last login:", + "UsersEdit.LastLoginNever": "Never", + "UsersEdit.CreatedAt": "Created:", + "UsersEdit.ActiveSessionsTitle": "Active Sessions", + "UsersEdit.RevokeAllButton": "Revoke All", + "UsersEdit.NoActiveSessions": "No active sessions.", + "UsersEdit.ColType": "Type", + "UsersEdit.ColApplication": "Application", + "UsersEdit.ColCreated": "Created", + "UsersEdit.ColExpires": "Expires", + "UsersEdit.SessionTypeRefresh": "Refresh", + "UsersEdit.SessionTypeAccess": "Access", + "UsersEdit.SessionExpiresNever": "Never", + "UsersEdit.RevokeButton": "Revoke", + "UsersEdit.ConfirmDeactivateTitle": "Deactivate Account", + "UsersEdit.ConfirmDeactivateDescription": "This user will no longer be able to sign in. Are you sure?", + "UsersEdit.ConfirmDeactivateAction": "Deactivate", + "UsersEdit.ConfirmReverifyTitle": "Force Re-verification", + "UsersEdit.ConfirmReverifyDescription": "This will require the user to re-verify their email address. Are you sure?", + "UsersEdit.ConfirmReverifyAction": "Force Re-verification", + "UsersEdit.ConfirmDisable2faTitle": "Disable Two-Factor Authentication", + "UsersEdit.ConfirmDisable2faDescription": "This will disable 2FA and reset the authenticator for this user. Are you sure?", + "UsersEdit.ConfirmDisable2faAction": "Disable 2FA", + "UsersEdit.ConfirmRevokeAllTitle": "Revoke All Sessions", + "UsersEdit.ConfirmRevokeAllDescription": "This will invalidate all active sessions for this user. They will need to sign in again.", + "UsersEdit.ConfirmRevokeAllAction": "Revoke All", + "UsersEdit.CancelButton": "Cancel", + "Roles.Title": "Roles", + "Roles.Description": "Manage application roles and permissions.", + "Roles.CreateButton": "Create Role", + "Roles.ColName": "Name", + "Roles.ColDescription": "Description", + "Roles.ColUsers": "Users", + "Roles.ColPermissions": "Permissions", + "Roles.ColCreated": "Created", + "Roles.EditButton": "Edit", + "Roles.DeleteButton": "Delete", + "Roles.DeleteDialog.Title": "Delete Role", + "Roles.DeleteDialog.Confirm": "Are you sure you want to delete \"{name}\"? This will remove the role from all users.", + "Roles.DeleteDialog.CancelButton": "Cancel", + "Roles.DeleteDialog.DeleteButton": "Delete", + "Roles.DeleteError": "Cannot delete role with assigned users.", + "RolesCreate.BreadcrumbRoles": "Roles", + "RolesCreate.BreadcrumbCreate": "Create Role", + "RolesCreate.Title": "Create Role", + "RolesCreate.FieldName": "Name", + "RolesCreate.FieldDescription": "Description", + "RolesCreate.FieldPermissions": "Permissions", + "RolesCreate.SubmitButton": "Create Role", + "RolesEdit.BreadcrumbRoles": "Roles", + "RolesEdit.BreadcrumbEdit": "Edit Role", + "RolesEdit.Title": "Edit Role", + "RolesEdit.TabDetails": "Details", + "RolesEdit.TabPermissions": "Permissions", + "RolesEdit.TabUsers": "Users", + "RolesEdit.FieldName": "Name", + "RolesEdit.FieldDescription": "Description", + "RolesEdit.SaveButton": "Save", + "RolesEdit.RolePermissionsTitle": "Role Permissions", + "RolesEdit.SavePermissionsButton": "Save Permissions", + "RolesEdit.AssignedUsersTitle": "Assigned Users ({count})", + "RolesEdit.NoUsersAssigned": "No users assigned to this role.", + "RolesEdit.ColName": "Name", + "RolesEdit.ColEmail": "Email", + "RolesEdit.EditUserButton": "Edit" +} diff --git a/modules/Admin/src/SimpleModule.Admin/Locales/keys.ts b/modules/Admin/src/SimpleModule.Admin/Locales/keys.ts new file mode 100644 index 00000000..d0fe2050 --- /dev/null +++ b/modules/Admin/src/SimpleModule.Admin/Locales/keys.ts @@ -0,0 +1,166 @@ +export const AdminKeys = { + Roles: { + ColCreated: 'Roles.ColCreated', + ColDescription: 'Roles.ColDescription', + ColName: 'Roles.ColName', + ColPermissions: 'Roles.ColPermissions', + ColUsers: 'Roles.ColUsers', + CreateButton: 'Roles.CreateButton', + DeleteButton: 'Roles.DeleteButton', + DeleteDialog: { + CancelButton: 'Roles.DeleteDialog.CancelButton', + Confirm: 'Roles.DeleteDialog.Confirm', + DeleteButton: 'Roles.DeleteDialog.DeleteButton', + Title: 'Roles.DeleteDialog.Title', + }, + DeleteError: 'Roles.DeleteError', + Description: 'Roles.Description', + EditButton: 'Roles.EditButton', + Title: 'Roles.Title', + }, + RolesCreate: { + BreadcrumbCreate: 'RolesCreate.BreadcrumbCreate', + BreadcrumbRoles: 'RolesCreate.BreadcrumbRoles', + FieldDescription: 'RolesCreate.FieldDescription', + FieldName: 'RolesCreate.FieldName', + FieldPermissions: 'RolesCreate.FieldPermissions', + SubmitButton: 'RolesCreate.SubmitButton', + Title: 'RolesCreate.Title', + }, + RolesEdit: { + AssignedUsersTitle: 'RolesEdit.AssignedUsersTitle', + BreadcrumbEdit: 'RolesEdit.BreadcrumbEdit', + BreadcrumbRoles: 'RolesEdit.BreadcrumbRoles', + ColEmail: 'RolesEdit.ColEmail', + ColName: 'RolesEdit.ColName', + EditUserButton: 'RolesEdit.EditUserButton', + FieldDescription: 'RolesEdit.FieldDescription', + FieldName: 'RolesEdit.FieldName', + NoUsersAssigned: 'RolesEdit.NoUsersAssigned', + RolePermissionsTitle: 'RolesEdit.RolePermissionsTitle', + SaveButton: 'RolesEdit.SaveButton', + SavePermissionsButton: 'RolesEdit.SavePermissionsButton', + TabDetails: 'RolesEdit.TabDetails', + TabPermissions: 'RolesEdit.TabPermissions', + TabUsers: 'RolesEdit.TabUsers', + Title: 'RolesEdit.Title', + }, + Users: { + AllRoles: 'Users.AllRoles', + AllStatuses: 'Users.AllStatuses', + ColCreated: 'Users.ColCreated', + ColEmail: 'Users.ColEmail', + ColName: 'Users.ColName', + ColRoles: 'Users.ColRoles', + ColStatus: 'Users.ColStatus', + CreateButton: 'Users.CreateButton', + EditButton: 'Users.EditButton', + EmptyDescription: 'Users.EmptyDescription', + EmptySearch: 'Users.EmptySearch', + EmptyTitle: 'Users.EmptyTitle', + NextButton: 'Users.NextButton', + Pagination: 'Users.Pagination', + PreviousButton: 'Users.PreviousButton', + SearchButton: 'Users.SearchButton', + SearchPlaceholder: 'Users.SearchPlaceholder', + StatusActive: 'Users.StatusActive', + StatusDeactivated: 'Users.StatusDeactivated', + StatusLocked: 'Users.StatusLocked', + Title: 'Users.Title', + TotalCount: 'Users.TotalCount', + Unverified: 'Users.Unverified', + }, + UsersCreate: { + BreadcrumbCreate: 'UsersCreate.BreadcrumbCreate', + BreadcrumbUsers: 'UsersCreate.BreadcrumbUsers', + ErrorCreateFailed: 'UsersCreate.ErrorCreateFailed', + ErrorPasswordMismatch: 'UsersCreate.ErrorPasswordMismatch', + FieldConfirmPassword: 'UsersCreate.FieldConfirmPassword', + FieldDisplayName: 'UsersCreate.FieldDisplayName', + FieldEmail: 'UsersCreate.FieldEmail', + FieldEmailConfirmed: 'UsersCreate.FieldEmailConfirmed', + FieldPassword: 'UsersCreate.FieldPassword', + FieldRoles: 'UsersCreate.FieldRoles', + SubmitButton: 'UsersCreate.SubmitButton', + Title: 'UsersCreate.Title', + }, + UsersEdit: { + AccountActiveMessage: 'UsersEdit.AccountActiveMessage', + AccountDeactivatedMessage: 'UsersEdit.AccountDeactivatedMessage', + AccountLockTitle: 'UsersEdit.AccountLockTitle', + AccountLockedMessage: 'UsersEdit.AccountLockedMessage', + AccountStatusTitle: 'UsersEdit.AccountStatusTitle', + ActiveSessionsTitle: 'UsersEdit.ActiveSessionsTitle', + BadgeDeactivated: 'UsersEdit.BadgeDeactivated', + BadgeLocked: 'UsersEdit.BadgeLocked', + BadgeYou: 'UsersEdit.BadgeYou', + BreadcrumbEdit: 'UsersEdit.BreadcrumbEdit', + BreadcrumbUsers: 'UsersEdit.BreadcrumbUsers', + CancelButton: 'UsersEdit.CancelButton', + CannotDeactivateSelf: 'UsersEdit.CannotDeactivateSelf', + CannotLockSelf: 'UsersEdit.CannotLockSelf', + ColApplication: 'UsersEdit.ColApplication', + ColCreated: 'UsersEdit.ColCreated', + ColExpires: 'UsersEdit.ColExpires', + ColType: 'UsersEdit.ColType', + ConfirmDeactivateAction: 'UsersEdit.ConfirmDeactivateAction', + ConfirmDeactivateDescription: 'UsersEdit.ConfirmDeactivateDescription', + ConfirmDeactivateTitle: 'UsersEdit.ConfirmDeactivateTitle', + ConfirmDisable2faAction: 'UsersEdit.ConfirmDisable2faAction', + ConfirmDisable2faDescription: 'UsersEdit.ConfirmDisable2faDescription', + ConfirmDisable2faTitle: 'UsersEdit.ConfirmDisable2faTitle', + ConfirmReverifyAction: 'UsersEdit.ConfirmReverifyAction', + ConfirmReverifyDescription: 'UsersEdit.ConfirmReverifyDescription', + ConfirmReverifyTitle: 'UsersEdit.ConfirmReverifyTitle', + ConfirmRevokeAllAction: 'UsersEdit.ConfirmRevokeAllAction', + ConfirmRevokeAllDescription: 'UsersEdit.ConfirmRevokeAllDescription', + ConfirmRevokeAllTitle: 'UsersEdit.ConfirmRevokeAllTitle', + CreatedAt: 'UsersEdit.CreatedAt', + DeactivateButton: 'UsersEdit.DeactivateButton', + DeactivateWarning: 'UsersEdit.DeactivateWarning', + DetailsTitle: 'UsersEdit.DetailsTitle', + DirectPermissionsDescription: 'UsersEdit.DirectPermissionsDescription', + DirectPermissionsTitle: 'UsersEdit.DirectPermissionsTitle', + Disable2faButton: 'UsersEdit.Disable2faButton', + EmailNotVerified: 'UsersEdit.EmailNotVerified', + EmailVerificationStatus: 'UsersEdit.EmailVerificationStatus', + EmailVerificationTitle: 'UsersEdit.EmailVerificationTitle', + EmailVerified: 'UsersEdit.EmailVerified', + ErrorPasswordMismatch: 'UsersEdit.ErrorPasswordMismatch', + FailedLoginAttempts: 'UsersEdit.FailedLoginAttempts', + FieldConfirmPassword: 'UsersEdit.FieldConfirmPassword', + FieldDisplayName: 'UsersEdit.FieldDisplayName', + FieldEmail: 'UsersEdit.FieldEmail', + FieldEmailConfirmed: 'UsersEdit.FieldEmailConfirmed', + FieldNewPassword: 'UsersEdit.FieldNewPassword', + ForceReverifyButton: 'UsersEdit.ForceReverifyButton', + LastLogin: 'UsersEdit.LastLogin', + LastLoginNever: 'UsersEdit.LastLoginNever', + LockButton: 'UsersEdit.LockButton', + LoginInfoTitle: 'UsersEdit.LoginInfoTitle', + NoActiveSessions: 'UsersEdit.NoActiveSessions', + NoRolesDefined: 'UsersEdit.NoRolesDefined', + ReactivateButton: 'UsersEdit.ReactivateButton', + ResetPasswordButton: 'UsersEdit.ResetPasswordButton', + ResetPasswordTitle: 'UsersEdit.ResetPasswordTitle', + RevokeAllButton: 'UsersEdit.RevokeAllButton', + RevokeButton: 'UsersEdit.RevokeButton', + RolesTitle: 'UsersEdit.RolesTitle', + SaveDetailsButton: 'UsersEdit.SaveDetailsButton', + SavePermissionsButton: 'UsersEdit.SavePermissionsButton', + SaveRolesButton: 'UsersEdit.SaveRolesButton', + SessionExpiresNever: 'UsersEdit.SessionExpiresNever', + SessionTypeAccess: 'UsersEdit.SessionTypeAccess', + SessionTypeRefresh: 'UsersEdit.SessionTypeRefresh', + TabDetails: 'UsersEdit.TabDetails', + TabRoles: 'UsersEdit.TabRoles', + TabSecurity: 'UsersEdit.TabSecurity', + TabSessions: 'UsersEdit.TabSessions', + Title: 'UsersEdit.Title', + TwoFactorEnabled: 'UsersEdit.TwoFactorEnabled', + TwoFactorNotEnabled: 'UsersEdit.TwoFactorNotEnabled', + TwoFactorStatus: 'UsersEdit.TwoFactorStatus', + TwoFactorTitle: 'UsersEdit.TwoFactorTitle', + UnlockButton: 'UsersEdit.UnlockButton', + }, +} as const; diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Roles.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Roles.tsx index e8c68ab0..a3545eae 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Roles.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Roles.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -17,6 +18,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { AdminKeys } from '../../Locales/keys'; interface Role { id: string; @@ -32,6 +34,7 @@ interface Props { } export default function Roles({ roles }: Props) { + const { t } = useTranslation('Admin'); const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string; @@ -43,7 +46,7 @@ export default function Roles({ roles }: Props) { router.delete(`/admin/roles/${deleteTarget.id}`, { onError: () => { setDeleteTarget(null); - setDeleteError('Cannot delete role with assigned users.'); + setDeleteError(t(AdminKeys.Roles.DeleteError)); }, onSuccess: () => setDeleteTarget(null), }); @@ -74,9 +77,13 @@ export default function Roles({ roles }: Props) { return ( <> router.get('/admin/roles/create')}>Create Role} + title={t(AdminKeys.Roles.Title)} + description={t(AdminKeys.Roles.Description)} + actions={ + + } data={roles} filterBar={errorAlert} > @@ -84,11 +91,11 @@ export default function Roles({ roles }: Props) { - Name - Description - Users - Permissions - Created + {t(AdminKeys.Roles.ColName)} + {t(AdminKeys.Roles.ColDescription)} + {t(AdminKeys.Roles.ColUsers)} + {t(AdminKeys.Roles.ColPermissions)} + {t(AdminKeys.Roles.ColCreated)} @@ -115,14 +122,14 @@ export default function Roles({ roles }: Props) { size="sm" onClick={() => router.get(`/admin/roles/${role.id}/edit`)} > - Edit + {t(AdminKeys.Roles.EditButton)} @@ -136,18 +143,17 @@ export default function Roles({ roles }: Props) { !open && setDeleteTarget(null)}> - Delete Role + {t(AdminKeys.Roles.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.name}”? This will remove - the role from all users. + {t(AdminKeys.Roles.DeleteDialog.Confirm, { name: deleteTarget?.name ?? '' })} diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesCreate.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesCreate.tsx index dc220b39..5dd095ba 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesCreate.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesCreate.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -15,6 +16,7 @@ import { Input, Label, } from '@simplemodule/ui'; +import { AdminKeys } from '../../Locales/keys'; import { PermissionGroups } from '../components/PermissionGroups'; interface Props { @@ -22,6 +24,8 @@ interface Props { } export default function RolesCreate({ permissionsByModule }: Props) { + const { t } = useTranslation('Admin'); + function handleSubmit(e: React.FormEvent) { e.preventDefault(); router.post('/admin/roles', new FormData(e.currentTarget)); @@ -32,30 +36,32 @@ export default function RolesCreate({ permissionsByModule }: Props) { - Roles + + {t(AdminKeys.RolesCreate.BreadcrumbRoles)} + - Create Role + {t(AdminKeys.RolesCreate.BreadcrumbCreate)} -

Create Role

+

{t(AdminKeys.RolesCreate.Title)}

- + - + - +
- +
diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesEdit.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesEdit.tsx index ebf590e2..3b87ad02 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesEdit.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/RolesEdit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -23,6 +24,7 @@ import { TableHeader, TableRow, } from '@simplemodule/ui'; +import { AdminKeys } from '../../Locales/keys'; import { PermissionGroups } from '../components/PermissionGroups'; import { TabNav } from '../components/TabNav'; @@ -47,12 +49,6 @@ interface Props { tab: string; } -const tabs = [ - { id: 'details', label: 'Details' }, - { id: 'permissions', label: 'Permissions' }, - { id: 'users', label: 'Users' }, -]; - export default function RolesEdit({ role, users, @@ -60,20 +56,30 @@ export default function RolesEdit({ permissionsByModule, tab, }: Props) { + const { t } = useTranslation('Admin'); + + const tabs = [ + { id: 'details', label: t(AdminKeys.RolesEdit.TabDetails) }, + { id: 'permissions', label: t(AdminKeys.RolesEdit.TabPermissions) }, + { id: 'users', label: t(AdminKeys.RolesEdit.TabUsers) }, + ]; + return ( - Roles + + {t(AdminKeys.RolesEdit.BreadcrumbRoles)} + - Edit Role + {t(AdminKeys.RolesEdit.BreadcrumbEdit)} -

Edit Role

+

{t(AdminKeys.RolesEdit.Title)}

@@ -88,18 +94,18 @@ export default function RolesEdit({ > - + - + - + @@ -109,7 +115,7 @@ export default function RolesEdit({ {tab === 'permissions' && ( - Role Permissions + {t(AdminKeys.RolesEdit.RolePermissionsTitle)}
@@ -134,17 +140,19 @@ export default function RolesEdit({ {tab === 'users' && ( - Assigned Users ({users.length}) + + {t(AdminKeys.RolesEdit.AssignedUsersTitle, { count: String(users.length) })} + {users.length === 0 ? ( -

No users assigned to this role.

+

{t(AdminKeys.RolesEdit.NoUsersAssigned)}

) : (
- Name - Email + {t(AdminKeys.RolesEdit.ColName)} + {t(AdminKeys.RolesEdit.ColEmail)} @@ -159,7 +167,7 @@ export default function RolesEdit({ size="sm" onClick={() => router.get(`/admin/users/${u.id}/edit`)} > - Edit + {t(AdminKeys.RolesEdit.EditUserButton)} diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Users.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Users.tsx index 4227fe17..f3e40145 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Users.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/Users.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -17,6 +18,7 @@ import { TableRow, } from '@simplemodule/ui'; import { type FormEvent, useState } from 'react'; +import { AdminKeys } from '../../Locales/keys'; interface User { id: string; @@ -40,12 +42,6 @@ interface Props { filterRole: string; } -function userStatus(user: User) { - if (user.isDeactivated) return { label: 'Deactivated', variant: 'default' as const }; - if (user.isLockedOut) return { label: 'Locked', variant: 'danger' as const }; - return { label: 'Active', variant: 'success' as const }; -} - export default function Users({ users, search, @@ -56,8 +52,17 @@ export default function Users({ filterStatus, filterRole, }: Props) { + const { t } = useTranslation('Admin'); const [searchValue, setSearchValue] = useState(search); + function userStatus(user: User) { + if (user.isDeactivated) + return { label: t(AdminKeys.Users.StatusDeactivated), variant: 'default' as const }; + if (user.isLockedOut) + return { label: t(AdminKeys.Users.StatusLocked), variant: 'danger' as const }; + return { label: t(AdminKeys.Users.StatusActive), variant: 'success' as const }; + } + function navigate(params: Record) { router.get( '/admin/users', @@ -77,11 +82,11 @@ export default function Users({ setSearchValue(e.target.value)} - placeholder="Search by name or email..." + placeholder={t(AdminKeys.Users.SearchPlaceholder)} className="flex-1" /> {allRoles.length > 0 && ( @@ -104,10 +109,10 @@ export default function Users({ onValueChange={(v) => navigate({ filterRole: v === '__all__' ? '' : v })} > - + - All roles + {t(AdminKeys.Users.AllRoles)} {allRoles.map((role) => ( {role} @@ -121,18 +126,24 @@ export default function Users({ return ( router.get('/admin/users/create')}>Create User} + title={t(AdminKeys.Users.Title)} + description={t(AdminKeys.Users.TotalCount, { count: String(totalCount) })} + actions={ + + } data={users} filterBar={filterBar} - emptyTitle="No users found" + emptyTitle={t(AdminKeys.Users.EmptyTitle)} emptyDescription={ - search ? `No users matching "${search}".` : 'Get started by creating your first user.' + search ? t(AdminKeys.Users.EmptySearch, { search }) : t(AdminKeys.Users.EmptyDescription) } emptyAction={ !search ? ( - + ) : undefined } > @@ -141,11 +152,11 @@ export default function Users({
- Name - Email - Roles - Status - Created + {t(AdminKeys.Users.ColName)} + {t(AdminKeys.Users.ColEmail)} + {t(AdminKeys.Users.ColRoles)} + {t(AdminKeys.Users.ColStatus)} + {t(AdminKeys.Users.ColCreated)} @@ -159,7 +170,7 @@ export default function Users({ {user.email} {!user.emailConfirmed && ( - unverified + {t(AdminKeys.Users.Unverified)} )} @@ -184,7 +195,7 @@ export default function Users({ size="sm" onClick={() => router.get(`/admin/users/${user.id}/edit`)} > - Edit + {t(AdminKeys.Users.EditButton)} @@ -196,7 +207,10 @@ export default function Users({ {totalPages > 1 && (
- Page {page} of {totalPages} + {t(AdminKeys.Users.Pagination, { + page: String(page), + totalPages: String(totalPages), + })}
diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersCreate.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersCreate.tsx index 5870ec1f..37636250 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersCreate.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersCreate.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -17,6 +18,7 @@ import { Label, } from '@simplemodule/ui'; import { useState } from 'react'; +import { AdminKeys } from '../../Locales/keys'; interface Role { id: string; @@ -29,21 +31,20 @@ interface Props { } export default function UsersCreate({ allRoles }: Props) { + const { t } = useTranslation('Admin'); const [formError, setFormError] = useState(null); function handleSubmit(e: React.FormEvent) { e.preventDefault(); const formData = new FormData(e.currentTarget); if (formData.get('password') !== formData.get('confirmPassword')) { - setFormError('Passwords do not match.'); + setFormError(t(AdminKeys.UsersCreate.ErrorPasswordMismatch)); return; } setFormError(null); router.post('/admin/users', formData, { onError: () => { - setFormError( - 'Failed to create user. The email may already be in use or the password does not meet requirements.', - ); + setFormError(t(AdminKeys.UsersCreate.ErrorCreateFailed)); }, }); } @@ -53,15 +54,17 @@ export default function UsersCreate({ allRoles }: Props) { - Users + + {t(AdminKeys.UsersCreate.BreadcrumbUsers)} + - Create User + {t(AdminKeys.UsersCreate.BreadcrumbCreate)} -

Create User

+

{t(AdminKeys.UsersCreate.Title)}

@@ -73,31 +76,33 @@ export default function UsersCreate({ allRoles }: Props) { )} - + - + - + - + {allRoles.length > 0 && ( - +
{allRoles.map((role) => (
@@ -114,7 +119,7 @@ export default function UsersCreate({ allRoles }: Props) { )} - + diff --git a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersEdit.tsx b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersEdit.tsx index 747b53c7..3bbcc802 100644 --- a/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersEdit.tsx +++ b/modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersEdit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Breadcrumb, @@ -32,6 +33,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { AdminKeys } from '../../Locales/keys'; import { PermissionGroups } from '../components/PermissionGroups'; import { TabNav } from '../components/TabNav'; @@ -73,13 +75,6 @@ interface Props { currentUserId: string; } -const tabs = [ - { id: 'details', label: 'Details' }, - { id: 'roles', label: 'Roles & Permissions' }, - { id: 'security', label: 'Security' }, - { id: 'sessions', label: 'Sessions' }, -]; - type ConfirmAction = 'deactivate' | 'reverify' | 'disable2fa' | 'revokeAll' | null; export default function UsersEdit({ @@ -91,11 +86,19 @@ export default function UsersEdit({ tab, currentUserId, }: Props) { + const { t } = useTranslation('Admin'); const [confirmAction, setConfirmAction] = useState(null); const [passwordError, setPasswordError] = useState(null); const isSelf = user.id === currentUserId; + const tabs = [ + { id: 'details', label: t(AdminKeys.UsersEdit.TabDetails) }, + { id: 'roles', label: t(AdminKeys.UsersEdit.TabRoles) }, + { id: 'security', label: t(AdminKeys.UsersEdit.TabSecurity) }, + { id: 'sessions', label: t(AdminKeys.UsersEdit.TabSessions) }, + ]; + function handleConfirmAction() { switch (confirmAction) { case 'deactivate': @@ -119,25 +122,24 @@ export default function UsersEdit({ { title: string; description: string; action: string } > = { deactivate: { - title: 'Deactivate Account', - description: 'This user will no longer be able to sign in. Are you sure?', - action: 'Deactivate', + title: t(AdminKeys.UsersEdit.ConfirmDeactivateTitle), + description: t(AdminKeys.UsersEdit.ConfirmDeactivateDescription), + action: t(AdminKeys.UsersEdit.ConfirmDeactivateAction), }, reverify: { - title: 'Force Re-verification', - description: 'This will require the user to re-verify their email address. Are you sure?', - action: 'Force Re-verification', + title: t(AdminKeys.UsersEdit.ConfirmReverifyTitle), + description: t(AdminKeys.UsersEdit.ConfirmReverifyDescription), + action: t(AdminKeys.UsersEdit.ConfirmReverifyAction), }, disable2fa: { - title: 'Disable Two-Factor Authentication', - description: 'This will disable 2FA and reset the authenticator for this user. Are you sure?', - action: 'Disable 2FA', + title: t(AdminKeys.UsersEdit.ConfirmDisable2faTitle), + description: t(AdminKeys.UsersEdit.ConfirmDisable2faDescription), + action: t(AdminKeys.UsersEdit.ConfirmDisable2faAction), }, revokeAll: { - title: 'Revoke All Sessions', - description: - 'This will invalidate all active sessions for this user. They will need to sign in again.', - action: 'Revoke All', + title: t(AdminKeys.UsersEdit.ConfirmRevokeAllTitle), + description: t(AdminKeys.UsersEdit.ConfirmRevokeAllDescription), + action: t(AdminKeys.UsersEdit.ConfirmRevokeAllAction), }, }; @@ -148,19 +150,25 @@ export default function UsersEdit({ - Users + + {t(AdminKeys.UsersEdit.BreadcrumbUsers)} + - Edit User + {t(AdminKeys.UsersEdit.BreadcrumbEdit)}
-

Edit User

- {isSelf && You} - {user.isDeactivated && Deactivated} - {user.isLockedOut && !user.isDeactivated && Locked} +

{t(AdminKeys.UsersEdit.Title)}

+ {isSelf && {t(AdminKeys.UsersEdit.BadgeYou)}} + {user.isDeactivated && ( + {t(AdminKeys.UsersEdit.BadgeDeactivated)} + )} + {user.isLockedOut && !user.isDeactivated && ( + {t(AdminKeys.UsersEdit.BadgeLocked)} + )}
@@ -169,7 +177,7 @@ export default function UsersEdit({ <> - Details + {t(AdminKeys.UsersEdit.DetailsTitle)}
- + - + @@ -195,10 +203,10 @@ export default function UsersEdit({ defaultChecked={user.emailConfirmed} /> - +
@@ -206,28 +214,32 @@ export default function UsersEdit({ - Account Status + {t(AdminKeys.UsersEdit.AccountStatusTitle)} {user.isDeactivated ? (
-

This account has been deactivated.

+

+ {t(AdminKeys.UsersEdit.AccountDeactivatedMessage)} +

) : isSelf ? ( -

You cannot deactivate your own account.

+

+ {t(AdminKeys.UsersEdit.CannotDeactivateSelf)} +

) : (

- Deactivating will lock the account and prevent login. + {t(AdminKeys.UsersEdit.DeactivateWarning)}

)} @@ -240,7 +252,7 @@ export default function UsersEdit({ <> - Roles + {t(AdminKeys.UsersEdit.RolesTitle)}
))} {allRoles.length === 0 && ( -

No roles defined.

+

+ {t(AdminKeys.UsersEdit.NoRolesDefined)} +

)}
- + - Direct Permissions + {t(AdminKeys.UsersEdit.DirectPermissionsTitle)}

- These permissions are granted directly to this user, bypassing role assignments. + {t(AdminKeys.UsersEdit.DirectPermissionsDescription)}

{ @@ -295,7 +309,7 @@ export default function UsersEdit({ namePrefix="permissions" />
@@ -307,7 +321,7 @@ export default function UsersEdit({ <> - Reset Password + {t(AdminKeys.UsersEdit.ResetPasswordTitle)}
)} - + - + - +
@@ -344,29 +360,33 @@ export default function UsersEdit({ - Account Lock + {t(AdminKeys.UsersEdit.AccountLockTitle)} {user.isLockedOut ? (
-

This account is locked.

+

+ {t(AdminKeys.UsersEdit.AccountLockedMessage)} +

) : isSelf ? ( -

You cannot lock your own account.

+

{t(AdminKeys.UsersEdit.CannotLockSelf)}

) : (
-

This account is active.

+

+ {t(AdminKeys.UsersEdit.AccountActiveMessage)} +

)} @@ -375,15 +395,19 @@ export default function UsersEdit({ - Email Verification + {t(AdminKeys.UsersEdit.EmailVerificationTitle)}

- Status: {user.emailConfirmed ? 'Verified' : 'Not verified'} + {t(AdminKeys.UsersEdit.EmailVerificationStatus, { + status: user.emailConfirmed + ? t(AdminKeys.UsersEdit.EmailVerified) + : t(AdminKeys.UsersEdit.EmailNotVerified), + })}

{user.emailConfirmed && ( )}
@@ -391,15 +415,19 @@ export default function UsersEdit({ - Two-Factor Authentication + {t(AdminKeys.UsersEdit.TwoFactorTitle)}

- Status: {user.twoFactorEnabled ? 'Enabled' : 'Not enabled'} + {t(AdminKeys.UsersEdit.TwoFactorStatus, { + status: user.twoFactorEnabled + ? t(AdminKeys.UsersEdit.TwoFactorEnabled) + : t(AdminKeys.UsersEdit.TwoFactorNotEnabled), + })}

{user.twoFactorEnabled && ( )}
@@ -407,22 +435,26 @@ export default function UsersEdit({ - Login Info + {t(AdminKeys.UsersEdit.LoginInfoTitle)}
- Failed login attempts: + + {t(AdminKeys.UsersEdit.FailedLoginAttempts)} + {user.accessFailedCount}
- Last login: + {t(AdminKeys.UsersEdit.LastLogin)} - {user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : 'Never'} + {user.lastLoginAt + ? new Date(user.lastLoginAt).toLocaleString() + : t(AdminKeys.UsersEdit.LastLoginNever)}
- Created: + {t(AdminKeys.UsersEdit.CreatedAt)} {new Date(user.createdAt).toLocaleString()} @@ -437,25 +469,25 @@ export default function UsersEdit({
- Active Sessions + {t(AdminKeys.UsersEdit.ActiveSessionsTitle)} {activeSessions.length > 0 && ( )}
{activeSessions.length === 0 ? ( -

No active sessions.

+

{t(AdminKeys.UsersEdit.NoActiveSessions)}

) : (
- Type - Application - Created - Expires + {t(AdminKeys.UsersEdit.ColType)} + {t(AdminKeys.UsersEdit.ColApplication)} + {t(AdminKeys.UsersEdit.ColCreated)} + {t(AdminKeys.UsersEdit.ColExpires)} @@ -464,7 +496,9 @@ export default function UsersEdit({ - {session.type === 'refresh_token' ? 'Refresh' : 'Access'} + {session.type === 'refresh_token' + ? t(AdminKeys.UsersEdit.SessionTypeRefresh) + : t(AdminKeys.UsersEdit.SessionTypeAccess)} @@ -478,7 +512,7 @@ export default function UsersEdit({ {session.expirationDate ? new Date(session.expirationDate).toLocaleString() - : 'Never'} + : t(AdminKeys.UsersEdit.SessionExpiresNever)} @@ -511,7 +545,7 @@ export default function UsersEdit({ } @@ -184,7 +189,9 @@ export default function Browse({ result, filters }: Props) { {/* Quick date presets */}
- Quick range: + + {t(AuditLogsKeys.Browse.QuickRange)} + {DATE_PRESETS.map((preset) => ( + {hasActiveFilters && ( )}
@@ -292,15 +307,15 @@ export default function Browse({ result, filters }: Props) { {result.items.length === 0 ? ( -

No audit logs found

+

{t(AuditLogsKeys.Browse.EmptyTitle)}

{hasActiveFilters - ? 'Try adjusting your filters or selecting a different date range.' - : 'Audit entries will appear here as activity occurs.'} + ? t(AuditLogsKeys.Browse.EmptyWithFilters) + : t(AuditLogsKeys.Browse.EmptyNoFilters)}

{hasActiveFilters && ( )}
@@ -311,14 +326,14 @@ export default function Browse({ result, filters }: Props) {
- Time - Source - User - Action - Module - Path - Status - Duration + {t(AuditLogsKeys.Browse.ColTime)} + {t(AuditLogsKeys.Browse.ColSource)} + {t(AuditLogsKeys.Browse.ColUser)} + {t(AuditLogsKeys.Browse.ColAction)} + {t(AuditLogsKeys.Browse.ColModule)} + {t(AuditLogsKeys.Browse.ColPath)} + {t(AuditLogsKeys.Browse.ColStatus)} + {t(AuditLogsKeys.Browse.ColDuration)} @@ -395,7 +410,11 @@ export default function Browse({ result, filters }: Props) { {result.totalCount > 0 && (
- Showing {startItem}\u2013{endItem} of {result.totalCount.toLocaleString()} entries + {t(AuditLogsKeys.Browse.Showing, { + start: startItem, + end: endItem, + total: result.totalCount.toLocaleString(), + })} {totalPages > 1 && (
diff --git a/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Dashboard.tsx b/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Dashboard.tsx index 07c4cd40..9b764b9e 100644 --- a/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Dashboard.tsx +++ b/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Dashboard.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Button, Card, @@ -30,6 +31,7 @@ import { XAxis, YAxis, } from 'recharts'; +import { AuditLogsKeys } from '../Locales/keys'; import type { DashboardStats, NamedCount } from '../types'; interface Props { @@ -228,6 +230,7 @@ function HBarCard({ // ---- Main Dashboard ---- export default function Dashboard({ stats, from, to, userId, users }: Props) { + const { t } = useTranslation('AuditLogs'); const [dateFrom, setDateFrom] = useState(new Date(from)); const [dateTo, setDateTo] = useState(new Date(to)); const [selectedUser, setSelectedUser] = useState(userId || '__all__'); @@ -311,8 +314,8 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) { return ( {/* Quick date presets */} @@ -327,21 +330,38 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) { ))}
- From - + + {t(AuditLogsKeys.Dashboard.FilterFrom)} + +
- To - + + {t(AuditLogsKeys.Dashboard.FilterTo)} + +
- User + + {t(AuditLogsKeys.Dashboard.FilterUser)} +
- +
} > {/* KPI Cards */}
browseWithFilter({})} /> - + 0 ? `${stats.averageDurationMs}ms` : '\u2014'} - subtitle="HTTP requests" + subtitle={t(AuditLogsKeys.Dashboard.KpiHttpRequests)} onClick={() => browseWithFilter({ source: '0' })} /> 0 ? `${stats.errorRate}%` : '0%'} accent={stats.errorRate > 5 ? 'danger' : 'default'} - subtitle="4xx + 5xx responses" + subtitle={t(AuditLogsKeys.Dashboard.KpiErrorRateSubtitle)} onClick={() => browseWithFilter({})} />
@@ -382,7 +405,9 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) { {stats.timeline.length > 0 && ( - Activity Timeline + + {t(AuditLogsKeys.Dashboard.ActivityTimeline)} + @@ -445,14 +470,19 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) {
{sourceData.length > 0 && ( )} {actionData.length > 0 && ( - + )}
@@ -460,14 +490,19 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) {
{statusData.length > 0 && ( )} {moduleData.length > 0 && ( - + )}
@@ -475,7 +510,7 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) {
{stats.topUsers.length > 0 && ( 0 && ( 0 && ( - Entity Types + + {t(AuditLogsKeys.Dashboard.EntityTypes)} + @@ -523,7 +560,9 @@ export default function Dashboard({ stats, from, to, userId, users }: Props) { {stats.hourlyDistribution.length > 0 && ( - Hourly Activity Distribution + + {t(AuditLogsKeys.Dashboard.HourlyDistribution)} + diff --git a/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Detail.tsx b/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Detail.tsx index d926a820..250b48bc 100644 --- a/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Detail.tsx +++ b/modules/AuditLogs/src/SimpleModule.AuditLogs/Views/Detail.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -19,6 +20,7 @@ import { TooltipTrigger, } from '@simplemodule/ui'; import { useEffect, useState } from 'react'; +import { AuditLogsKeys } from '../Locales/keys'; import type { AuditEntry } from '../types'; import { ACTION_LABELS, @@ -96,7 +98,15 @@ function hasUpdateStyle(changes: ChangeEntry[]): boolean { return changes.some((c) => 'old' in c || 'new' in c); } -function CopyButton({ text }: { text: string }) { +function CopyButton({ + text, + labelCopy, + labelCopied, +}: { + text: string; + labelCopy: string; + labelCopied: string; +}) { const [copied, setCopied] = useState(false); useEffect(() => { @@ -144,12 +154,13 @@ function CopyButton({ text }: { text: string }) { )} - {copied ? 'Copied!' : 'Copy to clipboard'} + {copied ? labelCopied : labelCopy} ); } export default function Detail({ entry, correlated }: Props) { + const { t } = useTranslation('AuditLogs'); const showHttp = !!entry.httpMethod; const showDomain = !!(entry.module || entry.entityType || entry.action != null); const showChanges = !!entry.changes; @@ -161,25 +172,25 @@ export default function Detail({ entry, correlated }: Props) { return ( router.get('/audit-logs/browse')}> - Back to Browse + {t(AuditLogsKeys.Detail.BackToBrowse)} } breadcrumbs={[ - { label: 'Audit Logs', href: '/audit-logs/browse' }, - { label: `Entry #${entry.id}` }, + { label: t(AuditLogsKeys.Detail.BreadcrumbAuditLogs), href: '/audit-logs/browse' }, + { label: t(AuditLogsKeys.Detail.BreadcrumbEntry, { id: entry.id }) }, ]} > {/* Overview Card */} - Overview + {t(AuditLogsKeys.Detail.OverviewTitle)}
- + {relativeTime(entry.timestamp)} @@ -187,20 +198,30 @@ export default function Detail({ entry, correlated }: Props) { {formatTimestamp(entry.timestamp)} - + {SOURCE_LABELS[entry.source] ?? `Unknown (${entry.source})`} - + {entry.correlationId} - + - {entry.userName || entry.userId} - {entry.ipAddress} - {entry.userAgent} + + {entry.userName || entry.userId} + + + {entry.ipAddress} + + + {entry.userAgent} +
@@ -209,33 +230,35 @@ export default function Detail({ entry, correlated }: Props) { {showHttp && ( - HTTP Details + {t(AuditLogsKeys.Detail.HttpDetailsTitle)}
- + {entry.httpMethod} {entry.path} - + {entry.queryString ? ( {entry.queryString} ) : ( '\u2014' )} - + {entry.statusCode} - + {entry.durationMs != null ? `${entry.durationMs}ms` : '\u2014'}
{entry.requestBody && (
-

Request Body

+

+ {t(AuditLogsKeys.Detail.RequestBody)} +

                     {formatJson(entry.requestBody)}
                   
@@ -249,14 +272,20 @@ export default function Detail({ entry, correlated }: Props) { {showDomain && ( - Domain Details + {t(AuditLogsKeys.Detail.DomainDetailsTitle)}
- {entry.module} - {entry.entityType} - {entry.entityId} - + + {entry.module} + + + {entry.entityType} + + + {entry.entityId} + + {entry.action != null ? ( {ACTION_LABELS[entry.action] ?? `Unknown (${entry.action})`} @@ -274,20 +303,20 @@ export default function Detail({ entry, correlated }: Props) { {showChanges && changes.length > 0 && ( - Changes + {t(AuditLogsKeys.Detail.ChangesTitle)}
- Field + {t(AuditLogsKeys.Detail.ColField)} {isUpdate ? ( <> - Old Value - New Value + {t(AuditLogsKeys.Detail.ColOldValue)} + {t(AuditLogsKeys.Detail.ColNewValue)} ) : ( - Value + {t(AuditLogsKeys.Detail.ColValue)} )} @@ -333,7 +362,7 @@ export default function Detail({ entry, correlated }: Props) { {showMetadata && ( - Metadata + {t(AuditLogsKeys.Detail.MetadataTitle)}
@@ -348,9 +377,9 @@ export default function Detail({ entry, correlated }: Props) {
           
             
               
-                Correlated Entries
+                {t(AuditLogsKeys.Detail.CorrelatedTitle)}
                 
-                  ({correlated.length} related)
+                  {t(AuditLogsKeys.Detail.CorrelatedRelated, { count: correlated.length })}
                 
               
             
@@ -358,12 +387,12 @@ export default function Detail({ entry, correlated }: Props) {
               
- ID - Time - Source - Action - Module - Path + {t(AuditLogsKeys.Detail.ColId)} + {t(AuditLogsKeys.Detail.ColTime)} + {t(AuditLogsKeys.Detail.ColSource)} + {t(AuditLogsKeys.Detail.ColAction)} + {t(AuditLogsKeys.Detail.ColModule)} + {t(AuditLogsKeys.Detail.ColPath)} diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/en.json b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/en.json new file mode 100644 index 00000000..ad36b053 --- /dev/null +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/en.json @@ -0,0 +1,55 @@ +{ + "Dashboard.Title": "Background Jobs", + "Dashboard.Description": "Overview of job processing activity", + "Dashboard.ActiveJobs": "Active Jobs", + "Dashboard.FailedJobs": "Failed Jobs", + "Dashboard.RecurringJobs": "Recurring Jobs", + "Dashboard.ViewAll": "View all", + "Dashboard.Manage": "Manage", + "Dashboard.CurrentlyRunning": "Currently Running", + "Dashboard.RecentFailures": "Recent Failures", + "Dashboard.BadgeFailed": "Failed", + + "List.Title": "All Jobs", + "List.TotalCount": "{{count}} total jobs", + "List.EmptyTitle": "No jobs found", + "List.EmptyDescription": "No background jobs have been executed yet.", + "List.ColJobType": "Job Type", + "List.ColState": "State", + "List.ColProgress": "Progress", + "List.ColCreated": "Created", + "List.ColCompleted": "Completed", + + "Detail.ModuleLabel": "Module: {{name}}", + "Detail.Cancel": "Cancel", + "Detail.Retry": "Retry", + "Detail.BackToList": "Back to List", + "Detail.StatusCard": "Status", + "Detail.RetryCount": "Retry #{{count}}", + "Detail.Processing": "Processing...", + "Detail.TimestampsCard": "Timestamps", + "Detail.Created": "Created", + "Detail.Started": "Started", + "Detail.Completed": "Completed", + "Detail.ErrorCard": "Error", + "Detail.LogsCard": "Logs ({{count}})", + + "Recurring.Title": "Recurring Jobs", + "Recurring.Description": "{{count}} recurring jobs configured", + "Recurring.EmptyTitle": "No recurring jobs", + "Recurring.EmptyDescription": "No recurring background jobs have been configured yet.", + "Recurring.ColName": "Name", + "Recurring.ColSchedule": "Schedule", + "Recurring.ColStatus": "Status", + "Recurring.ColLastRun": "Last Run", + "Recurring.Enabled": "Enabled", + "Recurring.Disabled": "Disabled", + "Recurring.ActionDisable": "Disable", + "Recurring.ActionEnable": "Enable", + "Recurring.ActionDelete": "Delete", + "Recurring.Never": "Never", + "Recurring.DeleteDialogTitle": "Delete Recurring Job", + "Recurring.DeleteDialogDescription": "Are you sure you want to delete this recurring job? This action cannot be undone.", + "Recurring.DeleteDialogCancel": "Cancel", + "Recurring.DeleteDialogConfirm": "Delete" +} diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/keys.ts b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/keys.ts new file mode 100644 index 00000000..71d770b2 --- /dev/null +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Locales/keys.ts @@ -0,0 +1,60 @@ +export const BackgroundJobsKeys = { + Dashboard: { + ActiveJobs: 'Dashboard.ActiveJobs', + BadgeFailed: 'Dashboard.BadgeFailed', + CurrentlyRunning: 'Dashboard.CurrentlyRunning', + Description: 'Dashboard.Description', + FailedJobs: 'Dashboard.FailedJobs', + Manage: 'Dashboard.Manage', + RecentFailures: 'Dashboard.RecentFailures', + RecurringJobs: 'Dashboard.RecurringJobs', + Title: 'Dashboard.Title', + ViewAll: 'Dashboard.ViewAll', + }, + Detail: { + BackToList: 'Detail.BackToList', + Cancel: 'Detail.Cancel', + Completed: 'Detail.Completed', + Created: 'Detail.Created', + ErrorCard: 'Detail.ErrorCard', + LogsCard: 'Detail.LogsCard', + ModuleLabel: 'Detail.ModuleLabel', + Processing: 'Detail.Processing', + Retry: 'Detail.Retry', + RetryCount: 'Detail.RetryCount', + Started: 'Detail.Started', + StatusCard: 'Detail.StatusCard', + TimestampsCard: 'Detail.TimestampsCard', + }, + List: { + ColCompleted: 'List.ColCompleted', + ColCreated: 'List.ColCreated', + ColJobType: 'List.ColJobType', + ColProgress: 'List.ColProgress', + ColState: 'List.ColState', + EmptyDescription: 'List.EmptyDescription', + EmptyTitle: 'List.EmptyTitle', + Title: 'List.Title', + TotalCount: 'List.TotalCount', + }, + Recurring: { + ActionDelete: 'Recurring.ActionDelete', + ActionDisable: 'Recurring.ActionDisable', + ActionEnable: 'Recurring.ActionEnable', + ColLastRun: 'Recurring.ColLastRun', + ColName: 'Recurring.ColName', + ColSchedule: 'Recurring.ColSchedule', + ColStatus: 'Recurring.ColStatus', + DeleteDialogCancel: 'Recurring.DeleteDialogCancel', + DeleteDialogConfirm: 'Recurring.DeleteDialogConfirm', + DeleteDialogDescription: 'Recurring.DeleteDialogDescription', + DeleteDialogTitle: 'Recurring.DeleteDialogTitle', + Description: 'Recurring.Description', + Disabled: 'Recurring.Disabled', + EmptyDescription: 'Recurring.EmptyDescription', + EmptyTitle: 'Recurring.EmptyTitle', + Enabled: 'Recurring.Enabled', + Never: 'Recurring.Never', + Title: 'Recurring.Title', + }, +} as const; diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Dashboard.tsx b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Dashboard.tsx index 48a727ac..ff8f33e6 100644 --- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Dashboard.tsx +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Dashboard.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -9,6 +10,7 @@ import { PageShell, Progress, } from '@simplemodule/ui'; +import { BackgroundJobsKeys } from '../../Locales/keys'; interface JobSummary { id: string; @@ -34,12 +36,17 @@ export default function Dashboard({ failedCount, recurringCount, }: Props) { + const { t } = useTranslation('BackgroundJobs'); + return ( - +
- Active Jobs + {t(BackgroundJobsKeys.Dashboard.ActiveJobs)}
{activeCount}
@@ -49,13 +56,13 @@ export default function Dashboard({ className="mt-2" onClick={() => router.get('/admin/jobs/list?state=Running')} > - View all + {t(BackgroundJobsKeys.Dashboard.ViewAll)}
- Failed Jobs + {t(BackgroundJobsKeys.Dashboard.FailedJobs)}
{failedCount}
@@ -65,13 +72,13 @@ export default function Dashboard({ className="mt-2" onClick={() => router.get('/admin/jobs/list?state=Failed')} > - View all + {t(BackgroundJobsKeys.Dashboard.ViewAll)}
- Recurring Jobs + {t(BackgroundJobsKeys.Dashboard.RecurringJobs)}
{recurringCount}
@@ -81,7 +88,7 @@ export default function Dashboard({ className="mt-2" onClick={() => router.get('/admin/jobs/recurring')} > - Manage + {t(BackgroundJobsKeys.Dashboard.Manage)}
@@ -90,7 +97,7 @@ export default function Dashboard({ {activeJobs.length > 0 && ( - Currently Running + {t(BackgroundJobsKeys.Dashboard.CurrentlyRunning)}
@@ -117,7 +124,7 @@ export default function Dashboard({ {failedJobs.length > 0 && ( - Recent Failures + {t(BackgroundJobsKeys.Dashboard.RecentFailures)}
@@ -129,7 +136,7 @@ export default function Dashboard({ onClick={() => router.get(`/admin/jobs/${job.id}`)} > {job.jobType} - Failed + {t(BackgroundJobsKeys.Dashboard.BadgeFailed)} ))}
diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Detail.tsx b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Detail.tsx index 7cb93d1b..108684f0 100644 --- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Detail.tsx +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Detail.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -10,6 +11,7 @@ import { Progress, } from '@simplemodule/ui'; import { useEffect } from 'react'; +import { BackgroundJobsKeys } from '../../Locales/keys'; import { stateVariant } from '../utils/jobState'; interface LogEntry { @@ -38,6 +40,8 @@ interface Props { } export default function Detail({ job }: Props) { + const { t } = useTranslation('BackgroundJobs'); + useEffect(() => { if (job.state !== 'Running') return; const interval = setInterval(() => { @@ -49,19 +53,21 @@ export default function Detail({ job }: Props) { return ( {job.state === 'Running' && ( )} {job.state === 'Failed' && ( - + )}
} @@ -69,18 +75,20 @@ export default function Detail({ job }: Props) {
- Status + {t(BackgroundJobsKeys.Detail.StatusCard)}
{job.state} {job.retryCount > 0 && ( - Retry #{job.retryCount} + + {t(BackgroundJobsKeys.Detail.RetryCount, { count: job.retryCount })} + )}
- {job.progressMessage ?? 'Processing...'} + {job.progressMessage ?? t(BackgroundJobsKeys.Detail.Processing)} {job.progressPercentage}%
@@ -90,22 +98,22 @@ export default function Detail({ job }: Props) { - Timestamps + {t(BackgroundJobsKeys.Detail.TimestampsCard)}
- Created + {t(BackgroundJobsKeys.Detail.Created)} {new Date(job.createdAt).toLocaleString()}
{job.startedAt && (
- Started + {t(BackgroundJobsKeys.Detail.Started)} {new Date(job.startedAt).toLocaleString()}
)} {job.completedAt && (
- Completed + {t(BackgroundJobsKeys.Detail.Completed)} {new Date(job.completedAt).toLocaleString()}
)} @@ -116,7 +124,7 @@ export default function Detail({ job }: Props) { {job.error && ( - Error + {t(BackgroundJobsKeys.Detail.ErrorCard)}
@@ -129,7 +137,9 @@ export default function Detail({ job }: Props) {
       {job.logs.length > 0 && (
         
           
-            Logs ({job.logs.length})
+            
+              {t(BackgroundJobsKeys.Detail.LogsCard, { count: job.logs.length })}
+            
           
           
             
diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/List.tsx b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/List.tsx index 65faa8b8..ca1627d0 100644 --- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/List.tsx +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/List.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, DataGridPage, @@ -10,6 +11,7 @@ import { TableHeader, TableRow, } from '@simplemodule/ui'; +import { BackgroundJobsKeys } from '../../Locales/keys'; import { stateVariant } from '../utils/jobState'; interface JobSummary { @@ -34,23 +36,25 @@ interface Props { } export default function List({ jobs }: Props) { + const { t } = useTranslation('BackgroundJobs'); + return ( {(pageData) => (
- Job Type - State - Progress - Created - Completed + {t(BackgroundJobsKeys.List.ColJobType)} + {t(BackgroundJobsKeys.List.ColState)} + {t(BackgroundJobsKeys.List.ColProgress)} + {t(BackgroundJobsKeys.List.ColCreated)} + {t(BackgroundJobsKeys.List.ColCompleted)} diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Recurring.tsx b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Recurring.tsx index 5c9faf07..0655f0c1 100644 --- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Recurring.tsx +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/Pages/Views/Recurring.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -17,6 +18,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { BackgroundJobsKeys } from '../../Locales/keys'; interface RecurringJob { id: string; @@ -34,6 +36,7 @@ interface Props { } export default function Recurring({ jobs }: Props) { + const { t } = useTranslation('BackgroundJobs'); const [deleteId, setDeleteId] = useState(null); function handleToggle(id: string) { @@ -49,20 +52,20 @@ export default function Recurring({ jobs }: Props) { return ( <> {(pageData) => (
- Name - Schedule - Status - Last Run + {t(BackgroundJobsKeys.Recurring.ColName)} + {t(BackgroundJobsKeys.Recurring.ColSchedule)} + {t(BackgroundJobsKeys.Recurring.ColStatus)} + {t(BackgroundJobsKeys.Recurring.ColLastRun)} @@ -77,19 +80,25 @@ export default function Recurring({ jobs }: Props) { - {job.isEnabled ? 'Enabled' : 'Disabled'} + {job.isEnabled + ? t(BackgroundJobsKeys.Recurring.Enabled) + : t(BackgroundJobsKeys.Recurring.Disabled)} - {job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : 'Never'} + {job.lastRunAt + ? new Date(job.lastRunAt).toLocaleString() + : t(BackgroundJobsKeys.Recurring.Never)}
@@ -103,17 +112,17 @@ export default function Recurring({ jobs }: Props) { !open && setDeleteId(null)}> - Delete Recurring Job + {t(BackgroundJobsKeys.Recurring.DeleteDialogTitle)} - Are you sure you want to delete this recurring job? This action cannot be undone. + {t(BackgroundJobsKeys.Recurring.DeleteDialogDescription)} diff --git a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/SimpleModule.BackgroundJobs.csproj b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/SimpleModule.BackgroundJobs.csproj index cb7b0b5d..d2faca7e 100644 --- a/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/SimpleModule.BackgroundJobs.csproj +++ b/modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/SimpleModule.BackgroundJobs.csproj @@ -19,4 +19,7 @@ %(Filename)Endpoint.cs + + + diff --git a/modules/Dashboard/src/SimpleModule.Dashboard/Locales/en.json b/modules/Dashboard/src/SimpleModule.Dashboard/Locales/en.json new file mode 100644 index 00000000..efad7108 --- /dev/null +++ b/modules/Dashboard/src/SimpleModule.Dashboard/Locales/en.json @@ -0,0 +1,35 @@ +{ + "Home.DashboardTitle": "Welcome back, {displayName}", + "Home.DashboardDescription": "Here's your development dashboard", + "Home.AccountCardTitle": "Account", + "Home.AccountCardDescription": "Manage your profile and security settings", + "Home.ApiDocsCardTitle": "API Docs", + "Home.ApiDocsCardDescription": "Explore endpoints and test requests", + "Home.HealthCardTitle": "Health", + "Home.HealthCardDescription": "Check system status and diagnostics", + "Home.UserInfoTitle": "User Info", + "Home.UserInfoLoading": "Loading user info", + "Home.UserInfoError": "Failed to load: {error}", + "Home.UserInfoLabelName": "Name", + "Home.UserInfoLabelEmail": "Email", + "Home.UserInfoLabelId": "ID", + "Home.UserInfoLabelRoles": "Roles", + "Home.TokenTesterTitle": "Token Tester", + "Home.TokenTesterSubtitle": "OAuth2 Authorization Code + PKCE", + "Home.TokenTesterDescription": "Obtain an access token using the {clientId} application.", + "Home.TokenTesterAuthorizing": "Authorizing", + "Home.TokenTesterGetToken": "Get Access Token", + "Home.TokenTesterDecodedClaims": "Decoded Claims", + "Home.TokenTesterColClaim": "Claim", + "Home.TokenTesterColValue": "Value", + "Home.ApiTesterTitle": "API Tester", + "Home.ApiTesterSubtitle": "Call Protected Endpoints", + "Home.ApiTesterDefaultResponse": "Click an endpoint above to make a request.", + "Home.LandingTitle": "SimpleModule", + "Home.LandingDescription": "Modular monolith framework for .NET with compile\u2011time module\u00a0discovery", + "Home.LandingGetStarted": "Get Started", + "Home.LandingCreateAccount": "Create Account", + "Home.LandingQuickStartTitle": "Quick Start (Development Only)", + "Home.LandingApiDocs": "API Docs", + "Home.LandingHealthCheck": "Health Check" +} diff --git a/modules/Dashboard/src/SimpleModule.Dashboard/Locales/keys.ts b/modules/Dashboard/src/SimpleModule.Dashboard/Locales/keys.ts new file mode 100644 index 00000000..ba25a2fd --- /dev/null +++ b/modules/Dashboard/src/SimpleModule.Dashboard/Locales/keys.ts @@ -0,0 +1,37 @@ +export const DashboardKeys = { + Home: { + AccountCardDescription: 'Home.AccountCardDescription', + AccountCardTitle: 'Home.AccountCardTitle', + ApiDocsCardDescription: 'Home.ApiDocsCardDescription', + ApiDocsCardTitle: 'Home.ApiDocsCardTitle', + ApiTesterDefaultResponse: 'Home.ApiTesterDefaultResponse', + ApiTesterSubtitle: 'Home.ApiTesterSubtitle', + ApiTesterTitle: 'Home.ApiTesterTitle', + DashboardDescription: 'Home.DashboardDescription', + DashboardTitle: 'Home.DashboardTitle', + HealthCardDescription: 'Home.HealthCardDescription', + HealthCardTitle: 'Home.HealthCardTitle', + LandingApiDocs: 'Home.LandingApiDocs', + LandingCreateAccount: 'Home.LandingCreateAccount', + LandingDescription: 'Home.LandingDescription', + LandingGetStarted: 'Home.LandingGetStarted', + LandingHealthCheck: 'Home.LandingHealthCheck', + LandingQuickStartTitle: 'Home.LandingQuickStartTitle', + LandingTitle: 'Home.LandingTitle', + TokenTesterAuthorizing: 'Home.TokenTesterAuthorizing', + TokenTesterColClaim: 'Home.TokenTesterColClaim', + TokenTesterColValue: 'Home.TokenTesterColValue', + TokenTesterDecodedClaims: 'Home.TokenTesterDecodedClaims', + TokenTesterDescription: 'Home.TokenTesterDescription', + TokenTesterGetToken: 'Home.TokenTesterGetToken', + TokenTesterSubtitle: 'Home.TokenTesterSubtitle', + TokenTesterTitle: 'Home.TokenTesterTitle', + UserInfoError: 'Home.UserInfoError', + UserInfoLabelEmail: 'Home.UserInfoLabelEmail', + UserInfoLabelId: 'Home.UserInfoLabelId', + UserInfoLabelName: 'Home.UserInfoLabelName', + UserInfoLabelRoles: 'Home.UserInfoLabelRoles', + UserInfoLoading: 'Home.UserInfoLoading', + UserInfoTitle: 'Home.UserInfoTitle', + }, +} as const; diff --git a/modules/Dashboard/src/SimpleModule.Dashboard/Pages/Home.tsx b/modules/Dashboard/src/SimpleModule.Dashboard/Pages/Home.tsx index 615d9976..b45ca94b 100644 --- a/modules/Dashboard/src/SimpleModule.Dashboard/Pages/Home.tsx +++ b/modules/Dashboard/src/SimpleModule.Dashboard/Pages/Home.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, @@ -19,6 +20,7 @@ import { TableRow, } from '@simplemodule/ui'; import React from 'react'; +import { DashboardKeys } from '../Locales/keys'; interface HomeProps { isAuthenticated: boolean; @@ -37,10 +39,11 @@ export default function Home({ isAuthenticated, displayName, isDevelopment }: Ho // --- Dashboard View --- function DashboardView({ displayName }: { displayName: string }) { + const { t } = useTranslation('Dashboard'); return ( {/* Quick Actions */}
@@ -61,10 +64,12 @@ function DashboardView({ displayName }: { displayName: string }) { - Account + {t(DashboardKeys.Home.AccountCardTitle)}
-

Manage your profile and security settings

+

+ {t(DashboardKeys.Home.AccountCardDescription)} +

@@ -85,10 +90,12 @@ function DashboardView({ displayName }: { displayName: string }) { - API Docs + {t(DashboardKeys.Home.ApiDocsCardTitle)} -

Explore endpoints and test requests

+

+ {t(DashboardKeys.Home.ApiDocsCardDescription)} +

@@ -109,10 +116,12 @@ function DashboardView({ displayName }: { displayName: string }) { - Health + {t(DashboardKeys.Home.HealthCardTitle)} -

Check system status and diagnostics

+

+ {t(DashboardKeys.Home.HealthCardDescription)} +

@@ -160,6 +169,7 @@ function InfoRow({ } function UserInfoPanel() { + const { t } = useTranslation('Dashboard'); const [userInfo, setUserInfo] = React.useState(null); const [error, setError] = React.useState(null); const [loading, setLoading] = React.useState(true); @@ -190,22 +200,32 @@ function UserInfoPanel() { return ( - User Info + {t(DashboardKeys.Home.UserInfoTitle)} {loading && (
- Loading user info + {t(DashboardKeys.Home.UserInfoLoading)}
)} - {error && Failed to load: {error}} + {error && ( + + {t(DashboardKeys.Home.UserInfoError, { error })} + + )} {userInfo && ( <> - - + + {userInfo.roles && ( )} @@ -271,6 +291,7 @@ function decodeToken(token: string): DecodedClaim[] | null { } function TokenTester() { + const { t } = useTranslation('Dashboard'); const [token, setToken] = React.useState(null); const [authorizing, setAuthorizing] = React.useState(false); @@ -344,25 +365,21 @@ function TokenTester() { return ( - Token Tester + {t(DashboardKeys.Home.TokenTesterTitle)} -

OAuth2 Authorization Code + PKCE

+

{t(DashboardKeys.Home.TokenTesterSubtitle)}

- Obtain an access token using the{' '} - - simplemodule-client - {' '} - application. + {t(DashboardKeys.Home.TokenTesterDescription, { clientId: 'simplemodule-client' })}

{token && ( @@ -372,12 +389,14 @@ function TokenTester() { {claims && ( <> -

Decoded Claims

+

+ {t(DashboardKeys.Home.TokenTesterDecodedClaims)} +

- Claim - Value + {t(DashboardKeys.Home.TokenTesterColClaim)} + {t(DashboardKeys.Home.TokenTesterColValue)} @@ -403,6 +422,7 @@ function TokenTester() { const API_ENDPOINTS = ['/api/users/me', '/api/users', '/api/products', '/api/orders']; function ApiTester() { + const { t } = useTranslation('Dashboard'); const [status, setStatus] = React.useState<{ loading: boolean; ok?: boolean; @@ -411,7 +431,7 @@ function ApiTester() { url?: string; error?: string; } | null>(null); - const [response, setResponse] = React.useState('Click an endpoint above to make a request.'); + const [response, setResponse] = React.useState(t(DashboardKeys.Home.ApiTesterDefaultResponse)); const getAccessToken = (): string | null => { const codeBlocks = document.querySelectorAll('.font-mono.text-xs.break-all'); @@ -459,10 +479,10 @@ function ApiTester() { return ( - API Tester + {t(DashboardKeys.Home.ApiTesterTitle)} -

Call Protected Endpoints

+

{t(DashboardKeys.Home.ApiTesterSubtitle)}

{API_ENDPOINTS.map((url) => (
{isDevelopment && ( - Quick Start (Development Only) + {t(DashboardKeys.Home.LandingQuickStartTitle)} Email:{' '} @@ -546,14 +569,14 @@ function LandingView({ isDevelopment }: { isDevelopment: boolean }) { href="/swagger" className="text-text-muted no-underline hover:text-primary transition-colors" > - API Docs + {t(DashboardKeys.Home.LandingApiDocs)} · - Health Check + {t(DashboardKeys.Home.LandingHealthCheck)} diff --git a/modules/Dashboard/src/SimpleModule.Dashboard/SimpleModule.Dashboard.csproj b/modules/Dashboard/src/SimpleModule.Dashboard/SimpleModule.Dashboard.csproj index ee45fc7a..18abacbb 100644 --- a/modules/Dashboard/src/SimpleModule.Dashboard/SimpleModule.Dashboard.csproj +++ b/modules/Dashboard/src/SimpleModule.Dashboard/SimpleModule.Dashboard.csproj @@ -2,6 +2,9 @@ net10.0 + + + diff --git a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/en.json b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/en.json new file mode 100644 index 00000000..9638da8e --- /dev/null +++ b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/en.json @@ -0,0 +1,29 @@ +{ + "Manage.Title": "Feature Flags", + "Manage.Description": "Manage feature flags across all modules.", + "Manage.ColName": "Name", + "Manage.ColDescription": "Description", + "Manage.ColDefault": "Default", + "Manage.ColEnabled": "Enabled", + "Manage.ColOverrides": "Overrides", + "Manage.DefaultOn": "on", + "Manage.DefaultOff": "off", + "Manage.OverridesButton": "Overrides", + "Manage.DeprecatedTitle": "Deprecated Flags", + "Manage.DeprecatedColName": "Name", + "Manage.DeprecatedColEnabled": "Enabled", + "Manage.DeprecatedBadge": "deprecated", + "Manage.OverrideDialog.Title": "Overrides for {flagName}", + "Manage.OverrideDialog.ColType": "Type", + "Manage.OverrideDialog.ColValue": "Value", + "Manage.OverrideDialog.ColEnabled": "Enabled", + "Manage.OverrideDialog.TypeUser": "User", + "Manage.OverrideDialog.TypeRole": "Role", + "Manage.OverrideDialog.EnabledYes": "Yes", + "Manage.OverrideDialog.EnabledNo": "No", + "Manage.OverrideDialog.DeleteButton": "Delete", + "Manage.OverrideDialog.TypeLabel": "Type", + "Manage.OverrideDialog.ValueLabel": "Value (User ID or Role Name)", + "Manage.OverrideDialog.EnabledLabel": "Enabled", + "Manage.OverrideDialog.AddButton": "Add Override" +} diff --git a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/keys.ts b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/keys.ts new file mode 100644 index 00000000..30bc5699 --- /dev/null +++ b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Locales/keys.ts @@ -0,0 +1,33 @@ +export const FeatureFlagsKeys = { + Manage: { + ColDefault: 'Manage.ColDefault', + ColDescription: 'Manage.ColDescription', + ColEnabled: 'Manage.ColEnabled', + ColName: 'Manage.ColName', + ColOverrides: 'Manage.ColOverrides', + DefaultOff: 'Manage.DefaultOff', + DefaultOn: 'Manage.DefaultOn', + DeprecatedBadge: 'Manage.DeprecatedBadge', + DeprecatedColEnabled: 'Manage.DeprecatedColEnabled', + DeprecatedColName: 'Manage.DeprecatedColName', + DeprecatedTitle: 'Manage.DeprecatedTitle', + Description: 'Manage.Description', + OverrideDialog: { + AddButton: 'Manage.OverrideDialog.AddButton', + ColEnabled: 'Manage.OverrideDialog.ColEnabled', + ColType: 'Manage.OverrideDialog.ColType', + ColValue: 'Manage.OverrideDialog.ColValue', + DeleteButton: 'Manage.OverrideDialog.DeleteButton', + EnabledLabel: 'Manage.OverrideDialog.EnabledLabel', + EnabledNo: 'Manage.OverrideDialog.EnabledNo', + EnabledYes: 'Manage.OverrideDialog.EnabledYes', + Title: 'Manage.OverrideDialog.Title', + TypeLabel: 'Manage.OverrideDialog.TypeLabel', + TypeRole: 'Manage.OverrideDialog.TypeRole', + TypeUser: 'Manage.OverrideDialog.TypeUser', + ValueLabel: 'Manage.OverrideDialog.ValueLabel', + }, + OverridesButton: 'Manage.OverridesButton', + Title: 'Manage.Title', + }, +} as const; diff --git a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/SimpleModule.FeatureFlags.csproj b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/SimpleModule.FeatureFlags.csproj index 5a667b10..34125f86 100644 --- a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/SimpleModule.FeatureFlags.csproj +++ b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/SimpleModule.FeatureFlags.csproj @@ -2,6 +2,9 @@ net10.0 + + + diff --git a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Views/Manage.tsx b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Views/Manage.tsx index b0009c3f..712d4f04 100644 --- a/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Views/Manage.tsx +++ b/modules/FeatureFlags/src/SimpleModule.FeatureFlags/Views/Manage.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -7,7 +8,6 @@ import { DialogContent, DialogHeader, DialogTitle, - DialogTrigger, Field, FieldGroup, Input, @@ -28,6 +28,7 @@ import { } from '@simplemodule/ui'; import { useState } from 'react'; import { OVERRIDE_TYPE_ROLE, OVERRIDE_TYPE_USER } from '../constants'; +import { FeatureFlagsKeys } from '../Locales/keys'; import type { FeatureFlag, FeatureFlagOverride } from '../types'; interface ManageProps { @@ -35,6 +36,7 @@ interface ManageProps { } export default function Manage({ flags: initialFlags }: ManageProps) { + const { t } = useTranslation('FeatureFlags'); const [flags, setFlags] = useState(initialFlags); const [selectedFlag, setSelectedFlag] = useState(null); const [overrides, setOverrides] = useState([]); @@ -97,17 +99,20 @@ export default function Manage({ flags: initialFlags }: ManageProps) { const deprecatedFlags = flags.filter((f) => f.isDeprecated); return ( - +
- Name - Description - Default - Enabled - Overrides + {t(FeatureFlagsKeys.Manage.ColName)} + {t(FeatureFlagsKeys.Manage.ColDescription)} + {t(FeatureFlagsKeys.Manage.ColDefault)} + {t(FeatureFlagsKeys.Manage.ColEnabled)} + {t(FeatureFlagsKeys.Manage.ColOverrides)} @@ -117,7 +122,9 @@ export default function Manage({ flags: initialFlags }: ManageProps) { {flag.description} - {flag.defaultEnabled ? 'on' : 'off'} + {flag.defaultEnabled + ? t(FeatureFlagsKeys.Manage.DefaultOn) + : t(FeatureFlagsKeys.Manage.DefaultOff)} @@ -128,7 +135,7 @@ export default function Manage({ flags: initialFlags }: ManageProps) { @@ -141,12 +148,14 @@ export default function Manage({ flags: initialFlags }: ManageProps) { {deprecatedFlags.length > 0 && ( -

Deprecated Flags

+

+ {t(FeatureFlagsKeys.Manage.DeprecatedTitle)} +

- Name - Enabled + {t(FeatureFlagsKeys.Manage.DeprecatedColName)} + {t(FeatureFlagsKeys.Manage.DeprecatedColEnabled)} @@ -155,7 +164,7 @@ export default function Manage({ flags: initialFlags }: ManageProps) { {flag.name}{' '} - deprecated + {t(FeatureFlagsKeys.Manage.DeprecatedBadge)} @@ -175,16 +184,18 @@ export default function Manage({ flags: initialFlags }: ManageProps) { - Overrides for {selectedFlag} + + {t(FeatureFlagsKeys.Manage.OverrideDialog.Title, { flagName: selectedFlag ?? '' })} + {overrides.length > 0 && (
- Type - Value - Enabled + {t(FeatureFlagsKeys.Manage.OverrideDialog.ColType)} + {t(FeatureFlagsKeys.Manage.OverrideDialog.ColValue)} + {t(FeatureFlagsKeys.Manage.OverrideDialog.ColEnabled)} @@ -192,13 +203,21 @@ export default function Manage({ flags: initialFlags }: ManageProps) { {overrides.map((o) => ( - {o.overrideType === OVERRIDE_TYPE_USER ? 'User' : 'Role'} + + {o.overrideType === OVERRIDE_TYPE_USER + ? t(FeatureFlagsKeys.Manage.OverrideDialog.TypeUser) + : t(FeatureFlagsKeys.Manage.OverrideDialog.TypeRole)} + {o.overrideValue} - {o.isEnabled ? 'Yes' : 'No'} + + {o.isEnabled + ? t(FeatureFlagsKeys.Manage.OverrideDialog.EnabledYes) + : t(FeatureFlagsKeys.Manage.OverrideDialog.EnabledNo)} + @@ -210,27 +229,37 @@ export default function Manage({ flags: initialFlags }: ManageProps) {
- + - + - + - + diff --git a/modules/FileStorage/src/SimpleModule.FileStorage/Locales/en.json b/modules/FileStorage/src/SimpleModule.FileStorage/Locales/en.json new file mode 100644 index 00000000..9edb2f84 --- /dev/null +++ b/modules/FileStorage/src/SimpleModule.FileStorage/Locales/en.json @@ -0,0 +1,17 @@ +{ + "Browse.BreadcrumbRoot": "Files", + "Browse.UploadButton": "Upload File", + "Browse.EmptyTitle": "No files yet", + "Browse.EmptyDescription": "Upload a file to get started.", + "Browse.ColName": "Name", + "Browse.ColSize": "Size", + "Browse.ColType": "Type", + "Browse.ColDate": "Date", + "Browse.FolderType": "Folder", + "Browse.DownloadButton": "Download", + "Browse.DeleteButton": "Delete", + "Browse.DeleteDialog.Title": "Delete File", + "Browse.DeleteDialog.Description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "Browse.DeleteDialog.CancelButton": "Cancel", + "Browse.DeleteDialog.ConfirmButton": "Delete" +} diff --git a/modules/FileStorage/src/SimpleModule.FileStorage/Locales/keys.ts b/modules/FileStorage/src/SimpleModule.FileStorage/Locales/keys.ts new file mode 100644 index 00000000..bb2fdc2e --- /dev/null +++ b/modules/FileStorage/src/SimpleModule.FileStorage/Locales/keys.ts @@ -0,0 +1,21 @@ +export const FileStorageKeys = { + Browse: { + BreadcrumbRoot: 'Browse.BreadcrumbRoot', + ColDate: 'Browse.ColDate', + ColName: 'Browse.ColName', + ColSize: 'Browse.ColSize', + ColType: 'Browse.ColType', + DeleteButton: 'Browse.DeleteButton', + DeleteDialog: { + CancelButton: 'Browse.DeleteDialog.CancelButton', + ConfirmButton: 'Browse.DeleteDialog.ConfirmButton', + Description: 'Browse.DeleteDialog.Description', + Title: 'Browse.DeleteDialog.Title', + }, + DownloadButton: 'Browse.DownloadButton', + EmptyDescription: 'Browse.EmptyDescription', + EmptyTitle: 'Browse.EmptyTitle', + FolderType: 'Browse.FolderType', + UploadButton: 'Browse.UploadButton', + }, +} as const; diff --git a/modules/FileStorage/src/SimpleModule.FileStorage/SimpleModule.FileStorage.csproj b/modules/FileStorage/src/SimpleModule.FileStorage/SimpleModule.FileStorage.csproj index 5fc50944..e0355133 100644 --- a/modules/FileStorage/src/SimpleModule.FileStorage/SimpleModule.FileStorage.csproj +++ b/modules/FileStorage/src/SimpleModule.FileStorage/SimpleModule.FileStorage.csproj @@ -2,6 +2,9 @@ net10.0 + + + diff --git a/modules/FileStorage/src/SimpleModule.FileStorage/Views/Browse.tsx b/modules/FileStorage/src/SimpleModule.FileStorage/Views/Browse.tsx index 05944adc..b6020578 100644 --- a/modules/FileStorage/src/SimpleModule.FileStorage/Views/Browse.tsx +++ b/modules/FileStorage/src/SimpleModule.FileStorage/Views/Browse.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Button, DataGridPage, @@ -16,6 +17,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useRef, useState } from 'react'; +import { FileStorageKeys } from '../Locales/keys'; import type { StoredFile } from '../types'; interface Props { @@ -45,8 +47,11 @@ function folderName(path: string): string { return parts[parts.length - 1]; } -function breadcrumbs(folder: string | null): { label: string; path: string | null }[] { - const crumbs: { label: string; path: string | null }[] = [{ label: 'Files', path: null }]; +function breadcrumbs( + folder: string | null, + rootLabel: string, +): { label: string; path: string | null }[] { + const crumbs: { label: string; path: string | null }[] = [{ label: rootLabel, path: null }]; if (!folder) return crumbs; const parts = folder.split('/'); for (let i = 0; i < parts.length; i++) { @@ -59,6 +64,7 @@ function breadcrumbs(folder: string | null): { label: string; path: string | nul } export default function Browse({ files, folders, currentFolder, parentFolder }: Props) { + const { t } = useTranslation('FileStorage'); const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string } | null>(null); const fileInputRef = useRef(null); @@ -87,7 +93,7 @@ export default function Browse({ files, folders, currentFolder, parentFolder }: } } - const crumbs = breadcrumbs(currentFolder); + const crumbs = breadcrumbs(currentFolder, t(FileStorageKeys.Browse.BreadcrumbRoot)); const hasContent = folders.length > 0 || files.length > 0; return ( @@ -119,21 +125,23 @@ export default function Browse({ files, folders, currentFolder, parentFolder }: actions={ <> - + } data={hasContent ? files : []} - emptyTitle="No files yet" - emptyDescription="Upload a file to get started." + emptyTitle={t(FileStorageKeys.Browse.EmptyTitle)} + emptyDescription={t(FileStorageKeys.Browse.EmptyDescription)} > {() => (
- Name - Size - Type - Date + {t(FileStorageKeys.Browse.ColName)} + {t(FileStorageKeys.Browse.ColSize)} + {t(FileStorageKeys.Browse.ColType)} + {t(FileStorageKeys.Browse.ColDate)} @@ -161,7 +169,9 @@ export default function Browse({ files, folders, currentFolder, parentFolder }: {folderName(f)} - Folder + + {t(FileStorageKeys.Browse.FolderType)} + @@ -179,14 +189,14 @@ export default function Browse({ files, folders, currentFolder, parentFolder }: size="sm" onClick={() => window.open(`/api/files/${file.id}/download`, '_blank')} > - Download + {t(FileStorageKeys.Browse.DownloadButton)} @@ -200,18 +210,19 @@ export default function Browse({ files, folders, currentFolder, parentFolder }: !open && setDeleteTarget(null)}> - Delete File + {t(FileStorageKeys.Browse.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.name}”? This action cannot - be undone. + {t(FileStorageKeys.Browse.DeleteDialog.Description, { + name: deleteTarget?.name ?? '', + })} diff --git a/modules/Marketplace/src/SimpleModule.Marketplace/Locales/en.json b/modules/Marketplace/src/SimpleModule.Marketplace/Locales/en.json new file mode 100644 index 00000000..c8ff137d --- /dev/null +++ b/modules/Marketplace/src/SimpleModule.Marketplace/Locales/en.json @@ -0,0 +1,31 @@ +{ + "Browse.Title": "Module Marketplace", + "Browse.Description": "{totalHits} modules available", + "Browse.SearchPlaceholder": "Search modules...", + "Browse.SearchButton": "Search", + "Browse.SortRelevance": "Relevance", + "Browse.SortDownloads": "Most Downloads", + "Browse.SortAlphabetical": "A-Z", + "Browse.BadgeInstalled": "Installed", + "Browse.LoadMore": "Load more modules", + "Browse.EmptyTitle": "No modules found", + "Browse.EmptyBody": "Try adjusting your search or filters.", + "Browse.ClearFilters": "Clear filters", + "Detail.DownloadsLabel": "Downloads", + "Detail.DownloadsSuffix": "downloads", + "Detail.VersionLabel": "Version", + "Detail.ProjectLabel": "Project", + "Detail.ProjectView": "View", + "Detail.LicenseLabel": "License", + "Detail.LicenseView": "View", + "Detail.BadgeInstalled": "Installed", + "Detail.TabSmCli": "SM CLI", + "Detail.TabDotnetCli": ".NET CLI", + "Detail.PackageInfoTitle": "Package Info", + "Detail.RecentVersionsTitle": "Recent Versions", + "Detail.TagsTitle": "Tags", + "Detail.BackToMarketplace": "Back to Marketplace", + "Detail.BreadcrumbMarketplace": "Marketplace", + "Detail.CopyButton": "Copy", + "Detail.CopiedButton": "Copied!" +} diff --git a/modules/Marketplace/src/SimpleModule.Marketplace/Locales/keys.ts b/modules/Marketplace/src/SimpleModule.Marketplace/Locales/keys.ts new file mode 100644 index 00000000..932ef650 --- /dev/null +++ b/modules/Marketplace/src/SimpleModule.Marketplace/Locales/keys.ts @@ -0,0 +1,35 @@ +export const MarketplaceKeys = { + Browse: { + BadgeInstalled: 'Browse.BadgeInstalled', + ClearFilters: 'Browse.ClearFilters', + Description: 'Browse.Description', + EmptyBody: 'Browse.EmptyBody', + EmptyTitle: 'Browse.EmptyTitle', + LoadMore: 'Browse.LoadMore', + SearchButton: 'Browse.SearchButton', + SearchPlaceholder: 'Browse.SearchPlaceholder', + SortAlphabetical: 'Browse.SortAlphabetical', + SortDownloads: 'Browse.SortDownloads', + SortRelevance: 'Browse.SortRelevance', + Title: 'Browse.Title', + }, + Detail: { + BackToMarketplace: 'Detail.BackToMarketplace', + BadgeInstalled: 'Detail.BadgeInstalled', + BreadcrumbMarketplace: 'Detail.BreadcrumbMarketplace', + CopiedButton: 'Detail.CopiedButton', + CopyButton: 'Detail.CopyButton', + DownloadsLabel: 'Detail.DownloadsLabel', + DownloadsSuffix: 'Detail.DownloadsSuffix', + LicenseLabel: 'Detail.LicenseLabel', + LicenseView: 'Detail.LicenseView', + PackageInfoTitle: 'Detail.PackageInfoTitle', + ProjectLabel: 'Detail.ProjectLabel', + ProjectView: 'Detail.ProjectView', + RecentVersionsTitle: 'Detail.RecentVersionsTitle', + TabDotnetCli: 'Detail.TabDotnetCli', + TabSmCli: 'Detail.TabSmCli', + TagsTitle: 'Detail.TagsTitle', + VersionLabel: 'Detail.VersionLabel', + }, +} as const; diff --git a/modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj b/modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj index a0aaba25..aa5996ad 100644 --- a/modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj +++ b/modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj @@ -12,4 +12,7 @@ %(Filename)Endpoint.cs + + + diff --git a/modules/Marketplace/src/SimpleModule.Marketplace/Views/Browse.tsx b/modules/Marketplace/src/SimpleModule.Marketplace/Views/Browse.tsx index dc29087c..d62ffecc 100644 --- a/modules/Marketplace/src/SimpleModule.Marketplace/Views/Browse.tsx +++ b/modules/Marketplace/src/SimpleModule.Marketplace/Views/Browse.tsx @@ -1,17 +1,13 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, Card, CardContent, CardFooter, Input, PageShell } from '@simplemodule/ui'; import { useState } from 'react'; +import { MarketplaceKeys } from '../Locales/keys'; import type { MarketplacePackage } from '../types'; import { categoryLabel, categoryNames, formatDownloads } from './utils'; const PAGE_SIZE = 24; -const sortOptions = [ - { value: 'Relevance', label: 'Relevance' }, - { value: 'Downloads', label: 'Most Downloads' }, - { value: 'Alphabetical', label: 'A-Z' }, -]; - interface Props { packages: MarketplacePackage[]; totalHits: number; @@ -31,8 +27,15 @@ export default function Browse({ skip, hasMore, }: Props) { + const { t } = useTranslation('Marketplace'); const [search, setSearch] = useState(query); + const sortOptions = [ + { value: 'Relevance', label: t(MarketplaceKeys.Browse.SortRelevance) }, + { value: 'Downloads', label: t(MarketplaceKeys.Browse.SortDownloads) }, + { value: 'Alphabetical', label: t(MarketplaceKeys.Browse.SortAlphabetical) }, + ]; + function buildParams(overrides: Record = {}) { const params = new URLSearchParams(); const merged = { @@ -65,16 +68,19 @@ export default function Browse({ } return ( - +
setSearch(e.target.value)} className="flex-1" /> - +
@@ -139,7 +145,9 @@ export default function Browse({ {categoryLabel(pkg.category)} - {pkg.isInstalled && Installed} + {pkg.isInstalled && ( + {t(MarketplaceKeys.Browse.BadgeInstalled)} + )}
)} @@ -177,15 +185,15 @@ export default function Browse({ > -

No modules found

-

Try adjusting your search or filters.

+

{t(MarketplaceKeys.Browse.EmptyTitle)}

+

{t(MarketplaceKeys.Browse.EmptyBody)}

{(query || selectedCategory !== 'All') && ( )}
diff --git a/modules/Marketplace/src/SimpleModule.Marketplace/Views/Detail.tsx b/modules/Marketplace/src/SimpleModule.Marketplace/Views/Detail.tsx index 89455b90..80073d0a 100644 --- a/modules/Marketplace/src/SimpleModule.Marketplace/Views/Detail.tsx +++ b/modules/Marketplace/src/SimpleModule.Marketplace/Views/Detail.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -15,6 +16,7 @@ import { } from '@simplemodule/ui'; import { useState } from 'react'; import Markdown from 'react-markdown'; +import { MarketplaceKeys } from '../Locales/keys'; import type { MarketplacePackageDetail } from '../types'; import { categoryLabel, formatDownloads } from './utils'; @@ -23,6 +25,7 @@ interface Props { } function CopyButton({ text }: { text: string }) { + const { t } = useTranslation('Marketplace'); const [copied, setCopied] = useState(false); function handleCopy() { @@ -33,7 +36,7 @@ function CopyButton({ text }: { text: string }) { return ( ); } @@ -82,25 +85,26 @@ const icons = { }; export default function Detail({ package: pkg }: Props) { + const { t } = useTranslation('Marketplace'); const smCommand = `sm install ${pkg.id}`; const dotnetCommand = `dotnet add package ${pkg.id}`; const infoItems = [ { icon: icons.download, - label: 'Downloads', + label: t(MarketplaceKeys.Detail.DownloadsLabel), value: {formatDownloads(pkg.totalDownloads)}, }, { icon: icons.tag, - label: 'Version', + label: t(MarketplaceKeys.Detail.VersionLabel), value: {pkg.latestVersion}, }, ...(pkg.projectLink ? [ { icon: icons.externalLink, - label: 'Project', + label: t(MarketplaceKeys.Detail.ProjectLabel), value: ( - View + {t(MarketplaceKeys.Detail.ProjectView)} ), }, @@ -118,7 +122,7 @@ export default function Detail({ package: pkg }: Props) { ? [ { icon: icons.document, - label: 'License', + label: t(MarketplaceKeys.Detail.LicenseLabel), value: ( - View + {t(MarketplaceKeys.Detail.LicenseView)} ), }, @@ -138,7 +142,10 @@ export default function Detail({ package: pkg }: Props) {
@@ -175,14 +182,17 @@ export default function Detail({ package: pkg }: Props) { > - {formatDownloads(pkg.totalDownloads)} downloads + {formatDownloads(pkg.totalDownloads)}{' '} + {t(MarketplaceKeys.Detail.DownloadsSuffix)} v{pkg.latestVersion}
{categoryLabel(pkg.category)} - {pkg.isInstalled && Installed} + {pkg.isInstalled && ( + {t(MarketplaceKeys.Detail.BadgeInstalled)} + )}
@@ -191,8 +201,10 @@ export default function Detail({ package: pkg }: Props) {
- SM CLI - .NET CLI + {t(MarketplaceKeys.Detail.TabSmCli)} + + {t(MarketplaceKeys.Detail.TabDotnetCli)} + @@ -219,7 +231,7 @@ export default function Detail({ package: pkg }: Props) {
- Package Info + {t(MarketplaceKeys.Detail.PackageInfoTitle)} {infoItems.map((item, i) => ( @@ -236,7 +248,7 @@ export default function Detail({ package: pkg }: Props) { {pkg.versions.length > 0 && ( - Recent Versions + {t(MarketplaceKeys.Detail.RecentVersionsTitle)} {pkg.versions @@ -246,7 +258,7 @@ export default function Detail({ package: pkg }: Props) {
{v.version} - {formatDownloads(v.downloads)} downloads + {formatDownloads(v.downloads)} {t(MarketplaceKeys.Detail.DownloadsSuffix)}
))} @@ -257,7 +269,7 @@ export default function Detail({ package: pkg }: Props) { {pkg.tags.length > 0 && ( - Tags + {t(MarketplaceKeys.Detail.TagsTitle)}
@@ -276,7 +288,7 @@ export default function Detail({ package: pkg }: Props) { className="w-full" onClick={() => router.get('/marketplace/browse')} > - Back to Marketplace + {t(MarketplaceKeys.Detail.BackToMarketplace)}
diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/en.json b/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/en.json new file mode 100644 index 00000000..c2a7fd6e --- /dev/null +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/en.json @@ -0,0 +1,70 @@ +{ + "Clients.Title": "Clients", + "Clients.Description.Singular": "{count} registered client", + "Clients.Description.Plural": "{count} registered clients", + "Clients.CreateButton": "Create Client", + "Clients.EmptyTitle": "No clients yet", + "Clients.EmptyDescription": "Get started by registering your first OpenID Connect client.", + "Clients.ColClientId": "Client ID", + "Clients.ColDisplayName": "Display Name", + "Clients.ColType": "Type", + "Clients.EditButton": "Edit", + "Clients.DeleteButton": "Delete", + "Clients.DeleteDialog.Title": "Delete Client", + "Clients.DeleteDialog.Description": "Are you sure you want to delete \"{clientId}\"? This OAuth client will be permanently removed.", + "Clients.DeleteDialog.CancelButton": "Cancel", + "Clients.DeleteDialog.DeleteButton": "Delete", + "ClientsCreate.Breadcrumb": "Clients", + "ClientsCreate.BreadcrumbPage": "Create Client", + "ClientsCreate.Title": "Create Client", + "ClientsCreate.ClientIdLabel": "Client ID", + "ClientsCreate.ClientIdPlaceholder": "my-app-client", + "ClientsCreate.DisplayNameLabel": "Display Name", + "ClientsCreate.DisplayNamePlaceholder": "My Application", + "ClientsCreate.ClientTypeLabel": "Client Type", + "ClientsCreate.ClientTypePublic": "Public", + "ClientsCreate.ClientTypeConfidential": "Confidential", + "ClientsCreate.ClientSecretLabel": "Client Secret", + "ClientsCreate.ClientSecretPlaceholder": "Enter a strong secret", + "ClientsCreate.ClientSecretDescription": "Required for confidential clients. Store this securely.", + "ClientsCreate.SubmitButton": "Create Client", + "ClientsEdit.Breadcrumb": "Clients", + "ClientsEdit.BreadcrumbPage": "Edit Client", + "ClientsEdit.Title": "Edit Client", + "ClientsEdit.TabDetails": "Details", + "ClientsEdit.TabUris": "URIs", + "ClientsEdit.TabPermissions": "Permissions", + "ClientsEdit.DisplayNameLabel": "Display Name", + "ClientsEdit.ClientTypeLabel": "Client Type", + "ClientsEdit.ClientTypePublic": "Public", + "ClientsEdit.ClientTypeConfidential": "Confidential", + "ClientsEdit.SaveChangesButton": "Save Changes", + "ClientsEdit.RedirectUrisTitle": "Redirect URIs", + "ClientsEdit.RedirectUrisLabel": "Redirect URIs", + "ClientsEdit.PostLogoutUrisLabel": "Post-Logout Redirect URIs", + "ClientsEdit.SaveUrisButton": "Save URIs", + "ClientsEdit.UriPlaceholder": "https://...", + "ClientsEdit.UriRemoveButton": "Remove", + "ClientsEdit.UriAddButton": "+ Add URI", + "ClientsEdit.PermissionsTitle": "Permissions", + "ClientsEdit.SavePermissionsButton": "Save Permissions", + "ClientsEdit.PermGroup.Endpoints": "Endpoints", + "ClientsEdit.PermGroup.GrantTypes": "Grant Types", + "ClientsEdit.PermGroup.ResponseTypes": "Response Types", + "ClientsEdit.PermGroup.Scopes": "Scopes", + "ClientsEdit.Perm.Authorization": "Authorization", + "ClientsEdit.Perm.Token": "Token", + "ClientsEdit.Perm.EndSession": "End Session", + "ClientsEdit.Perm.Revocation": "Revocation", + "ClientsEdit.Perm.Introspection": "Introspection", + "ClientsEdit.Perm.AuthorizationCode": "Authorization Code", + "ClientsEdit.Perm.RefreshToken": "Refresh Token", + "ClientsEdit.Perm.ClientCredentials": "Client Credentials", + "ClientsEdit.Perm.Implicit": "Implicit", + "ClientsEdit.Perm.Code": "Code", + "ClientsEdit.Perm.TokenResponse": "Token", + "ClientsEdit.Perm.OpenID": "OpenID", + "ClientsEdit.Perm.Profile": "Profile", + "ClientsEdit.Perm.Email": "Email", + "ClientsEdit.Perm.Roles": "Roles" +} diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/keys.ts b/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/keys.ts new file mode 100644 index 00000000..96282ea7 --- /dev/null +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/Locales/keys.ts @@ -0,0 +1,84 @@ +export const OpenIddictKeys = { + Clients: { + ColClientId: 'Clients.ColClientId', + ColDisplayName: 'Clients.ColDisplayName', + ColType: 'Clients.ColType', + CreateButton: 'Clients.CreateButton', + DeleteButton: 'Clients.DeleteButton', + DeleteDialog: { + CancelButton: 'Clients.DeleteDialog.CancelButton', + DeleteButton: 'Clients.DeleteDialog.DeleteButton', + Description: 'Clients.DeleteDialog.Description', + Title: 'Clients.DeleteDialog.Title', + }, + Description: { + Plural: 'Clients.Description.Plural', + Singular: 'Clients.Description.Singular', + }, + EditButton: 'Clients.EditButton', + EmptyDescription: 'Clients.EmptyDescription', + EmptyTitle: 'Clients.EmptyTitle', + Title: 'Clients.Title', + }, + ClientsCreate: { + Breadcrumb: 'ClientsCreate.Breadcrumb', + BreadcrumbPage: 'ClientsCreate.BreadcrumbPage', + ClientIdLabel: 'ClientsCreate.ClientIdLabel', + ClientIdPlaceholder: 'ClientsCreate.ClientIdPlaceholder', + ClientSecretDescription: 'ClientsCreate.ClientSecretDescription', + ClientSecretLabel: 'ClientsCreate.ClientSecretLabel', + ClientSecretPlaceholder: 'ClientsCreate.ClientSecretPlaceholder', + ClientTypeConfidential: 'ClientsCreate.ClientTypeConfidential', + ClientTypeLabel: 'ClientsCreate.ClientTypeLabel', + ClientTypePublic: 'ClientsCreate.ClientTypePublic', + DisplayNameLabel: 'ClientsCreate.DisplayNameLabel', + DisplayNamePlaceholder: 'ClientsCreate.DisplayNamePlaceholder', + SubmitButton: 'ClientsCreate.SubmitButton', + Title: 'ClientsCreate.Title', + }, + ClientsEdit: { + Breadcrumb: 'ClientsEdit.Breadcrumb', + BreadcrumbPage: 'ClientsEdit.BreadcrumbPage', + ClientTypeConfidential: 'ClientsEdit.ClientTypeConfidential', + ClientTypeLabel: 'ClientsEdit.ClientTypeLabel', + ClientTypePublic: 'ClientsEdit.ClientTypePublic', + DisplayNameLabel: 'ClientsEdit.DisplayNameLabel', + Perm: { + Authorization: 'ClientsEdit.Perm.Authorization', + AuthorizationCode: 'ClientsEdit.Perm.AuthorizationCode', + ClientCredentials: 'ClientsEdit.Perm.ClientCredentials', + Code: 'ClientsEdit.Perm.Code', + Email: 'ClientsEdit.Perm.Email', + EndSession: 'ClientsEdit.Perm.EndSession', + Implicit: 'ClientsEdit.Perm.Implicit', + Introspection: 'ClientsEdit.Perm.Introspection', + OpenID: 'ClientsEdit.Perm.OpenID', + Profile: 'ClientsEdit.Perm.Profile', + RefreshToken: 'ClientsEdit.Perm.RefreshToken', + Revocation: 'ClientsEdit.Perm.Revocation', + Roles: 'ClientsEdit.Perm.Roles', + Token: 'ClientsEdit.Perm.Token', + TokenResponse: 'ClientsEdit.Perm.TokenResponse', + }, + PermGroup: { + Endpoints: 'ClientsEdit.PermGroup.Endpoints', + GrantTypes: 'ClientsEdit.PermGroup.GrantTypes', + ResponseTypes: 'ClientsEdit.PermGroup.ResponseTypes', + Scopes: 'ClientsEdit.PermGroup.Scopes', + }, + PermissionsTitle: 'ClientsEdit.PermissionsTitle', + PostLogoutUrisLabel: 'ClientsEdit.PostLogoutUrisLabel', + RedirectUrisLabel: 'ClientsEdit.RedirectUrisLabel', + RedirectUrisTitle: 'ClientsEdit.RedirectUrisTitle', + SaveChangesButton: 'ClientsEdit.SaveChangesButton', + SavePermissionsButton: 'ClientsEdit.SavePermissionsButton', + SaveUrisButton: 'ClientsEdit.SaveUrisButton', + TabDetails: 'ClientsEdit.TabDetails', + TabPermissions: 'ClientsEdit.TabPermissions', + TabUris: 'ClientsEdit.TabUris', + Title: 'ClientsEdit.Title', + UriAddButton: 'ClientsEdit.UriAddButton', + UriPlaceholder: 'ClientsEdit.UriPlaceholder', + UriRemoveButton: 'ClientsEdit.UriRemoveButton', + }, +} as const; diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/Clients.tsx b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/Clients.tsx index bf45f16e..089dea43 100644 --- a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/Clients.tsx +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/Clients.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -17,6 +18,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OpenIddictKeys } from '../../Locales/keys'; interface Client { id: string; @@ -30,6 +32,7 @@ interface Props { } export default function Clients({ clients }: Props) { + const { t } = useTranslation('OpenIddict'); const [deleteTarget, setDeleteTarget] = useState<{ id: string; clientId: string; @@ -44,22 +47,28 @@ export default function Clients({ clients }: Props) { return ( <> router.get('/openiddict/clients/create')}>Create Client + } data={clients} - emptyTitle="No clients yet" - emptyDescription="Get started by registering your first OpenID Connect client." + emptyTitle={t(OpenIddictKeys.Clients.EmptyTitle)} + emptyDescription={t(OpenIddictKeys.Clients.EmptyDescription)} > {(pageData) => (
- Client ID - Display Name - Type + {t(OpenIddictKeys.Clients.ColClientId)} + {t(OpenIddictKeys.Clients.ColDisplayName)} + {t(OpenIddictKeys.Clients.ColType)} @@ -80,7 +89,7 @@ export default function Clients({ clients }: Props) { size="sm" onClick={() => router.get(`/openiddict/clients/${client.id}/edit`)} > - Edit + {t(OpenIddictKeys.Clients.EditButton)} @@ -103,18 +112,19 @@ export default function Clients({ clients }: Props) { !open && setDeleteTarget(null)}> - Delete Client + {t(OpenIddictKeys.Clients.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.clientId}”? This OAuth - client will be permanently removed. + {t(OpenIddictKeys.Clients.DeleteDialog.Description, { + clientId: deleteTarget?.clientId ?? '', + })} diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsCreate.tsx b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsCreate.tsx index c3a27a07..eb86dbd3 100644 --- a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsCreate.tsx +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsCreate.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -22,8 +23,10 @@ import { SelectValue, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OpenIddictKeys } from '../../Locales/keys'; export default function ClientsCreate() { + const { t } = useTranslation('OpenIddict'); const [clientType, setClientType] = useState('public'); function handleSubmit(e: React.FormEvent) { @@ -36,55 +39,76 @@ export default function ClientsCreate() { - Clients + + {t(OpenIddictKeys.ClientsCreate.Breadcrumb)} + - Create Client + {t(OpenIddictKeys.ClientsCreate.BreadcrumbPage)} -

Create Client

+

{t(OpenIddictKeys.ClientsCreate.Title)}

- - + + - - + + - + {clientType === 'confidential' && ( - + - Required for confidential clients. Store this securely. + {t(OpenIddictKeys.ClientsCreate.ClientSecretDescription)} )} - +
diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsEdit.tsx b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsEdit.tsx index 144c7f1d..257647e4 100644 --- a/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsEdit.tsx +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/Pages/OpenIddict/ClientsEdit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -27,6 +28,7 @@ import { TabsTrigger, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OpenIddictKeys } from '../../Locales/keys'; interface ClientDetail { id: string; @@ -43,51 +45,52 @@ interface Props { tab: string; } -const tabs = [ - { id: 'details', label: 'Details' }, - { id: 'uris', label: 'URIs' }, - { id: 'permissions', label: 'Permissions' }, +const tabIds = [ + { id: 'details', key: 'TabDetails' as const }, + { id: 'uris', key: 'TabUris' as const }, + { id: 'permissions', key: 'TabPermissions' as const }, ]; -const permissionGroups = [ +const permissionGroupDefs = [ { - label: 'Endpoints', + key: 'Endpoints' as const, permissions: [ - { value: 'ept:authorization', label: 'Authorization' }, - { value: 'ept:token', label: 'Token' }, - { value: 'ept:end_session', label: 'End Session' }, - { value: 'ept:revocation', label: 'Revocation' }, - { value: 'ept:introspection', label: 'Introspection' }, + { value: 'ept:authorization', key: 'Authorization' as const }, + { value: 'ept:token', key: 'Token' as const }, + { value: 'ept:end_session', key: 'EndSession' as const }, + { value: 'ept:revocation', key: 'Revocation' as const }, + { value: 'ept:introspection', key: 'Introspection' as const }, ], }, { - label: 'Grant Types', + key: 'GrantTypes' as const, permissions: [ - { value: 'gt:authorization_code', label: 'Authorization Code' }, - { value: 'gt:refresh_token', label: 'Refresh Token' }, - { value: 'gt:client_credentials', label: 'Client Credentials' }, - { value: 'gt:implicit', label: 'Implicit' }, + { value: 'gt:authorization_code', key: 'AuthorizationCode' as const }, + { value: 'gt:refresh_token', key: 'RefreshToken' as const }, + { value: 'gt:client_credentials', key: 'ClientCredentials' as const }, + { value: 'gt:implicit', key: 'Implicit' as const }, ], }, { - label: 'Response Types', + key: 'ResponseTypes' as const, permissions: [ - { value: 'rst:code', label: 'Code' }, - { value: 'rst:token', label: 'Token' }, + { value: 'rst:code', key: 'Code' as const }, + { value: 'rst:token', key: 'TokenResponse' as const }, ], }, { - label: 'Scopes', + key: 'Scopes' as const, permissions: [ - { value: 'scp:openid', label: 'OpenID' }, - { value: 'scp:profile', label: 'Profile' }, - { value: 'scp:email', label: 'Email' }, - { value: 'scp:roles', label: 'Roles' }, + { value: 'scp:openid', key: 'OpenID' as const }, + { value: 'scp:profile', key: 'Profile' as const }, + { value: 'scp:email', key: 'Email' as const }, + { value: 'scp:roles', key: 'Roles' as const }, ], }, ]; function UriList({ label, name, values }: { label: string; name: string; values: string[] }) { + const { t } = useTranslation('OpenIddict'); const [uris, setUris] = useState(values.length > 0 ? values : ['']); function addUri() { @@ -115,16 +118,16 @@ function UriList({ label, name, values }: { label: string; name: string; values: name={name} value={uri} onChange={(e) => updateUri(index, e.target.value)} - placeholder="https://..." + placeholder={t(OpenIddictKeys.ClientsEdit.UriPlaceholder)} /> ))} ); @@ -137,6 +140,7 @@ export default function ClientsEdit({ permissions, tab, }: Props) { + const { t } = useTranslation('OpenIddict'); const [selectedPermissions, setSelectedPermissions] = useState>(new Set(permissions)); function togglePermission(perm: string) { @@ -156,15 +160,17 @@ export default function ClientsEdit({ - Clients + + {t(OpenIddictKeys.ClientsEdit.Breadcrumb)} + - Edit Client + {t(OpenIddictKeys.ClientsEdit.BreadcrumbPage)} -

Edit Client

+

{t(OpenIddictKeys.ClientsEdit.Title)}

- {tabs.map((t) => ( - - {t.label} + {tabIds.map((tabDef) => ( + + {t(OpenIddictKeys.ClientsEdit[tabDef.key])} ))} @@ -197,7 +203,9 @@ export default function ClientsEdit({ > - + - + - + @@ -226,7 +240,7 @@ export default function ClientsEdit({ {tab === 'uris' && ( - Redirect URIs + {t(OpenIddictKeys.ClientsEdit.RedirectUrisTitle)}
- + - +
@@ -252,7 +270,7 @@ export default function ClientsEdit({ {tab === 'permissions' && ( - Permissions + {t(OpenIddictKeys.ClientsEdit.PermissionsTitle)}
- {permissionGroups.map((group) => ( -
-

{group.label}

+ {permissionGroupDefs.map((group) => ( +
+

+ {t(OpenIddictKeys.ClientsEdit.PermGroup[group.key])} +

{group.permissions.map((perm) => (
@@ -279,7 +299,7 @@ export default function ClientsEdit({ onCheckedChange={() => togglePermission(perm.value)} />
diff --git a/modules/OpenIddict/src/SimpleModule.OpenIddict/SimpleModule.OpenIddict.csproj b/modules/OpenIddict/src/SimpleModule.OpenIddict/SimpleModule.OpenIddict.csproj index 906f279d..4c802031 100644 --- a/modules/OpenIddict/src/SimpleModule.OpenIddict/SimpleModule.OpenIddict.csproj +++ b/modules/OpenIddict/src/SimpleModule.OpenIddict/SimpleModule.OpenIddict.csproj @@ -15,4 +15,7 @@ + + + diff --git a/modules/Orders/src/SimpleModule.Orders/Locales/en.json b/modules/Orders/src/SimpleModule.Orders/Locales/en.json new file mode 100644 index 00000000..a00c271d --- /dev/null +++ b/modules/Orders/src/SimpleModule.Orders/Locales/en.json @@ -0,0 +1,43 @@ +{ + "List.Title": "Orders", + "List.Description": "{count} total orders", + "List.CreateButton": "Create Order", + "List.EmptyTitle": "No orders yet", + "List.EmptyDescription": "Get started by creating your first order.", + "List.ColId": "ID", + "List.ColUser": "User", + "List.ColItems": "Items", + "List.ColTotal": "Total", + "List.ColCreated": "Created", + "List.EditButton": "Edit", + "List.DeleteButton": "Delete", + "List.DeleteDialog.Title": "Delete Order", + "List.DeleteDialog.Confirm": "Are you sure you want to delete order #{id}? This action cannot be undone.", + "List.CancelButton": "Cancel", + "Create.Title": "Create Order", + "Create.Breadcrumb": "Create Order", + "Create.UserIdLabel": "User ID", + "Create.UserIdPlaceholder": "Enter user ID", + "Create.ItemsLabel": "Items", + "Create.AddItemButton": "+ Add Item", + "Create.ProductLabel": "Product", + "Create.QuantityLabel": "Quantity for item {index}", + "Create.RemoveButton": "Remove item {index}", + "Create.TotalLabel": "Estimated Total", + "Create.SubmitButton": "Create Order", + "Edit.Title": "Edit Order #{id}", + "Edit.Breadcrumb": "Edit Order", + "Edit.UserIdLabel": "User ID", + "Edit.ItemsLabel": "Items", + "Edit.AddItemButton": "+ Add Item", + "Edit.QuantityLabel": "Quantity for item {index}", + "Edit.RemoveButton": "Remove item {index}", + "Edit.TotalLabel": "Estimated Total", + "Edit.SaveButton": "Save Changes", + "Edit.DangerZone": "Danger Zone", + "Edit.DangerZoneDescription": "Permanently delete this order. This action cannot be undone.", + "Edit.DeleteButton": "Delete Order", + "Edit.DeleteDialog.Title": "Delete Order", + "Edit.DeleteDialog.Confirm": "Are you sure you want to delete order #{id}? This action cannot be undone.", + "Edit.CancelButton": "Cancel" +} diff --git a/modules/Orders/src/SimpleModule.Orders/Locales/keys.ts b/modules/Orders/src/SimpleModule.Orders/Locales/keys.ts new file mode 100644 index 00000000..a45e77b8 --- /dev/null +++ b/modules/Orders/src/SimpleModule.Orders/Locales/keys.ts @@ -0,0 +1,53 @@ +export const OrdersKeys = { + Create: { + AddItemButton: 'Create.AddItemButton', + Breadcrumb: 'Create.Breadcrumb', + ItemsLabel: 'Create.ItemsLabel', + ProductLabel: 'Create.ProductLabel', + QuantityLabel: 'Create.QuantityLabel', + RemoveButton: 'Create.RemoveButton', + SubmitButton: 'Create.SubmitButton', + Title: 'Create.Title', + TotalLabel: 'Create.TotalLabel', + UserIdLabel: 'Create.UserIdLabel', + UserIdPlaceholder: 'Create.UserIdPlaceholder', + }, + Edit: { + AddItemButton: 'Edit.AddItemButton', + Breadcrumb: 'Edit.Breadcrumb', + CancelButton: 'Edit.CancelButton', + DangerZone: 'Edit.DangerZone', + DangerZoneDescription: 'Edit.DangerZoneDescription', + DeleteButton: 'Edit.DeleteButton', + DeleteDialog: { + Confirm: 'Edit.DeleteDialog.Confirm', + Title: 'Edit.DeleteDialog.Title', + }, + ItemsLabel: 'Edit.ItemsLabel', + QuantityLabel: 'Edit.QuantityLabel', + RemoveButton: 'Edit.RemoveButton', + SaveButton: 'Edit.SaveButton', + Title: 'Edit.Title', + TotalLabel: 'Edit.TotalLabel', + UserIdLabel: 'Edit.UserIdLabel', + }, + List: { + CancelButton: 'List.CancelButton', + ColCreated: 'List.ColCreated', + ColId: 'List.ColId', + ColItems: 'List.ColItems', + ColTotal: 'List.ColTotal', + ColUser: 'List.ColUser', + CreateButton: 'List.CreateButton', + DeleteButton: 'List.DeleteButton', + DeleteDialog: { + Confirm: 'List.DeleteDialog.Confirm', + Title: 'List.DeleteDialog.Title', + }, + Description: 'List.Description', + EditButton: 'List.EditButton', + EmptyDescription: 'List.EmptyDescription', + EmptyTitle: 'List.EmptyTitle', + Title: 'List.Title', + }, +} as const; diff --git a/modules/Orders/src/SimpleModule.Orders/Pages/Create.tsx b/modules/Orders/src/SimpleModule.Orders/Pages/Create.tsx index 918c93c3..abe96899 100644 --- a/modules/Orders/src/SimpleModule.Orders/Pages/Create.tsx +++ b/modules/Orders/src/SimpleModule.Orders/Pages/Create.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -21,6 +22,7 @@ import { SelectValue, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OrdersKeys } from '../Locales/keys'; import type { OrderItem } from '../types'; interface Product { @@ -34,6 +36,7 @@ interface Props { } export default function Create({ products }: Props) { + const { t } = useTranslation('Orders'); const [userId, setUserId] = useState(''); const [items, setItems] = useState([ { productId: products[0]?.id ?? 0, quantity: 1 }, @@ -70,36 +73,36 @@ export default function Create({ products }: Props) { - Orders + {t(OrdersKeys.List.Title)} - Create Order + {t(OrdersKeys.Create.Breadcrumb)} -

Create Order

+

{t(OrdersKeys.Create.Title)}

- + setUserId(e.target.value)} required - placeholder="Enter user ID" + placeholder={t(OrdersKeys.Create.UserIdPlaceholder)} />
- +
@@ -129,7 +132,10 @@ export default function Create({ products }: Props) { } min="1" className="w-20" - aria-label={`Quantity for item ${index + 1}`} + aria-label={t(OrdersKeys.Create.QuantityLabel).replace( + '{index}', + String(index + 1), + )} /> {items.length > 1 && ( @@ -149,12 +158,12 @@ export default function Create({ products }: Props) {
- Estimated Total + {t(OrdersKeys.Create.TotalLabel)} ${getTotal().toFixed(2)}
- + diff --git a/modules/Orders/src/SimpleModule.Orders/Pages/Edit.tsx b/modules/Orders/src/SimpleModule.Orders/Pages/Edit.tsx index d08e4a93..8e397cb1 100644 --- a/modules/Orders/src/SimpleModule.Orders/Pages/Edit.tsx +++ b/modules/Orders/src/SimpleModule.Orders/Pages/Edit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -29,6 +30,7 @@ import { SelectValue, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OrdersKeys } from '../Locales/keys'; import type { Order, OrderItem } from '../types'; interface Product { @@ -43,6 +45,7 @@ interface Props { } export default function Edit({ order, products }: Props) { + const { t } = useTranslation('Orders'); const [userId, setUserId] = useState(order.userId); const [items, setItems] = useState( order.items.map((i) => ({ productId: i.productId, quantity: i.quantity })), @@ -85,22 +88,24 @@ export default function Edit({ order, products }: Props) { - Orders + {t(OrdersKeys.List.Title)} - Edit Order + {t(OrdersKeys.Edit.Breadcrumb)} -

Edit Order #{order.id}

+

+ {t(OrdersKeys.Edit.Title).replace('{id}', String(order.id))} +

- +
- +
@@ -143,7 +148,10 @@ export default function Edit({ order, products }: Props) { } min="1" className="w-20" - aria-label={`Quantity for item ${index + 1}`} + aria-label={t(OrdersKeys.Edit.QuantityLabel).replace( + '{index}', + String(index + 1), + )} /> {items.length > 1 && ( @@ -163,12 +174,12 @@ export default function Edit({ order, products }: Props) {
- Estimated Total + {t(OrdersKeys.Edit.TotalLabel)} ${getTotal().toFixed(2)}
- + @@ -176,14 +187,12 @@ export default function Edit({ order, products }: Props) { - Danger Zone + {t(OrdersKeys.Edit.DangerZone)} -

- Permanently delete this order. This action cannot be undone. -

+

{t(OrdersKeys.Edit.DangerZoneDescription)}

@@ -191,17 +200,17 @@ export default function Edit({ order, products }: Props) { - Delete Order + {t(OrdersKeys.Edit.DeleteDialog.Title)} - Are you sure you want to delete order #{order.id}? This action cannot be undone. + {t(OrdersKeys.Edit.DeleteDialog.Confirm).replace('{id}', String(order.id))} diff --git a/modules/Orders/src/SimpleModule.Orders/Pages/List.tsx b/modules/Orders/src/SimpleModule.Orders/Pages/List.tsx index 0f522348..68411118 100644 --- a/modules/Orders/src/SimpleModule.Orders/Pages/List.tsx +++ b/modules/Orders/src/SimpleModule.Orders/Pages/List.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -17,6 +18,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { OrdersKeys } from '../Locales/keys'; import type { Order } from '../types'; interface Props { @@ -24,6 +26,7 @@ interface Props { } export default function List({ orders }: Props) { + const { t } = useTranslation('Orders'); const [deleteId, setDeleteId] = useState(null); function handleDelete() { @@ -35,22 +38,26 @@ export default function List({ orders }: Props) { return ( <> router.get('/orders/create')}>Create Order} + title={t(OrdersKeys.List.Title)} + description={t(OrdersKeys.List.Description).replace('{count}', String(orders.length))} + actions={ + + } data={orders} - emptyTitle="No orders yet" - emptyDescription="Get started by creating your first order." + emptyTitle={t(OrdersKeys.List.EmptyTitle)} + emptyDescription={t(OrdersKeys.List.EmptyDescription)} > {(pageData) => (
- ID - User - Items - Total - Created + {t(OrdersKeys.List.ColId)} + {t(OrdersKeys.List.ColUser)} + {t(OrdersKeys.List.ColItems)} + {t(OrdersKeys.List.ColTotal)} + {t(OrdersKeys.List.ColCreated)} @@ -75,10 +82,10 @@ export default function List({ orders }: Props) { size="sm" onClick={() => router.get(`/orders/${order.id}/edit`)} > - Edit + {t(OrdersKeys.List.EditButton)} @@ -92,17 +99,17 @@ export default function List({ orders }: Props) { !open && setDeleteId(null)}> - Delete Order + {t(OrdersKeys.List.DeleteDialog.Title)} - Are you sure you want to delete order #{deleteId}? This action cannot be undone. + {t(OrdersKeys.List.DeleteDialog.Confirm).replace('{id}', String(deleteId))} diff --git a/modules/Orders/src/SimpleModule.Orders/SimpleModule.Orders.csproj b/modules/Orders/src/SimpleModule.Orders/SimpleModule.Orders.csproj index 21f1f281..98802c88 100644 --- a/modules/Orders/src/SimpleModule.Orders/SimpleModule.Orders.csproj +++ b/modules/Orders/src/SimpleModule.Orders/SimpleModule.Orders.csproj @@ -2,6 +2,9 @@ net10.0 + + + diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/en.json b/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/en.json new file mode 100644 index 00000000..b6617627 --- /dev/null +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/en.json @@ -0,0 +1,47 @@ +{ + "Manage.Title": "Pages", + "Manage.Description": "Manage content pages", + "Manage.NewPage": "New Page", + "Manage.EmptyTitle": "No pages yet", + "Manage.EmptyDescription": "Get started by creating your first content page.", + "Manage.Table.Title": "Title", + "Manage.Table.Slug": "Slug", + "Manage.Table.Status": "Status", + "Manage.Table.Tags": "Tags", + "Manage.Table.Updated": "Updated", + "Manage.Status.Published": "Published", + "Manage.Status.Unpublished": "Unpublished", + "Manage.Status.Draft": "Draft", + "Manage.Tag.AddPlaceholder": "add tag", + "Manage.Tag.AddAriaLabel": "Add tag to {{title}}", + "Manage.Actions.AriaLabel": "Actions for {{title}}", + "Manage.Actions.SrOnly": "Actions", + "Manage.Actions.Edit": "Edit", + "Manage.Actions.ViewPage": "View Page", + "Manage.Actions.PreviewDraft": "Preview Draft", + "Manage.Actions.Publish": "Publish", + "Manage.Actions.Unpublish": "Unpublish", + "Manage.Actions.Delete": "Delete", + "Manage.DeleteDialog.Title": "Delete Page", + "Manage.DeleteDialog.Description": "Are you sure you want to delete \u201c{{title}}\u201d? This page will be permanently removed.", + "Manage.DeleteDialog.Cancel": "Cancel", + "Manage.DeleteDialog.Confirm": "Delete", + "Editor.SaveAsTemplate": "Save as Template", + "Editor.SaveDraft": "Save Draft", + "Editor.Saving": "Saving...", + "Editor.Saved": "Saved!", + "Editor.SaveTemplateDialog.Title": "Save as Template", + "Editor.SaveTemplateDialog.NameLabel": "Template name", + "Editor.SaveTemplateDialog.NamePlaceholder": "e.g. Landing Page", + "Editor.SaveTemplateDialog.Cancel": "Cancel", + "Editor.SaveTemplateDialog.Save": "Save", + "Editor.TemplatePicker.Title": "Create New Page", + "Editor.TemplatePicker.Subtitle": "Start from a template or create a blank page.", + "Editor.TemplatePicker.BlankPage": "Blank Page", + "Editor.TemplatePicker.Cancel": "Cancel", + "Editor.BackToPages": "Back to Pages", + "Viewer.DraftBanner": "Draft Preview \u2014 this version is not published", + "PagesList.Title": "Pages", + "PagesList.EmptyHeading": "No published pages yet", + "PagesList.EmptyDescription": "Published pages will appear here for visitors to read." +} diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/keys.ts b/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/keys.ts new file mode 100644 index 00000000..23a0a56d --- /dev/null +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Locales/keys.ts @@ -0,0 +1,69 @@ +export const PageBuilderKeys = { + Editor: { + BackToPages: 'Editor.BackToPages', + SaveAsTemplate: 'Editor.SaveAsTemplate', + SaveDraft: 'Editor.SaveDraft', + SaveTemplateDialog: { + Cancel: 'Editor.SaveTemplateDialog.Cancel', + NameLabel: 'Editor.SaveTemplateDialog.NameLabel', + NamePlaceholder: 'Editor.SaveTemplateDialog.NamePlaceholder', + Save: 'Editor.SaveTemplateDialog.Save', + Title: 'Editor.SaveTemplateDialog.Title', + }, + Saved: 'Editor.Saved', + Saving: 'Editor.Saving', + TemplatePicker: { + BlankPage: 'Editor.TemplatePicker.BlankPage', + Cancel: 'Editor.TemplatePicker.Cancel', + Subtitle: 'Editor.TemplatePicker.Subtitle', + Title: 'Editor.TemplatePicker.Title', + }, + }, + Manage: { + Actions: { + AriaLabel: 'Manage.Actions.AriaLabel', + Delete: 'Manage.Actions.Delete', + Edit: 'Manage.Actions.Edit', + PreviewDraft: 'Manage.Actions.PreviewDraft', + Publish: 'Manage.Actions.Publish', + SrOnly: 'Manage.Actions.SrOnly', + Unpublish: 'Manage.Actions.Unpublish', + ViewPage: 'Manage.Actions.ViewPage', + }, + DeleteDialog: { + Cancel: 'Manage.DeleteDialog.Cancel', + Confirm: 'Manage.DeleteDialog.Confirm', + Description: 'Manage.DeleteDialog.Description', + Title: 'Manage.DeleteDialog.Title', + }, + Description: 'Manage.Description', + EmptyDescription: 'Manage.EmptyDescription', + EmptyTitle: 'Manage.EmptyTitle', + NewPage: 'Manage.NewPage', + Status: { + Draft: 'Manage.Status.Draft', + Published: 'Manage.Status.Published', + Unpublished: 'Manage.Status.Unpublished', + }, + Table: { + Slug: 'Manage.Table.Slug', + Status: 'Manage.Table.Status', + Tags: 'Manage.Table.Tags', + Title: 'Manage.Table.Title', + Updated: 'Manage.Table.Updated', + }, + Tag: { + AddAriaLabel: 'Manage.Tag.AddAriaLabel', + AddPlaceholder: 'Manage.Tag.AddPlaceholder', + }, + Title: 'Manage.Title', + }, + PagesList: { + EmptyDescription: 'PagesList.EmptyDescription', + EmptyHeading: 'PagesList.EmptyHeading', + Title: 'PagesList.Title', + }, + Viewer: { + DraftBanner: 'Viewer.DraftBanner', + }, +} as const; diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/SimpleModule.PageBuilder.csproj b/modules/PageBuilder/src/SimpleModule.PageBuilder/SimpleModule.PageBuilder.csproj index 86f20d39..2b09f5c3 100644 --- a/modules/PageBuilder/src/SimpleModule.PageBuilder/SimpleModule.PageBuilder.csproj +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/SimpleModule.PageBuilder.csproj @@ -16,4 +16,7 @@ %(Filename)Endpoint.cs + + + diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Editor.tsx b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Editor.tsx index a58333fa..f5700d40 100644 --- a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Editor.tsx +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Editor.tsx @@ -1,5 +1,6 @@ import { router } from '@inertiajs/react'; import { Puck, usePuck } from '@puckeditor/core'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Button, Card, @@ -15,6 +16,7 @@ import { } from '@simplemodule/ui'; import { useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; +import { PageBuilderKeys } from '../Locales/keys'; import { puckConfig } from '../puck/config'; import type { Page, PageTemplate } from '../types'; @@ -24,6 +26,7 @@ interface Props { } function HeaderActions({ page }: { page: Page | null }) { + const { t } = useTranslation('PageBuilder'); const { appState } = usePuck(); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); @@ -80,22 +83,28 @@ function HeaderActions({ page }: { page: Page | null }) { return ( <> - Save as Template + {t(PageBuilderKeys.Editor.SaveTemplateDialog.Title)}
- + setTemplateName(e.target.value)} onKeyDown={(e) => { @@ -107,9 +116,11 @@ function HeaderActions({ page }: { page: Page | null }) {
+ -
@@ -118,6 +129,7 @@ function HeaderActions({ page }: { page: Page | null }) { } export default function Editor({ page, templates }: Props) { + const { t } = useTranslation('PageBuilder'); const [showTemplatePicker, setShowTemplatePicker] = useState(!page && !!templates?.length); // Hide the Blazor shell (sidebar + toggle) so the editor has full screen @@ -203,9 +215,11 @@ export default function Editor({ page, templates }: Props) { > - Create New Page + {t(PageBuilderKeys.Editor.TemplatePicker.Title)} -

Start from a template or create a blank page.

+

+ {t(PageBuilderKeys.Editor.TemplatePicker.Subtitle)} +

{templates?.map((t) => ( @@ -277,7 +291,7 @@ export default function Editor({ page, templates }: Props) { > - Back to Pages + {t(PageBuilderKeys.Editor.BackToPages)} {children} diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Manage.tsx b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Manage.tsx index d88313a3..a2d757ac 100644 --- a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Manage.tsx +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Manage.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -23,6 +24,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { PageBuilderKeys } from '../Locales/keys'; import type { PageSummary } from '../types'; interface Props { @@ -30,6 +32,7 @@ interface Props { } export default function Manage({ pages }: Props) { + const { t } = useTranslation('PageBuilder'); const [tagInputs, setTagInputs] = useState>({}); const [deleteTarget, setDeleteTarget] = useState<{ id: number; @@ -78,15 +81,19 @@ export default function Manage({ pages }: Props) { return ( <> router.get('/pages/new')}>New Page} + title={t(PageBuilderKeys.Manage.Title)} + description={t(PageBuilderKeys.Manage.Description)} + actions={ + + } data={pages} - emptyTitle="No pages yet" - emptyDescription="Get started by creating your first content page." + emptyTitle={t(PageBuilderKeys.Manage.EmptyTitle)} + emptyDescription={t(PageBuilderKeys.Manage.EmptyDescription)} emptyAction={ } > @@ -94,11 +101,11 @@ export default function Manage({ pages }: Props) {
- Title - Slug - Status - Tags - Updated + {t(PageBuilderKeys.Manage.Table.Title)} + {t(PageBuilderKeys.Manage.Table.Slug)} + {t(PageBuilderKeys.Manage.Table.Status)} + {t(PageBuilderKeys.Manage.Table.Tags)} + {t(PageBuilderKeys.Manage.Table.Updated)} @@ -113,9 +120,13 @@ export default function Manage({ pages }: Props) { variant={page.isPublished ? 'success' : 'default'} data-testid="status-badge" > - {page.isPublished ? 'Published' : 'Unpublished'} + {page.isPublished + ? t(PageBuilderKeys.Manage.Status.Published) + : t(PageBuilderKeys.Manage.Status.Unpublished)} - {page.hasDraft && Draft} + {page.hasDraft && ( + {t(PageBuilderKeys.Manage.Status.Draft)} + )} @@ -138,13 +149,15 @@ export default function Manage({ pages }: Props) { }} > setTagInputs((prev) => ({ ...prev, [page.id]: e.target.value })) } - aria-label={`Add tag to ${page.title}`} + aria-label={t(PageBuilderKeys.Manage.Tag.AddAriaLabel, { + title: page.title, + })} /> @@ -155,7 +168,13 @@ export default function Manage({ pages }: Props) { - router.get(`/pages/${page.id}/edit`)}> - Edit + {t(PageBuilderKeys.Manage.Actions.Edit)} {page.isPublished && ( window.open(`/pages/view/${page.slug}`, '_blank')} > - View Page + {t(PageBuilderKeys.Manage.Actions.ViewPage)} )} {page.hasDraft && ( window.open(`/pages/view/${page.slug}/draft`, '_blank')} > - Preview Draft + {t(PageBuilderKeys.Manage.Actions.PreviewDraft)} )} handleTogglePublish(page.id, page.isPublished)} > - {page.isPublished ? 'Unpublish' : 'Publish'} + {page.isPublished + ? t(PageBuilderKeys.Manage.Actions.Unpublish) + : t(PageBuilderKeys.Manage.Actions.Publish)} setDeleteTarget({ id: page.id, title: page.title })} > - Delete + {t(PageBuilderKeys.Manage.Actions.Delete)} @@ -216,18 +239,19 @@ export default function Manage({ pages }: Props) { !open && setDeleteTarget(null)}> - Delete Page + {t(PageBuilderKeys.Manage.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.title}”? This page will be - permanently removed. + {t(PageBuilderKeys.Manage.DeleteDialog.Description, { + title: deleteTarget?.title ?? '', + })} diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/PagesList.tsx b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/PagesList.tsx index 1e42e688..02caafbf 100644 --- a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/PagesList.tsx +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/PagesList.tsx @@ -1,5 +1,7 @@ import { Link } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Card, CardContent, PageShell } from '@simplemodule/ui'; +import { PageBuilderKeys } from '../Locales/keys'; import type { PageSummary } from '../types'; interface Props { @@ -7,8 +9,9 @@ interface Props { } export default function PagesList({ pages }: Props) { + const { t } = useTranslation('PageBuilder'); return ( - + {pages.length === 0 ? (
-

No published pages yet

+

+ {t(PageBuilderKeys.PagesList.EmptyHeading)} +

- Published pages will appear here for visitors to read. + {t(PageBuilderKeys.PagesList.EmptyDescription)}

) : ( diff --git a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Viewer.tsx b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Viewer.tsx index fcfdf65b..a852e5f8 100644 --- a/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Viewer.tsx +++ b/modules/PageBuilder/src/SimpleModule.PageBuilder/Views/Viewer.tsx @@ -1,6 +1,8 @@ import { Render } from '@puckeditor/core/rsc'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, Container } from '@simplemodule/ui'; import { useMemo } from 'react'; +import { PageBuilderKeys } from '../Locales/keys'; import { puckConfig } from '../puck/config'; import type { Page } from '../types'; @@ -10,6 +12,7 @@ interface Props { } export default function Viewer({ page, isDraft }: Props) { + const { t } = useTranslation('PageBuilder'); const data = useMemo(() => { try { const parsed = JSON.parse(page.content); @@ -26,7 +29,7 @@ export default function Viewer({ page, isDraft }: Props) { {isDraft && ( - Draft Preview — this version is not published + {t(PageBuilderKeys.Viewer.DraftBanner)} )} diff --git a/modules/Products/src/SimpleModule.Products/Locales/en.json b/modules/Products/src/SimpleModule.Products/Locales/en.json index e7fdd3e1..a73fc43b 100644 --- a/modules/Products/src/SimpleModule.Products/Locales/en.json +++ b/modules/Products/src/SimpleModule.Products/Locales/en.json @@ -9,7 +9,11 @@ "Manage.DeleteButton": "Delete", "Manage.DeleteDialog.Title": "Delete Product", "Manage.DeleteDialog.Confirm": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "Manage.ColId": "ID", + "Manage.ColName": "Name", + "Manage.ColPrice": "Price", "Manage.CancelButton": "Cancel", + "Manage.DeleteDialog.DeleteButton": "Delete", "Create.Title": "Create Product", "Create.Breadcrumb": "Create Product", "Create.SubmitButton": "Create", diff --git a/modules/Products/src/SimpleModule.Products/Locales/keys.ts b/modules/Products/src/SimpleModule.Products/Locales/keys.ts index 798b6572..d87ad1d9 100644 --- a/modules/Products/src/SimpleModule.Products/Locales/keys.ts +++ b/modules/Products/src/SimpleModule.Products/Locales/keys.ts @@ -28,10 +28,14 @@ export const ProductsKeys = { }, Manage: { CancelButton: 'Manage.CancelButton', + ColId: 'Manage.ColId', + ColName: 'Manage.ColName', + ColPrice: 'Manage.ColPrice', CreateButton: 'Manage.CreateButton', DeleteButton: 'Manage.DeleteButton', DeleteDialog: { Confirm: 'Manage.DeleteDialog.Confirm', + DeleteButton: 'Manage.DeleteDialog.DeleteButton', Title: 'Manage.DeleteDialog.Title', }, EditButton: 'Manage.EditButton', diff --git a/modules/Products/src/SimpleModule.Products/Views/Browse.tsx b/modules/Products/src/SimpleModule.Products/Views/Browse.tsx index 6e386eb3..57cc7efd 100644 --- a/modules/Products/src/SimpleModule.Products/Views/Browse.tsx +++ b/modules/Products/src/SimpleModule.Products/Views/Browse.tsx @@ -1,9 +1,15 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Card, CardContent, PageShell } from '@simplemodule/ui'; +import { ProductsKeys } from '../Locales/keys'; import type { Product } from '../types'; export default function Browse({ products }: { products: Product[] }) { + const { t } = useTranslation('Products'); return ( - +
{products.map((p) => ( diff --git a/modules/Products/src/SimpleModule.Products/Views/Create.tsx b/modules/Products/src/SimpleModule.Products/Views/Create.tsx index f5ad82a6..c7a113d5 100644 --- a/modules/Products/src/SimpleModule.Products/Views/Create.tsx +++ b/modules/Products/src/SimpleModule.Products/Views/Create.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -15,8 +16,10 @@ import { Input, Label, } from '@simplemodule/ui'; +import { ProductsKeys } from '../Locales/keys'; export default function Create() { + const { t } = useTranslation('Products'); function handleSubmit(e: React.FormEvent) { e.preventDefault(); const formData = new FormData(e.currentTarget); @@ -32,22 +35,27 @@ export default function Create() { - Create Product + {t(ProductsKeys.Create.Breadcrumb)} -

Create Product

+

{t(ProductsKeys.Create.Title)}

- - + + - + - +
diff --git a/modules/Products/src/SimpleModule.Products/Views/Edit.tsx b/modules/Products/src/SimpleModule.Products/Views/Edit.tsx index 9e6cb928..e827ba15 100644 --- a/modules/Products/src/SimpleModule.Products/Views/Edit.tsx +++ b/modules/Products/src/SimpleModule.Products/Views/Edit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -24,6 +25,7 @@ import { Label, } from '@simplemodule/ui'; import { useState } from 'react'; +import { ProductsKeys } from '../Locales/keys'; import type { Product } from '../types'; interface Props { @@ -31,6 +33,7 @@ interface Props { } export default function Edit({ product }: Props) { + const { t } = useTranslation('Products'); const [showDeleteDialog, setShowDeleteDialog] = useState(false); function handleSubmit(e: React.FormEvent) { @@ -53,22 +56,22 @@ export default function Edit({ product }: Props) { - Edit Product + {t(ProductsKeys.Edit.Breadcrumb)} -

Edit Product

+

{t(ProductsKeys.Edit.Title)}

- + - + - +
@@ -87,14 +90,12 @@ export default function Edit({ product }: Props) { - Danger Zone + {t(ProductsKeys.Edit.DangerZone)} -

- Permanently delete this product. This action cannot be undone. -

+

{t(ProductsKeys.Edit.DeleteWarning)}

@@ -102,18 +103,17 @@ export default function Edit({ product }: Props) { - Delete Product + {t(ProductsKeys.Edit.DeleteDialog.Title)} - Are you sure you want to delete “{product.name}”? This action cannot be - undone. + {t(ProductsKeys.Edit.DeleteDialog.Confirm, { name: product.name })} diff --git a/modules/Products/src/SimpleModule.Products/Views/Manage.tsx b/modules/Products/src/SimpleModule.Products/Views/Manage.tsx index b0dd230f..61cc0b41 100644 --- a/modules/Products/src/SimpleModule.Products/Views/Manage.tsx +++ b/modules/Products/src/SimpleModule.Products/Views/Manage.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Button, DataGridPage, @@ -16,6 +17,7 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { ProductsKeys } from '../Locales/keys'; import type { Product } from '../types'; interface Props { @@ -23,6 +25,7 @@ interface Props { } export default function Manage({ products }: Props) { + const { t } = useTranslation('Products'); const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string; @@ -37,20 +40,24 @@ export default function Manage({ products }: Props) { return ( <> router.get('/products/create')}>Create Product} + actions={ + + } data={products} - emptyTitle="No products yet" - emptyDescription="Get started by creating your first product." + emptyTitle={t(ProductsKeys.Manage.EmptyTitle)} + emptyDescription={t(ProductsKeys.Manage.EmptyDescription)} > {(pageData) => (
- ID - Name - Price + {t(ProductsKeys.Manage.ColId)} + {t(ProductsKeys.Manage.ColName)} + {t(ProductsKeys.Manage.ColPrice)} @@ -67,14 +74,14 @@ export default function Manage({ products }: Props) { size="sm" onClick={() => router.get(`/products/${product.id}/edit`)} > - Edit + {t(ProductsKeys.Manage.EditButton)} @@ -88,18 +95,17 @@ export default function Manage({ products }: Props) { !open && setDeleteTarget(null)}> - Delete Product + {t(ProductsKeys.Manage.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.name}”? This action cannot - be undone. + {t(ProductsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name })} diff --git a/modules/Settings/src/SimpleModule.Settings/Locales/en.json b/modules/Settings/src/SimpleModule.Settings/Locales/en.json new file mode 100644 index 00000000..92989989 --- /dev/null +++ b/modules/Settings/src/SimpleModule.Settings/Locales/en.json @@ -0,0 +1,25 @@ +{ + "AdminSettings.Title": "Settings", + "AdminSettings.TabSystem": "System", + "AdminSettings.TabApplication": "Application", + "UserSettings.Title": "My Settings", + "UserSettings.BadgeOverridden": "Overridden", + "UserSettings.BadgeDefault": "Default", + "UserSettings.ResetButton": "Reset", + "MenuManager.Title": "Menu Manager", + "MenuManager.Description": "Configure the public navigation menu. Add, reorder, and organize menu items.", + "MenuManager.BreadcrumbSettings": "Settings", + "MenuManager.BreadcrumbMenuManager": "Menu Manager", + "MenuManager.CardTreeTitle": "Menu Tree", + "MenuManager.ItemsCount": "{count} items", + "MenuManager.AddButton": "Add", + "MenuManager.AddTooltip": "Add a new top-level menu item", + "MenuManager.AddChildButton": "Child", + "MenuManager.AddChildTooltip": "Add a child item under \"{label}\"", + "MenuManager.EmptyTitle": "No menu items yet", + "MenuManager.EmptyDescription": "Click \"Add\" to create your first menu item.", + "MenuManager.EditorTitle": "Item Editor", + "MenuManager.EditorEditTitle": "Edit: {label}", + "MenuManager.NoItemSelectedTitle": "No item selected", + "MenuManager.NoItemSelectedDescription": "Select a menu item from the tree to edit its properties." +} diff --git a/modules/Settings/src/SimpleModule.Settings/Locales/keys.ts b/modules/Settings/src/SimpleModule.Settings/Locales/keys.ts new file mode 100644 index 00000000..f25ee642 --- /dev/null +++ b/modules/Settings/src/SimpleModule.Settings/Locales/keys.ts @@ -0,0 +1,31 @@ +export const SettingsKeys = { + AdminSettings: { + TabApplication: 'AdminSettings.TabApplication', + TabSystem: 'AdminSettings.TabSystem', + Title: 'AdminSettings.Title', + }, + MenuManager: { + AddButton: 'MenuManager.AddButton', + AddChildButton: 'MenuManager.AddChildButton', + AddChildTooltip: 'MenuManager.AddChildTooltip', + AddTooltip: 'MenuManager.AddTooltip', + BreadcrumbMenuManager: 'MenuManager.BreadcrumbMenuManager', + BreadcrumbSettings: 'MenuManager.BreadcrumbSettings', + CardTreeTitle: 'MenuManager.CardTreeTitle', + Description: 'MenuManager.Description', + EditorEditTitle: 'MenuManager.EditorEditTitle', + EditorTitle: 'MenuManager.EditorTitle', + EmptyDescription: 'MenuManager.EmptyDescription', + EmptyTitle: 'MenuManager.EmptyTitle', + ItemsCount: 'MenuManager.ItemsCount', + NoItemSelectedDescription: 'MenuManager.NoItemSelectedDescription', + NoItemSelectedTitle: 'MenuManager.NoItemSelectedTitle', + Title: 'MenuManager.Title', + }, + UserSettings: { + BadgeDefault: 'UserSettings.BadgeDefault', + BadgeOverridden: 'UserSettings.BadgeOverridden', + ResetButton: 'UserSettings.ResetButton', + Title: 'UserSettings.Title', + }, +} as const; diff --git a/modules/Settings/src/SimpleModule.Settings/SimpleModule.Settings.csproj b/modules/Settings/src/SimpleModule.Settings/SimpleModule.Settings.csproj index 66735204..ec2442ac 100644 --- a/modules/Settings/src/SimpleModule.Settings/SimpleModule.Settings.csproj +++ b/modules/Settings/src/SimpleModule.Settings/SimpleModule.Settings.csproj @@ -8,6 +8,9 @@ + + + %(Filename)Endpoint.cs diff --git a/modules/Settings/src/SimpleModule.Settings/Views/AdminSettings.tsx b/modules/Settings/src/SimpleModule.Settings/Views/AdminSettings.tsx index 2c0d1361..c086ee6d 100644 --- a/modules/Settings/src/SimpleModule.Settings/Views/AdminSettings.tsx +++ b/modules/Settings/src/SimpleModule.Settings/Views/AdminSettings.tsx @@ -1,7 +1,9 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { PageShell, Tabs, TabsContent, TabsList, TabsTrigger } from '@simplemodule/ui'; import { useMemo, useState } from 'react'; import type { SettingDefinition } from '../components/SettingField'; import SettingGroup from '../components/SettingGroup'; +import { SettingsKeys } from '../Locales/keys'; interface StoredSetting { key: string; @@ -15,6 +17,7 @@ interface AdminSettingsProps { } export default function AdminSettings({ definitions, settings }: AdminSettingsProps) { + const { t } = useTranslation('Settings'); const [settingsMap, setSettingsMap] = useState>(() => { const map: Record = {}; for (const s of settings) { @@ -46,11 +49,13 @@ export default function AdminSettings({ definitions, settings }: AdminSettingsPr }; return ( - + - System - Application + {t(SettingsKeys.AdminSettings.TabSystem)} + + {t(SettingsKeys.AdminSettings.TabApplication)} + {Object.entries(groupBy(systemDefs)).map(([group, defs]) => ( diff --git a/modules/Settings/src/SimpleModule.Settings/Views/MenuManager.tsx b/modules/Settings/src/SimpleModule.Settings/Views/MenuManager.tsx index 2b913b5d..62dc6b6b 100644 --- a/modules/Settings/src/SimpleModule.Settings/Views/MenuManager.tsx +++ b/modules/Settings/src/SimpleModule.Settings/Views/MenuManager.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -15,6 +16,7 @@ import { import { useCallback, useState } from 'react'; import MenuItemEditor from '../components/MenuItemEditor'; import MenuTree from '../components/MenuTree'; +import { SettingsKeys } from '../Locales/keys'; interface MenuItemDto { id: number; @@ -69,6 +71,7 @@ function countItems(items: MenuItemDto[]): number { } export default function MenuManager({ menus: initial, availablePages }: MenuManagerProps) { + const { t } = useTranslation('Settings'); const [menus, setMenus] = useState(initial); const [selectedId, setSelectedId] = useState(null); const [saving, setSaving] = useState(false); @@ -149,16 +152,23 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana return (
- Menu Tree - {totalItems > 0 && {totalItems} items} + + {t(SettingsKeys.MenuManager.CardTreeTitle)} + + {totalItems > 0 && ( + {t(SettingsKeys.MenuManager.ItemsCount, { count: totalItems })} + )}
@@ -179,10 +189,10 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana > - Add + {t(SettingsKeys.MenuManager.AddButton)} - Add a new top-level menu item + {t(SettingsKeys.MenuManager.AddTooltip)} {selectedItem && selectedDepth < 2 && ( @@ -203,11 +213,11 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana > - Child + {t(SettingsKeys.MenuManager.AddChildButton)} - Add a child item under “{selectedItem.label}” + {t(SettingsKeys.MenuManager.AddChildTooltip, { label: selectedItem.label })} )} @@ -226,9 +236,11 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana > -

No menu items yet

+

+ {t(SettingsKeys.MenuManager.EmptyTitle)} +

- Click “Add” to create your first menu item. + {t(SettingsKeys.MenuManager.EmptyDescription)}

) : ( @@ -249,7 +261,9 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana - {selectedItem ? `Edit: ${selectedItem.label}` : 'Item Editor'} + {selectedItem + ? t(SettingsKeys.MenuManager.EditorEditTitle, { label: selectedItem.label }) + : t(SettingsKeys.MenuManager.EditorTitle)} @@ -273,9 +287,11 @@ export default function MenuManager({ menus: initial, availablePages }: MenuMana > -

No item selected

+

+ {t(SettingsKeys.MenuManager.NoItemSelectedTitle)} +

- Select a menu item from the tree to edit its properties. + {t(SettingsKeys.MenuManager.NoItemSelectedDescription)}

)} diff --git a/modules/Settings/src/SimpleModule.Settings/Views/UserSettings.tsx b/modules/Settings/src/SimpleModule.Settings/Views/UserSettings.tsx index 026cb56c..0d3adb61 100644 --- a/modules/Settings/src/SimpleModule.Settings/Views/UserSettings.tsx +++ b/modules/Settings/src/SimpleModule.Settings/Views/UserSettings.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Badge, Button, @@ -10,6 +11,7 @@ import { import { useState } from 'react'; import type { SettingDefinition } from '../components/SettingField'; import SettingField from '../components/SettingField'; +import { SettingsKeys } from '../Locales/keys'; interface UserSettingView { definition: SettingDefinition; @@ -22,6 +24,7 @@ interface UserSettingsProps { } export default function UserSettings({ settings: initial }: UserSettingsProps) { + const { t } = useTranslation('Settings'); const [settings, setSettings] = useState(initial); const handleSave = async (key: string, value: string) => { @@ -55,7 +58,7 @@ export default function UserSettings({ settings: initial }: UserSettingsProps) { return ( -

My Settings

+

{t(SettingsKeys.UserSettings.Title)}

{Object.entries(groups).map(([group, items]) => ( @@ -71,17 +74,19 @@ export default function UserSettings({ settings: initial }: UserSettingsProps) {
{s.isOverridden ? ( <> - Overridden + + {t(SettingsKeys.UserSettings.BadgeOverridden)} + ) : ( - Default + {t(SettingsKeys.UserSettings.BadgeDefault)} )}
diff --git a/modules/Tenants/src/SimpleModule.Tenants/Locales/en.json b/modules/Tenants/src/SimpleModule.Tenants/Locales/en.json new file mode 100644 index 00000000..fdef191c --- /dev/null +++ b/modules/Tenants/src/SimpleModule.Tenants/Locales/en.json @@ -0,0 +1,66 @@ +{ + "Browse.Title": "Tenants", + "Browse.Description": "Browse all tenants.", + "Browse.HostCount_one": "{count} host", + "Browse.HostCount_other": "{count} hosts", + "Manage.Title": "Manage Tenants", + "Manage.Description": "{count} total tenants", + "Manage.CreateButton": "Create Tenant", + "Manage.EmptyTitle": "No tenants yet", + "Manage.EmptyDescription": "Get started by creating your first tenant.", + "Manage.ColId": "ID", + "Manage.ColName": "Name", + "Manage.ColSlug": "Slug", + "Manage.ColStatus": "Status", + "Manage.ColHosts": "Hosts", + "Manage.ColEdition": "Edition", + "Manage.EditButton": "Edit", + "Manage.FeaturesButton": "Features", + "Manage.DeleteButton": "Delete", + "Manage.DeleteDialog.Title": "Delete Tenant", + "Manage.DeleteDialog.Confirm": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "Manage.CancelButton": "Cancel", + "Manage.DeleteDialog.DeleteButton": "Delete", + "Create.Title": "Create Tenant", + "Create.Breadcrumb": "Create Tenant", + "Create.NameLabel": "Name", + "Create.NamePlaceholder": "Tenant name", + "Create.SlugLabel": "Slug", + "Create.SlugPlaceholder": "tenant-slug", + "Create.AdminEmailLabel": "Admin Email", + "Create.AdminEmailPlaceholder": "admin@example.com", + "Create.EditionLabel": "Edition", + "Create.EditionPlaceholder": "Standard", + "Create.ValidUntilLabel": "Valid Until", + "Create.SubmitButton": "Create", + "Edit.Title": "Edit Tenant", + "Edit.Breadcrumb": "Edit {name}", + "Edit.ManageFeaturesButton": "Manage Features", + "Edit.NameLabel": "Name", + "Edit.AdminEmailLabel": "Admin Email", + "Edit.EditionLabel": "Edition", + "Edit.ValidUntilLabel": "Valid Until", + "Edit.SaveButton": "Save Changes", + "Edit.StatusSection": "Status", + "Edit.HostsSection": "Hosts", + "Edit.ColHostName": "Host Name", + "Edit.ColActive": "Active", + "Edit.ActiveYes": "Yes", + "Edit.ActiveNo": "No", + "Edit.RemoveHostButton": "Remove", + "Edit.AddHostButton": "Add Host", + "Edit.NewHostPlaceholder": "new-host.example.com", + "Features.Title": "Feature Flags for {name}", + "Features.Breadcrumb": "Feature Flags", + "Features.EmptyTitle": "No Feature Flags Available", + "Features.EmptyDescription": "The Feature Flags module is not installed or has no flags.", + "Features.ColFlag": "Flag", + "Features.ColDescription": "Description", + "Features.ColGlobal": "Global", + "Features.ColTenantOverride": "Tenant Override", + "Features.On": "On", + "Features.Off": "Off", + "Features.Enabled": "Enabled", + "Features.Disabled": "Disabled", + "Features.ResetButton": "Reset" +} diff --git a/modules/Tenants/src/SimpleModule.Tenants/Locales/keys.ts b/modules/Tenants/src/SimpleModule.Tenants/Locales/keys.ts new file mode 100644 index 00000000..88c25a8b --- /dev/null +++ b/modules/Tenants/src/SimpleModule.Tenants/Locales/keys.ts @@ -0,0 +1,78 @@ +export const TenantsKeys = { + Browse: { + Description: 'Browse.Description', + HostCount_one: 'Browse.HostCount_one', + HostCount_other: 'Browse.HostCount_other', + Title: 'Browse.Title', + }, + Create: { + AdminEmailLabel: 'Create.AdminEmailLabel', + AdminEmailPlaceholder: 'Create.AdminEmailPlaceholder', + Breadcrumb: 'Create.Breadcrumb', + EditionLabel: 'Create.EditionLabel', + EditionPlaceholder: 'Create.EditionPlaceholder', + NameLabel: 'Create.NameLabel', + NamePlaceholder: 'Create.NamePlaceholder', + SlugLabel: 'Create.SlugLabel', + SlugPlaceholder: 'Create.SlugPlaceholder', + SubmitButton: 'Create.SubmitButton', + Title: 'Create.Title', + ValidUntilLabel: 'Create.ValidUntilLabel', + }, + Edit: { + ActiveNo: 'Edit.ActiveNo', + ActiveYes: 'Edit.ActiveYes', + AddHostButton: 'Edit.AddHostButton', + AdminEmailLabel: 'Edit.AdminEmailLabel', + Breadcrumb: 'Edit.Breadcrumb', + ColActive: 'Edit.ColActive', + ColHostName: 'Edit.ColHostName', + EditionLabel: 'Edit.EditionLabel', + HostsSection: 'Edit.HostsSection', + ManageFeaturesButton: 'Edit.ManageFeaturesButton', + NameLabel: 'Edit.NameLabel', + NewHostPlaceholder: 'Edit.NewHostPlaceholder', + RemoveHostButton: 'Edit.RemoveHostButton', + SaveButton: 'Edit.SaveButton', + StatusSection: 'Edit.StatusSection', + Title: 'Edit.Title', + ValidUntilLabel: 'Edit.ValidUntilLabel', + }, + Features: { + Breadcrumb: 'Features.Breadcrumb', + ColDescription: 'Features.ColDescription', + ColFlag: 'Features.ColFlag', + ColGlobal: 'Features.ColGlobal', + ColTenantOverride: 'Features.ColTenantOverride', + Disabled: 'Features.Disabled', + EmptyDescription: 'Features.EmptyDescription', + EmptyTitle: 'Features.EmptyTitle', + Enabled: 'Features.Enabled', + Off: 'Features.Off', + On: 'Features.On', + ResetButton: 'Features.ResetButton', + Title: 'Features.Title', + }, + Manage: { + CancelButton: 'Manage.CancelButton', + ColEdition: 'Manage.ColEdition', + ColHosts: 'Manage.ColHosts', + ColId: 'Manage.ColId', + ColName: 'Manage.ColName', + ColSlug: 'Manage.ColSlug', + ColStatus: 'Manage.ColStatus', + CreateButton: 'Manage.CreateButton', + DeleteButton: 'Manage.DeleteButton', + DeleteDialog: { + Confirm: 'Manage.DeleteDialog.Confirm', + DeleteButton: 'Manage.DeleteDialog.DeleteButton', + Title: 'Manage.DeleteDialog.Title', + }, + Description: 'Manage.Description', + EditButton: 'Manage.EditButton', + EmptyDescription: 'Manage.EmptyDescription', + EmptyTitle: 'Manage.EmptyTitle', + FeaturesButton: 'Manage.FeaturesButton', + Title: 'Manage.Title', + }, +} as const; diff --git a/modules/Tenants/src/SimpleModule.Tenants/SimpleModule.Tenants.csproj b/modules/Tenants/src/SimpleModule.Tenants/SimpleModule.Tenants.csproj index 8ba34ef9..c439d805 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/SimpleModule.Tenants.csproj +++ b/modules/Tenants/src/SimpleModule.Tenants/SimpleModule.Tenants.csproj @@ -9,6 +9,9 @@
+ + + %(Filename)Endpoint.cs diff --git a/modules/Tenants/src/SimpleModule.Tenants/Views/Browse.tsx b/modules/Tenants/src/SimpleModule.Tenants/Views/Browse.tsx index 1c3b6dc8..35af4d86 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/Views/Browse.tsx +++ b/modules/Tenants/src/SimpleModule.Tenants/Views/Browse.tsx @@ -1,4 +1,6 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Card, CardContent, PageShell } from '@simplemodule/ui'; +import { TenantsKeys } from '../Locales/keys'; import { statusColors, statusLabels } from './tenantStatus'; interface BrowseTenant { @@ -10,22 +12,30 @@ interface BrowseTenant { } export default function Browse({ tenants }: { tenants: BrowseTenant[] }) { + const { t } = useTranslation('Tenants'); + return ( - +
- {tenants.map((t) => ( - + {tenants.map((tenant) => ( +
- {t.name} - ({t.slug}) + {tenant.name} + ({tenant.slug})
- {t.hostCount} host{t.hostCount !== 1 ? 's' : ''} + {tenant.hostCount}{' '} + {t( + tenant.hostCount !== 1 + ? TenantsKeys.Browse.HostCount_other + : TenantsKeys.Browse.HostCount_one, + { count: tenant.hostCount }, + )} - - {statusLabels[t.status]} + + {statusLabels[tenant.status]}
diff --git a/modules/Tenants/src/SimpleModule.Tenants/Views/Create.tsx b/modules/Tenants/src/SimpleModule.Tenants/Views/Create.tsx index 81d2183e..42465bb0 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/Views/Create.tsx +++ b/modules/Tenants/src/SimpleModule.Tenants/Views/Create.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -15,8 +16,11 @@ import { Input, Label, } from '@simplemodule/ui'; +import { TenantsKeys } from '../Locales/keys'; export default function Create() { + const { t } = useTranslation('Tenants'); + function handleSubmit(e: React.FormEvent) { e.preventDefault(); const formData = new FormData(e.currentTarget); @@ -28,52 +32,61 @@ export default function Create() { - Tenants + {t(TenantsKeys.Manage.Title)} - Create Tenant + {t(TenantsKeys.Create.Breadcrumb)} -

Create Tenant

+

{t(TenantsKeys.Create.Title)}

- - + + - + - + - - + + - + - +
diff --git a/modules/Tenants/src/SimpleModule.Tenants/Views/Edit.tsx b/modules/Tenants/src/SimpleModule.Tenants/Views/Edit.tsx index ce3e6715..8532ee3a 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/Views/Edit.tsx +++ b/modules/Tenants/src/SimpleModule.Tenants/Views/Edit.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -22,10 +23,12 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { TenantsKeys } from '../Locales/keys'; import type { Tenant } from '../types'; import { statusLabels } from './tenantStatus'; export default function Edit({ tenant }: { tenant: Tenant }) { + const { t } = useTranslation('Tenants'); const [newHost, setNewHost] = useState(''); function handleSubmit(e: React.FormEvent) { @@ -57,18 +60,18 @@ export default function Edit({ tenant }: { tenant: Tenant }) { - Tenants + {t(TenantsKeys.Manage.Title)} - Edit {tenant.name} + {t(TenantsKeys.Edit.Breadcrumb, { name: tenant.name })}
-

Edit Tenant

+

{t(TenantsKeys.Edit.Title)}

@@ -77,11 +80,11 @@ export default function Edit({ tenant }: { tenant: Tenant }) {
- + - + - + - + - + @@ -114,7 +117,7 @@ export default function Edit({ tenant }: { tenant: Tenant }) { -

Status

+

{t(TenantsKeys.Edit.StatusSection)}

{[0, 1, 2].map((s) => (
- Host Name - Active + {t(TenantsKeys.Edit.ColHostName)} + {t(TenantsKeys.Edit.ColActive)} @@ -146,10 +149,12 @@ export default function Edit({ tenant }: { tenant: Tenant }) { {tenant.hosts.map((host) => ( {host.hostName} - {host.isActive ? 'Yes' : 'No'} + + {host.isActive ? t(TenantsKeys.Edit.ActiveYes) : t(TenantsKeys.Edit.ActiveNo)} + @@ -159,11 +164,11 @@ export default function Edit({ tenant }: { tenant: Tenant }) { )}
setNewHost(e.target.value)} /> - +
diff --git a/modules/Tenants/src/SimpleModule.Tenants/Views/Features.tsx b/modules/Tenants/src/SimpleModule.Tenants/Views/Features.tsx index f3d9fc6d..15ce384e 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/Views/Features.tsx +++ b/modules/Tenants/src/SimpleModule.Tenants/Views/Features.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Breadcrumb, BreadcrumbItem, @@ -17,6 +18,7 @@ import { TableHeader, TableRow, } from '@simplemodule/ui'; +import { TenantsKeys } from '../Locales/keys'; interface FeatureFlag { name: string; @@ -47,6 +49,7 @@ interface Props { } export default function Features({ tenant, flags, tenantOverrides }: Props) { + const { t } = useTranslation('Tenants'); const overrideMap = new Map(tenantOverrides.map((o) => [o.flagName, o])); function handleToggle(flagName: string, currentlyEnabled: boolean) { @@ -64,10 +67,8 @@ export default function Features({ tenant, flags, tenantOverrides }: Props) { if (flags.length === 0) { return ( -

No Feature Flags Available

-

- The Feature Flags module is not installed or has no flags. -

+

{t(TenantsKeys.Features.EmptyTitle)}

+

{t(TenantsKeys.Features.EmptyDescription)}

); } @@ -77,7 +78,7 @@ export default function Features({ tenant, flags, tenantOverrides }: Props) { - Tenants + {t(TenantsKeys.Manage.Title)} @@ -85,21 +86,23 @@ export default function Features({ tenant, flags, tenantOverrides }: Props) { - Feature Flags + {t(TenantsKeys.Features.Breadcrumb)} -

Feature Flags for {tenant.name}

+

+ {t(TenantsKeys.Features.Title, { name: tenant.name })} +

- Flag - Description - Global - Tenant Override + {t(TenantsKeys.Features.ColFlag)} + {t(TenantsKeys.Features.ColDescription)} + {t(TenantsKeys.Features.ColGlobal)} + {t(TenantsKeys.Features.ColTenantOverride)} @@ -116,7 +119,9 @@ export default function Features({ tenant, flags, tenantOverrides }: Props) { {flag.description || '-'} - {flag.isEnabled ? 'On' : 'Off'} + {flag.isEnabled + ? t(TenantsKeys.Features.On) + : t(TenantsKeys.Features.Off)} @@ -125,13 +130,15 @@ export default function Features({ tenant, flags, tenantOverrides }: Props) { size="sm" onClick={() => handleToggle(flag.name, effectiveState)} > - {effectiveState ? 'Enabled' : 'Disabled'} + {effectiveState + ? t(TenantsKeys.Features.Enabled) + : t(TenantsKeys.Features.Disabled)} {override && ( )} diff --git a/modules/Tenants/src/SimpleModule.Tenants/Views/Manage.tsx b/modules/Tenants/src/SimpleModule.Tenants/Views/Manage.tsx index 5ad445ba..c1563f9d 100644 --- a/modules/Tenants/src/SimpleModule.Tenants/Views/Manage.tsx +++ b/modules/Tenants/src/SimpleModule.Tenants/Views/Manage.tsx @@ -1,4 +1,5 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Button, DataGridPage, @@ -16,10 +17,12 @@ import { TableRow, } from '@simplemodule/ui'; import { useState } from 'react'; +import { TenantsKeys } from '../Locales/keys'; import type { Tenant } from '../types'; import { statusColors, statusLabels } from './tenantStatus'; export default function Manage({ tenants }: { tenants: Tenant[] }) { + const { t } = useTranslation('Tenants'); const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string } | null>(null); function handleDelete() { @@ -31,23 +34,27 @@ export default function Manage({ tenants }: { tenants: Tenant[] }) { return ( <> router.get('/tenants/create')}>Create Tenant} + title={t(TenantsKeys.Manage.Title)} + description={t(TenantsKeys.Manage.Description, { count: tenants.length })} + actions={ + + } data={tenants} - emptyTitle="No tenants yet" - emptyDescription="Get started by creating your first tenant." + emptyTitle={t(TenantsKeys.Manage.EmptyTitle)} + emptyDescription={t(TenantsKeys.Manage.EmptyDescription)} > {(pageData) => (
- ID - Name - Slug - Status - Hosts - Edition + {t(TenantsKeys.Manage.ColId)} + {t(TenantsKeys.Manage.ColName)} + {t(TenantsKeys.Manage.ColSlug)} + {t(TenantsKeys.Manage.ColStatus)} + {t(TenantsKeys.Manage.ColHosts)} + {t(TenantsKeys.Manage.ColEdition)} @@ -71,21 +78,21 @@ export default function Manage({ tenants }: { tenants: Tenant[] }) { size="sm" onClick={() => router.get(`/tenants/${tenant.id}/edit`)} > - Edit + {t(TenantsKeys.Manage.EditButton)} @@ -99,18 +106,17 @@ export default function Manage({ tenants }: { tenants: Tenant[] }) { !open && setDeleteTarget(null)}> - Delete Tenant + {t(TenantsKeys.Manage.DeleteDialog.Title)} - Are you sure you want to delete “{deleteTarget?.name}”? This action cannot - be undone. + {t(TenantsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name })} diff --git a/modules/Users/src/SimpleModule.Users/Locales/en.json b/modules/Users/src/SimpleModule.Users/Locales/en.json new file mode 100644 index 00000000..febdec87 --- /dev/null +++ b/modules/Users/src/SimpleModule.Users/Locales/en.json @@ -0,0 +1,53 @@ +{ + "TwoFactor.Title": "Two-factor authentication (2FA)", + "TwoFactor.AuthenticatorAppTitle": "Authenticator app", + "TwoFactor.NoRecoveryCodesTitle": "You have no recovery codes left.", + "TwoFactor.NoRecoveryCodesDescription": "You must", + "TwoFactor.NoRecoveryCodesDescriptionSuffix": "before you can log in with a recovery code.", + "TwoFactor.NoRecoveryCodesLinkText": "generate a new set of recovery codes", + "TwoFactor.OneRecoveryCodeTitle": "You have 1 recovery code left.", + "TwoFactor.OneRecoveryCodeDescription": "You can", + "TwoFactor.OneRecoveryCodeLinkText": "generate a new set of recovery codes", + "TwoFactor.FewRecoveryCodesTitle": "You have {count} recovery codes left.", + "TwoFactor.FewRecoveryCodesDescription": "You should", + "TwoFactor.FewRecoveryCodesLinkText": "generate a new set of recovery codes", + "TwoFactor.ForgetBrowser": "Forget this browser", + "TwoFactor.Disable2fa": "Disable 2FA", + "TwoFactor.ResetRecoveryCodes": "Reset recovery codes", + "TwoFactor.AddAuthenticatorApp": "Add authenticator app", + "TwoFactor.SetUpAuthenticatorApp": "Set up authenticator app", + "TwoFactor.ResetAuthenticatorApp": "Reset authenticator app", + "TwoFactor.Status.BrowserForgotten": "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2FA code.", + "TwoFactor.Status.2faDisabled": "2FA has been disabled. You can reenable 2FA when you setup an authenticator app.", + "TwoFactor.Status.AuthenticatorVerified": "Your authenticator app has been verified.", + "EnableAuthenticator.Title": "Configure authenticator app", + "EnableAuthenticator.Intro": "To use an authenticator app go through the following steps:", + "EnableAuthenticator.Step1": "Download a two-factor authenticator app like Microsoft Authenticator or Google Authenticator.", + "EnableAuthenticator.Step2": "Scan the QR Code or enter this key into your two factor authenticator app. Spaces and casing do not matter.", + "EnableAuthenticator.Step3": "Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below.", + "EnableAuthenticator.QrCodeAlt": "QR Code for authenticator app", + "EnableAuthenticator.VerificationCodeLabel": "Verification Code", + "EnableAuthenticator.VerificationCodePlaceholder": "Please enter the code.", + "EnableAuthenticator.VerifyButton": "Verify", + "EnableAuthenticator.Status.AuthenticatorReset": "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", + "EnableAuthenticator.Error.InvalidCode": "Verification code is invalid.", + "Disable2fa.Title": "Disable two-factor authentication (2FA)", + "Disable2fa.WarningTitle": "This action only disables 2FA.", + "Disable2fa.WarningDescription": "Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key used in an authenticator app you should", + "Disable2fa.ResetKeysLinkText": "reset your authenticator keys", + "Disable2fa.DisableButton": "Disable 2FA", + "ResetAuthenticator.Title": "Reset authenticator key", + "ResetAuthenticator.WarningTitle": "If you reset your authenticator key your authenticator app will not work until you reconfigure it.", + "ResetAuthenticator.WarningDescription": "This process disables 2FA until you verify your authenticator app. If you do not complete your authenticator app configuration you may lose access to your account.", + "ResetAuthenticator.ResetButton": "Reset authenticator key", + "GenerateRecoveryCodes.Title": "Generate two-factor authentication (2FA) recovery codes", + "GenerateRecoveryCodes.WarningTitle": "Put these codes in a safe place.", + "GenerateRecoveryCodes.WarningDescription1": "If you lose your device and don't have the recovery codes you will lose access to your account.", + "GenerateRecoveryCodes.WarningDescription2": "Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key used in an authenticator app you should {resetLink}.", + "GenerateRecoveryCodes.ResetKeysLinkText": "reset your authenticator keys", + "GenerateRecoveryCodes.GenerateButton": "Generate Recovery Codes", + "ShowRecoveryCodes.Title": "Recovery codes", + "ShowRecoveryCodes.WarningTitle": "Put these codes in a safe place.", + "ShowRecoveryCodes.WarningDescription": "If you lose your device and don't have the recovery codes you will lose access to your account.", + "ShowRecoveryCodes.BackButton": "Back to two-factor authentication" +} diff --git a/modules/Users/src/SimpleModule.Users/Locales/keys.ts b/modules/Users/src/SimpleModule.Users/Locales/keys.ts new file mode 100644 index 00000000..45ffbf26 --- /dev/null +++ b/modules/Users/src/SimpleModule.Users/Locales/keys.ts @@ -0,0 +1,71 @@ +export const UsersKeys = { + Disable2fa: { + DisableButton: 'Disable2fa.DisableButton', + ResetKeysLinkText: 'Disable2fa.ResetKeysLinkText', + Title: 'Disable2fa.Title', + WarningDescription: 'Disable2fa.WarningDescription', + WarningTitle: 'Disable2fa.WarningTitle', + }, + EnableAuthenticator: { + Error: { + InvalidCode: 'EnableAuthenticator.Error.InvalidCode', + }, + Intro: 'EnableAuthenticator.Intro', + QrCodeAlt: 'EnableAuthenticator.QrCodeAlt', + Status: { + AuthenticatorReset: 'EnableAuthenticator.Status.AuthenticatorReset', + }, + Step1: 'EnableAuthenticator.Step1', + Step2: 'EnableAuthenticator.Step2', + Step3: 'EnableAuthenticator.Step3', + Title: 'EnableAuthenticator.Title', + VerificationCodeLabel: 'EnableAuthenticator.VerificationCodeLabel', + VerificationCodePlaceholder: 'EnableAuthenticator.VerificationCodePlaceholder', + VerifyButton: 'EnableAuthenticator.VerifyButton', + }, + GenerateRecoveryCodes: { + GenerateButton: 'GenerateRecoveryCodes.GenerateButton', + ResetKeysLinkText: 'GenerateRecoveryCodes.ResetKeysLinkText', + Title: 'GenerateRecoveryCodes.Title', + WarningDescription1: 'GenerateRecoveryCodes.WarningDescription1', + WarningDescription2: 'GenerateRecoveryCodes.WarningDescription2', + WarningTitle: 'GenerateRecoveryCodes.WarningTitle', + }, + ResetAuthenticator: { + ResetButton: 'ResetAuthenticator.ResetButton', + Title: 'ResetAuthenticator.Title', + WarningDescription: 'ResetAuthenticator.WarningDescription', + WarningTitle: 'ResetAuthenticator.WarningTitle', + }, + ShowRecoveryCodes: { + BackButton: 'ShowRecoveryCodes.BackButton', + Title: 'ShowRecoveryCodes.Title', + WarningDescription: 'ShowRecoveryCodes.WarningDescription', + WarningTitle: 'ShowRecoveryCodes.WarningTitle', + }, + TwoFactor: { + AddAuthenticatorApp: 'TwoFactor.AddAuthenticatorApp', + AuthenticatorAppTitle: 'TwoFactor.AuthenticatorAppTitle', + Disable2fa: 'TwoFactor.Disable2fa', + FewRecoveryCodesDescription: 'TwoFactor.FewRecoveryCodesDescription', + FewRecoveryCodesLinkText: 'TwoFactor.FewRecoveryCodesLinkText', + FewRecoveryCodesTitle: 'TwoFactor.FewRecoveryCodesTitle', + ForgetBrowser: 'TwoFactor.ForgetBrowser', + NoRecoveryCodesDescription: 'TwoFactor.NoRecoveryCodesDescription', + NoRecoveryCodesDescriptionSuffix: 'TwoFactor.NoRecoveryCodesDescriptionSuffix', + NoRecoveryCodesLinkText: 'TwoFactor.NoRecoveryCodesLinkText', + NoRecoveryCodesTitle: 'TwoFactor.NoRecoveryCodesTitle', + OneRecoveryCodeDescription: 'TwoFactor.OneRecoveryCodeDescription', + OneRecoveryCodeLinkText: 'TwoFactor.OneRecoveryCodeLinkText', + OneRecoveryCodeTitle: 'TwoFactor.OneRecoveryCodeTitle', + ResetAuthenticatorApp: 'TwoFactor.ResetAuthenticatorApp', + ResetRecoveryCodes: 'TwoFactor.ResetRecoveryCodes', + SetUpAuthenticatorApp: 'TwoFactor.SetUpAuthenticatorApp', + Status: { + '2faDisabled': 'TwoFactor.Status.2faDisabled', + AuthenticatorVerified: 'TwoFactor.Status.AuthenticatorVerified', + BrowserForgotten: 'TwoFactor.Status.BrowserForgotten', + }, + Title: 'TwoFactor.Title', + }, +} as const; diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/Disable2fa.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/Disable2fa.tsx index 26dda033..5efbece5 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/Disable2fa.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/Disable2fa.tsx @@ -1,8 +1,12 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, AlertTitle, Button } from '@simplemodule/ui'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; export default function Disable2fa() { + const { t } = useTranslation('Users'); + function handleSubmit(e: React.FormEvent) { e.preventDefault(); router.post('/Identity/Account/Manage/Disable2fa'); @@ -10,15 +14,14 @@ export default function Disable2fa() { return ( -

Disable two-factor authentication (2FA)

+

{t(UsersKeys.Disable2fa.Title)}

- This action only disables 2FA. + {t(UsersKeys.Disable2fa.WarningTitle)} - Disabling 2FA does not change the keys used in authenticator apps. If you wish to change - the key used in an authenticator app you should{' '} + {t(UsersKeys.Disable2fa.WarningDescription)}{' '} - reset your authenticator keys + {t(UsersKeys.Disable2fa.ResetKeysLinkText)} . @@ -26,7 +29,7 @@ export default function Disable2fa() {
diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/EnableAuthenticator.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/EnableAuthenticator.tsx index 951256f4..5aed2b64 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/EnableAuthenticator.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/EnableAuthenticator.tsx @@ -1,7 +1,9 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, Button, Input, Label } from '@simplemodule/ui'; import QRCode from 'qrcode'; import { useEffect, useState } from 'react'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; interface Props { @@ -9,21 +11,22 @@ interface Props { authenticatorUri: string; } -const statusMessages: Record = { - 'authenticator-reset': - 'Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.', -}; - -const errorMessages: Record = { - 'invalid-code': 'Verification code is invalid.', -}; - export default function EnableAuthenticator({ sharedKey, authenticatorUri }: Props) { + const { t } = useTranslation('Users'); const [qrCodeUrl, setQrCodeUrl] = useState(null); const params = new URLSearchParams(window.location.search); const status = params.get('status'); const error = params.get('error'); + + const statusMessages: Record = { + 'authenticator-reset': t(UsersKeys.EnableAuthenticator.Status.AuthenticatorReset), + }; + + const errorMessages: Record = { + 'invalid-code': t(UsersKeys.EnableAuthenticator.Error.InvalidCode), + }; + const statusMessage = status ? statusMessages[status] : null; const errorMessage = error ? errorMessages[error] : null; @@ -39,7 +42,7 @@ export default function EnableAuthenticator({ sharedKey, authenticatorUri }: Pro return ( -

Configure authenticator app

+

{t(UsersKeys.EnableAuthenticator.Title)}

{statusMessage && ( @@ -53,45 +56,38 @@ export default function EnableAuthenticator({ sharedKey, authenticatorUri }: Pro )} -

- To use an authenticator app go through the following steps: -

+

{t(UsersKeys.EnableAuthenticator.Intro)}

  1. -

    - Download a two-factor authenticator app like Microsoft Authenticator or Google - Authenticator. -

    +

    {t(UsersKeys.EnableAuthenticator.Step1)}

  2. -

    - Scan the QR Code or enter this key into your two factor authenticator app. Spaces and - casing do not matter. -

    +

    {t(UsersKeys.EnableAuthenticator.Step2)}

    {sharedKey} {qrCodeUrl && ( - QR Code for authenticator app + {t(UsersKeys.EnableAuthenticator.QrCodeAlt)} )}
  3. -

    - Once you have scanned the QR code or input the key above, your two factor authentication - app will provide you with a unique code. Enter the code in the confirmation box below. -

    +

    {t(UsersKeys.EnableAuthenticator.Step3)}

    - +
    - +
diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/GenerateRecoveryCodes.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/GenerateRecoveryCodes.tsx index 920618dc..1e3e1436 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/GenerateRecoveryCodes.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/GenerateRecoveryCodes.tsx @@ -1,8 +1,12 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, AlertTitle, Button } from '@simplemodule/ui'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; export default function GenerateRecoveryCodes() { + const { t } = useTranslation('Users'); + function handleSubmit(e: React.FormEvent) { e.preventDefault(); router.post('/Identity/Account/Manage/GenerateRecoveryCodes'); @@ -10,22 +14,16 @@ export default function GenerateRecoveryCodes() { return ( -

- Generate two-factor authentication (2FA) recovery codes -

+

{t(UsersKeys.GenerateRecoveryCodes.Title)}

- Put these codes in a safe place. + {t(UsersKeys.GenerateRecoveryCodes.WarningTitle)} -

- If you lose your device and don't have the recovery codes you will lose access to - your account. -

+

{t(UsersKeys.GenerateRecoveryCodes.WarningDescription1)}

- Generating new recovery codes does not change the keys used in authenticator apps. If - you wish to change the key used in an authenticator app you should{' '} + {t(UsersKeys.GenerateRecoveryCodes.WarningDescription2)}{' '} - reset your authenticator keys + {t(UsersKeys.GenerateRecoveryCodes.ResetKeysLinkText)} .

@@ -34,7 +32,7 @@ export default function GenerateRecoveryCodes() {
diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/ResetAuthenticator.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/ResetAuthenticator.tsx index d6e6df73..bce220c1 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/ResetAuthenticator.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/ResetAuthenticator.tsx @@ -1,8 +1,12 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, AlertTitle, Button } from '@simplemodule/ui'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; export default function ResetAuthenticator() { + const { t } = useTranslation('Users'); + function handleSubmit(e: React.FormEvent) { e.preventDefault(); router.post('/Identity/Account/Manage/ResetAuthenticator'); @@ -10,22 +14,16 @@ export default function ResetAuthenticator() { return ( -

Reset authenticator key

+

{t(UsersKeys.ResetAuthenticator.Title)}

- - If you reset your authenticator key your authenticator app will not work until you - reconfigure it. - - - This process disables 2FA until you verify your authenticator app. If you do not complete - your authenticator app configuration you may lose access to your account. - + {t(UsersKeys.ResetAuthenticator.WarningTitle)} + {t(UsersKeys.ResetAuthenticator.WarningDescription)}
diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/ShowRecoveryCodes.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/ShowRecoveryCodes.tsx index c84f64dc..c298af9e 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/ShowRecoveryCodes.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/ShowRecoveryCodes.tsx @@ -1,4 +1,6 @@ +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, AlertTitle, Button } from '@simplemodule/ui'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; interface Props { @@ -7,9 +9,11 @@ interface Props { } export default function ShowRecoveryCodes({ recoveryCodes, statusMessage }: Props) { + const { t } = useTranslation('Users'); + return ( -

Recovery codes

+

{t(UsersKeys.ShowRecoveryCodes.Title)}

{statusMessage && ( @@ -18,11 +22,8 @@ export default function ShowRecoveryCodes({ recoveryCodes, statusMessage }: Prop )} - Put these codes in a safe place. - - If you lose your device and don't have the recovery codes you will lose access to - your account. - + {t(UsersKeys.ShowRecoveryCodes.WarningTitle)} + {t(UsersKeys.ShowRecoveryCodes.WarningDescription)}
@@ -42,7 +43,7 @@ export default function ShowRecoveryCodes({ recoveryCodes, statusMessage }: Prop window.location.href = '/Identity/Account/Manage/TwoFactorAuthentication'; }} > - Back to two-factor authentication + {t(UsersKeys.ShowRecoveryCodes.BackButton)} ); diff --git a/modules/Users/src/SimpleModule.Users/Pages/Account/TwoFactorAuthentication.tsx b/modules/Users/src/SimpleModule.Users/Pages/Account/TwoFactorAuthentication.tsx index a917bde4..70d09f1d 100644 --- a/modules/Users/src/SimpleModule.Users/Pages/Account/TwoFactorAuthentication.tsx +++ b/modules/Users/src/SimpleModule.Users/Pages/Account/TwoFactorAuthentication.tsx @@ -1,5 +1,7 @@ import { router } from '@inertiajs/react'; +import { useTranslation } from '@simplemodule/client/use-translation'; import { Alert, AlertDescription, AlertTitle, Button } from '@simplemodule/ui'; +import { UsersKeys } from '../../Locales/keys'; import ManageLayout from './ManageLayout'; interface Props { @@ -9,21 +11,21 @@ interface Props { recoveryCodesLeft: number; } -const statusMessages: Record = { - 'browser-forgotten': - 'The current browser has been forgotten. When you login again from this browser you will be prompted for your 2FA code.', - '2fa-disabled': - '2FA has been disabled. You can reenable 2FA when you setup an authenticator app.', - 'authenticator-verified': 'Your authenticator app has been verified.', -}; - export default function TwoFactorAuthentication({ hasAuthenticator, is2faEnabled, isMachineRemembered, recoveryCodesLeft, }: Props) { + const { t } = useTranslation('Users'); const status = new URLSearchParams(window.location.search).get('status'); + + const statusMessages: Record = { + 'browser-forgotten': t(UsersKeys.TwoFactor.Status.BrowserForgotten), + '2fa-disabled': t(UsersKeys.TwoFactor.Status['2faDisabled']), + 'authenticator-verified': t(UsersKeys.TwoFactor.Status.AuthenticatorVerified), + }; + const statusMessage = status ? statusMessages[status] : null; function handleForgetBrowser(e: React.FormEvent) { @@ -33,7 +35,7 @@ export default function TwoFactorAuthentication({ return ( -

Two-factor authentication (2FA)

+

{t(UsersKeys.TwoFactor.Title)}

{statusMessage && ( @@ -45,30 +47,30 @@ export default function TwoFactorAuthentication({ <> {recoveryCodesLeft === 0 && ( - You have no recovery codes left. + {t(UsersKeys.TwoFactor.NoRecoveryCodesTitle)} - You must{' '} + {t(UsersKeys.TwoFactor.NoRecoveryCodesDescription)}{' '} - generate a new set of recovery codes + {t(UsersKeys.TwoFactor.NoRecoveryCodesLinkText)} {' '} - before you can log in with a recovery code. + {t(UsersKeys.TwoFactor.NoRecoveryCodesDescriptionSuffix)} )} {recoveryCodesLeft === 1 && ( - You have 1 recovery code left. + {t(UsersKeys.TwoFactor.OneRecoveryCodeTitle)} - You can{' '} + {t(UsersKeys.TwoFactor.OneRecoveryCodeDescription)}{' '} - generate a new set of recovery codes + {t(UsersKeys.TwoFactor.OneRecoveryCodeLinkText)} . @@ -77,14 +79,16 @@ export default function TwoFactorAuthentication({ {recoveryCodesLeft >= 2 && recoveryCodesLeft <= 3 && ( - You have {recoveryCodesLeft} recovery codes left. + + {t(UsersKeys.TwoFactor.FewRecoveryCodesTitle, { count: String(recoveryCodesLeft) })} + - You should{' '} + {t(UsersKeys.TwoFactor.FewRecoveryCodesDescription)}{' '} - generate a new set of recovery codes + {t(UsersKeys.TwoFactor.FewRecoveryCodesLinkText)} . @@ -95,7 +99,7 @@ export default function TwoFactorAuthentication({ {isMachineRemembered && (
)} @@ -104,13 +108,13 @@ export default function TwoFactorAuthentication({ variant="outline" onClick={() => router.get('/Identity/Account/Manage/Disable2fa')} > - Disable 2FA + {t(UsersKeys.TwoFactor.Disable2fa)}
@@ -119,21 +123,21 @@ export default function TwoFactorAuthentication({ )} -

Authenticator app

+

{t(UsersKeys.TwoFactor.AuthenticatorAppTitle)}

{!hasAuthenticator ? ( ) : (
)} diff --git a/modules/Users/src/SimpleModule.Users/SimpleModule.Users.csproj b/modules/Users/src/SimpleModule.Users/SimpleModule.Users.csproj index 8b043d1c..338423a3 100644 --- a/modules/Users/src/SimpleModule.Users/SimpleModule.Users.csproj +++ b/modules/Users/src/SimpleModule.Users/SimpleModule.Users.csproj @@ -2,6 +2,9 @@ net10.0 + + + diff --git a/template/SimpleModule.Host/ClientApp/app.tsx b/template/SimpleModule.Host/ClientApp/app.tsx index cd1c8f38..d8e6dbc9 100644 --- a/template/SimpleModule.Host/ClientApp/app.tsx +++ b/template/SimpleModule.Host/ClientApp/app.tsx @@ -38,6 +38,33 @@ function clearTimers() { } } +// Intercept plain clicks from the Blazor layout (sidebar, nav, dropdowns) and +// route them through Inertia so the page swap is SPA-style — no full reload. +const nonInertiaPathPrefixes = ['/Identity/', '/swagger', '/health', '/connect/']; + +document.addEventListener('click', (event) => { + const link = (event.target as Element).closest?.('a'); + if (!link?.href) return; + + // Respect modifier keys (new tab), target attrs, downloads + if (event.defaultPrevented) return; + if (event.button !== 0) return; + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + if (link.target && link.target !== '_self') return; + if (link.hasAttribute('download')) return; + if (link.dataset.inertia === 'false') return; + + // Only same-origin + if (link.origin !== window.location.origin) return; + + // Skip non-Inertia server routes (Identity, Swagger, health checks, OAuth) + const path = link.pathname; + if (nonInertiaPathPrefixes.some((prefix) => path.startsWith(prefix))) return; + + event.preventDefault(); + router.visit(link.href); +}); + router.on('start', () => { clearTimers(); startTimer = setTimeout(() => {