From e429bf83a786a6fa90be5d6b4fadc26da0394ed9 Mon Sep 17 00:00:00 2001 From: oscar24357 Date: Thu, 23 Jul 2026 22:26:45 +0100 Subject: [PATCH 1/2] ci: add conventional-commits changelog and release workflow --- .github/workflows/ci.yml | 15 ++++++ .github/workflows/release.yml | 95 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 6 +++ CONTRIBUTING.md | 52 ++++++++++++++++++- commitlint.config.json | 3 ++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 commitlint.config.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7752fc7..dd369ad3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,21 @@ on: branches: [main] jobs: + commitlint: + name: Lint commit messages + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm install --save-dev @commitlint/cli @commitlint/config-conventional + - name: Lint commits in PR + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + contracts: name: Contract tests runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..367cbb43 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Changelog, GitHub Release & npm publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history for changelog generation + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Extract the version from the tag (strips leading 'v') + - name: Set version env + run: echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + # Generate / update CHANGELOG.md at the repo root using conventional-changelog + - name: Update CHANGELOG.md + working-directory: . + run: | + npx --yes conventional-changelog-cli \ + -p angular \ + -i CHANGELOG.md \ + -s \ + -r 0 + + # Extract only the section for this release to use as the GitHub Release body + - name: Extract release notes + id: notes + working-directory: . + run: | + NOTES=$(npx --yes conventional-changelog-cli -p angular -r 1 --outfile /dev/stdout 2>/dev/null) + # Store as multiline env var + { + echo "RELEASE_NOTES<> "$GITHUB_ENV" + + # Commit the updated CHANGELOG.md back to the default branch + - name: Commit CHANGELOG.md + working-directory: . + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + # Only commit if there are staged changes + git diff --cached --quiet || git commit -m "chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]" + git push origin HEAD:main + + # Create the GitHub Release + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + body: ${{ env.RELEASE_NOTES }} + + # Build and publish @stellarcred/sdk to npm + - name: Build SDK + working-directory: frontend/packages/sdk + run: pnpm build + + - name: Publish to npm + working-directory: frontend/packages/sdk + run: pnpm publish --no-git-checks --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..996dce58 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This file is generated automatically from conventional commit messages. +See [Conventional Commits](https://www.conventionalcommits.org/) for the commit message format. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2c8fe58..f014af19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,57 @@ Please do **not** open a public issue for security vulnerabilities. See [SECURIT ## Commit messages -Use the imperative mood and be specific: `add income_proof circuit`, `fix check_claim threshold comparison`, `remove SmileID dependency`. +StellarCred uses [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must have a structured prefix so the changelog and release notes are generated automatically. + +| Prefix | When to use | +|--------|-------------| +| `feat:` | A new feature visible to users or integrators | +| `fix:` | A bug fix | +| `docs:` | Documentation only | +| `chore:` | Build process, tooling, dependency updates | +| `refactor:` | Code change that isn't a fix or feature | +| `test:` | Adding or updating tests | +| `ci:` | CI/CD pipeline changes | + +Examples: + +``` +feat: add income_proof circuit +fix: correct check_claim threshold comparison off-by-one +docs: document NPM_TOKEN secret setup +chore: bump soroban-sdk to 26.0.1 +``` + +Breaking changes: add `BREAKING CHANGE:` in the commit footer, or append `!` after the type (`feat!:`). + +Commit messages are linted automatically on pull requests via `commitlint`. + +## Releasing + +Releases are tag-driven. The GitHub Actions release workflow fires on any tag matching `v*` and: + +1. Regenerates `CHANGELOG.md` from the full conventional commit history. +2. Commits the updated changelog back to `main`. +3. Creates a GitHub Release with the changelog section for that version as the body. +4. Builds and publishes `@stellarcred/sdk` to npm. + +### Cutting a release + +```bash +# Bump the version in frontend/packages/sdk/package.json, then: +git add frontend/packages/sdk/package.json +git commit -m "chore: release v" +git tag v +git push origin main --tags +``` + +The workflow handles everything else. + +### Required secret + +The repository must have an `NPM_TOKEN` secret set under **Settings → Secrets and variables → Actions**. +Generate the token at [npmjs.com](https://www.npmjs.com) with **Automation** type and **Read and write** scope for the `@stellarcred` scope (or the package name). +Never commit the token value. ## License diff --git a/commitlint.config.json b/commitlint.config.json new file mode 100644 index 00000000..c30e5a97 --- /dev/null +++ b/commitlint.config.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} From 1ee3026b3c39264255470f4b1ca556ee69c1d339 Mon Sep 17 00:00:00 2001 From: oscar24357 Date: Sun, 26 Jul 2026 08:40:39 +0100 Subject: [PATCH 2/2] feat: add i18n with next-intl, en/es catalogs, and language switcher --- frontend/app/apps/[id]/page.tsx | 40 ++--- frontend/app/apps/page.tsx | 146 +++------------- frontend/app/holder/page.tsx | 85 +++++----- frontend/app/issuer/page.tsx | 57 +++---- frontend/app/layout.tsx | 3 + frontend/app/page.tsx | 205 +++++++++-------------- frontend/app/verifier/page.tsx | 147 ++++------------ frontend/app/verify/page.tsx | 153 ++++------------- frontend/components/LangSwitcher.tsx | 39 +++++ frontend/components/SiteNav.tsx | 64 ++++--- frontend/i18n.ts | 10 ++ frontend/lib/i18n.ts | 15 ++ frontend/lib/locale-context.tsx | 55 ++++++ frontend/messages/en.json | 241 +++++++++++++++++++++++++++ frontend/messages/es.json | 241 +++++++++++++++++++++++++++ frontend/package.json | 1 + 16 files changed, 903 insertions(+), 599 deletions(-) create mode 100644 frontend/components/LangSwitcher.tsx create mode 100644 frontend/i18n.ts create mode 100644 frontend/lib/i18n.ts create mode 100644 frontend/lib/locale-context.tsx create mode 100644 frontend/messages/en.json create mode 100644 frontend/messages/es.json diff --git a/frontend/app/apps/[id]/page.tsx b/frontend/app/apps/[id]/page.tsx index 1d66aa74..0196c000 100644 --- a/frontend/app/apps/[id]/page.tsx +++ b/frontend/app/apps/[id]/page.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { IconLock, IconCheck, @@ -21,6 +22,7 @@ function ProtocolDetailInner() { const { id } = useParams<{ id: string }>(); const { address } = useWallet(); const searchParams = useSearchParams(); + const t = useTranslations("apps"); const scVerified = searchParams.get("sc_verified") === "true"; const scWallet = searchParams.get("sc_wallet"); @@ -62,9 +64,9 @@ function ProtocolDetailInner() { if (!protocol) { return (
-

Protocol not found.

+

{t("protocolNotFound")}

- Back to Apps + {t("backToApps")}
); @@ -81,7 +83,7 @@ function ProtocolDetailInner() { className="row faint" style={{ fontSize: "0.8125rem", gap: "0.35rem", marginBottom: "0.5rem", textDecoration: "none" }} > - Apps + {t("backToApps")}
{protocol.icon} @@ -112,8 +114,8 @@ function ProtocolDetailInner() { > - Verification complete.{" "} - You were returned here from StellarCred automatically. + {t("verificationComplete")}{" "} + {t("returnedFrom")}
)} @@ -146,8 +148,7 @@ function ProtocolDetailInner() {
{protocol.stat.sub}
- {/* Requirements */} - Requirements + {t("requirements")}
{protocol.requirements.map((r, i) => (
@@ -162,28 +163,23 @@ function ProtocolDetailInner() { {statuses[i] ? ( - Proved + {t("proved")} ) : ( - Needed + {t("needed")} )}
))}
{checked && !eligible && ( - - Get verified + + {t("getVerified")} )} - {!activeWallet && (

- Connect your wallet to check eligibility. + {t("connectToCheck")}

)} @@ -200,8 +196,8 @@ function ProtocolDetailInner() { {protocol.actionLabel} {checked && ( eligible - ? Access granted - : Access denied + ? {t("accessGranted")} + : {t("accessDenied")} )} @@ -224,14 +220,14 @@ function ProtocolDetailInner() { disabled={!eligible} > {eligible ? protocol.actionLabel : ( - <> Prove eligibility first + <> {t("proveFirst")} )}

{eligible - ? `${protocol.name} read ProofRegistry.check_claim and found valid proofs for your address. No personal data was shared.` - : `${protocol.name} only reads ProofRegistry.check_claim for your address — it never sees the credential data behind your proofs.`} + ? t("noPersonalData", { name: protocol.name }) + : t("noPersonalDataDenied", { name: protocol.name })}

diff --git a/frontend/app/apps/page.tsx b/frontend/app/apps/page.tsx index 3626ad2d..aaea070a 100644 --- a/frontend/app/apps/page.tsx +++ b/frontend/app/apps/page.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect, useState, useMemo } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { IconCheck, IconCircle, IconSearch, IconFilter } from "@tabler/icons-react"; import { WalletButton } from "@/components/WalletButton"; import { useWallet } from "@/lib/wallet-context"; @@ -28,6 +29,7 @@ function ProtocolCard({ activeWallet: string | null; }) { const router = useRouter(); + const t = useTranslations("apps"); const [statuses, setStatuses] = useState(protocol.requirements.map(() => false)); const [checked, setChecked] = useState(false); const eligible = statuses.every(Boolean); @@ -69,46 +71,21 @@ function ProtocolCard({ {protocol.requirements.map((r, i) => { const isClaimMet = checked && statuses[i]; return ( - + {CLAIM_LABELS[r.type] || r.type} ); })} -

- {protocol.tagline} -

-

- {protocol.description} -

-
+

{protocol.tagline}

+

{protocol.description}

+
{protocol.stat.label}
{protocol.stat.value}
{protocol.stat.sub}
-
Requirements
+
{t("requirements")}
{protocol.requirements.map((r, i) => (
@@ -121,8 +98,8 @@ function ProtocolCard({ {statuses[i] - ? Proved - : Needed} + ? {t("proved")} + : {t("needed")}}
))}
@@ -133,6 +110,7 @@ function ProtocolCard({ function AppsInner() { const { address } = useWallet(); const searchParams = useSearchParams(); + const t = useTranslations("apps"); const scVerified = searchParams.get("sc_verified") === "true"; const scWallet = searchParams.get("sc_wallet"); const activeWallet = address ?? scWallet ?? null; @@ -165,79 +143,40 @@ function AppsInner() { <>
- Demo protocols -

Apps

+ {t("eyebrow")} +

{t("title")}

-
- Any protocol, any claim.{" "} - Each app below gates access on a different credential type — one read-only call to{" "} +
+ {t("subtitle")}{" "} + Each app below gates access on a different credential type — one read-only call to{" "} ProofRegistry.is_verified. The protocol never sees the credential, the commitment, or the proof itself.
{scVerified && ( -
+
- Verification complete.{" "} - You were returned here from StellarCred automatically. + {t("verificationComplete")}{" "} + {t("returnedFrom")}
)} - {/* Search and filter */}
- + setSearch(e.target.value)} - style={{ - width: "100%", - padding: "0.65rem 0.75rem 0.65rem 2.25rem", - borderRadius: "var(--radius)", - border: "1px solid var(--border)", - background: "var(--bg-raised)", - color: "var(--text)", - fontSize: "0.875rem", - }} + style={{ width: "100%", padding: "0.65rem 0.75rem 0.65rem 2.25rem", borderRadius: "var(--radius)", border: "1px solid var(--border)", background: "var(--bg-raised)", color: "var(--text)", fontSize: "0.875rem" }} />
@@ -245,40 +184,13 @@ function AppsInner() { {CREDENTIAL_TYPES.map((claim) => { const isActive = selectedClaims.has(claim); return ( - ); })} {selectedClaims.size > 0 && ( - )} @@ -286,15 +198,10 @@ function AppsInner() {
{filtered.length === 0 ? ( -
+
-

No apps match

-

- Try adjusting your search or removing claim filters. -

+

{t("noResults")}

+

{t("noResultsHint")}

) : (
@@ -318,4 +225,3 @@ export default function AppsPage() { ); } - diff --git a/frontend/app/holder/page.tsx b/frontend/app/holder/page.tsx index 6e4747a5..54a22d0f 100644 --- a/frontend/app/holder/page.tsx +++ b/frontend/app/holder/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; import { IconArrowLeft, IconArrowRight, @@ -67,6 +68,7 @@ function CredCard({ onRemove: () => void; }) { const status = proofStatus(c); + const t = useTranslations("holder"); return (
@@ -83,7 +85,7 @@ function CredCard({ <> {" · "} - expires in {daysRemaining(c)}d + {t("expiresIn", { days: daysRemaining(c) })} {c.provedTxHash && ( <> @@ -101,28 +103,28 @@ function CredCard({ )} {status === "expired" && ( - <> · expired + <> · {t("expired")} )}
{/* right: badges + button + trash */}
- Held - {status === "proved" && On-chain} + {t("held")} + {status === "proved" && {t("onChain")}}
@@ -220,7 +222,7 @@ export default function HolderPage() { {/* ── Credentials to prove ── */} {unproved.length > 0 && (
- Ready to prove + {t("sectionReady")} {unproved.map((c) => ( 0 && (
- On-chain · active proofs + {t("sectionOnChain")} {proved.map((c) => ( 0 && (

- Connect a wallet to generate and submit proofs. + {t("connectWalletToProve")}

)} @@ -267,7 +269,7 @@ export default function HolderPage() { onClick={() => setImporting(true)} > - Import credential JSON + {t("importJson")} )}
@@ -281,6 +283,7 @@ export default function HolderPage() { function ImportPanel({ onImport, onCancel }: { onImport: (c: Credential) => void; onCancel: () => void }) { const [json, setJson] = useState(""); const [error, setError] = useState(""); + const t = useTranslations("holder"); function onAdd() { try { onImport(parseCredential(json)); } @@ -289,18 +292,18 @@ function ImportPanel({ onImport, onCancel }: { onImport: (c: Credential) => void return (
- Import credential + {t("importTitle")}