From 76caf178f0a8cf3b9e98ab484701b46efddad219 Mon Sep 17 00:00:00 2001 From: James Griffin Date: Wed, 29 Jul 2026 20:34:55 -0300 Subject: [PATCH] Say "student" and "profile" in the UI, not "child" and "family" Sweep the translatable strings across the frontend templates, the admin screens, the editor previews and the block inserter entry. Nothing else moves: the database columns, request parameters, form field names, CSS classes, the us_family shortcode and the us-scheduler/family block name are contracts with existing installs and with post content people have already saved, so renaming them would break sites for no user-visible gain. Two strings are reworded rather than swapped, because the direct substitution reads wrong: - The students list said "Child of Jane" and now says "Managed by Jane". "Student of Jane" would read as a teacher's pupil, which is exactly the wrong idea in a music studio. - A managed account is now "a managed student account" rather than "a student account", which would not distinguish it from the account holder. The guardian feature doc gains a short section on the split, so the next person to work on it does not read the mismatch as drift and "fix" it. Closes #144 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 +++ assets/js/blocks.js | 9 ++++++--- docs/features/parent-guardian-accounts.md | 19 ++++++++++++++++--- docs/features/student-administration.md | 4 ++-- src/Auth/RegistrationPage.php | 4 ++-- src/BlockPreview.php | 8 ++++---- src/Guardian/ChildLoginGate.php | 2 +- src/Guardian/FamilyPage.php | 8 ++++---- src/Guardian/GuardianService.php | 12 ++++++------ templates/admin/student-detail.php | 6 +++--- templates/admin/students.php | 4 ++-- templates/frontend/family-page.php | 10 +++++----- templates/frontend/register-page.php | 10 +++++----- tests/Unit/Auth/RegistrationPageTest.php | 4 ++-- tests/Unit/Guardian/FamilyPageTest.php | 10 +++++----- 15 files changed, 66 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca9737..a596d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.3.1] +### Changed +- The interface now says **student** where it said "child" and **profile** where it said "family". The `[us_family]` page is headed **Your profile**, its form is **Add a student**, signup asks for a **Student's name**, and the wp-admin students list and student screen both label the relationship **Profile**. Two strings were reworded rather than swapped: the students list reads **Managed by _name_** (a bare "Student of _name_" would read as a teacher's pupil), and a managed account is described as a **managed student account** so it is not confused with the account holder. Internal names — database columns, request parameters, form field names, the `us_family` shortcode and the `us-scheduler/family` block — are unchanged, since they are contracts with existing installs and saved post content. + ### Fixed - Upcoming lesson rows no longer render on top of each other. The row's text sits in inline elements that a theme can pull out of normal flow, which dropped the date and time onto the lesson title and the status pill onto the Cancel button; those elements are now pinned into flow alongside the rest of the panel's theme-proofing. The rows held behind **Show all** also stayed visible under the `div { display: block }` reset that many themes still carry, since `[hidden]` is only a browser default — they are now hidden for real. diff --git a/assets/js/blocks.js b/assets/js/blocks.js index 2ce1ff0..d643f28 100644 --- a/assets/js/blocks.js +++ b/assets/js/blocks.js @@ -284,10 +284,13 @@ }, { name: 'us-scheduler/family', - title: __('Family', 'unsupervised-schedular'), - description: __('Lets a parent or guardian add, edit and remove the children they book lessons for.', 'unsupervised-schedular'), + title: __('Profile', 'unsupervised-schedular'), + description: __('Lets a parent or guardian add, edit and remove the students they book lessons for.', 'unsupervised-schedular'), icon: 'groups', - keywords: ['family', 'children', 'guardian', 'parent'], + // 'family' and 'children' are kept as search terms only — they are + // never displayed, and the block answered to them before it was + // renamed, so anyone reaching for the old word still finds it. + keywords: ['profile', 'students', 'family', 'children', 'guardian', 'parent'], shortcode: 'us_family', attributes: { loginPageId: { type: 'number', default: 0 }, diff --git a/docs/features/parent-guardian-accounts.md b/docs/features/parent-guardian-accounts.md index 1d6baf8..ed0cbe2 100644 --- a/docs/features/parent-guardian-accounts.md +++ b/docs/features/parent-guardian-accounts.md @@ -9,6 +9,19 @@ A guardian may also be a student in their own right — they appear in their own "who is this for?" selector alongside their children, so a parent taking lessons next to their kids needs only the one account. +## Vocabulary: "child" in the code, "student" in the UI + +The interface says **student** and **profile**; the code says **child** and +**family**. This is deliberate, not drift. Every identifier below — the +`us_guardian_links` columns, `GuardianService::createChild()`, the `children[]` +request parameters, the `child_name` form fields, the `us-scheduler/family` +block name and the `[us_family]` shortcode — is a stable contract with the +database, saved post content and existing installs, so renaming them would break +sites for no user-visible gain. Only the strings a person reads were changed. + +When adding to this feature, keep the split: internal names follow the +data model, translatable strings follow the interface. + ## Core Decision: children are accountless WordPress users Every `student_id` column in `src/Schema.php` (`us_lessons`, `us_payments`, @@ -159,7 +172,7 @@ child. ## Managing children -`[us_family]` (block: **Family**) renders the guardian's manage-children screen: +`[us_family]` (block: **Profile**) renders the guardian's manage-children screen: list the children, add one, edit a name/date of birth, remove one. - **Add** creates another accountless child user and links it. Account-scope @@ -219,11 +232,11 @@ than being left as a single-student-only path. ## Admin -- **Students list** gains a **Guardian / Children** column: a child links to its +- **Students list** gains a **Profile** column: a child links to its guardian's detail screen, a guardian lists its children as links. Children are listed alongside every other student rather than nested, so nothing about finding a student changes. -- **Student detail** gains a **Family** panel — the guardian (for a child) or +- **Student detail** gains a **Profile** panel — the guardian (for a child) or the children (for a guardian), each a link to the other's screen — and the credit balance shown is the **payer's** balance, labelled with whose it is, so an admin looking at a child sees the family balance that will actually settle diff --git a/docs/features/student-administration.md b/docs/features/student-administration.md index 95c7d74..4a0abfc 100644 --- a/docs/features/student-administration.md +++ b/docs/features/student-administration.md @@ -88,8 +88,8 @@ All actions are nonce-protected POSTs handled on the detail page: `tests/Unit/Payment/PaymentRepositoryTest.php` ## Family Relationships -The students list gains a **Family** column — a child links to their guardian, -a guardian lists their children — and the student screen a **Family** panel. A +The students list gains a **Profile** column — a child links to their guardian, +a guardian lists their children — and the student screen a **Profile** panel. A child's listed email is their guardian's, since a child's own address is an undeliverable placeholder, and the credit balance shown is the payer's, labelled with whose account holds it. See `parent-guardian-accounts.md`. diff --git a/src/Auth/RegistrationPage.php b/src/Auth/RegistrationPage.php index d2f3c6c..5f1e85a 100644 --- a/src/Auth/RegistrationPage.php +++ b/src/Auth/RegistrationPage.php @@ -286,14 +286,14 @@ class RegistrationPage { // Everything is validated before a single user is created, so a bad child // block never leaves a half-registered family behind. if ( $isGuardian && [] === $children ) { - return esc_html__( 'Please add at least one child, or uncheck the parent/guardian option.', 'unsupervised-schedular' ); + return esc_html__( 'Please add at least one student, or uncheck the parent/guardian option.', 'unsupervised-schedular' ); } foreach ( $isGuardian ? array_column( $children, 'answers' ) : [ $answers ] as $set ) { foreach ( $accountQuestions as $question ) { if ( $question->isRequired && '' === trim( (string) ( $set[ (int) $question->id ] ?? '' ) ) ) { return $isGuardian - ? esc_html__( 'Please answer all required registration questions for each child.', 'unsupervised-schedular' ) + ? esc_html__( 'Please answer all required registration questions for each student.', 'unsupervised-schedular' ) : esc_html__( 'Please answer all required registration questions.', 'unsupervised-schedular' ); } } diff --git a/src/BlockPreview.php b/src/BlockPreview.php index cbe11cc..2457500 100644 --- a/src/BlockPreview.php +++ b/src/BlockPreview.php @@ -189,16 +189,16 @@ class BlockPreview { '

%s

' . '

' . '

', - esc_html__( 'Add a child', 'unsupervised-schedular' ), + esc_html__( 'Add a student', 'unsupervised-schedular' ), esc_html__( 'Name', 'unsupervised-schedular' ), esc_html__( 'Date of birth', 'unsupervised-schedular' ), - esc_html__( 'Add child', 'unsupervised-schedular' ) + esc_html__( 'Add student', 'unsupervised-schedular' ) ); return sprintf( '
%s

%s

    %s
%s
', - self::note( __( 'Editor preview — signed-in guardians see and manage their own children here.', 'unsupervised-schedular' ) ), - esc_html__( 'Your family', 'unsupervised-schedular' ), + self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ), + esc_html__( 'Your profile', 'unsupervised-schedular' ), $children, $add ); diff --git a/src/Guardian/ChildLoginGate.php b/src/Guardian/ChildLoginGate.php index 58d3509..e60bc8c 100644 --- a/src/Guardian/ChildLoginGate.php +++ b/src/Guardian/ChildLoginGate.php @@ -35,7 +35,7 @@ class ChildLoginGate { if ( $user instanceof \WP_User && GuardianService::isChild( (int) $user->ID ) ) { return new \WP_Error( 'us_child_account', - esc_html__( 'This is a child account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' ) + esc_html__( 'This is a managed student account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' ) ); } diff --git a/src/Guardian/FamilyPage.php b/src/Guardian/FamilyPage.php index e93543f..9805662 100644 --- a/src/Guardian/FamilyPage.php +++ b/src/Guardian/FamilyPage.php @@ -50,7 +50,7 @@ class FamilyPage { '

%s %s.

', esc_html__( 'Please', 'unsupervised-schedular' ), esc_url( $this->loginUrl( $loginPageId ) ), - esc_html__( 'log in to manage your family', 'unsupervised-schedular' ) + esc_html__( 'log in to manage your profile', 'unsupervised-schedular' ) ); } @@ -168,7 +168,7 @@ class FamilyPage { private function firstMissingAnswer( array $questions, array $answers ): ?\WP_Error { foreach ( $questions as $question ) { if ( $question->isRequired && '' === trim( (string) ( $answers[ (int) $question->id ] ?? '' ) ) ) { - return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this child.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this student.', 'unsupervised-schedular' ) ); } } @@ -237,9 +237,9 @@ class FamilyPage { */ private function noticeFor( string $result ): string { return match ( $result ) { - self::RESULT_ADDED => __( 'Child added.', 'unsupervised-schedular' ), + self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ), self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ), - self::RESULT_REMOVED => __( 'Child removed.', 'unsupervised-schedular' ), + self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ), default => '', }; } diff --git a/src/Guardian/GuardianService.php b/src/Guardian/GuardianService.php index 315d7f0..3ff8282 100644 --- a/src/Guardian/GuardianService.php +++ b/src/Guardian/GuardianService.php @@ -51,7 +51,7 @@ class GuardianService { public function createChild( int $guardianId, string $name, string $dateOfBirth = '', string $relationship = '' ): int|\WP_Error { $name = trim( $name ); if ( '' === $name ) { - return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) ); } $email = $this->childEmail(); @@ -89,7 +89,7 @@ class GuardianService { if ( $linkId <= 0 ) { $this->deleteUser( $userId ); - return new \WP_Error( 'link_failed', __( 'Could not add this child. Please contact the studio.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'link_failed', __( 'Could not add this student. Please contact the studio.', 'unsupervised-schedular' ) ); } return $userId; @@ -104,12 +104,12 @@ class GuardianService { */ public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): ?\WP_Error { if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) { - return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) ); } $name = trim( $name ); if ( '' === $name ) { - return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) ); } $result = wp_update_user( @@ -139,13 +139,13 @@ class GuardianService { */ public function removeChild( int $guardianId, int $studentId ): ?\WP_Error { if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) { - return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) ); } if ( [] !== $this->bookings->findByStudent( $studentId ) || [] !== $this->enrollments->findByStudent( $studentId ) ) { return new \WP_Error( 'has_history', - __( 'This child has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' ) + __( 'This student has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' ) ); } diff --git a/templates/admin/student-detail.php b/templates/admin/student-detail.php index dd65952..94b799e 100644 --- a/templates/admin/student-detail.php +++ b/templates/admin/student-detail.php @@ -110,7 +110,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void -

+

add_query_arg(['page' => $pageSlug, 'student_id' => $id], admin_url('admin.php')); ?>

@@ -123,7 +123,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void ); ?>

-

+

@@ -293,7 +293,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void diff --git a/templates/admin/students.php b/templates/admin/students.php index c9e4cfe..b2ddd81 100644 --- a/templates/admin/students.php +++ b/templates/admin/students.php @@ -25,7 +25,7 @@ $familyCell = static function (array $student) use ($pageSlug): string { if ($student['guardian'] !== null) { return sprintf( /* translators: %s: linked name of the parent/guardian who books for this student. */ - esc_html__('Child of %s', 'unsupervised-schedular'), + esc_html__('Managed by %s', 'unsupervised-schedular'), $link($student['guardian']['id'], $student['guardian']['name']) ); } @@ -51,7 +51,7 @@ $familyCell = static function (array $student) use ($pageSlug): string { - + diff --git a/templates/frontend/family-page.php b/templates/frontend/family-page.php index b0658f8..466ed63 100644 --- a/templates/frontend/family-page.php +++ b/templates/frontend/family-page.php @@ -16,7 +16,7 @@ if (! defined('ABSPATH')) { */ ?>
-

+

@@ -27,7 +27,7 @@ if (! defined('ABSPATH')) { -

+

    @@ -74,7 +74,7 @@ if (! defined('ABSPATH')) { -

    +

    @@ -90,7 +90,7 @@ if (! defined('ABSPATH')) {

    - +

    - +

diff --git a/templates/frontend/register-page.php b/templates/frontend/register-page.php index 2db52c0..6a756e2 100644 --- a/templates/frontend/register-page.php +++ b/templates/frontend/register-page.php @@ -75,18 +75,18 @@ if (! defined('ABSPATH')) {

-

+

- +

- +

@@ -107,7 +107,7 @@ if (! defined('ABSPATH')) {

- +

diff --git a/tests/Unit/Auth/RegistrationPageTest.php b/tests/Unit/Auth/RegistrationPageTest.php index 1d1b1a8..e0b1ecd 100644 --- a/tests/Unit/Auth/RegistrationPageTest.php +++ b/tests/Unit/Auth/RegistrationPageTest.php @@ -667,7 +667,7 @@ class RegistrationPageTest extends TestCase $result = $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false); - self::assertStringContainsString('at least one child', $result); + self::assertStringContainsString('at least one student', $result); } /** @@ -694,7 +694,7 @@ class RegistrationPageTest extends TestCase Functions\expect('wp_insert_user')->never(); $this->ctx['guardians']->shouldNotReceive('createChild'); - self::assertStringContainsString('for each child', $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false)); + self::assertStringContainsString('for each student', $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false)); } /** diff --git a/tests/Unit/Guardian/FamilyPageTest.php b/tests/Unit/Guardian/FamilyPageTest.php index b7f183b..3730484 100644 --- a/tests/Unit/Guardian/FamilyPageTest.php +++ b/tests/Unit/Guardian/FamilyPageTest.php @@ -86,7 +86,7 @@ class FamilyPageTest extends TestCase $html = $this->page->render([]); - self::assertStringContainsString('log in to manage your family', $html); + self::assertStringContainsString('log in to manage your profile', $html); } public function testRenderListsTheGuardiansChildren(): void @@ -100,7 +100,7 @@ class FamilyPageTest extends TestCase self::assertStringContainsString('Ada', $html); self::assertStringContainsString('2015-04-02', $html); - self::assertStringContainsString('Add a child', $html); + self::assertStringContainsString('Add a student', $html); } public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void @@ -161,7 +161,7 @@ class FamilyPageTest extends TestCase $_POST = ['us_family_action' => 'add', 'child_name' => '']; $this->questions->shouldReceive('findByScope')->once()->andReturn([]); - $this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each child a name.')); + $this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each student a name.')); $this->answers->shouldNotReceive('insert'); $captured = null; @@ -207,7 +207,7 @@ class FamilyPageTest extends TestCase $_POST = ['us_family_action' => 'remove', 'child_id' => '42']; $this->guardians->shouldReceive('removeChild')->once()->andReturn( - new \WP_Error('has_history', 'This child has lessons or enrolments on record.') + new \WP_Error('has_history', 'This student has lessons or enrolments on record.') ); $captured = null; @@ -271,6 +271,6 @@ class FamilyPageTest extends TestCase $this->guardians->shouldReceive('children')->andReturn([]); $this->questions->shouldReceive('findByScope')->andReturn([]); - self::assertStringContainsString('Child added.', $this->page->render([])); + self::assertStringContainsString('Student added.', $this->page->render([])); } } -- 2.54.0