<?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[China Phrases Build Notes]]></title><description><![CDATA[China Phrases Build Notes]]></description><link>https://chinaphrases.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Nov 2026 09:03:42 GMT</lastBuildDate><atom:link href="https://chinaphrases.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Offline text and offline speech are two different promises in a travel PWA]]></title><description><![CDATA[Why cached text does not prove offline audio works: local voice selection, speech events, cancellation and two focused tests in a travel PWA.]]></description><link>https://chinaphrases.hashnode.dev/offline-text-and-offline-speech-are-two-different-promises-in-a-travel-pwa</link><guid isPermaLink="true">https://chinaphrases.hashnode.dev/offline-text-and-offline-speech-are-two-different-promises-in-a-travel-pwa</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Li Xin · China Phrases]]></dc:creator><pubDate>Sat, 31 Oct 2026 10:16:21 GMT</pubDate><content:encoded><![CDATA[<p>A downloaded phrase card and a working voice are separate capabilities. A travel PWA can have all of its text available without a network connection while its speech button still cannot produce sound. Treating both as a single offline-ready state hides a dependency the application does not fully control.</p>
<p>I build China Phrases, a small communication tool for international visitors to China. This development note was written with AI assistance and checked against our current implementation and two focused automated tests on 31 October 2026. It is not a claim of physical-device or audible-quality testing.</p>
<h2>A useful fallback starts with the task</h2>
<p>At a ticket counter, the immediate task is to communicate a specific question. For example: 请帮我确认一下入口。 — Please help me confirm the entrance. The Chinese text should remain readable even if speech is unavailable. Sound is another way to convey the same message, not a prerequisite for opening it.</p>
<p>Our presentation view renders the Chinese message independently of the speech engine. Voice status is shown alongside the playback control. A missing voice or synthesis error therefore does not need to replace the entire conversation screen with an error page.</p>
<h2>Ask the device what it can supply</h2>
<p>Calling a browser speech API does not by itself mean that the selected voice is local. MDN distinguishes locally supplied and remotely supplied voices through SpeechSynthesisVoice.localService:
<a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisVoice/localService">https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisVoice/localService</a></p>
<p>Our voice selection requires localService to be exactly true, then checks a Chinese-language allowlist and prioritizes zh-CN. We deliberately do not fall back to a remote voice. That choice can make playback unavailable on a device that could speak through an online service. It keeps the product's device-only playback boundary more predictable.</p>
<p>The available voice list is itself a runtime input. MDN documents getVoices() as returning the voices available on the current device, and its example refreshes the list when voiceschanged fires:
<a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/getVoices">https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/getVoices</a></p>
<p>We query that list rather than assuming a named voice is installed. A voiceschanged listener refreshes availability when playback is neither pending nor active. This allows a later-arriving local voice to become usable without hard-coding a device vendor's voice name. It does not prove that a particular phone will actually play intelligible audio offline.</p>
<h2>Requested is not playing</h2>
<p>A successful call to speak() is too early to show a playing state. Our wrapper enters pending first and waits for the utterance's start event. End returns to ready; an error produces an error state. If pending lasts eight seconds, the wrapper cancels and reports failure instead of leaving an indefinite spinner.</p>
<p>Cancellation also needs to invalidate callbacks. Each stop advances a generation counter. The utterance callbacks retain the generation they were created with and ignore events from an earlier generation. Otherwise a delayed start event from a cancelled phrase could make the new screen say it was speaking the old message.</p>
<p>The eight-second limit is an application decision, not a browser guarantee. Likewise, the language filter is a selection rule, not a pronunciation-quality test. Those distinctions belong in an engineering description even when they would be unnecessary detail in the main traveller interface.</p>
<h2>What the tests establish</h2>
<p>I reran two existing focused tests for this note. Both passed. The first supplies a fake engine with a remote Mandarin voice and a local zh-HK voice: neither is accepted by our selection rule. It then adds a local zh-CN voice, emits voiceschanged, verifies selection, checks that a cancelled utterance cannot change the state, and exercises a synthesis error.</p>
<p>The second holds the fake engine in pending, triggers the timeout and checks that the result becomes an error rather than playing. These tests verify our control logic. They do not install a voice pack, disconnect a physical phone or listen to its output.</p>
<h2>Make the offline claim small enough to verify</h2>
<p>For a real-device check, I would separately verify downloaded text after a cold reopen with networking disabled, then play a short phrase on that same device. I would repeat after cancelling speech and reopening the presentation view. This is an acceptance checklist, not a report that those device checks have already been completed.</p>
<p>Our current product uses device speech; it does not ship a prerecorded audio pack. Its offline text and voice checks should therefore remain distinct. Adding a downloaded web bundle cannot, on its own, be evidence that the operating system has the required voice.</p>
<p>The current interface includes a voice test and a text presentation fallback. You can inspect the free flow here:
<a href="https://phrases.donthink.top/app/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=offline_speech">https://phrases.donthink.top/app/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=offline_speech</a></p>
<p>For a small travel tool, a dependable fallback is more useful than an oversized capability label. Show what is ready, show what failed, and keep the question readable.</p>
]]></content:encoded></item><item><title><![CDATA[Why our food-allergy selector forgets its choices on reload]]></title><description><![CDATA[A deployed travel-app case study: keeping allergy selections out of URLs, storage and analytics, with explicit reload behavior and QA limits.]]></description><link>https://chinaphrases.hashnode.dev/why-our-food-allergy-selector-forgets-its-choices-on-reload</link><guid isPermaLink="true">https://chinaphrases.hashnode.dev/why-our-food-allergy-selector-forgets-its-choices-on-reload</guid><dc:creator><![CDATA[Li Xin · China Phrases]]></dc:creator><pubDate>Sat, 31 Oct 2026 07:28:42 GMT</pubDate><content:encoded><![CDATA[<p>A food-allergy selector does not need an account profile to do its job. In our travel web app, selected allergens live in a JavaScript variable for the current page lifetime. They are not copied into the address bar, persistent storage, account records or analytics. Reloading deliberately empties the list.</p>
<p>I develop China Phrases. This is an implementation note about a feature deployed on 31 October 2026, written with AI assistance and checked against the implementation and browser behavior. It is not medical advice, a clinical validation or a report of customer outcomes.</p>
<h2>Separate public content from private choices</h2>
<p>The public resource is a translated ingredient dictionary, regional questions to ask a restaurant, and a few prepared replies. None of that requires knowing which ingredients a particular person must avoid.</p>
<p>The private resource is the person's selection. Keeping these two kinds of data separate lets the dictionary be included in the existing offline application bundle without storing an individual allergy profile in that bundle.</p>
<p>The app keeps an array of allowlisted ingredient IDs in page memory. Generating the Chinese message filters unknown IDs and duplicates. The generated message uses translated labels from the dictionary, rather than interpreting arbitrary input as HTML. Choosing a destination changes the contextual reminder; it does not send the chosen ingredients to the server.</p>
<h2>Why sessionStorage was not the answer</h2>
<p>A tab-scoped storage name sounds temporary, but it does not imply that a reload clears the values. MDN documents that sessionStorage survives page reloads and restores: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage">https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage</a></p>
<p>For this flow, that lifetime is longer than the promise we wanted to make. The interface explicitly says that refreshing clears the choices. Ordinary page memory matches that behavior more directly than persisting the list and implementing a separate deletion routine.</p>
<p>There is a cost: after an accidental reload, users must select their allergens again. We accept that friction, show the lifetime rule beside the controls, provide an explicit Clear selection action, and disable the presentation button until the list is nonempty. This is a product tradeoff, not a claim that every health-related interface should forget everything.</p>
<h2>Navigation can accidentally become storage</h2>
<p>Avoiding localStorage is only one part of the design. A developer can still place a selection in query parameters, a share link, navigation history state or an event payload.</p>
<p>Our existing routes describe destinations and travel tasks. The allergy flow does not append selected ingredient IDs to those routes. Modal navigation remembers the kind of screen and destination, but not the selected allergy list. On reload, a stale presentation-screen history entry falls back to the empty selection screen instead of displaying an empty statement as if it were ready to show.</p>
<p>The traffic counter records aggregate page and referral information. It does not collect the selected ingredients, checkbox values or restaurant replies. This is deliberately a boundary in the implementation, not a consent checkbox added after collection.</p>
<h2>A reply is not a safety certificate</h2>
<p>The screen shows the selected ingredients in Chinese with the traveller's language underneath. Staff can indicate that a listed ingredient is present, shared equipment may transfer it, they cannot confirm, or they have checked and need to discuss the details.</p>
<p>None of these buttons produces a green safe-to-eat result. FARE explains why allergen cross-contact must be considered separately from an obvious ingredient: <a href="https://www.foodallergy.org/resources/avoiding-cross-contact">https://www.foodallergy.org/resources/avoiding-cross-contact</a></p>
<p>The tool helps start a conversation about ingredients and preparation. It does not know the actual kitchen process, cover every allergen, or replace a person's allergy care plan. Unlisted allergens need an appropriate translated card checked by a professional.</p>
<h2>What was actually verified</h2>
<p>Six focused tests cover the ten destination reminders and five language entries, allowlisted message construction, separation from persistent routes and analytics, multilingual discovery, cautious reply behavior, and inclusion in the offline build. The full frontend suite passed 122 tests.</p>
<p>Browser checks at 360 and 390 CSS pixels exercised multiple selections, changing destination, showing Chinese, an unable-to-confirm reply, returning to edit, and refreshing. Returning and presenting again removed the old reply. Reloading cleared the ingredient choices. The checks also caught two ordinary usability defects: a small native select and a generic paragraph rule overriding the Chinese text size. Those were corrected before deployment.</p>
<p>After deployment, the production app was opened through its real domain and its previous service-worker cache updated using the visible Update button. The English selection-to-reply flow worked there. This is desktop Chrome responsive testing, not a physical iPhone or Android test. Medical review, native-language review and audible voice quality are not claimed by these results.</p>
<p>The implemented flow is available from Food allergies on the app home or destination pages: <a href="https://phrases.donthink.top/app/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=allergy_privacy">https://phrases.donthink.top/app/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=allergy_privacy</a></p>
<p>For a small communication tool, the useful engineering question was not how to synchronize more user data. It was which data the conversation actually required us to retain, and for how long.</p>
]]></content:encoded></item><item><title><![CDATA[Designing a reply screen that can admit it does not understand]]></title><description><![CDATA[How a travel tool clears stale answers, separates missing fields and avoids turning a selected reply into a false success state.]]></description><link>https://chinaphrases.hashnode.dev/designing-a-reply-screen-that-can-admit-it-does-not-understand</link><guid isPermaLink="true">https://chinaphrases.hashnode.dev/designing-a-reply-screen-that-can-admit-it-does-not-understand</guid><category><![CDATA[UX]]></category><dc:creator><![CDATA[Li Xin · China Phrases]]></dc:creator><pubDate>Sat, 31 Oct 2026 05:28:01 GMT</pubDate><content:encoded><![CDATA[<p>A prepared phrase can get a conversation started, but a reply screen needs an explicit way to stop. When an answer is unclear, clear the old selection, clear the old numbers, and remove any next step that depended on them. Otherwise a helpful interface can display a confident result that nobody actually confirmed.</p>
<p>I develop China Phrases, a small communication tool for international visitors to China. This article describes one implementation decision in the project, checked against its source on 31 October 2026. Research and drafting were AI-assisted. The examples below illustrate interface behavior; they are not reports of a traveller using the tool or measurements of comprehension.</p>
<h2>Two readers share one small screen</h2>
<p>The person holding the phone may read English, Korean, Japanese or Russian. The person answering at a counter may read Chinese. An interface designed only for the phone owner misses half the interaction.</p>
<p>Our prepared reply view puts the Chinese question first, then the traveller's selected interface language. Reply choices pair a Chinese label with its corresponding translation. Amount and time fields give the other person a way to enter a concrete value. That is a constrained form, not live translation or speech recognition. It works only for the choices and fields the card actually supports.</p>
<p>For example, a luggage-storage question may need three separate values: the storage fee, any deposit, and the pickup time. A single amount labelled price cannot explain all three. The data model needs to preserve the distinction before typography or button styling can help.</p>
<h2>Model uncertainty as state</h2>
<p>The current state contains a selected reply, a dictionary of entered values, and an uncertainty flag. The important transitions are small:</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>Selected reply</th>
<th>Entered values</th>
<th>Uncertain</th>
</tr>
</thead>
<tbody><tr>
<td>Select a different reply</td>
<td>New choice</td>
<td>Cleared</td>
<td>No</td>
</tr>
<tr>
<td>Confirm entered fields</td>
<td>Retained</td>
<td>Replaced with current values</td>
<td>No</td>
</tr>
<tr>
<td>Answer is unclear</td>
<td>Cleared</td>
<td>Cleared</td>
<td>Yes</td>
</tr>
<tr>
<td>Clear or start another card</td>
<td>Cleared</td>
<td>Cleared</td>
<td>No</td>
</tr>
</tbody></table>
<p>Selecting another reply clears the old fields because those values might belong to an answer that no longer applies. The unclear action also removes the generated summary and any suggested branch. It does not merely add a warning underneath yesterday's confident-looking answer.</p>
<p>This rule matters even when the tool has no generative model. Stale interface state can mislead a person just as effectively as a bad sentence.</p>
<h2>Separate a selected answer from a completed task</h2>
<p>A reply button means someone selected that prepared reply. It does not prove that a reservation exists, payment settled, or admission was granted. The wording of the result should describe what the interface actually knows.</p>
<p>In this implementation, a reply can lead to a relevant follow-up card, a request for staff assistance, or a limited confirmation about a selected method. There is no universal success action for every positive-sounding response. In particular, a waitlist or an unconfirmed transaction must not silently lead to an entry-success state.</p>
<p>The same distinction applies to fields. Blank values stay missing. If a card needs more information, the summary says that information is pending. A refusal can disable fields that would otherwise imply the service was available. A zero fee and a blank fee are different answers.</p>
<h2>Give the conversation a repair path</h2>
<p>An other-or-unclear control is useful only if its next screen helps the two people recover. Our repair path replaces the previous result with an unconfirmed state and a prepared request for clarification. It does not invent another answer.</p>
<p>One original example of a narrower question is: “Please write the latest pickup time.” The Chinese equivalent is “请写下最晚取行李时间。” This asks for a value with a clear role. It is easier to verify than assuming an unfamiliar spoken response means yes. A date or tomorrow-versus-today question still needs to be made explicit when the situation requires it.</p>
<p>A prepared interface cannot cover every response. If none of the choices fits, the user needs another communication method or help from staff. Showing that boundary is part of the design, not a failure message to hide.</p>
<h2>Test the wrong transitions</h2>
<p>On 31 October 2026, three existing tests related to these decisions passed. One checks that the unclear action removes the previous choice and values, and that a subsequent deliberate selection exits uncertainty. Another checks that luggage costs and directional transport fields are represented separately. The third checks that unconfirmed transactions and waitlists do not imply successful entry.</p>
<p>These are targeted state and content tests. They do not establish that the Chinese is suitable in every setting, that every translation has been reviewed by a native speaker, or that a rushed visitor will understand the screen. Those require separate language review and observed usability work.</p>
<h2>A reusable design check</h2>
<p>Before shipping a two-person reply screen, ask four questions: can the respondent see the question in their language; can they distinguish each requested value; can they reject the offered choices; and does a changed answer invalidate the old result?</p>
<p>These checks apply to prepared travel phrases, reception desks and other constrained forms. They are useful before adding an AI conversation layer, because a more fluent answer cannot repair an interface that keeps the wrong state.</p>
<p>You can inspect the public interaction in <a href="https://phrases.donthink.top/app/?lang=en&amp;city=all&amp;utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=reply_state">China Phrases</a>. This is my commercial project with free sample cards. It provides prepared phrases, not booking services or a guarantee that a real-world task is complete.</p>
]]></content:encoded></item><item><title><![CDATA[A page view is not a customer: a small referral counter for a travel PWA]]></title><description><![CDATA[A developer case study on UTM labels, referrer limits, privacy-aware page views and why referral counts do not prove customer acquisition.]]></description><link>https://chinaphrases.hashnode.dev/a-page-view-is-not-a-customer-a-small-referral-counter-for-a-travel-pwa</link><guid isPermaLink="true">https://chinaphrases.hashnode.dev/a-page-view-is-not-a-customer-a-small-referral-counter-for-a-travel-pwa</guid><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Li Xin · China Phrases]]></dc:creator><pubDate>Sat, 31 Oct 2026 04:58:28 GMT</pubDate><content:encoded><![CDATA[<p>A tiny product can learn which links bring page opens without storing every URL, search term or user action. The trade-off is deliberate: the result is a referral counter, not a customer attribution system.</p>
<p>I develop China Phrases, a prepared-phrase tool for visitors to China. This is a developer case study of its tracking implementation, with AI-assisted research and drafting. It does not report visitor identities, customer data or acquisition results.</p>
<h2>Start with the question the counter can answer</h2>
<p>The useful question is: which labelled links or referring sites are associated with online page opens? That is narrower than asking which campaign acquired a paying customer. A reload can add another page view. A preview or automated request can also distort a small sample. Without a visitor identifier, do not label the total “people.” Without a tested connection to a transaction, do not label it “sales.”</p>
<p>The same caution applies to AI referrals. A referring hostname can be evidence of a link visit from that service. It is not proof that a model indexed the site, trained on it or recommended it organically. Keep those claims out of the dashboard unless you have separate evidence.</p>
<h2>Normalize before sending</h2>
<p>The client reads the current URL but sends only a small event: public page path, attribution kind, source, medium and campaign. The app route is collapsed to /app/; query parameters that describe the visitor's search, card state or purchase flow do not belong in the event.</p>
<p>Campaign labels use a restricted character set and length. Invalid labels are discarded. The source selection is simple:</p>
<ol>
<li>Use a valid utm_source, with valid medium and campaign labels.</li>
<li>Otherwise inspect the referrer's origin and hostname, without sending the complete URL.</li>
<li>Classify same-origin navigation separately from an external referral.</li>
<li>If there is no usable source, retain a direct-or-unavailable bucket.</li>
</ol>
<p>MDN documents that document.referrer can be empty for direct navigation. More generally, absence of referral data is not evidence that a user typed the address. Naming the bucket honestly is cheaper than trying to reconstruct something the browser did not tell you.</p>
<h2>Treat attribution fields as untrusted input</h2>
<p>A restricted syntax reduces accidental collection but does not make a label trustworthy. A campaign string that looks valid might still contain information it should not contain. Use a small, documented naming convention such as blog_launch or metro_guide; do not put an email, order reference or person's name into a link.</p>
<p>Client filtering also is not a security boundary. The server must validate fields and permitted paths again, reject unexpected data and apply limits. Display incoming labels as text rather than HTML. In this project, the analytics table is separate from accounts and orders, and the reporting endpoint requires an authorized server-side session.</p>
<h2>Avoid creating a dependency for the main task</h2>
<p>The visitor may be standing in front of a ticket counter. An analytics failure must not interrupt the phrase they need. The client sends one best-effort event after the document becomes visible. It omits credentials on that request, uses a no-referrer policy and catches network failures without surfacing a blocking message.</p>
<p>This does not mean that hosting infrastructure never processes an IP address. It means the aggregate analytics table is not a visitor profile store. Infrastructure security logs and rate limiting have their own behavior and need their own accurate description.</p>
<p>The implementation skips admin/API paths and local-file previews, respects the privacy opt-out signals it checks, and avoids counting a hidden page until it becomes visible. Offline use is not backfilled later. Those choices keep the measurement modest and useful, while leaving known gaps in the data.</p>
<h2>Test the data you refuse to collect</h2>
<p>On 31 October 2026, I reran the project's four existing client/admin-rendering tests with Node's test runner. All four passed. They cover:</p>
<ul>
<li>Sending a normalized path and campaign labels instead of sensitive URL query data, with credentials omitted.</li>
<li>Skipping privacy opt-outs, local files and admin pages, and sending once when a hidden document becomes visible.</li>
<li>Discarding invalid UTM values and distinguishing internal navigation.</li>
<li>Escaping source labels in the admin view and showing missing pre-launch dates as missing, rather than inventing historical zeroes.</li>
</ul>
<p>These are bounded unit tests, not a claim that every device or browser has been validated. They also do not establish that a particular campaign generated genuine human visits. That is a separate interpretation problem.</p>
<h2>Read the report conservatively</h2>
<p>Before drawing a conclusion, record the reporting period, when measurement began, known QA labels and what counts as one event. A new publication can have a working public URL while its referral total is still unknown. A zero in one instrument can coexist with unmeasured visits. Keep “not observed,” “not measured” and “confirmed zero within this scope” distinct.</p>
<p>For a small product, this is enough to compare the early direction of channels without pretending to know more about visitors than the system measures. Improve attribution only when there is a concrete decision that requires it.</p>
<p>The project discussed here is <a href="https://phrases.donthink.top/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=referral_counter">China Phrases</a>. It is a commercial tool with free samples, not an analytics service.</p>
<p>Reference: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/referrer">MDN — Document.referrer</a>, checked 31 October 2026. Implementation and test claims were checked against the project's local source and the four-test run on that date.</p>
]]></content:encoded></item></channel></rss>