<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[North Growth Lab Engineering]]></title><description><![CDATA[Practical engineering notes on conversion-focused websites, technical SEO, landing pages, growth systems, web applications, and native mobile products.]]></description><link>https://northgrowthlab.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a8c991613e270be88ff7a2c/2def9c09-6447-4d41-ba99-f23975d2deec.png</url><title>North Growth Lab Engineering</title><link>https://northgrowthlab.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 02 Sep 2026 16:12:52 GMT</lastBuildDate><atom:link href="https://northgrowthlab.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Architecture Behind a Multilingual SwiftUI Verb Trainer]]></title><description><![CDATA[A language-learning app can look simple on the surface: search for a verb, open a conjugation table, press a pronunciation button, and answer a few questions.
The implementation becomes much less simp]]></description><link>https://northgrowthlab.hashnode.dev/the-architecture-behind-a-multilingual-swiftui-verb-trainer</link><guid isPermaLink="true">https://northgrowthlab.hashnode.dev/the-architecture-behind-a-multilingual-swiftui-verb-trainer</guid><category><![CDATA[Swift]]></category><category><![CDATA[iOS]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[mobile app development]]></category><dc:creator><![CDATA[North Growth Lab]]></dc:creator><pubDate>Tue, 25 Aug 2026 10:01:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8c991613e270be88ff7a2c/9a95c276-9c05-4358-b21f-b8c8972ef7f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A language-learning app can look simple on the surface: search for a verb, open a conjugation table, press a pronunciation button, and answer a few questions.</p>
<p>The implementation becomes much less simple when the same product must handle irregular forms, different grammatical systems, offline use, deterministic practice, source attribution, and a UI that remains fast as the catalog grows.</p>
<p>We encountered those constraints while building <strong>Verb Lab</strong>, a native iOS product at North Growth Lab. This article is not a launch announcement or a claim about learning outcomes. It is a practical account of the architecture decisions that made the current prototype testable and maintainable.</p>
<h2>1. Treat linguistic content as product data, not view copy</h2>
<p>Our first useful boundary was between authoring data and runtime data.</p>
<p>Large dictionaries, frequency sources, generated inflections, and translation candidates belong in the build pipeline. They are useful inputs, but they should not automatically become user-facing content. The app bundles only reviewed release-core records with the fields the runtime actually needs: lemma, language, definition, examples, moods, tenses, subjects, forms, and spoken text.</p>
<p>That decision creates two different systems:</p>
<ul>
<li>an <strong>authoring pipeline</strong> that imports, normalizes, audits, and exports content;</li>
<li>a <strong>runtime catalog</strong> that is small enough to load predictably and strict enough to test exhaustively.</li>
</ul>
<p>This is more work than dropping a large JSON file into the application bundle. It also prevents a common failure mode: the interface quietly becoming the quality-control layer for unreviewed data.</p>
<h2>2. Keep the learning engine independent of SwiftUI</h2>
<p>The domain model and practice generator live in a separate Swift package. SwiftUI views consume the results, but they do not decide which tense to test, how distractors are selected, or how a language record is interpreted.</p>
<p>Conceptually, the dependency direction is:</p>
<pre><code class="language-text">versioned content -&gt; domain model -&gt; practice/search services -&gt; SwiftUI views
</code></pre>
<p>The UI never needs to know where a conjugation record came from. The practice engine never needs to know whether the answer will be displayed in a list, a card, or a future widget.</p>
<p>This boundary gave us three immediate benefits:</p>
<ol>
<li>Core behavior can be tested with <code>swift test</code> without launching a simulator.</li>
<li>Content-validation tools can reuse the same domain assumptions as the app.</li>
<li>UI redesigns do not require a rewrite of the learning logic.</li>
</ol>
<p>For a content-heavy product, this separation is not academic architecture. It is what makes iteration affordable.</p>
<h2>3. Make daily practice deterministic</h2>
<p>Random quizzes are easy to generate and surprisingly difficult to debug. If a question is malformed only for one combination of verb, tense, and subject, a fully random session may be impossible to reproduce.</p>
<p>Verb Lab derives a daily seed from the calendar day. The engine then uses stable offsets to choose a verb, a preferred conjugation, a form, and distractors. A new practice round adds another deterministic offset.</p>
<p>The simplified pattern looks like this:</p>
<pre><code class="language-swift">let daySeed = daysSinceEpoch(startOfDay)
let verbIndex = positiveModulo(daySeed + position * 7, verbs.count)
let tenseIndex = positiveModulo(daySeed + position * 3, tenses.count)
let formIndex = positiveModulo(daySeed + position * 5, forms.count)
</code></pre>
<p>The exact multipliers are less important than the property they create: the same date, catalog, and round produce the same session.</p>
<p>That makes a bug report reproducible. It also gives a learner a coherent daily set without requiring an account, remote scheduler, or server-generated session.</p>
<h2>4. Preserve language differences inside a shared model</h2>
<p>English, French, German, and Spanish do not fit one universal tense list. A shared interface should not erase those differences.</p>
<p>We use one general verb model, but the practice engine selects preferred mood-and-tense pairs by language. If a preferred group is unavailable, it falls back to valid conjugations already present in that verb record.</p>
<p>This is a useful middle ground:</p>
<ul>
<li>the application does not duplicate its entire architecture for every language;</li>
<li>the domain layer still represents language-specific moods, auxiliaries, subjects, and compound forms;</li>
<li>the view renders the data it receives instead of pretending every grammar system has the same shape.</li>
</ul>
<p>Shared code is valuable only when it preserves the distinctions the product exists to teach.</p>
<h2>5. Build an offline-first core before adding accounts</h2>
<p>Search, favorites, practice, progress, and pronunciation do not require a network request in the current core experience.</p>
<p>The reviewed catalogs ship with the application. Favorites and practice statistics use local storage. Progress is small and explicit: total answers, correct answers, completed sessions, the last completion date, and current and best streaks.</p>
<p>This reduced the first-release surface area significantly. We did not need to introduce authentication, synchronization conflicts, an availability dependency, or a privacy-sensitive analytics identity just to prove the core interaction.</p>
<p>Offline-first does not mean cloud features are forbidden. It means the product remains useful when the cloud is absent. Sync can be added later if it solves a demonstrated cross-device problem.</p>
<h2>6. Isolate native speech behind a service</h2>
<p>Pronunciation is part of the learning loop, so the app uses Apple's <code>AVSpeechSynthesizer</code> with language-specific voices.</p>
<p>Speech has its own state and lifecycle: the current phrase, selected rate, playback status, cancellation, and delegate callbacks. Keeping that behavior in a service prevents each view from becoming a collection of audio edge cases.</p>
<p>The views ask for an utterance. The service decides how to stop the previous phrase, select the voice, configure the rate, and publish playback state.</p>
<p>This also leaves a clean upgrade path. Recorded native-speaker audio or speech scoring could be introduced behind the same product boundary without rewriting every conjugation row.</p>
<h2>7. Test the content as aggressively as the code</h2>
<p>In a language product, a valid build can still ship broken content.</p>
<p>Our checks cover both software behavior and release data. The current active release cores contain 1,150 reviewed verb records, and tests audit properties such as duplicate lemmas, invalid language identifiers, empty or malformed forms, translation-key coverage, and accidental exposure of authoring-only entries.</p>
<p>The build pipeline also keeps source attribution and linguistic-resource licences with the project. Content provenance is an architectural requirement, not a paragraph to reconstruct immediately before release.</p>
<p>A practical release gate for this kind of product should answer four questions:</p>
<ol>
<li>Can the parser load every bundled record?</li>
<li>Can search and practice use every released record safely?</li>
<li>Can we explain where the underlying data came from and how it may be used?</li>
<li>Has reviewed content remained separate from generated or experimental material?</li>
</ol>
<h2>8. Keep commercial state outside the learning engine</h2>
<p>The prototype includes a StoreKit 2 subscription flow for testing entitlement, restoration, and localized pricing behavior. The learning engine does not depend directly on StoreKit.</p>
<p>Instead, entitlement controls which reviewed content or explanation paths may be requested. That prevents transaction logic from leaking into verb models and practice questions.</p>
<p>It also makes local testing safer: a bundled StoreKit configuration can exercise monthly, annual, restore, and management flows without charging a real account.</p>
<h2>What we would measure after launch</h2>
<p>Architecture can prove that a product is testable. It cannot prove that the product is valuable.</p>
<p>The next evidence should come from actual use:</p>
<ul>
<li>how quickly learners find the form they searched for;</li>
<li>whether a lookup becomes a completed practice session;</li>
<li>which language pairs fail because content is incomplete or unclear;</li>
<li>whether people return for repeated practice;</li>
<li>which speech and explanation controls are used at the moment of confusion.</li>
</ul>
<p>Until those signals exist, we avoid claiming retention, adoption, or learning improvement.</p>
<h2>A reusable checklist for content-heavy mobile apps</h2>
<p>If you are designing a similar product, ask:</p>
<ul>
<li>Is authoring data separated from reviewed runtime data?</li>
<li>Can the domain and business logic run without the UI framework?</li>
<li>Can a reported session or generated result be reproduced?</li>
<li>Does shared code preserve real domain differences?</li>
<li>Which workflows genuinely need an account or network request?</li>
<li>Are device capabilities isolated behind services?</li>
<li>Are content quality, provenance, and licences part of the release gate?</li>
<li>Can payment and entitlement change without contaminating the core model?</li>
</ul>
<p>Those boundaries matter more than the number of screens in the first prototype.</p>
<p>You can explore the interactive product preview and the evidence behind the build in the <a href="https://www.northgrowthlab.com/portfolio?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=verblab_architecture_20260825#verb-lab">North Growth Lab portfolio</a>.</p>
<hr />
<p><em>Disclosure: Verb Lab is a North Growth Lab product in development. The architecture and capabilities described here are implemented in the current prototype. This article does not claim App Store availability, adoption, retention, or learning outcomes.</em></p>
]]></content:encoded></item></channel></rss>