feat: typed i18n token contract for data-source providers#32
Merged
Conversation
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.
|
…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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the string-as-key i18n workaround in the data-source detail panel with a typed
I18nTokencontract carried in theDataSourceDetail/DataSourceResultpayloads. 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/stringssubpath):I18nTokentype ({ $t: string, values?: Record<string, string|number> }),Translatableunion, andisI18nTokenguard.resolveToken()pure resolver with lookup order integration catalog → framework catalog, ICU MessageFormat interpolation viaintl-messageformat, and locale → fallback-locale chaining. Returns the bare key when unresolved (visible bug beats silent English leak).token()helper +sharedTtyped constants over a shared en/de vocabulary catalog (Source, Last Updated, Open, …).check-translationsextended to enforce en↔de parity across the framework catalog and every per-integrationstrings/(85 catalogs).Wiring:
/api/integrationsnow returns{ integrations, frameworkStrings }(was a bare array); client consumers and the SSR fetcher updated.FrameworkStringsProvider+useDataSourceI18nResolver()hook;DataSourceSections.tsxlookup 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, andDataSourceResult.summaryare nowI18nToken-only — raw English at a label slot is a compile error. Value slots (rowsright column,items,content) keep astringunion for upstream pass-through (operator descriptions, prices, addresses). 122 legacydataSources.*keys removed frompackages/i18n.Notes:
3138098) is mislabeledrefactor(fuel)but contains the ev-charging changes — a parallel-commit race; the real fuel work isa19f578. Cosmetic; a squash-merge hides it.rowsshape 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.