AkiraAkira.dev
4 min read

The library that came out of an app

For three days @akira-io/ui shipped on npm and answered Semana anterior to people who had never asked for Portuguese. That is what extraction costs.

also in FR PT

Forty-four strings in @akira-io/ui defaulted to Portuguese. The combobox, the confirm dialog, the faceted filter, the command palette, the tour, and every label in the date filter. Two of them had no override at all, so a consumer could not fix them from outside even if they wanted to. That is not a translation gap. It is a birthmark.

The package has one because it was never written as a package. Its first commit is 122 files and 13,055 lines, none of them typed that day, under a message that does not pretend otherwise: chore: import component library from nosferry-ui. A components folder living inside a Laravel application moved out because a second application needed the same thing.

A library extracted from a working app reaches version 1 with problems solved that a greenfield package would not have found yet. The bill is that the app comes attached.

What the app hands over

78 components. 30 test files. That is not writing speed, it is the inventory of something already in production.

And not only components. src/shells came along, the page arrangements the app used. So did src/blocks, whole compositions rather than primitives. So did src/editor.ts, with TipTap underneath it. A package started from nothing writes Button in week one and reaches a rich text editor in November, if it reaches it. This one had the editor before it had its own name.

Having components was never the hard part of a component library. Knowing which ones a real application asks for is, and that answer only exists once the application stops being hypothetical.

The part that stays

The package exports src/inertia.ts, src/inertia-tour-progress.ts and src/inertia-table-filters.ts. A generic React library has none of this. It exists because the code grew up inside a Laravel app running Inertia, where table filters had to survive a round trip to the backend.

That surface was kept on purpose, and marked: @inertiajs/react is an optional peer dependency. Install the package in a plain React app and nothing about it is visible. Install it in an Inertia app and the adapter you were about to hand-write is already there.

Inherited and chosen look identical in a diff. The Portuguese defaults and the Inertia exports arrived in the same commit, in the same 13,055 lines. Separating them was the whole job of the first month.

Four breaking changes in thirty-three days

Version 1.0.0 landed on 3 August, three days after the import, carrying four breaking changes. One of them was default every user-facing string to english.

The Portuguese survived an earlier cleanup that declared the file done, and the reason is worth stating plainly:

// src/blocks/date-filter/types.ts, before 1.0.0
{ value: 'today', label: 'Hoje' },
{ value: 'yesterday', label: 'Ontem' },
{ value: 'previous_week', label: 'Semana anterior' },
{ value: 'previous_month', label: 'Mês anterior' },

The test in src/blocks/date-filter/date-filter.test.ts asserted those labels, overriding one of them with all: 'Sempre'. The suite was green because it was guarding the defect. A test written inside an application encodes that application’s assumptions, and it keeps encoding them after the folder moves house.

Today the Portuguese lives in src/locales/pt.ts as a typed object next to fr.ts, a new label cannot ship without a translation, and the test asserts the English defaults with a parallel block proving the locale still produces the old wording.

The obvious objection is that starting from the package would have avoided all of it. It would also have produced 78 components nobody had used. A tour with persisted progress, table filters that outlive a redirect, collapsible page shells: none of those APIs come out right on the first guess without an application complaining first. The app was the acceptance test, and it ran for months before there was a version 1.

Four breaking changes in thirty-three days reads as instability. It is the opposite. Breaking is cheap in the first month and expensive in the sixth, and that window was used while it was open. Version 2.4.0 shipped on 2 September and broke nothing.

Extraction does not give you a library. It gives you an app wearing a package name, and a short, loud month to tell the two apart.

share