@moshevents/copy 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # mosh-copy
2
+
3
+ **Single source of truth for user-facing copy** across web, admin, and iOS. Edit strings
4
+ in one place (`copy.json`), regenerate, and every platform consumes the same text — no more
5
+ the-same-string-hardcoded-three-times-and-drifting. Same shape as `mosh-tokens`.
6
+
7
+ ```
8
+ copy.json ← the ONLY file you hand-edit (nested by area; leaves are strings or {$value,$comment})
9
+ build.mjs ← dependency-free generator (plain node)
10
+ dist/ → npm package @moshevents/copy (compiled to lib/*.js + *.d.ts at publish)
11
+ copy.ts → web + admin: typed `copy` object (static strings + {param} → typed functions)
12
+ copy.md → human / designer / translator view
13
+ Sources/MoshCopy/ → SwiftPM package MoshCopy (iOS)
14
+ Copy.swift → nested type-safe `Copy` enums (static lets + funcs)
15
+ Package.swift ← SwiftPM manifest
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ npm run build # regenerate dist/ from copy.json
22
+ npm run check # build + fail if dist/ is stale (use in CI)
23
+ ```
24
+
25
+ No install required (Node ≥ 18). Commit `dist/` so outputs are reviewable in diffs.
26
+
27
+ ## Authoring
28
+
29
+ ```jsonc
30
+ {
31
+ "events": {
32
+ "empty": { "title": "No upcoming events" }, // plain string
33
+ "attendeeCount": { // with metadata + interpolation
34
+ "$value": "{name} invited you", // {param} → a typed function
35
+ "$comment": "context for translators / the showcase"
36
+ },
37
+ "attendeeCount": { // plural → typed function, selects by count
38
+ "$plural": { "zero": "Nobody going", "one": "{count} person going", "other": "{count} going" }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Plural categories: optional `zero`, `one`, required `other`. Selection is English CLDR
45
+ (0→zero, 1→one, else other); the API is `count`-keyed and emits `(count: Int)` on iOS /
46
+ `{ count: number }` on web. Full per-locale plural rules arrive with localization.
47
+
48
+ ## Consuming as a package (the supported path)
49
+
50
+ **Web / admin — `@moshevents/copy` (public on npmjs).** No `.npmrc`, no token — `npm i @moshevents/copy` and import:
51
+
52
+ ```ts
53
+ import { copy } from "@moshevents/copy";
54
+ copy.events.empty.title; // "No upcoming events"
55
+ copy.events.attendeeCount({ count: 3 }); // "3 going"
56
+ ```
57
+
58
+ `npm update @moshevents/copy` pulls new copy — no vendored `src/copy.ts` to sync.
59
+
60
+ **iOS — SwiftPM `MoshCopy`.** Xcode → Add Package Dependencies →
61
+ `https://github.com/moshorg/mosh-copy`, pin a version, then:
62
+
63
+ ```swift
64
+ import MoshCopy
65
+ Copy.Events.Empty.title
66
+ Copy.Invite.greeting(name: "Sam") // type-safe; a wrong key won't compile
67
+ ```
68
+
69
+ SwiftPM resolves straight from the git tag — no registry/publish needed.
70
+
71
+ ## Releasing
72
+
73
+ Publishing is **Trusted Publishing (OIDC)** — no token or secret. One-time setup:
74
+
75
+ 1. Create the `moshevents` org on npmjs (claims the `@moshevents` scope).
76
+ 2. **First publish manually** (Trusted Publishing is configured on an existing package):
77
+ `npm login && npm publish` from this repo (runs `build.mjs` + `tsc`).
78
+ 3. On npmjs → the package → **Settings → Trusted Publisher** → add GitHub Actions:
79
+ repo `moshorg/mosh-copy`, workflow `publish.yml`.
80
+
81
+ After that, bump `version` and cut a **GitHub Release** (or run the `Publish` workflow) — it
82
+ publishes to npmjs via OIDC, with provenance, no secret. The same tag is what iOS pins to.
83
+
84
+ ## Human-facing view
85
+
86
+ `dist/copy.md` renders every string for review. Same as `mosh-tokens`, this bundle can be
87
+ synced to a **claude.ai design/content project** via `/design-sync` so a writer or designer
88
+ can browse and propose copy without checking out code.
89
+
90
+ ## Migration (incremental, low-risk)
91
+
92
+ The repo ships seeded with a few genuinely-shared strings. Migrate the rest gradually:
93
+ replace a hardcoded string with a `copy.*` reference, area by area. Once a screen is migrated,
94
+ add a lint nudging new literals through keys (mirrors the tokens guard). No big-bang rewrite.
95
+
96
+ ## Deferred — out of scope for now
97
+
98
+ - **Localization** — when you actually translate, emit Apple `.xcstrings` (String Catalog) +
99
+ per-locale web bundles instead of single-locale literals. The key structure is already i18n-ready.
100
+ - **Typed numeric params** — `{count}` currently types as `String` on iOS / `string | number`
101
+ on web; a `$params` hint could generate `Int`/typed args.
102
+ - **Server-driven copy** — for high-churn / marketing strings that must change without an iOS
103
+ release, serve them from the API; keep static UI chrome in this build.
package/lib/copy.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ export declare const copy: {
2
+ readonly app: {
3
+ readonly downloadCta: "Download the app";
4
+ };
5
+ readonly events: {
6
+ readonly empty: {
7
+ readonly title: "No upcoming events";
8
+ };
9
+ readonly attendeeCount: (p: {
10
+ count: number;
11
+ }) => string;
12
+ };
13
+ readonly account: {
14
+ readonly deactivate: {
15
+ readonly title: "Deactivate or delete?";
16
+ readonly body: "Taking a break? Deactivating hides your account until you're ready to come back. Deleting is permanent — you won't be able to recover your account or content after 30 days.";
17
+ };
18
+ };
19
+ readonly invite: {
20
+ readonly greeting: (p: {
21
+ name: string | number;
22
+ }) => string;
23
+ };
24
+ };
25
+ export type Copy = typeof copy;
package/lib/copy.js ADDED
@@ -0,0 +1,29 @@
1
+ // AUTO-GENERATED from copy.json by build.mjs — do not edit by hand.
2
+ /* Mosh Copy — web/admin. Import: import { copy } from "./copy"; */
3
+ export const copy = {
4
+ app: {
5
+ downloadCta: "Download the app",
6
+ },
7
+ events: {
8
+ empty: {
9
+ title: "No upcoming events",
10
+ },
11
+ attendeeCount: (p) => {
12
+ const n = Number(p.count);
13
+ if (n === 0)
14
+ return `Nobody going yet`;
15
+ if (n === 1)
16
+ return `${p.count} person going`;
17
+ return `${p.count} going`;
18
+ },
19
+ },
20
+ account: {
21
+ deactivate: {
22
+ title: "Deactivate or delete?",
23
+ body: "Taking a break? Deactivating hides your account until you're ready to come back. Deleting is permanent — you won't be able to recover your account or content after 30 days.",
24
+ },
25
+ },
26
+ invite: {
27
+ greeting: (p) => `Hey ${p.name}, you're invited`,
28
+ },
29
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@moshevents/copy",
3
+ "version": "0.2.0",
4
+ "description": "Mosh — single source of truth for user-facing copy. Typed `copy` for web/admin; iOS consumes the SwiftPM target.",
5
+ "type": "module",
6
+ "files": [
7
+ "lib/copy.js",
8
+ "lib/copy.d.ts"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./lib/copy.d.ts",
13
+ "import": "./lib/copy.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "node build.mjs",
18
+ "prepublishOnly": "node build.mjs && tsc",
19
+ "check": "node build.mjs && git --no-pager diff --exit-code dist/copy.ts dist/copy.md Sources || (echo '\\noutputs are stale — run npm run build and commit' && exit 1)"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.6.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/moshorg/mosh-copy.git"
30
+ }
31
+ }