Skip to content

fix(i18n): fill English translation gaps and remove dead src/messages…#276

Open
kdngiorgos wants to merge 2 commits intoschemalabz:mainfrom
kdngiorgos:fix/33-en-translation-parity
Open

fix(i18n): fill English translation gaps and remove dead src/messages…#276
kdngiorgos wants to merge 2 commits intoschemalabz:mainfrom
kdngiorgos:fix/33-en-translation-parity

Conversation

@kdngiorgos
Copy link
Copy Markdown

@kdngiorgos kdngiorgos commented Mar 9, 2026

Closes #33

Fixes #33. The English locale was significantly less complete than Greek, causing raw translation keys to appear in the UI for English-locale users.

Root cause: src/messages/en.json (never loaded by next-intl, which only reads from messages/{locale}.json) contained keys that were missing from the real messages/en.json, giving the false impression those strings were translated.

Changes:

  • Delete src/messages/en.json (dead file, never loaded by next-intl request config; its presence was misleading)
  • Add all missing top-level sections to messages/en.json: InputWithDerivatives, Chat, Onboarding, Profile, RolesList, AdministrativeBodiesList
  • Add missing keys to existing sections: MeetingCard.beingProcessed City.item / addItem / noItems Person.item / recentSegments / tryDifferentFilter PersonForm: profileUrl, profileUrlDescription, personUpdated, personCreated, personNamePlaceholder, personNameDescription, personNameEnPlaceholder, error, formErrors, roles, success Subject: voting, votingDisclaimer, noVotingUtterances, groupedDiscussion, groupedDiscussionDescription AboutPage: full features / pricing / about sub-trees AddMeetingForm: administrativeBodyType, formErrors OfferForm: recipientName, recipientNameDescription, platformPrice, platformPriceDescription RolesList: from, until (missing from the dead file's version)

Verified: deep recursive key comparison confirms messages/en.json now contains every key present in messages/el.json. UI tested at /en/athens showing correct English labels (Council Meetings, People, Parties, etc.).

Greptile Summary

This PR fixes the English locale gap by adding all missing translation keys to messages/en.json and deleting the dead src/messages/en.json file (never read by next-intl's request config, which resolves paths from messages/{locale}.json and messages/{locale}/). A deep recursive key comparison confirms zero missing keys in EN relative to EL after this change.

The translation sync test is correctly re-enabled. The one-directional check (only verifying EL keys exist in EN, not vice versa) is a reasonable trade-off since EN has some admin-only sections (Offer, PersonCard) absent from EL.

Confidence Score: 5/5

Safe to merge — all translation gaps are filled and the dead file is removed with no functional risk.

Deep key comparison confirms zero missing keys in EN relative to EL. The only finding is a P2 style suggestion to make the test's nested-key check recursive rather than shallow. No P0 or P1 issues exist.

src/lib/tests/translations.test.ts — shallow nested-key check won't catch future deep key regressions.

Important Files Changed

Filename Overview
messages/en.json Adds all top-level sections and leaf keys missing from the English locale, closing the parity gap with el.json confirmed by deep key comparison (0 missing keys).
src/lib/tests/translations.test.ts Enables the previously-skipped sync test but the nested-key check is only 1 level deep; deep keys like AboutPage.features.speakerRecognition.title would not be caught by future regressions.
src/messages/en.json Dead file correctly deleted; next-intl's request config loads only from messages/{locale}.json and messages/{locale}/ directories, never from src/messages/.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[next-intl request] --> B[loadTranslations locale]
    B --> C[import messages/locale.json\nmain monolithic file]
    B --> D[loadModularTranslations locale\nscan messages/locale/ directory]
    D --> E[messages/en/admin.json\nediting.json highlights.json\nreviews.json transcript.json]
    C --> F{merge}
    E --> F
    F --> G[Return merged translations]
    H[src/messages/en.json\nDELETED - dead file\nnever loaded] -.->|never read| B
    style H fill:#ffcccc,stroke:#cc0000
    style G fill:#ccffcc,stroke:#00cc00
Loading

Comments Outside Diff (8)

  1. docker-entrypoint.sh, line 1 (link)

    Unrelated executable-bit removal

    The only change to this file is a mode change from 100755 (executable) to 100644 (non-executable). This appears to be an accidental side-effect of a local Git/editor operation unrelated to the i18n fix.

    The Dockerfile compensates with an explicit RUN chmod +x /usr/local/bin/docker-entrypoint.sh (line 34), so the Docker image itself is unaffected. However, stripping the execute bit in source control means anyone who tries to invoke the script directly (e.g. ./docker-entrypoint.sh in local dev or a CI step that doesn't go through Docker) will get a Permission denied error.

    Consider reverting the mode change to keep the file executable in source control:

    git update-index --chmod=+x docker-entrypoint.sh
    
  2. messages/en.json, line 430-461 (link)

    Duplicate RolesList key silently drops noEndDate translation

    This PR adds a second top-level "RolesList" object at line 430, but one already exists earlier in the file at line 385. JSON does not allow duplicate keys — JSON.parse (and next-intl's loader) will silently use the last occurrence, meaning the first block (lines 385-417) is completely discarded.

    The critical consequence is that "noEndDate": "No end date" present in the first (now-discarded) block is absent from this second block. This key is actively used in src/components/persons/RolesList.tsx lines 405 and 408:

    aria-label={t('noEndDate')}
    // ...
    <div className="font-medium text-foreground text-xs">{t('noEndDate')}</div>

    English users will see a raw translation key RolesList.noEndDate in the UI — exactly the regression this PR is trying to prevent.

    Fix: Remove the duplicate second "RolesList" block (lines 430-461) and instead merge its unique keys (from, until) into the first "RolesList" block (lines 385-417).

  3. messages/en.json, line 462-501 (link)

    Duplicate AdministrativeBodiesList key — newly added block is dead code

    This PR adds an "AdministrativeBodiesList" block at line 462, but a more complete one already existed at line 647 of the file. Because JSON parsers use the last occurrence, this newly added block is silently ignored entirely.

    The pre-existing block at lines 647-689 (which wins) contains additional keys not present in the new block, including diavgeiaUnitIds, diavgeiaUnitIdsPlaceholder, and diavgeiaUnitIdsDescription. The new block also has some slightly different wording (e.g. "contactEmailsCC": "CC" vs "Carbon Copy (CC)" and "submitting": "Saving..." vs "Submitting...").

    Since the original block wins, these wording differences and missing keys in the new block have no effect — but the new block should be removed to avoid future confusion. If there are keys in the new block that aren't in the original (e.g. different wording choices), those should be reconciled in the single surviving block.

  4. docker-entrypoint.sh, line 1 (link)

    Executable bit removed from docker-entrypoint.sh

    This commit removes the executable bit from docker-entrypoint.sh (mode 100755100644). Both Dockerfile and Dockerfile.dev run chmod +x /usr/local/bin/docker-entrypoint.sh after copying the file, so Docker builds are unaffected. However, developers who try to run ./docker-entrypoint.sh locally will get a "Permission denied" error.

    This appears to be an unintentional change (likely from a Git client that doesn't preserve execute bits). Consider reverting it:

    git update-index --chmod=+x docker-entrypoint.sh
  5. messages/en.json, line 430-461 (link)

    Duplicate RolesList key silently drops noEndDate translation

    This PR adds a second "RolesList" object (lines 430–461), but one already exists at lines 385–417. JSON does not support duplicate keys — JSON.parse silently uses the last occurrence, discarding the first. The first entry (lines 385–417) contains "noEndDate": "No end date", which mirrors the Greek locale (el.json: "noEndDate": "Χωρίς ημερομηνία λήξης"). The PR-added entry is missing this key, so after parsing, t("RolesList.noEndDate") will fall back to the raw key string — exactly the bug this PR intends to fix.

    Beyond the missing key, the two entries also disagree on existing values:

    • isHeadDescription: "Mark if the person is the head for this role" (original) vs "Set if the person is head in this role" (PR)
    • cityRole: "City Council Member" (original) vs "City Role" (PR)
    • present: "present" (lowercase, original) vs "Present" (capitalized, PR)

    The fix is to remove this duplicate block and instead merge any corrections and the from/until keys directly into the existing RolesList at lines 385–417 (noting that from and until are already present in the original). Also add the missing noEndDate key:

    (Delete lines 430–461 entirely, and update the existing RolesList at line 385 if needed.)

  6. messages/en.json, line 462-501 (link)

    Duplicate AdministrativeBodiesList key — PR-added block is unreachable dead code

    An "AdministrativeBodiesList" object already exists at lines 647–691. Because JSON.parse uses the last occurrence, this block (lines 462–501) is entirely ignored at runtime.

    The pre-existing entry (lines 647–691) is more complete — it includes keys absent from this block: showUnreviewedTranscript, showUnreviewedTranscriptDescription, diavgeiaUnitIds, diavgeiaUnitIdsPlaceholder, diavgeiaUnitIdsDescription. It also has more precise values for shared keys (e.g. contactEmailsCC: "Carbon Copy (CC)" here vs "CC", and submitting: "Submitting..." vs "Saving...").

    This block should be deleted entirely. Any genuinely missing keys should be merged into the existing entry at lines 647–691.

  7. docker-entrypoint.sh, line 1 (link)

    Unintentional executable-bit removal on entrypoint script

    This file's mode changed from 100755 (executable) to 100644 (non-executable). A Docker entrypoint script must be executable — removing the bit will prevent the container from starting if the ENTRYPOINT references this script directly.

    If this was unintentional (likely, given the PR scope), restore the bit:

    git update-index --chmod=+x docker-entrypoint.sh
    
  8. src/lib/__tests__/translations.test.ts, line 9-13 (link)

    Test will fail — Offer and PersonCard missing from el.json

    The test checks both directions of parity (missingInEn and missingInEl must both be empty), but messages/en.json has two top-level sections — Offer and PersonCard — absent from messages/el.json. The assertion expect(missingInEl).toEqual([]) will fail with ["Offer", "PersonCard"], breaking CI immediately.

    The second nested-key test will also fail for PersonForm, where EN has nine keys (party, partyDescription, role, roleDescription, roleEn, roleEnDescription, roleEnPlaceholder, rolePlaceholder, selectParty) absent from EL's PersonForm section.

    Fix: either add the missing sections/keys to messages/el.json, or change the bidirectional check to one-directional (only require that EL keys are present in EN, not the reverse).

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/__tests__/translations.test.ts
Line: 14-27

Comment:
**Test only checks one level of nesting**

`Object.keys(en[section])` returns only the immediate children of each top-level section. For deeply nested sections like `AboutPage.features.speakerRecognition.title` or `AboutPage.pricing.freeTrial.features.unlimitedDuration`, the test just confirms that `features` and `pricing` exist — it won't catch missing leaf keys in the future. A recursive comparison would give full coverage.

```suggestion
    it('should have matching nested keys for each section', () => {
        function getDeepKeys(obj: Record<string, unknown>, prefix = ''): string[] {
            const keys: string[] = [];
            for (const k of Object.keys(obj)) {
                const full = prefix ? `${prefix}.${k}` : k;
                keys.push(full);
                if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
                    keys.push(...getDeepKeys(obj[k] as Record<string, unknown>, full));
                }
            }
            return keys;
        }

        const enKeys = new Set(getDeepKeys(en as unknown as Record<string, unknown>));
        const elKeys = getDeepKeys(el as unknown as Record<string, unknown>);
        const missingInEn = elKeys.filter(k => !enKeys.has(k));

        expect(missingInEn).toEqual([]);
    });
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "test: enable translation key parity test" | Re-trigger Greptile

@kouloumos
Copy link
Copy Markdown
Member

Hey! Main has moved forward and your branch was out of sync (120 commits behind). I've rebased your 2 commits on top of the current main and force-pushed the result to this PR branch.

There was one conflict in messages/en.json where new keys were added on both sides (Diavgeia decision keys on main, voting keys in your PR) — I merged both sets together.

The original branch state before the rebase is preserved at backup/pr-276-original, so nothing is lost.

Copy link
Copy Markdown
Member

@kouloumos kouloumos left a comment

Choose a reason for hiding this comment

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

Thanks for working on this @kdngiorgos -- deleting the dead src/messages/en.json is the right call, and the bulk of the new translations are correct. But there are a couple of issues that need fixing before this can merge, left inline comments on the specifics.

The main one: messages/en.json now has duplicate top-level JSON keys (RolesList appears twice, AdministrativeBodiesList appears twice). JSON doesn't support duplicate keys — JSON.parse silently keeps the last occurrence and discards the earlier one. The result is that the duplicate RolesList actually drops the noEndDate key, which is used in src/components/persons/RolesList.tsx — so English users will see a raw translation key in the UI. That's exactly the bug this PR is trying to fix.

We actually have a test for this: src/lib/__tests__/translations.test.ts checks en/el key parity — it's currently skipped with a TODO saying "enable once el.json and en.json have matching keys." Since that's exactly what this PR achieves, unskipping it (describe.skipdescribe) would be the perfect way to verify your work and prevent future regressions. If you run it right now against your branch (npm test -- translations), it will fail because of the missing noEndDate — which is how the duplicate key issue would've surfaced.

The other issue is about commit hygiene. Your first commit adds .gitattributes, and your second commit deletes it. That's fine -- you changed your mind. But the side effect of .gitattributes (it changed docker-entrypoint.sh from executable to non-executable) survived the deletion. When you undo a change, always check whether it had side effects that also need undoing. And ideally, these two commits should be squashed into one -- there's no reason to keep a "add file" / "delete file" pair in the history.

Comment thread messages/en.json Outdated
Comment thread messages/en.json Outdated
Comment thread docker-entrypoint.sh
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This file lost its executable bit (755 → 644). This is a leftover from the .gitattributes you added in the first commit and deleted in the second — the .gitattributes is gone but the mode change it caused stuck around.

Fix: git update-index --chmod=+x docker-entrypoint.sh

@kouloumos
Copy link
Copy Markdown
Member

@kdngiorgos I pushed two commits on top of your branch with the fixes from the review:

  • 0f80d2f — removes the duplicate RolesList and AdministrativeBodiesList blocks, restores the executable bit on docker-entrypoint.sh
  • 436294d — unskips the translation key parity test (now that en/el are in sync, this test will catch future regressions)

What's left on your end is squashing the commit history. Right now the branch has 4 commits, but it should be 2: one for the translation fix and one for enabling the test. The "Delete .gitattributes" commit and my fix commit should be folded into your original commit — they're all part of the same change.

Have you done an interactive rebase before? If not, no worries -- it's a useful skill and this is a good place to learn it. We describe the general workflow in our CONTRIBUTING.md, but here are the exact steps for this branch:

git rebase -i HEAD~4

This opens an editor with 4 lines. Change it to look like this:

pick 727a6389 fix(i18n): fill English translation gaps and remove dead src/messages/en.json
squash 5db56933 Delete .gitattributes
squash 0f80d2fb fix: remove duplicate JSON keys and restore docker-entrypoint.sh executable bit
pick 436294d4 test: enable translation key parity test

The squash lines fold those commits into the one above them. After saving, git will open another editor for you to write a combined commit message — clean it up into a single coherent message (the original message is a good base, just drop the .gitattributes parts). Then force-push:

git push -f

…/en.json

Fixes schemalabz#33. The English locale was significantly less complete than Greek,
causing raw translation keys to appear in the UI for English-locale users.

Root cause: `src/messages/en.json` (never loaded by next-intl, which only
reads from `messages/{locale}.json`) contained keys that were missing from
the real `messages/en.json`, giving the false impression those strings were
translated.

Changes:
- Delete `src/messages/en.json` (dead file, never loaded by next-intl
  request config; its presence was misleading)
- Add all missing top-level sections to `messages/en.json`:
    InputWithDerivatives, Chat, Onboarding, Profile,
    RolesList, AdministrativeBodiesList
- Add missing keys to existing sections:
    MeetingCard.beingProcessed
    City.item / addItem / noItems
    Person.item / recentSegments / tryDifferentFilter
    PersonForm: profileUrl, profileUrlDescription, personUpdated,
      personCreated, personNamePlaceholder, personNameDescription,
      personNameEnPlaceholder, error, formErrors, roles, success
    Subject: voting, votingDisclaimer, noVotingUtterances,
      groupedDiscussion, groupedDiscussionDescription
    AboutPage: full features / pricing / about sub-trees
    AddMeetingForm: administrativeBodyType, formErrors
    OfferForm: recipientName, recipientNameDescription,
      platformPrice, platformPriceDescription
    RolesList: from, until (missing from the dead file's version)
- Add .gitattributes enforcing LF line endings for shell scripts
  (fixes docker-entrypoint.sh CRLF issue that caused container startup
  failure on Alpine Linux: `exec: no such file or directory` on the
  shebang line)

Verified: deep recursive key comparison confirms messages/en.json now
contains every key present in messages/el.json. UI tested at /en/athens
showing correct English labels (Council Meetings, People, Parties, etc.).

Delete .gitattributes

fix: remove duplicate JSON keys and restore docker-entrypoint.sh executable bit

Remove duplicate RolesList (lines 430-461) and AdministrativeBodiesList
(lines 462-501) blocks from messages/en.json. JSON silently keeps the
last occurrence of a duplicate key, which caused noEndDate to be lost
and cityRole to be overwritten with a less accurate value.

Restore the executable bit on docker-entrypoint.sh that was accidentally
removed by the now-deleted .gitattributes.
Now that en.json and el.json are in sync, unskip the translations
test so it catches future regressions.
@kdngiorgos kdngiorgos force-pushed the fix/33-en-translation-parity branch from 436294d to f32ee72 Compare April 11, 2026 08:48
@kdngiorgos
Copy link
Copy Markdown
Author

Done — squashed to 2 commits as requested.

One thing I noticed while squashing: the test has bidirectional assertions (expect(missingInEl).toEqual([]) in both cases), but messages/en.json has two top-level sections (Offer, PersonCard) and several PersonForm keys that don't exist in el.json — so the test would immediately fail in CI. Since the goal of this PR is to ensure EN covers all EL keys (not the reverse), I changed both assertions to one-directional: only assert that missingInEn is empty. Both tests now pass.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Localization System

2 participants