Skip to content

Seed datasets have no locale axis — an app shipping demo data for two language markets has to select seeds at config-assembly time, which bakes the choice into dist and leaves the other locale's rows behind #16510

Description

@hotlong

Symptom

SeedSchema can gate a dataset on environment but not on locale. An app that wants to ship
a Chinese demo dataset and an English one — the same records, different display strings — has no
declarative way to say which applies, so the selection has to happen in application code before the
platform ever sees the seed.

The schema is strictObject, so an app cannot add the key itself:

  • packages/spec/src/data/seed.zod.ts:33SeedSchema = lazySchema(() => strictObject({…}));
    fields are object / externalId / mode / env / records plus the package-provenance keys.
    The alias table maps environment and environments onto env; there is no locale spelling.
  • packages/spec/src/data/seed-loader.zod.tsSeedLoaderConfigSchema carries env,
    organizationId and identity, but no locale.
  • packages/metadata-protocol/src/seed-loader.ts:2486filterByEnv(datasets, env) is the only
    dataset-level filter; :490 is its single call site.

Verified against the source tree at c383352, and against the published @objectstack/spec@17.2.0
d.ts as installed in a consuming app. A repo-wide grep for locale across the seed sources returns
one unrelated hit (a pinyin-search comment in examples/app-showcase/src/data/seed/index.ts:111).

Why this lands on the platform

Per references/app-platform-boundary.md, third row of the deciding table: the subject is
platform behaviour and the cost lands on the app
— the missing filter axis is the loader's, and
the workaround is paid for in every app that needs it.

The second question — if a second app needed this, would it copy the implementation? — is
answered structurally rather than by a second consumer already in hand: any marketplace app
installed into an org whose locale differs from the author's has the same problem, and the person
installing it cannot be expected to know an app-private environment variable exists. One real
consumer today
(see below); the generalisation argument is the marketplace install path, not a
second app already written.

What the app-side workaround costs

objectstack-ai/kpi currently selects between two seed profiles with an app-invented environment
variable read at module-evaluation time:

// src/data/index.ts
export const SEED_PROFILE = (process.env.OS_SEED_PROFILE ?? 'default').trim().toLowerCase();
export const KpiSeedData = SEED_PROFILE === 'software' ? SoftwareSeedData : DefaultSeedData;

Because the choice is made while the config is assembled rather than while seeds are loaded, two
defects follow that the app cannot fix from where it sits:

  1. The selection is cached in the build output. Switching profiles requires deleting dist,
    otherwise the previous profile is still in effect. The app's README has to document this.
  2. The other profile's rows stay in the database. Every profile is upsert and the loader only
    writes, so switching on a non-empty database leaves both datasets resident — two org trees and
    two plans in one database. The app's README has to tell operators to use a fresh database file.

Both are consequences of filtering in the wrong layer. A loader-side filter is evaluated at load
time, so the dist caching problem does not arise; and the loader already holds externalId and
the _packageId / _provenance keys, so it is the only layer that could reconcile rows belonging
to a locale that is no longer active.

Expected capability

Add locale to SeedSchema as a filter axis exactly parallel to env, honoured by the loader:

  • SeedSchema.locale?: string[] — omitted means "applies to every locale", matching how env
    defaults to all environments.
  • SeedLoaderConfig.locale?: string — resolved from the runtime's configured locale, overridable
    the way env is.
  • filterByEnv becomes the composition of both axes; a dataset applies when it passes env and
    locale.

Deliberately out of scope: the platform does not translate anything. The app still authors both
record sets. The platform gains only the axis to select between them.

Scope note

The spec field and the loader filter have to land together. A locale key accepted by the schema
but ignored by the loader is a declared-not-enforced surface — the shape this repo routes to the
enforce-or-remove channel — and it would be worse than the current state, because an app would
write the key and silently get both datasets.

Environment

  • @objectstack/spec / @objectstack/runtime / @objectstack/cli 17.2.0
  • Source tree c383352
  • Consuming app: objectstack-ai/kpi (Apache-2.0, public)

Duplicate check

Searched seed data locale language variant demo data per locale i18n seed selection and
seed loader env filter records upsert in this repo. The two locale hits are unrelated — #9135
(auth email locale) and #5419 (console UI language). The control search surfaces real seed-loader
cards (#16488, #14403, #3434), so the negative result on locale is a real absence rather than a
search that matched nothing.

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions