Skip to content

fix: normalize translator/illustrator/curator names (#93)#94

Open
fabiodalez-dev wants to merge 2 commits intomainfrom
fix/93-normalize-translators-illustrators
Open

fix: normalize translator/illustrator/curator names (#93)#94
fabiodalez-dev wants to merge 2 commits intomainfrom
fix/93-normalize-translators-illustrators

Conversation

@fabiodalez-dev
Copy link
Owner

@fabiodalez-dev fabiodalez-dev commented Mar 16, 2026

Summary

  • Apply AuthorNormalizer::normalize() to traduttore, illustratore, and curatore fields in BookRepository (create, update, and scraping paths)
  • Apply JavaScript normalizeAuthorName() to scraped translator/illustrator values in the book form

Previously, only author names were standardized (title case, "Surname, Name" → "Name Surname", particle handling for de/von/di/etc.). Translator, illustrator, and curator fields were saved as raw text, leading to inconsistent formatting.

Test plan

  • Create a book with translator "ROSSI, MARIO" → verify saved as "Mario Rossi"
  • Edit a book, set illustrator "VON HAGEN, VICTOR" → verify saved as "Victor von Hagen"
  • Import via ISBN scraping → verify translator/illustrator are normalized
  • Verify existing books with already-normalized names are not affected

Fixes #93

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Translator, illustrator, and curator names are consistently normalized when creating and updating book records.
    • Updates now respect soft-deleted records, preventing unintended modifications to deleted books.
  • User Interface

    • Book form inputs (visible and imported/scraped values) now show normalized author, translator, and illustrator names.
    • ISBN import/scrape flow applies the same normalization; deduplication and processing use normalized names for consistency.

Apply AuthorNormalizer::normalize() to traduttore, illustratore, and
curatore fields in BookRepository::createBasic(), updateBasic(), and
updateOptionals(). This ensures the same name standardization that
authors receive (title case, "Surname, Name" → "Name Surname",
particle handling for de/von/di/etc.) is also applied to these roles.

Also normalize scraped translator/illustrator values in the JavaScript
form using the existing normalizeAuthorName() function.

Fixes #93
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Apply consistent normalization of translator, illustrator, and curator names in the form (including ISBN/scrape paths) and repository create/update methods; also add AND deleted_at IS NULL to an UPDATE WHERE clause to respect soft deletes.

Changes

Cohort / File(s) Summary
Data Persistence
app/Models/BookRepository.php
Trim/cast and normalize traduttore, illustratore, and curatore via \App\Support\AuthorNormalizer::normalize in createBasic, updateBasic, and updateOptionals; normalize mapped/scraped translator/illustrator values; add AND deleted_at IS NULL to an UPDATE WHERE clause.
Form Input Processing
app/Views/libri/partials/book_form.php
Introduce a shared JS normalizeAuthorName(name) helper; replace inline per-field normalization with this helper for authors, traduttore, and illustratore when populating inputs and scraped fields (including ISBN import flow), and normalize before deduplication/processing.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User as Browser
    participant FormJS as "BookForm JS"
    participant Normalizer as "AuthorNormalizer"
    participant Repo as "BookRepository"
    participant DB as "Database"
    rect rgba(200,200,255,0.5)
    User->>FormJS: trigger ISBN import / fill form
    FormJS->>Normalizer: normalizeAuthorName(translator)
    FormJS->>Normalizer: normalizeAuthorName(illustrator)
    FormJS->>FormJS: populate inputs and scraped_* with normalized names
    end
    rect rgba(200,255,200,0.5)
    FormJS->>Repo: submit form data (normalized fields)
    Repo->>Normalizer: normalize(traduttore/illustratore/curatore) when present
    Repo->>DB: UPDATE/INSERT normalized fields (WHERE ... AND deleted_at IS NULL)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I nibbled commas, flipped each name,
From "Surname, First" to proper frame.
Translators, illustrators, tidy and neat,
Curators aligned—oh, what a feat!
A rabbit's hop for data complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: normalize translator/illustrator/curator names' clearly and concisely describes the main change in the changeset, matching the primary objective of applying name normalization to these author-related fields.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from issue #93 by applying AuthorNormalizer::normalize() to traduttore, illustratore, and curatore fields in BookRepository across create, update, and scraping workflows, achieving consistent name standardization.
Out of Scope Changes check ✅ Passed All changes are scoped to applying author name normalization to translator, illustrator, and curator fields in the repository layer and view layer, with no unrelated modifications detected outside the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/93-normalize-translators-illustrators
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Models/BookRepository.php`:
- Around line 998-1000: The updateOptionals() logic updates the libri table by
id but omits soft-delete filtering, allowing updates to soft-deleted rows;
update the database queries in updateOptionals() (and the similar update blocks
around the same code paths) so they include a condition limiting affected rows
to non-deleted records (deleted_at IS NULL or the framework's soft-delete
predicate) when targeting the libri table by id, ensuring only non-deleted libri
are modified.
- Around line 369-370: In createBasic() and updateBasic(), prevent TypeError by
only calling \App\Support\AuthorNormalizer::normalize when the incoming
traduttore value is a string and not empty; for example, where $val =
$data['traduttore'] ?? null replace the unconditional normalize call with a
guard that checks is_string($val) && $val !== '' and only then calls
AuthorNormalizer::normalize($val), otherwise pass the original $val (null or the
non-string) through — mirror the defensive pattern used in updateOptionals()
(which casts/guards) so non-string array payloads don't trigger normalize(string
$name).

In `@app/Views/libri/partials/book_form.php`:
- Around line 3731-3758: The translator and illustrator handlers reference
normalizeAuthorName which is currently declared inside the authors conditional
(authorsChoice ...) and thus can be undefined; move or hoist the
normalizeAuthorName function out of that conditional into a module/global scope
so it is available to the translator and illustrator blocks (and any other
callers), then update references in the translator (input[name="traduttore"],
scraped_translator) and illustrator (input[name="illustratore"],
scraped_illustrator) try-catch blocks to use the now-top-level
normalizeAuthorName; ensure no duplicate definitions remain inside the authors
conditional.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe152298-7d1b-42bd-a9a3-71e19a4c8635

📥 Commits

Reviewing files that changed from the base of the PR and between 54d3b9f and 892ed5f.

📒 Files selected for processing (2)
  • app/Models/BookRepository.php
  • app/Views/libri/partials/book_form.php

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
app/Views/libri/partials/book_form.php (1)

3731-3758: ⚠️ Potential issue | 🟠 Major

normalizeAuthorName can be undefined in translator/illustrator handlers.

At Line [3734] and Line [3750], normalizeAuthorName(...) is used outside the block where it is declared (Line [3404]). When that authors block doesn’t run, these calls fail and normalization is skipped.

🐛 Proposed fix
+            const normalizeRoleName = (name) => {
+                if (typeof normalizeAuthorName === 'function') {
+                    return normalizeAuthorName(name);
+                }
+                const value = (name || '').trim();
+                if (!value.includes(',')) return value;
+                const [surname, firstName] = value.split(',', 2).map((p) => p.trim());
+                return surname && firstName ? `${firstName} ${surname}` : value;
+            };
+
             // Handle translator (traduttore) — normalize same as authors
             try {
                 if (data.translator) {
-                    const normalized = normalizeAuthorName(data.translator);
+                    const normalized = normalizeRoleName(data.translator);
@@
             // Handle illustrator (illustratore) — normalize same as authors
             try {
                 if (data.illustrator) {
-                    const normalized = normalizeAuthorName(data.illustrator);
+                    const normalized = normalizeRoleName(data.illustrator);
#!/bin/bash
# Verify declaration scope vs usage for normalizeAuthorName in ISBN import flow
rg -n "const normalizeAuthorName|normalizeAuthorName\\(data\\.(translator|illustrator)\\)" app/Views/libri/partials/book_form.php
sed -n '3395,3765p' app/Views/libri/partials/book_form.php | nl -ba
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/libri/partials/book_form.php` around lines 3731 - 3758, The
translator and illustrator handlers call normalizeAuthorName (used with
data.translator and data.illustrator and applied to inputs named
"traduttore"/"illustratore" and IDs "scraped_translator"/"scraped_illustrator")
but that function is declared only inside the authors block, so it may be
undefined; fix by hoisting or moving the normalizeAuthorName declaration to a
shared scope above these handlers (or, alternatively, guard the calls with
typeof normalizeAuthorName === 'function' and fall back to using the raw value)
so both the translator and illustrator handlers can always call it safely.
app/Models/BookRepository.php (2)

983-1043: ⚠️ Potential issue | 🟠 Major

updateOptionals() update query still omits soft-delete filter.

At Line [1042], the UPDATE libri ... WHERE id=? statement can modify soft-deleted rows. This method should apply the same non-deleted predicate used elsewhere.

✅ Proposed fix
-        $sql = 'UPDATE libri SET ' . implode(', ', $set) . ', updated_at=NOW() WHERE id=?';
+        $sql = 'UPDATE libri SET ' . implode(', ', $set) . ', updated_at=NOW() WHERE id=? AND deleted_at IS NULL';

As per coding guidelines, "Every query on libri table MUST include soft-delete condition: AND deleted_at IS NULL.

#!/bin/bash
# Verify soft-delete predicate presence in updateOptionals query
rg -n "function updateOptionals|UPDATE libri SET|deleted_at IS NULL" app/Models/BookRepository.php -C3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 983 - 1043, The UPDATE in
updateOptionals builds $sql = 'UPDATE libri SET ... WHERE id=?' which can affect
soft-deleted rows; change the WHERE to include the soft-delete predicate (e.g.
'WHERE id=? AND deleted_at IS NULL') when building $sql in updateOptionals so it
matches other queries on the libri table, and ensure the parameter binding/order
for the id (types and $vals) remains correct after this change.

369-378: ⚠️ Potential issue | 🟠 Major

Guard role-name normalization to strings only.

At Line [370], Line [374], and Line [378], non-string payloads can hit AuthorNormalizer::normalize(...) under strict types. Also, Line [999], Line [1022], and Line [1025] currently coerce non-strings to text (e.g. arrays → "Array"), which can store invalid values.

🛡️ Proposed fix
         if ($this->hasColumn('traduttore')) {
             $val = $data['traduttore'] ?? null;
-            $addField('traduttore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : $val);
+            $val = is_string($val) ? trim($val) : null;
+            $addField('traduttore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : null);
         }
@@
         if ($this->hasColumn('illustratore')) {
             $val = $data['illustratore'] ?? null;
-            $addField('illustratore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : $val);
+            $val = is_string($val) ? trim($val) : null;
+            $addField('illustratore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : null);
         }
@@
         if ($this->hasColumn('curatore')) {
             $val = $data['curatore'] ?? null;
-            $addField('curatore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : $val);
+            $val = is_string($val) ? trim($val) : null;
+            $addField('curatore', 's', $val !== null && $val !== '' ? \App\Support\AuthorNormalizer::normalize($val) : null);
         }
#!/bin/bash
# Verify strict typing + normalize contract + current callsites
rg -n "declare\\(strict_types=1\\)|AuthorNormalizer::normalize\\(|\\['(traduttore|illustratore|curatore)'\\]" app/Models/BookRepository.php
rg -n "function normalize\\(" app/Support/AuthorNormalizer.php

Also applies to: 705-714, 998-1000, 1021-1025

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 369 - 378, The role-name
normalization calls (for keys 'traduttore', 'illustratore', 'curatore' inside
BookRepository methods that use $addField and hasColumn) must only pass string
values into AuthorNormalizer::normalize to avoid strict_types failures and
accidental coercion like "Array"; update each site (the blocks referencing
AuthorNormalizer::normalize and the earlier similar blocks around 705-714,
998-1000, 1021-1025) to guard with an is_string($val) check (and non-empty)
before calling AuthorNormalizer::normalize($val), otherwise leave $val as-is
(null or original non-string) so non-string payloads are not coerced or
normalized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/Models/BookRepository.php`:
- Around line 983-1043: The UPDATE in updateOptionals builds $sql = 'UPDATE
libri SET ... WHERE id=?' which can affect soft-deleted rows; change the WHERE
to include the soft-delete predicate (e.g. 'WHERE id=? AND deleted_at IS NULL')
when building $sql in updateOptionals so it matches other queries on the libri
table, and ensure the parameter binding/order for the id (types and $vals)
remains correct after this change.
- Around line 369-378: The role-name normalization calls (for keys 'traduttore',
'illustratore', 'curatore' inside BookRepository methods that use $addField and
hasColumn) must only pass string values into AuthorNormalizer::normalize to
avoid strict_types failures and accidental coercion like "Array"; update each
site (the blocks referencing AuthorNormalizer::normalize and the earlier similar
blocks around 705-714, 998-1000, 1021-1025) to guard with an is_string($val)
check (and non-empty) before calling AuthorNormalizer::normalize($val),
otherwise leave $val as-is (null or original non-string) so non-string payloads
are not coerced or normalized.

In `@app/Views/libri/partials/book_form.php`:
- Around line 3731-3758: The translator and illustrator handlers call
normalizeAuthorName (used with data.translator and data.illustrator and applied
to inputs named "traduttore"/"illustratore" and IDs
"scraped_translator"/"scraped_illustrator") but that function is declared only
inside the authors block, so it may be undefined; fix by hoisting or moving the
normalizeAuthorName declaration to a shared scope above these handlers (or,
alternatively, guard the calls with typeof normalizeAuthorName === 'function'
and fall back to using the raw value) so both the translator and illustrator
handlers can always call it safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4002068-28e7-4b88-8bf1-d11850cf284b

📥 Commits

Reviewing files that changed from the base of the PR and between 54d3b9f and 892ed5f.

📒 Files selected for processing (2)
  • app/Models/BookRepository.php
  • app/Views/libri/partials/book_form.php

- Guard AuthorNormalizer::normalize() with is_string() + trim() in
  createBasic() and updateBasic() to prevent TypeError from non-string
  payloads under strict_types=1
- Add AND deleted_at IS NULL to updateOptionals() UPDATE query per
  project soft-delete rule
- Hoist normalizeAuthorName() out of the authors conditional block so
  translator/illustrator handlers can always access it
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Models/BookRepository.php`:
- Around line 1004-1006: In updateOptionals(), avoid blindly string-casting
arbitrary payloads before calling \App\Support\AuthorNormalizer::normalize for
the roles ['traduttore','illustratore','curatore']; add the same guard used in
createBasic()/updateBasic() so you only normalize real non-empty string input
(e.g. check that $data[$c] is a string/scalar and that trim((string)$data[$c])
!== '' or equivalent) before assigning $cols[$c] =
AuthorNormalizer::normalize(...), otherwise skip setting the column; apply the
same guard to the other similar block at the region corresponding to lines
1027-1031.

In `@app/Views/libri/partials/book_form.php`:
- Around line 3398-3412: The normalizeAuthorName function currently only
reorders "Surname, Name" but doesn't apply the repository's casing/particle
rules; update normalizeAuthorName to mirror
App\Support\AuthorNormalizer::normalize by normalizing all-caps input to Title
Case, preserving and lowercasing name particles (e.g., "von", "de", "da") as
AuthorNormalizer does, and applying the same handling for initials and
multi-part surnames so imported values match saved repository state; locate the
normalizeAuthorName arrow function in the form script and either call a new
endpoint that runs AuthorNormalizer::normalize or reimplement the same
normalization rules (matching particle list and casing rules) inside
normalizeAuthorName.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b1e25ea5-dcc4-43be-8860-939fab5885af

📥 Commits

Reviewing files that changed from the base of the PR and between 54d3b9f and 95f7334.

📒 Files selected for processing (2)
  • app/Models/BookRepository.php
  • app/Views/libri/partials/book_form.php

Comment on lines +1004 to 1006
} elseif (in_array($c, ['traduttore', 'illustratore', 'curatore'], true)) {
$cols[$c] = \App\Support\AuthorNormalizer::normalize((string) $data[$c]);
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Apply the same guard in updateOptionals().

This path still string-casts arbitrary payloads before normalizing. That means whitespace-only values can end up as '', and malformed array payloads can still be persisted as "Array", even though createBasic() / updateBasic() now reject them.

🛠️ Proposed fix
-                } elseif (in_array($c, ['traduttore', 'illustratore', 'curatore'], true)) {
-                    $cols[$c] = \App\Support\AuthorNormalizer::normalize((string) $data[$c]);
+                } elseif (in_array($c, ['traduttore', 'illustratore', 'curatore'], true)) {
+                    $value = is_string($data[$c]) ? trim($data[$c]) : null;
+                    if ($value !== null && $value !== '') {
+                        $cols[$c] = \App\Support\AuthorNormalizer::normalize($value);
+                    }
                 } else {
                     $cols[$c] = $data[$c];
                 }
@@
-        if ($this->hasColumn('traduttore') && !isset($cols['traduttore']) && !empty($data['scraped_translator'])) {
-            $cols['traduttore'] = \App\Support\AuthorNormalizer::normalize((string) $data['scraped_translator']);
+        if ($this->hasColumn('traduttore') && !isset($cols['traduttore'])) {
+            $value = is_string($data['scraped_translator'] ?? null) ? trim($data['scraped_translator']) : '';
+            if ($value !== '') {
+                $cols['traduttore'] = \App\Support\AuthorNormalizer::normalize($value);
+            }
         }
-        if ($this->hasColumn('illustratore') && !isset($cols['illustratore']) && !empty($data['scraped_illustrator'])) {
-            $cols['illustratore'] = \App\Support\AuthorNormalizer::normalize((string) $data['scraped_illustrator']);
+        if ($this->hasColumn('illustratore') && !isset($cols['illustratore'])) {
+            $value = is_string($data['scraped_illustrator'] ?? null) ? trim($data['scraped_illustrator']) : '';
+            if ($value !== '') {
+                $cols['illustratore'] = \App\Support\AuthorNormalizer::normalize($value);
+            }
         }

Also applies to: 1027-1031

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 1004 - 1006, In
updateOptionals(), avoid blindly string-casting arbitrary payloads before
calling \App\Support\AuthorNormalizer::normalize for the roles
['traduttore','illustratore','curatore']; add the same guard used in
createBasic()/updateBasic() so you only normalize real non-empty string input
(e.g. check that $data[$c] is a string/scalar and that trim((string)$data[$c])
!== '' or equivalent) before assigning $cols[$c] =
AuthorNormalizer::normalize(...), otherwise skip setting the column; apply the
same guard to the other similar block at the region corresponding to lines
1027-1031.

Comment on lines +3398 to +3412
// Normalize person names: "Surname, Name" → "Name Surname" (shared by authors, translator, illustrator)
const normalizeAuthorName = (name) => {
name = (name || '').trim();
if (name.includes(',')) {
const parts = name.split(',', 2);
if (parts.length === 2) {
const surname = parts[0].trim();
const firstName = parts[1].trim();
if (surname && firstName) {
return firstName + ' ' + surname;
}
}
}
return name;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mirror AuthorNormalizer in the import helper.

normalizeAuthorName() only reorders Surname, Name. It still leaves all-caps input and particle casing untouched, so importing ROSSI, MARIO / VON HAGEN, VICTOR will still populate the form with MARIO ROSSI / VICTOR VON HAGEN. If you want the imported form state to match what the repository now saves, this helper needs the same normalization rules as App\Support\AuthorNormalizer::normalize().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/libri/partials/book_form.php` around lines 3398 - 3412, The
normalizeAuthorName function currently only reorders "Surname, Name" but doesn't
apply the repository's casing/particle rules; update normalizeAuthorName to
mirror App\Support\AuthorNormalizer::normalize by normalizing all-caps input to
Title Case, preserving and lowercasing name particles (e.g., "von", "de", "da")
as AuthorNormalizer does, and applying the same handling for initials and
multi-part surnames so imported values match saved repository state; locate the
normalizeAuthorName arrow function in the form script and either call a new
endpoint that runs AuthorNormalizer::normalize or reimplement the same
normalization rules (matching particle list and casing rules) inside
normalizeAuthorName.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standardisation of translators and illustrators

1 participant