From 8e8df51b6a77ffdf07852ad5775368b777451ffd Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Thu, 15 Oct 2026 00:14:58 +0000 Subject: [PATCH] feat: Mukoko Lingo page and placement; releases 0.11.0 live (#85) Mukoko Lingo, a partner app developed by Nyuchi Learning as part of Mukoko, gets its own page at /mukoko-lingo, a product card on the home page beside the Toddle Enhancement Extension, a header link, and a place in the footer's Products column. JSON-LD, Open Graph, sitemap and llms.txt match. Every claim is from Lingo's public repository; the old home-page copy (French, Portuguese, "free for personal use") is gone. Releases: 0.11.0 is on the Chrome Web Store and current; anchors for the 0.10.x and 0.11.x test builds; 0.12.0 is not listed. extensionVersions gains changedIn (0.9.0) so "versions before" wording stays true. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq --- CHANGELOG.md | 34 ++ CONTRIBUTING.md | 25 +- public/llms.txt | 41 +- src/assets/lingo/icon.png | Bin 0 -> 49079 bytes src/data/legal.ts | 24 +- src/data/releases.ts | 40 ++ src/data/site.ts | 57 ++- src/pages/index.astro | 62 +-- src/pages/legal/cookies.astro | 2 +- src/pages/legal/data.astro | 4 +- src/pages/legal/privacy.astro | 2 +- src/pages/legal/security.astro | 15 +- src/pages/mukoko-lingo.astro | 364 ++++++++++++++++++ src/pages/toddle-enhancement-extension.astro | 11 +- .../releases.astro | 2 +- tests/links.test.ts | 3 +- tests/seo.test.ts | 77 ++++ 17 files changed, 689 insertions(+), 74 deletions(-) create mode 100644 src/assets/lingo/icon.png create mode 100644 src/pages/mukoko-lingo.astro diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d6f01..e725dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ numbers below were the site's earlier, hand-kept numbering and are retired; the ### Added +- **Mukoko Lingo has its own page, `/mukoko-lingo`.** Mukoko Lingo is a + partner app developed by Nyuchi Learning, part of Mukoko, Africa's super + app. The page says what it does today, who it is for, what schools get + and how to start, in the extension page's style, with the app's own icon. + Every claim is from Lingo's public repository (mukoko-dev/mukoko-lingo: + README.md, app.json, BRANDING.md and the app's own copy). It sits beside + the Toddle Enhancement Extension wherever the extension appears: a second + product card on the home page, "Mukoko Lingo" in the header (where + "Tools" is now "Toddle extension"), and the footer's "Tools" column, now + "Products". It has a SoftwareApplication node in its JSON-LD, Open Graph + tags, a sitemap entry and an entry under Products in llms.txt. + `mukokoLingo` in `src/data/site.ts` holds its links. + `tests/seo.test.ts` checks all of that. - **`npm run clean`, and after every merge.** `scripts/clean.mjs` removes the build output (`dist/`, `.astro/`, `.vercel/output/`), the QA report, the tools' caches, finished Claude Code worktrees and local branches @@ -20,6 +33,27 @@ numbers below were the site's earlier, hand-kept numbering and are retired; the ### Changed +- **Mukoko Lingo, described as it is.** The home page card and llms.txt + said Lingo was "free for personal use", "Shona and Ndebele primary" and + taught French and Portuguese. Its repository says it teaches Shona, + Ndebele, English and Chinese, and that starting is free; the copy now + says that, and links to the live app at lingo.mukoko.com rather than + mukoko.com/lingo. The home page's "three surfaces" are now the two + products and two destinations. +- **0.11.0 is on the Chrome Web Store, and is the current version (#85).** + It is 0.10.0 with a security fix to how licence keys are checked. The + Releases page lists it first, marked on the store, from the extension's + CHANGELOG; the test builds tagged since 0.10.0 (0.10.1 to 0.10.5, 0.11.1 + and 0.11.2) open its notes, and 0.12.0, not released, is not listed. + `extensionVersions.current` is 0.11.0; the new `changedIn` (0.9.0) keeps + "versions before 0.9.0" true where the old licence host and flags key are + named. The product page, the Security page (which now describes the + 0.11.0 fix), the Releases description and llms.txt name 0.11.0. The + anchors for 0.9.6 and 0.9.7 the issue asked for were already there; the + test that every tagged test build has one now covers the 0.10.x and + 0.11.x builds too. +- **`webStore`'s type is a named `Url`**, so Prettier 3.3 and 3.9 format + `src/data/site.ts` the same way; the org lint gate (3.9.4) had flagged it. - **After paying: a claim link, not a key.** The extension page's checkout note now says the licence email carries a one-time link (never the key), that it works once for 72 hours and a new one can be asked for, that diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d6a131..29accf8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,18 +5,19 @@ three predictable places. ## What gets changed where -| Change | File | -| ------------------------------------------------------- | ----------------------------------------------------- | -| Nav links, footer columns, legal links, the wordmark | `src/data/site.ts` | -| Links to the extension (store listing, releases, guide) | `src/data/site.ts` (`toddleExtension`) | -| The extension's release notes | `src/data/releases.ts` | -| Facts on the legal pages (versions, prices, contacts) | `src/data/legal.ts` | -| Consent categories and the banner's version | `src/data/consent.ts` | -| The three destination cards | the `surfaces` array in `src/pages/index.astro` | -| Home page copy | `src/pages/index.astro` | -| Extension page copy | `src/pages/toddle-enhancement-extension.astro` | -| Header / footer structure | `src/components/SiteHeader.astro`, `SiteFooter.astro` | -| ``, metadata, theme bootstrap | `src/layouts/BaseLayout.astro` | +| Change | File | +| ------------------------------------------------------- | ------------------------------------------------------------------- | +| Nav links, footer columns, legal links, the wordmark | `src/data/site.ts` | +| Links to the extension (store listing, releases, guide) | `src/data/site.ts` (`toddleExtension`) | +| The extension's release notes | `src/data/releases.ts` | +| Facts on the legal pages (versions, prices, contacts) | `src/data/legal.ts` | +| Consent categories and the banner's version | `src/data/consent.ts` | +| The destination cards | the `surfaces` array in `src/pages/index.astro` | +| Home page copy | `src/pages/index.astro` | +| Extension page copy | `src/pages/toddle-enhancement-extension.astro` | +| Mukoko Lingo's links / page copy | `src/data/site.ts` (`mukokoLingo`) / `src/pages/mukoko-lingo.astro` | +| Header / footer structure | `src/components/SiteHeader.astro`, `SiteFooter.astro` | +| ``, metadata, theme bootstrap | `src/layouts/BaseLayout.astro` | ## What must not be changed here diff --git a/public/llms.txt b/public/llms.txt index f103ec9..5bbbcf5 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,8 +1,9 @@ # Nyuchi Learning -> learning.nyuchi.com — the Nyuchi Learning surface of Nyuchi Africa. Two -> things live here: signposts to where the learning content moved, and the -> classroom software Nyuchi Web Services builds for schools. +> learning.nyuchi.com — the Nyuchi Learning surface of Nyuchi Africa. It is +> the home of Nyuchi Learning's two products, the Toddle Enhancement +> Extension and Mukoko Lingo, and of signposts to where the learning content +> moved. Nyuchi Africa is a technology company in Zimbabwe building open-source and community-based platforms. Nyuchi Web Services is its development division. @@ -106,8 +107,10 @@ community-based platforms. Nyuchi Web Services is its development division. a copy. Schools deploy it from the Chrome Web Store by extension ID, with a Google Admin console force-install (or Group Policy, Intune or a macOS profile); an organisation key is pasted in once by each teacher, since it - cannot be pushed by policy. Teachers get each minor release (0.9.0 is - the current one) from the Chrome Web Store; versions 0.8.3 to 0.8.13 were + cannot be pushed by policy. Teachers get each minor release (0.11.0 is + the current one: 0.10.0 with a security fix to how licence keys are + checked, so that a cancelled key cannot be made to work again by + writing it differently) from the Chrome Web Store; versions 0.8.3 to 0.8.13 were test builds for schools piloting it, and their changes arrived together in 0.9.0. Release notes for every version, in plain words: @@ -123,6 +126,32 @@ community-based platforms. Nyuchi Web Services is its development division. https://support.nyuchi.com/en/collections/19764027-how-to-use-the-toddle-enhancement-extension Security: https://learning.nyuchi.com/legal/security +- [Mukoko Lingo](https://learning.nyuchi.com/mukoko-lingo): a partner app + developed by Nyuchi Learning, part of Mukoko, Africa's super app. + AI-assisted, skills-based language learning for individuals, schools and + businesses across Africa. Use it in a web browser at + https://lingo.mukoko.com; schools, businesses and admins use the console + at https://lingo.nyuchi.com/console. It teaches Shona, Ndebele, Chinese + and English (Swahili is in the language picker but unfinished, and is not + a supported language). A learner gets five phrases a day, drilled with + flash cards and a short quiz, with spaced repetition scheduling what comes + back and when. Shamwari ("friend" in Shona), the AI tutor, answers + questions in chat and adapts to the learner's measured proficiency rather + than a level they picked; Shamwari never claims to be human. Pronunciation + guides, cultural context, and progress tracking (streaks, XP, bookmarks, + skill proficiency). For schools: teachers create classes, set assignments + against specific phrases and skills, and see progress per student; + assessments are diagnostic, formative and summative, graded on the server; + rosters sync over OneRoster from Clever, ClassLink and PowerSchool; + organisation admins manage users, content, moderation, guardrails, + analytics and API keys. Moderation runs in three layers: rule-based + guardrails, an AI-based check, and prompt-injection detection on anything + a learner types to the tutor. Sign-in is WorkOS AuthKit; the tutor runs on + Cloudflare Workers AI, called from Lingo's servers. Starting is free. + Open source under the MIT licence, (c) Nyuchi Learning: + https://github.com/mukoko-dev/mukoko-lingo. Enquiries: + support@nyuchi.com. + ## Where the learning content went - [Bundu Education](https://bundu.org/education): open, evidence-based K-12 @@ -130,8 +159,6 @@ community-based platforms. Nyuchi Web Services is its development division. - [Nyuchi Learning programmes](https://nyuchi.com/learning): commercial cohort-based and self-paced training in software, AI and digital skills, localised for Africa with offline study packs. -- [Mukoko Lingo](https://mukoko.com/lingo): African language learning, free for - personal use. Shona and Ndebele primary. ## Legal diff --git a/src/assets/lingo/icon.png b/src/assets/lingo/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..006d0e943927a2fc431fce97be7539a91a5554dd GIT binary patch literal 49079 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�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 B%3uHh literal 0 HcmV?d00001 diff --git a/src/data/legal.ts b/src/data/legal.ts index b7fb253..e8b52a2 100644 --- a/src/data/legal.ts +++ b/src/data/legal.ts @@ -114,25 +114,29 @@ export const pricing = [ * (0.9.0, …) is released from the extension's `main` branch and published to * the store. Teachers get minors. * - * `current` is what teachers have from the Chrome Web Store: 0.9.0, published - * on 7 October 2026. `previous` is the store release before it, named only - * where something it did still matters to a teacher updating from it (flags - * it hid stay concealed), said as history. What changed in the test builds - * between the two (0.8.3 carried 0.8.2's code) is said as "versions before - * `current`": the old licence host and the old flags storage key. There is no `next` while no minor is reviewed and waiting for the + * `current` is what teachers have from the Chrome Web Store: 0.11.0, a + * security fix on top of 0.10.0 (published 13 October 2026). `previous` is + * the store release named only where something it did still matters to a + * teacher updating from it (flags 0.8.2 hid stay concealed), said as history. + * `changedIn` is the release that moved the licence host to licenses.nyuchi.com + * and renamed the flags storage key: "versions before `changedIn`" asked the + * old host and used the old key. It stays 0.9.0 when `current` moves. + * There is no `next` while no minor is reviewed and waiting for the * store. When one is, add `next` here, say "from version " and what * `current` does wherever the two differ, so each claim stays true on both * sides of the release; when it ships, move it to `current` and drop that * wording again. */ export const extensionVersions = { - current: "0.9.0", + current: "0.11.0", previous: "0.8.2", + /** The release that moved the licence host and renamed the flags key. */ + changedIn: "0.9.0", /** The oldest version security reports are accepted for. */ supportedFrom: "0.8.2", } as const; -const { current, previous } = extensionVersions; +const { changedIn, previous } = extensionVersions; /** * The licence key and Chrome sync, said once. Since 0.9.0 the key alone is @@ -229,7 +233,7 @@ export const extensionDataFacts = [ }, { claim: "What it stores: settings and licence details, and no student data.", - detail: `In Chrome's storage for the extension, on your device: your switches (the extension on or off, student flags, the student sidebar, My classes, primary teacher names, gradebook tools, and student details on the Attendance dashboard); your licence key, if you entered one; the result of the last check for cancelled keys (whether your key is on the list, the list's date and when the check was made, never the list itself); and the email address of the Toddle account last seen signed in, with the time, so the extension can check who a licence is for. That email is read on every Toddle page, with or without a licence key, and stays on the device. ${licenceSync.detail} In the browser's storage for Toddle's own site: tee-settings, a copy of your switches and of which features the licence allows (no key and no email); tee-blur-flags (gbx-hide-flags in versions before ${current}), whether flags are concealed, so they are concealed before the page is drawn; tee-course-view, whether you chose My classes in the home page filter; and tee-academic-year, the number of the academic year Toddle last asked for. Nothing it reads about students, classes, results or attendance is ever written to storage.`, + detail: `In Chrome's storage for the extension, on your device: your switches (the extension on or off, student flags, the student sidebar, My classes, primary teacher names, gradebook tools, and student details on the Attendance dashboard); your licence key, if you entered one; the result of the last check for cancelled keys (whether your key is on the list, the list's date and when the check was made, never the list itself); and the email address of the Toddle account last seen signed in, with the time, so the extension can check who a licence is for. That email is read on every Toddle page, with or without a licence key, and stays on the device. ${licenceSync.detail} In the browser's storage for Toddle's own site: tee-settings, a copy of your switches and of which features the licence allows (no key and no email); tee-blur-flags (gbx-hide-flags in versions before ${changedIn}), whether flags are concealed, so they are concealed before the page is drawn; tee-course-view, whether you chose My classes in the home page filter; and tee-academic-year, the number of the academic year Toddle last asked for. Nothing it reads about students, classes, results or attendance is ever written to storage.`, }, { claim: "What it holds in memory, and for how long.", @@ -244,7 +248,7 @@ export const extensionDataFacts = [ { claim: "Outside connection two, only while a licence is entered: a daily check for cancelled keys.", - detail: `At most once a day, and only while a licence key is entered, the extension's background worker makes one plain request to https://licenses.nyuchi.com/v1/revocations (versions before ${current} asked licences.nyuchi.dev, the same server); a key just entered is checked within 5 minutes. It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, checks its own key against that list on your machine, and keeps only the result. Cloudflare, which runs the server, sees your IP address as with any web request; Nyuchi does not log it. If the list cannot be fetched, the last result stands, so a network outage never switches a licence off.`, + detail: `At most once a day, and only while a licence key is entered, the extension's background worker makes one plain request to https://licenses.nyuchi.com/v1/revocations (versions before ${changedIn} asked licences.nyuchi.dev, the same server); a key just entered is checked within 5 minutes. It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, checks its own key against that list on your machine, and keeps only the result. Cloudflare, which runs the server, sees your IP address as with any web request; Nyuchi does not log it. If the list cannot be fetched, the last result stands, so a network outage never switches a licence off.`, }, { claim: "Exports are local.", diff --git a/src/data/releases.ts b/src/data/releases.ts index 5beec81..c6c9deb 100644 --- a/src/data/releases.ts +++ b/src/data/releases.ts @@ -82,10 +82,50 @@ export const taggedTestBuilds: readonly string[] = [ "0.9.8", "0.9.9", "0.9.10", + "0.10.1", + "0.10.2", + "0.10.3", + "0.10.4", + "0.10.5", + "0.11.1", + "0.11.2", ]; /** Newest first. */ export const releases: Release[] = [ + { + /* No date: the CHANGELOG does not give the day 0.11.0 reached the store. + Add it when it is confirmed. The test builds after 0.10.0 open these + notes; their own changes are in neither 0.10.0 nor 0.11.0. */ + version: "0.11.0", + status: "store", + summary: + "A security fix: a licence key that was cancelled or refunded can no longer be made to work again by editing it. Nothing else changes for teachers, and every key that works today keeps working.", + sections: [ + { + heading: "Fixed", + items: [ + "A cancelled licence key could be made to work again by typing it slightly differently. A key must now be in exactly the form it was issued in; spaces before or after it are still ignored. A key stored in any other form stops working the next time Toddle loads.", + "Only a licence is accepted as a licence. The signed list of cancelled keys can no longer pass for a key. Keys issued so far keep working.", + ], + }, + { + heading: "Test builds", + items: [ + "Versions 0.10.1 to 0.10.5, 0.11.1 and 0.11.2 are test builds for schools piloting the extension. Their changes are not in this release; they will reach the Chrome Web Store in a later one.", + ], + }, + ], + alsoIds: [ + "0.10.1", + "0.10.2", + "0.10.3", + "0.10.4", + "0.10.5", + "0.11.1", + "0.11.2", + ], + }, { version: "0.10.0", status: "store", diff --git a/src/data/site.ts b/src/data/site.ts index e08342c..691c9fd 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -12,7 +12,7 @@ export const site = { url: "https://learning.nyuchi.com", parent: { name: "Nyuchi Africa", url: "https://nyuchi.com" }, description: - "The Nyuchi Learning surface: training programmes, the open Bundu Education frameworks, and the classroom tools Nyuchi Web Services builds for schools.", + "The Nyuchi Learning surface: training programmes, the open Bundu Education frameworks, Mukoko Lingo for languages, and the classroom tools Nyuchi Web Services builds for schools.", } as const; /** @@ -51,11 +51,13 @@ export const legalPages: NavLink[] = [ { label: "Legal notice", href: "/legal/notice" }, ]; +/* The two products come first, side by side, so they read as Nyuchi + Learning's own; the ecosystem destinations follow. */ export const primaryNav: NavLink[] = [ - { label: "Tools", href: "/toddle-enhancement-extension" }, + { label: "Toddle extension", href: "/toddle-enhancement-extension" }, + { label: "Mukoko Lingo", href: "/mukoko-lingo" }, { label: "Programmes", href: "https://nyuchi.com/learning", external: true }, { label: "Frameworks", href: "https://bundu.org/education", external: true }, - { label: "Languages", href: "https://mukoko.com/lingo", external: true }, ]; export const headerCta: NavLink = { @@ -78,21 +80,24 @@ export const footerColumns: { title: string; links: NavLink[] }[] = [ href: "https://bundu.org/education", external: true, }, - { label: "Languages", href: "https://mukoko.com/lingo", external: true }, ], }, { - title: "Tools", + title: "Products", links: [ { label: "Toddle Enhancement Extension", href: "/toddle-enhancement-extension", }, { - label: "Deploy to a school", + label: "Deploy the extension", href: "/toddle-enhancement-extension#install", }, - { label: "Releases", href: "/toddle-enhancement-extension/releases" }, + { + label: "Extension releases", + href: "/toddle-enhancement-extension/releases", + }, + { label: "Mukoko Lingo", href: "/mukoko-lingo" }, { label: "Send feedback", href: "/feedback" }, { label: "Get in touch", href: "mailto:support@nyuchi.com" }, ], @@ -156,15 +161,17 @@ export const MINERALS = [ * every call to action is "Add to Chrome". (Set it back to null and the pages * fall back to an enquiry, with no other edit.) */ +/** A link that may not exist yet. Named so that every Prettier version + formats the cast below the same way. */ +type Url = string | null; + export const toddleExtension = { name: "Toddle Enhancement Extension", tagline: "The Toddle gradebook, with the views it is missing — and a way to project it.", /** The Chrome Web Store listing (live since version 0.7.1). */ webStore: - "https://chromewebstore.google.com/detail/ofliokikjmkkdkinbdnadbjjdmkjdjfi" as - | string - | null, + "https://chromewebstore.google.com/detail/ofliokikjmkkdkinbdnadbjjdmkjdjfi" as Url, /** The store item ID, which a school's IT uses to force-install it. */ extensionId: "ofliokikjmkkdkinbdnadbjjdmkjdjfi", supportEmail: "support@nyuchi.com", @@ -180,6 +187,36 @@ export const toddleExtension = { licenceHost: "licenses.nyuchi.com", } as const; +/** + * Mukoko Lingo, described once: a partner app developed by Nyuchi Learning, + * part of Mukoko, Africa's super app. + * + * Every fact here comes from Lingo's own public repository + * (mukoko-dev/mukoko-lingo on GitHub: README.md, app.json, BRANDING.md and + * the app's own copy in lib/data/translations.ts). Nothing is added that the + * repository does not say. In particular there is no app store link and no + * school price here, because the repository has neither; add them when they + * exist. The source link lives on the page itself (src/pages/mukoko-lingo.astro): + * tests/links.test.ts keeps GitHub links out of this file, because the + * extension's repository is private. + */ +export const mukokoLingo = { + name: "Mukoko Lingo", + tagline: + "AI-assisted, skills-based language learning for individuals, schools and businesses across Africa.", + /** The live app, as the repository's README names it. */ + url: "https://lingo.mukoko.com", + /** The console for schools, businesses and admins. */ + console: "https://lingo.nyuchi.com/console", + /** The four languages the repository calls canonical. Swahili is in the + app's picker but the README calls it unfinished, so it is not listed. */ + languages: ["Shona", "Ndebele", "English", "Chinese"], + /** BCP 47 codes for the same four, for structured data. */ + languageCodes: ["sn", "nd", "en", "zh"], + /** School and business enquiries. */ + enquiry: "mailto:support@nyuchi.com?subject=Mukoko%20Lingo", +} as const; + /** * The feedback form posts to Formspree, which emails each submission to Nyuchi. * The site stays static: there is no endpoint of ours to run or secure. diff --git a/src/pages/index.astro b/src/pages/index.astro index 896dbf1..37375f6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,12 +4,12 @@ * * The standalone learning site that used to live here was split into three * surfaces in the marketing monorepo, so the destination cards remain the - * front door. What is new is that this domain is also where the classroom - * tools Nyuchi Web Services builds for schools now live — starting with the - * Toddle Enhancement Extension. + * front door. What is new is that this domain is also where Nyuchi Learning's + * products live: the Toddle Enhancement Extension, and Mukoko Lingo, the + * partner app developed by Nyuchi Learning as part of Mukoko. */ import BaseLayout from "../layouts/BaseLayout.astro"; -import { toddleExtension } from "../data/site"; +import { mukokoLingo, toddleExtension } from "../data/site"; // One switch drives the call to action everywhere: "Add to Chrome" with the // store listing set, an enquiry without it. Never a link that 404s. @@ -36,20 +36,11 @@ const surfaces = [ href: "https://nyuchi.com/learning", mineral: "gold", }, - { - name: "Mukoko Lingo", - eyebrow: "Consumer surface", - description: - "African language learning, free for personal use. Shona and Ndebele primary, plus English, French, Portuguese, and travel phrase courses. The same content library Bundu Education and Nyuchi Learning draw from.", - cta: "Start learning", - href: "https://mukoko.com/lingo", - mineral: "tanzanite", - }, ]; const title = "Nyuchi Learning — programmes, frameworks, and classroom tools"; const description = - "Nyuchi Learning: commercial training at nyuchi.com/learning, the open Bundu Education frameworks, Mukoko Lingo for languages, and the classroom tools Nyuchi Web Services builds for schools."; + "Nyuchi Learning: the Toddle Enhancement Extension and Mukoko Lingo, commercial training at nyuchi.com/learning, and the open Bundu Education frameworks."; --- @@ -60,15 +51,18 @@ const description = Learning, and the tools that carry it.

- The learning content that used to live here is now spread across three surfaces of - the Bundu Family ecosystem. This domain is also where the classroom software we - build for schools lives. + Two products live here: a Chrome extension that makes the Toddle gradebook easier + to read, and Mukoko Lingo, an app for learning African languages. The learning + content that used to live here has moved to the Bundu Family ecosystem.

- +
+

Our products

+
@@ -93,25 +87,49 @@ const description =
+ +
+ +
+
+

Language learning · Nyuchi Learning, part of Mukoko

+

+ {mukokoLingo.name} +

+

+ Learn Shona, Ndebele, English or Chinese, five phrases a day, with Shamwari, an + AI tutor that adapts to your level. Teachers set assignments and see each + student's progress. +

+
+ +
+
+

Where the learning went

- One library, three surfaces. + Frameworks and programmes.

The Foundation publishes the open frameworks at Bundu Education. Nyuchi Learning is the commercial vehicle organisations use when they want help implementing them. - Mukoko Lingo is the consumer app for individuals who want to learn a language. Same - content library underneath; different surfaces for different audiences.

-
+
{ surfaces.map((surface) => (
diff --git a/src/pages/legal/cookies.astro b/src/pages/legal/cookies.astro index 54ffc46..e370009 100644 --- a/src/pages/legal/cookies.astro +++ b/src/pages/legal/cookies.astro @@ -201,7 +201,7 @@ const rows = [ storage: tee-settings, a copy of the switches and of which features the licence allows (not the key, and no email); tee-blur-flags (gbx-hide-flags in versions before {extensionVersions.current}), whether flags are concealed; + > in versions before {extensionVersions.changedIn}), whether flags are concealed; tee-course-view, whether you chose My classes in the home page filter; and tee-academic-year, the number of the academic year Toddle is showing. None of it is about a student, and the extension sends none of it diff --git a/src/pages/legal/data.astro b/src/pages/legal/data.astro index 329957b..79576bd 100644 --- a/src/pages/legal/data.astro +++ b/src/pages/legal/data.astro @@ -201,7 +201,7 @@ const subprocessors = [

And four small values in the browser's storage for Toddle's own site: tee-settings, a copy of the switches and of which features the licence allows (no key and no - email); tee-blur-flags (gbx-hide-flags in versions before {extensionVersions.current}), whether flags are concealed; tee-course-view, + email); tee-blur-flags (gbx-hide-flags in versions before {extensionVersions.changedIn}), whether flags are concealed; tee-course-view, whether the teacher chose My classes in the home page filter; and tee-academic-year, the number of the academic year Toddle last asked for. No student data is stored. @@ -246,7 +246,7 @@ const subprocessors = [ A daily check for cancelled keys, only while a licence is entered: one plain request from the extension's background worker to https://licenses.nyuchi.com/v1/revocations (versions - before {extensionVersions.current} asked licences.nyuchi.dev, the + before {extensionVersions.changedIn} asked licences.nyuchi.dev, the same server), sending no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the SHA-256 fingerprints of cancelled keys and checks its own key locally. Cloudflare diff --git a/src/pages/legal/privacy.astro b/src/pages/legal/privacy.astro index add5ece..94cdaaf 100644 --- a/src/pages/legal/privacy.astro +++ b/src/pages/legal/privacy.astro @@ -143,7 +143,7 @@ const description =

  • What is sent. One plain request to https://licenses.nyuchi.com/v1/revocations, from the extension's - background worker (versions before {extensionVersions.current} asked + background worker (versions before {extensionVersions.changedIn} asked licences.nyuchi.dev, the same server). It carries no licence key, no identifiers, no cookies and no Toddle data.
  • diff --git a/src/pages/legal/security.astro b/src/pages/legal/security.astro index 6e29780..ec2f740 100644 --- a/src/pages/legal/security.astro +++ b/src/pages/legal/security.astro @@ -61,7 +61,7 @@ const glance = [ }, { label: "Reviewed", - value: `Two adversarial reviews: ${firstReview.label}, every finding fixed in ${firstReview.version}; and ${secondReview.label}, every finding fixed in ${secondReview.version}, a test build, whose fixes reached schools in ${security.version}.`, + value: `Two adversarial reviews: ${firstReview.label}, every finding fixed in ${firstReview.version}; and ${secondReview.label}, every finding fixed in ${secondReview.version}, a test build, whose fixes reached schools in ${extensionVersions.changedIn}. Version ${security.version} fixed two weaknesses in how licence keys were checked, set out under the second review.`, }, ]; @@ -312,7 +312,7 @@ const severityClass: Record = { A daily check for cancelled keys. At most once a day, only while a licence is entered, the background worker makes one plain request to https://licenses.nyuchi.com/v1/revocations (versions - before {extensionVersions.current} asked licences.nyuchi.dev, the + before {extensionVersions.changedIn} asked licences.nyuchi.dev, the same server). It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, and @@ -536,6 +536,17 @@ const severityClass: Record = { network (the same two outside requests as before), and the list of packages the build uses.

    +

    + Version {security.version}: licence keys. + Two weaknesses in how the extension checked licence keys were fixed in + {security.version}. A cancelled or refunded key could be made to work again by + writing it in a different but equivalent form, which the check for cancelled keys + did not recognise; a key is now accepted only in exactly the form it was issued in. + And because the same signature covers keys and the list of cancelled keys, the + extension now accepts only a licence as a licence. Both concerned whether a licence + is valid; neither touched what the extension reads from Toddle. Every key, as it was + issued, keeps working. +

    We publish what was found, not only that it was fixed, because a school deciding whether to trust software should know. diff --git a/src/pages/mukoko-lingo.astro b/src/pages/mukoko-lingo.astro new file mode 100644 index 0000000..761bca3 --- /dev/null +++ b/src/pages/mukoko-lingo.astro @@ -0,0 +1,364 @@ +--- +/** + * The product page for Mukoko Lingo: a partner app developed by Nyuchi + * Learning, part of Mukoko, Africa's super app. + * + * Built the way the Toddle Enhancement Extension's page is built: a hero with + * one call to action, a grid of what it does, a section for schools, and the + * plain facts a school's IT lead will ask for. + * + * Every claim is taken from Lingo's own public repository + * (mukoko-dev/mukoko-lingo: README.md, app.json, BRANDING.md, and the app's + * own copy in lib/data/translations.ts and app/welcome). Nothing is promised + * that the repository does not say: no app store link, no school price and no + * language beyond the four it calls canonical. Add them here when they exist. + * + * The icon is the app's own (assets/images/icon.png in that repository, MIT + * licensed). Its PNG wordmarks still read "Nyuchi Lingo", the old name, so + * they are not used. + */ +import { Image } from "astro:assets"; +import BaseLayout from "../layouts/BaseLayout.astro"; +import { mukokoLingo } from "../data/site"; +import lingoIcon from "../assets/lingo/icon.png"; + +/* The repository is public, so linking to it is safe (unlike the extension's, + which is private and must never be linked). */ +const source = "https://github.com/mukoko-dev/mukoko-lingo"; +const licence = `${source}/blob/main/LICENSE`; + +const languageList = "Shona, Ndebele, English and Chinese"; + +/* What it does today, from the README's "What it is" and the app's own + feature list on its welcome page. */ +const features = [ + { + mineral: "cobalt", + title: "Five phrases a day", + body: "Each day's lesson is five phrases. You practise them with flash cards, then a short quiz.", + }, + { + mineral: "tanzanite", + title: "Review at the right time", + body: "Spaced repetition decides which phrases come back, and when, so earlier phrases keep returning for practice.", + }, + { + mineral: "gold", + title: "Shamwari, the AI tutor", + body: "Ask Shamwari (“friend” in Shona) a question in chat. It adapts to the level the app has measured, not to a level you picked for yourself.", + }, + { + mineral: "malachite", + title: "Four languages side by side", + body: `${languageList}, with phrases compared across them. Pronunciation guides for all four.`, + }, + { + mineral: "terracotta", + title: "Cultural context", + body: "Usage examples, and when a phrase is the right thing to say. Phrases for real situations: greetings, getting around, shopping, emergencies and work.", + }, + { + mineral: "sodalite", + title: "Progress you can see", + body: "Streaks, XP, bookmarks, and how strong you are in each skill, on one dashboard.", + }, +]; + +/* Who it is for: the app's own "Perfect for" list, plus the schools and + businesses the README names. */ +const audiences = [ + { + badge: "badge badge-info", + label: "For a learner", + title: "Learn the language around you", + body: "Travellers, people living and working somewhere new, and locals building on a language they already have.", + }, + { + badge: "badge badge-success", + label: "For a teacher", + title: "Set the work, see the progress", + body: "Create a class, set assignments against specific phrases and skills, and watch each student's progress.", + }, + { + badge: "badge badge-accent", + label: "For a school or business", + title: "Run it for a whole organisation", + body: "Sync rosters from your school's system, and manage users, content, moderation and analytics from one console.", + }, +]; + +const facts = [ + { + label: "Languages", + value: `${languageList}. The app's screens are available in the same four. Swahili appears in the language picker, but it is not finished, so we do not count it yet.`, + }, + { + label: "Where it runs", + value: `In a web browser, at ${mukokoLingo.url.replace("https://", "")}. Schools, businesses and admins use the console at ${mukokoLingo.console.replace("https://", "")}.`, + }, + { + label: "Assessments", + value: "Diagnostic, formative and summative. They are graded on the server, not on the learner's device, so a score cannot be edited before it is recorded.", + }, + { + label: "Class rosters", + value: "Rosters sync in over OneRoster from Clever, ClassLink and PowerSchool.", + }, + { + label: "Moderation", + value: "Three layers on what learners type to the tutor: rule-based guardrails, an AI check, and detection of attempts to manipulate the tutor (prompt injection). Shamwari never claims to be human.", + }, + { + label: "Sign-in and AI", + value: "Sign-in is through WorkOS AuthKit. The tutor runs on Cloudflare Workers AI, called from Lingo's own servers; the AI credentials never reach a learner's device.", + }, + { + label: "Admin controls", + value: "Organisation admins manage users, content, moderation, guardrails, analytics and API keys.", + }, + { + label: "Source code", + value: "Open source, under the MIT licence. Anyone can read how it works.", + }, +]; + +const title = "Mukoko Lingo — language learning — Nyuchi Learning"; +const description = + "Mukoko Lingo teaches Shona, Ndebele, English and Chinese: five phrases a day, spaced repetition, and Shamwari, an AI tutor. Classes, assignments and assessments for schools. Developed by Nyuchi Learning, part of Mukoko."; + +/* The claims here are the ones a search result or an assistant will repeat, + so they are the repository's, word for word where possible. No `offers`: + the app says "Start for free", but there is no published price for schools, + and a price of 0 would claim more than that. */ +const schema = [ + { + "@type": "SoftwareApplication", + "@id": "https://learning.nyuchi.com/mukoko-lingo#software", + name: mukokoLingo.name, + applicationCategory: "EducationalApplication", + operatingSystem: "Web browser", + description, + url: "https://learning.nyuchi.com/mukoko-lingo", + sameAs: [mukokoLingo.url], + inLanguage: [...mukokoLingo.languageCodes], + author: { + "@type": "Organization", + name: "Nyuchi Learning", + parentOrganization: { "@id": "https://learning.nyuchi.com/#organization" }, + }, + publisher: { "@id": "https://learning.nyuchi.com/#organization" }, + brand: { "@type": "Brand", name: "Mukoko" }, + license: "https://opensource.org/licenses/MIT", + featureList: [ + "Teaches Shona, Ndebele, English and Chinese", + "Five phrases a day, with flash cards and a short quiz", + "Spaced repetition schedules what comes back and when", + "Shamwari, an AI tutor that adapts to the learner's measured proficiency", + "Pronunciation guides and cultural context", + "Classes, assignments and per-student progress for teachers", + "Diagnostic, formative and summative assessments, graded on the server", + "Roster sync over OneRoster from Clever, ClassLink and PowerSchool", + "Three layers of content moderation on what learners type to the tutor", + ], + }, +]; +--- + + + +

    +
    +

    Language learning · Nyuchi Learning

    +
    + +

    + {mukokoLingo.name} +

    +
    +

    + Learn Shona, Ndebele, English or Chinese, five phrases a day, with an AI tutor that + meets you at your level. Teachers can set the work and see who is keeping up. +

    +

    + A partner app developed by Nyuchi Learning, part of + Mukoko, + Africa's super app. +

    + +

    + In your browser at lingo.mukoko.com · starting is free +

    +
    +
    + + +
    +
    +

    What it does today

    +

    + Five phrases a day, and a tutor to ask. +

    +
    +
    + { + features.map((feature) => ( +
    +
    + )) + } +
    +
    + + +
    +
    +
    +

    Who it is for

    +

    + One app, from one learner to a whole school. +

    +
    +
    + { + audiences.map((audience) => ( +
    +

    {audience.label}

    +

    {audience.title}

    +

    {audience.body}

    +
    + )) + } +
    +
    +
    + + +
    +
    +

    For schools

    +

    + Language practice you can assign and measure. +

    +

    + Lingo has two halves. Learners use the app. Teachers and admins use the console, + where the class work happens. +

    +
    + +
    +
    +

    For teachers

    +
      +
    • Create classes.
    • +
    • Set assignments against specific phrases and skills.
    • +
    • See each student's progress, skill by skill.
    • +
    • + Use diagnostic, formative and summative assessments. They are graded on the + server, so the score you see is the score the student earned. +
    • +
    +
    + +
    +

    For school leaders and IT

    +
      +
    • Sync class rosters over OneRoster from Clever, ClassLink or PowerSchool.
    • +
    • Manage users, content, moderation, guardrails, analytics and API keys.
    • +
    • + Three layers of moderation check what students type to the AI tutor. +
    • +
    • Open source code your IT team can read.
    • +
    +
    +
    + + +
    + + +
    +
    +
    +

    For whoever has to sign this off

    +

    The facts, plainly.

    +
    +
    + { + facts.map((fact) => ( +
    +
    + {fact.label} +
    +
    {fact.value}
    +
    + )) + } +
    + +
    +
    + + +
    +
    +

    Get it

    +

    + Open it in your browser and start. +

    +

    + Go to + lingo.mukoko.com, + choose a language and take today's five phrases. Starting is free. For a class or a + whole school, write to us. +

    +

    + Who makes it. Mukoko Lingo is a partner app + developed by Nyuchi Learning, part of Mukoko, Africa's super app. Nyuchi Learning is + part of Nyuchi Africa, which also makes the + Toddle Enhancement Extension. +

    + +
    +
    + diff --git a/src/pages/toddle-enhancement-extension.astro b/src/pages/toddle-enhancement-extension.astro index 0169141..6f396a7 100644 --- a/src/pages/toddle-enhancement-extension.astro +++ b/src/pages/toddle-enhancement-extension.astro @@ -8,9 +8,10 @@ * switch gets the most room because it is the part people will be asked to * justify. * - * It describes version 0.9.0 (`extensionVersions.current`), on the Chrome Web - * Store since 7 October 2026, and names 0.8.2 only where updating from it - * matters. + * It describes version 0.11.0 (`extensionVersions.current`), on the Chrome + * Web Store: 0.10.0 with a security fix to how licence keys are checked. It + * names 0.8.2 only where updating from it matters, and 0.9.0 + * (`extensionVersions.changedIn`) only for the old licence host. * Flags are "concealed", never "hidden": the extension no longer hides * Toddle's own flags (see `flags` in src/data/legal.ts). */ @@ -200,7 +201,7 @@ const facts = [ { label: "Its two outside connections", value: - `The Send feedback form, only when you press Send: what you typed and the version number, to our feedback form, processed by Formspree. And, only while a licence is entered, at most one request a day to licenses.nyuchi.com (versions before ${extensionVersions.current} asked licences.nyuchi.dev, the same server) for a signed list of cancelled keys' fingerprints, which sends no key, no identifiers and no Toddle data; if it fails, the last result stands.`, + `The Send feedback form, only when you press Send: what you typed and the version number, to our feedback form, processed by Formspree. And, only while a licence is entered, at most one request a day to licenses.nyuchi.com (versions before ${extensionVersions.changedIn} asked licences.nyuchi.dev, the same server) for a signed list of cancelled keys' fingerprints, which sends no key, no identifiers and no Toddle data; if it fails, the last result stands.`, }, { label: "What it stores", @@ -514,7 +515,7 @@ const schema = [ toolbar menu's Enter a licence key. If your network filters outbound traffic, allow {toddleExtension.licenceHost} (and licences.nyuchi.dev, which versions - before {extensionVersions.current} asked, for any computer Chrome has not yet updated) so the daily check for cancelled keys can run. If it is blocked, licences keep + before {extensionVersions.changedIn} asked, for any computer Chrome has not yet updated) so the daily check for cancelled keys can run. If it is blocked, licences keep working on the last result.

    diff --git a/src/pages/toddle-enhancement-extension/releases.astro b/src/pages/toddle-enhancement-extension/releases.astro index b1f925e..012e746 100644 --- a/src/pages/toddle-enhancement-extension/releases.astro +++ b/src/pages/toddle-enhancement-extension/releases.astro @@ -16,7 +16,7 @@ import { toddleExtension } from "../../data/site"; const title = "Releases — Toddle Enhancement Extension — Nyuchi Learning"; const description = - "What changed in each version of the Toddle Enhancement Extension, in plain words for teachers: version 0.10.0, on the Chrome Web Store since 13 October 2026, and every version before it."; + "What changed in each version of the Toddle Enhancement Extension, in plain words for teachers: version 0.11.0, the current version on the Chrome Web Store, and every version before it."; const storeCta = toddleExtension.webStore ? { label: "Add to Chrome", href: toddleExtension.webStore } diff --git a/tests/links.test.ts b/tests/links.test.ts index 017f424..88c2bdb 100644 --- a/tests/links.test.ts +++ b/tests/links.test.ts @@ -18,7 +18,8 @@ describe("ecosystem destinations", () => { for (const url of [ "https://bundu.org/education", "https://nyuchi.com/learning", - "https://mukoko.com/lingo", + // Mukoko Lingo's live app, as its own repository's README names it. + "https://lingo.mukoko.com", ]) { it(`links to ${url}`, () => { expect(`${home}${siteData}`).toContain(url); diff --git a/tests/seo.test.ts b/tests/seo.test.ts index fa43457..b2524fd 100644 --- a/tests/seo.test.ts +++ b/tests/seo.test.ts @@ -20,6 +20,7 @@ const PAGES = [ "index.html", "toddle-enhancement-extension/index.html", "toddle-enhancement-extension/releases/index.html", + "mukoko-lingo/index.html", "legal/privacy/index.html", "legal/terms/index.html", "legal/security/index.html", @@ -268,6 +269,20 @@ describe("the releases page", () => { expect(page).toContain('id="v0.9.10"'); }); + it("says 0.11.0 is on the store, and gives the later test builds its anchor", () => { + const v0110 = release("0.11.0"); + expect(v0110).toContain("On the Chrome Web Store"); + expect(v0110).not.toContain("Coming soon to the Chrome Web Store"); + expect(page).toContain('id="v0.10.1"'); + expect(page).toContain('id="v0.11.2"'); + }); + + it("lists 0.11.0 first, and does not list 0.12.0, which is not released", () => { + expect(page.search(/

  • { expect(page).not.toMatch(/github\.com/i); expect(page).not.toMatch(/\.zip\b|\.crx\b/i); @@ -306,3 +321,65 @@ describe("prices", () => { expect(page).toContain("US$7 off the organisation licence"); }); }); + +describe("Mukoko Lingo", () => { + /* A partner app developed by Nyuchi Learning, part of Mukoko. It sits + beside the extension wherever the extension appears, so the two read as + Nyuchi Learning's products. */ + const page = built("mukoko-lingo/index.html"); + + it("is in the header, the footer and on the home page", () => { + const home = built("index.html"); + const header = home.split("")[0] ?? ""; + const footer = home.split("")[0] ?? ""], + ]) { + expect(html, where).toContain('href="/mukoko-lingo"'); + expect(html, where).toContain('href="/toddle-enhancement-extension"'); + } + }); + + it("says who developed it", () => { + expect(page).toContain("developed by Nyuchi Learning, part of"); + expect(page).toContain("Mukoko,"); + }); + + it("describes it as a SoftwareApplication, as the extension is", () => { + const match = + /