fix: ActivitiesCard crashes — t() in a prop default has no scope - #231
Conversation
Part of donetick#145. Narrowed version of this PR, as announced in donetick#145 and donetick#168. The original diff covered twelve files; six were rewritten under me since — the modal unification, donetick#186, and the AddTaskModal autocomplete work. This reduces the PR to the seven files still untouched on `develop`: AssigneePickerField, AttachmentPickerField, DueDatePickerField, LabelsPickerField, ProjectPickerField, SmartTaskTitleInput, SubTask The rewritten half goes back in the queue behind your work, to be re-extracted against the current shape rather than replayed. Rebased on current `develop` after donetick#210 landed — the conflict was in the dictionaries only, and it shrank as a result: 20 keys to `en/chores.json` and 1 to `en/common.json`, since donetick#210 already carried the rest. AttachmentPickerField is re-extracted on top of your z-index refactor (462af85) rather than carried over; my branch predates it and still imported the removed `constants/zIndex`. English only — no translations, no behaviour change; every t() value is checked to appear character-for-character in the code it replaces (23 call sites). Also run through `eslint --rule no-undef`, which is what I should have been doing before donetick#216 and donetick#210 — see donetick#231.
`ActivitiesCard` takes its title as `({ title = t('activity.title') })`.
A default parameter is evaluated in the function's own scope, where `t`
does not exist: the only `t` in the file is bound inside `ActivityItem`,
a separate component. `Sidepanel` renders the card without a `title`, and
`activities` is `enabled: true` in `DEFAULT_SIDEPANEL_CONFIG`, so the
default is always evaluated on a desktop-width screen.
The hook moves into the component body and the title falls back there
instead. Same rendered output; three render sites now read `displayTitle`.
This is my regression: it arrived with donetick#216, which described itself as
"no behaviour change". It was not caught because a bundler cannot flag it
— an unbound `t` is indistinguishable from a global — and my checks only
verified that keys and imports existed, not that `t` was in scope. It is
visible in the built bundle: `({title:e=t("activity.title")})` keeps the
literal `t` because the minifier cannot rename a free variable, while a
working call nearby minifies to `q=e("common.confirm")`.
I have added an eslint `no-undef` pass to my own pipeline and run it
before sending anything from now on.
…eral
`CHORE_STATUS` is a module-level object literal, and a `useTranslation`
call ended up inside it:
const CHORE_STATUS = {
const { t } = useTranslation('chores')
NO_DUE_DATE: 'No due date',
That is a syntax error, so the file cannot be parsed at all. It does not
break the build because nothing imports `ChoresOverview` — vite never
compiles it — but it breaks eslint, editors, and anything else that walks
the whole tree.
The hook moves into the component, which is where the file's `t()` calls
actually live.
This one arrived with donetick#210, the same way donetick#216 brought the ActivitiesCard
crash: my tooling wrote the hook in mechanically and nothing downstream
parsed the result. Both are now covered by the `no-undef` pass I described
in the other commit — it reports the parse error too.
2ae6802 to
64aca50
Compare
|
Added a second fix to this PR — same root cause, found while rebasing my
const CHORE_STATUS = {
const { t } = useTranslation('chores')
NO_DUE_DATE: 'No due date',It doesn't break the build because nothing imports that file, so vite never So that's two defects from my extraction PRs: a runtime crash from #216 and I'd rather you knew the count than found the second one yourself later. |
ActivitiesCardtakes its title as({ title = t('activity.title') }).A default parameter is evaluated in the function's own scope, where
tdoes not exist: the only
tin the file is bound insideActivityItem,a separate component.
Sidepanelrenders the card without atitle, andactivitiesisenabled: trueinDEFAULT_SIDEPANEL_CONFIG, so thedefault is always evaluated on a desktop-width screen.
The hook moves into the component body and the title falls back there
instead. Same rendered output; three render sites now read
displayTitle.This is my regression: it arrived with #216, which described itself as
"no behaviour change". It was not caught because a bundler cannot flag it
— an unbound
tis indistinguishable from a global — and my checks onlyverified that keys and imports existed, not that
twas in scope. It isvisible in the built bundle:
({title:e=t("activity.title")})keeps theliteral
tbecause the minifier cannot rename a free variable, while aworking call nearby minifies to
q=e("common.confirm").I have added an eslint
no-undefpass to my own pipeline and run itbefore sending anything from now on.
How to see it
Sidepanelrenders the card without atitle, andactivitiesisenabled: trueinDEFAULT_SIDEPANEL_CONFIG, so any logged-in user on ascreen wider than
lghits it.It is also visible without running anything. Build
developand grep thebundle:
The translator is the bare letter
t— the minifier cannot rename a freevariable, so it leaves it as written. A working call a few lines away
minifies to
q=e("common.confirm"), whereeis the boundtfrom theenclosing hook. That difference is the whole bug.