@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 +103 -0
- package/lib/copy.d.ts +25 -0
- package/lib/copy.js +29 -0
- package/package.json +31 -0
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
|
+
}
|