Skip to content

feat: typed i18n token contract for data-source providers#32

Merged
Medformatik merged 25 commits into
mainfrom
feat/i18n-tokens-data-source-contract
May 28, 2026
Merged

feat: typed i18n token contract for data-source providers#32
Medformatik merged 25 commits into
mainfrom
feat/i18n-tokens-data-source-contract

Conversation

@Medformatik

Copy link
Copy Markdown
Collaborator

Summary

Replaces the string-as-key i18n workaround in the data-source detail panel with a typed I18nToken contract carried in the DataSourceDetail / DataSourceResult payloads. After this change, no locale-specific text crosses the data-source API boundary — providers emit locale-agnostic tokens that the client resolves against per-integration string catalogs plus a shared framework catalog.

Foundation (@openmapx/integration-framework/strings subpath):

  • New I18nToken type ({ $t: string, values?: Record<string, string|number> }), Translatable union, and isI18nToken guard.
  • resolveToken() pure resolver with lookup order integration catalog → framework catalog, ICU MessageFormat interpolation via intl-messageformat, and locale → fallback-locale chaining. Returns the bare key when unresolved (visible bug beats silent English leak).
  • token() helper + sharedT typed constants over a shared en/de vocabulary catalog (Source, Last Updated, Open, …).
  • check-translations extended to enforce en↔de parity across the framework catalog and every per-integration strings/ (85 catalogs).

Wiring:

  • /api/integrations now returns { integrations, frameworkStrings } (was a bare array); client consumers and the SSR fetcher updated.
  • Web: FrameworkStringsProvider + useDataSourceI18nResolver() hook; DataSourceSections.tsx lookup tables (ROW_LABEL_KEYS, SECTION_TITLE_KEYS, DETAIL_TEXT_KEYS, duration regexes) deleted in favour of a resolver walk.

Per-integration migrations (mappers now emit tokens at every label slot): parking (+ 3 tariff parsers), ev-charging, fuel, webcam (6 providers), bike/car/scooter-sharing (shared mobility-core mapper, ICU plurals), and geocoding-db-ris.

Hard break: DataSourceDetailSection.title/columns/imageAlt, row label slots, and DataSourceResult.summary are now I18nToken-only — raw English at a label slot is a compile error. Value slots (rows right column, items, content) keep a string union for upstream pass-through (operator descriptions, prices, addresses). 122 legacy dataSources.* keys removed from packages/i18n.

Notes:

  • One mid-branch commit (3138098) is mislabeled refactor(fuel) but contains the ev-charging changes — a parallel-commit race; the real fuel work is a19f578. Cosmetic; a squash-merge hides it.
  • rows shape is a discriminated union [I18nToken, Translatable][] | Translatable[][] (the EV connector grid needs >2 cells per row).

Plan: docs/plans/2026-05-28-i18n-tokens-for-data-source-contract.md.

Add tokenized en/de catalogs covering the section, row, value, summary
and format namespaces consumed by the shared mobility mapper. ICU
plural rules drive bike-count and slot-count strings so the resolver
renders locale-correct text once the mapper switch lands.
Add tokenized en/de catalogs mirroring the bike-sharing namespace so
the shared mobility mapper resolves locale-correct labels when invoked
under the car-sharing integration. Plural rules cover car-count and
slot-count summaries.
Add the scooter-sharing en/de strings catalog and flip the shared
mobility mapper to emit I18nToken values for every section title, row
label and enumerated value (form factor, propulsion, availability,
status). Each consuming integration (bike, car, scooter) ships an
identical key namespace; the per-integration resolver renders the
locale-correct text.

Tokens use plain `{ $t }` literals via an inlined I18nTokenLike type
so mobility-core stays free of the integration-framework dependency.
Result summaries become single tokens (`summary.available`,
`format.batteryPercent`, `format.distanceKm`) — slot counts and access
methods move to the station-detail Availability table to avoid
client-side concatenation of locale fragments. The vehicle name and
the `usageInfo.type` access label remain English pass-throughs during
the transitional phase tightened by Task 4.1.
…nToken-only

The local StationDetail type is attached to Place.dataSourceDetail via an
`as unknown as Place` cast, so it never received the Task 4.1 hard-break that
made every other integration's section title I18nToken-only. Tightening the
local title slot restores the compile-time guarantee against raw-string label
leaks here too.
@changeset-bot

changeset-bot Bot commented May 28, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c073778

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…frameworkStrings

The field was a transitional holding spot from the API-wiring task; the client
resolver reads framework strings from FrameworkStringsProvider/useFrameworkStrings
instead, so the registry field and its constructor param were never read.
…U templates

A malformed catalog template (unbalanced braces, bad plural syntax) made
IntlMessageFormat throw inside the render path, crashing the panel. Wrap
formatting in try/catch and fall back to the raw template — consistent with
the resolver's existing "visible bug beats crash" fallback for missing keys.
…n-3-cell grid union

The prior rows type `[I18nToken, Translatable][] | Translatable[][]` did not
actually enforce token labels: `Translatable[][]` is a superset that admits a
raw-string label in a 2-cell row, so the hard-break's "no raw English at a
label slot" guarantee did not hold for rows. Constrain the grid member to 3+
cells (`[Translatable, Translatable, Translatable, ...Translatable[]][]`) so a
2-cell row must satisfy the token-label-strict member. Renderer is unchanged
(still dispatches on row.length === 2); this is type-only tightening.

Ripple: ev-charging connector rows and db-ris platform rows are annotated as
explicit grid tuples; mobility-core row arrays and the db-ris transfer-times
label are tightened to token labels (unknown DB traveler enums pass through a
`value.travelerLiteral` token instead of a raw string).
action-semantic-pull-request@v6 reports its result via the commit-statuses
API, which needs `statuses: write`. The workflow only granted
`pull-requests: write`, so the job failed with "Resource not accessible by
integration" on every PR. The title rules themselves were passing.

Note: under `pull_request_target` the workflow runs from the base branch, so
this only takes effect once merged to main.
…y shim

Task 4.2 deleted 122 dataSources.* keys as "integration-emitted", but the live
web-shell shim dataSourceSummaryI18n.ts maps parking filter labels and
string-summary fallbacks to ~17 of them via dynamic t(variableKey) calls that
the static check-translations script can't see. The result was broken key-path
labels (e.g. "rowType", "parkingGarage") in the parking filter sidebar in both
locales. Restore the referenced keys — they are web-shell filter chrome, not
data-source detail content, so they belong alongside the kept filter* keys.
…token resolution

usageInfo.type/cost were rendered as raw strings, so mobility-core's
`Access: ${method}` and ev-charging's "Public" fallback leaked English in the
DE locale. Widen usageInfo.type/cost to Translatable and resolve them in the
panel renderer; mobility-core now emits the format.accessMethod token and
ev-charging falls back to sharedT.value.public.

Also localize the vehicle accessories cell: a row value may now be a
Translatable[] that the client resolves and joins, so accessories emit
value.accessory.* tokens (English fallback for unknown enums) instead of a
pre-joined English string.
…ing in resolver

An empty-string catalog entry short-circuited the fallback chain, rendering a
blank label instead of falling back to the fallback locale, framework catalog,
or bare key. lookupKey now treats "" as absent so resolution continues.
@Medformatik
Medformatik merged commit 09af109 into main May 28, 2026
1 of 3 checks passed
@Medformatik
Medformatik deleted the feat/i18n-tokens-data-source-contract branch May 28, 2026 08:41
Medformatik added a commit that referenced this pull request May 28, 2026
…th (#33)

The web Docker build broke after #32: Turbopack couldn't resolve .js
extensions on .ts sources in the integration-framework strings subpath and the
DataSourceSections hook import. tsc/vitest rewrite .js->.ts so it only surfaced
in `pnpm build`. Drop the extensions to match the workspace convention.
Verified with a full local web build (exit 0).
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.

1 participant