@moshevents/copy
v0.8.11
Published
Mosh — single source of truth for user-facing copy. Typed `copy` for web/admin; iOS consumes the SwiftPM target.
Readme
mosh-copy
Single source of truth for user-facing copy across web, admin, and iOS. Edit strings
in one place (copy.json), regenerate, and every platform consumes the same text — no more
the-same-string-hardcoded-three-times-and-drifting. Same shape as mosh-tokens.
copy.json ← the ONLY file you hand-edit (nested by area; leaves are strings or {$value,$comment})
build.mjs ← dependency-free generator (plain node)
dist/ → npm package @moshevents/copy (compiled to lib/*.js + *.d.ts at publish)
copy.ts → web + admin: typed `copy` object (static strings + {param} → typed functions)
copy.md → human / designer / translator view
Sources/MoshCopy/ → SwiftPM package MoshCopy (iOS)
Copy.swift → nested type-safe `Copy` enums (static lets + funcs)
Package.swift ← SwiftPM manifestUsage
npm run build # regenerate dist/ from copy.json
npm run check # build + fail if dist/ is stale (use in CI)No install required (Node ≥ 18). Commit dist/ so outputs are reviewable in diffs.
Authoring
{
"events": {
"empty": { "title": "No upcoming events" }, // plain string
"attendeeCount": { // with metadata + interpolation
"$value": "{name} invited you", // {param} → a typed function
"$comment": "context for translators / the showcase"
},
"attendeeCount": { // plural → typed function, selects by count
"$plural": { "zero": "Nobody going", "one": "{count} person going", "other": "{count} going" }
}
}
}Plural categories: optional zero, one, required other. Selection is English CLDR
(0→zero, 1→one, else other); the API is count-keyed and emits (count: Int) on iOS /
{ count: number } on web. Full per-locale plural rules arrive with localization.
Consuming as a package (the supported path)
Web / admin — @moshevents/copy (public on npmjs). No .npmrc, no token — npm i @moshevents/copy and import:
import { copy } from "@moshevents/copy";
copy.events.empty.title; // "No upcoming events"
copy.events.attendeeCount({ count: 3 }); // "3 going"npm update @moshevents/copy pulls new copy — no vendored src/copy.ts to sync.
iOS — SwiftPM MoshCopy. Xcode → Add Package Dependencies →
https://github.com/moshorg/mosh-copy, pin a version, then:
import MoshCopy
Copy.Events.Empty.title
Copy.Invite.greeting(name: "Sam") // type-safe; a wrong key won't compileSwiftPM resolves straight from the git tag — no registry/publish needed.
Releasing
Publishing is Trusted Publishing (OIDC) — no token or secret. One-time setup:
- Create the
mosheventsorg on npmjs (claims the@mosheventsscope). - First publish manually (Trusted Publishing is configured on an existing package):
npm login && npm publishfrom this repo (runsbuild.mjs+tsc). - On npmjs → the package → Settings → Trusted Publisher → add GitHub Actions:
repo
moshorg/mosh-copy, workflowpublish.yml.
After that, bump version and cut a GitHub Release (or run the Publish workflow) — it
publishes to npmjs via OIDC, with provenance, no secret. The same tag is what iOS pins to.
Human-facing view
dist/copy.md renders every string for review. Same as mosh-tokens, this bundle can be
synced to a claude.ai design/content project via /design-sync so a writer or designer
can browse and propose copy without checking out code.
Migration (incremental, low-risk)
The repo ships seeded with a few genuinely-shared strings. Migrate the rest gradually:
replace a hardcoded string with a copy.* reference, area by area. Once a screen is migrated,
add a lint nudging new literals through keys (mirrors the tokens guard). No big-bang rewrite.
Deferred — out of scope for now
- Localization — when you actually translate, emit Apple
.xcstrings(String Catalog) + per-locale web bundles instead of single-locale literals. The key structure is already i18n-ready. - Typed numeric params —
{count}currently types asStringon iOS /string | numberon web; a$paramshint could generateInt/typed args. - Server-driven copy — for high-churn / marketing strings that must change without an iOS release, serve them from the API; keep static UI chrome in this build.
