<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ashwin Sathian</title>
    <description>The latest articles on DEV Community by Ashwin Sathian (@ashwinsathian).</description>
    <link>https://dev.to/ashwinsathian</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F301069%2F130dfdc7-a278-4df3-a335-cd410fd0a44b.JPG</url>
      <title>DEV Community: Ashwin Sathian</title>
      <link>https://dev.to/ashwinsathian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashwinsathian"/>
    <language>en</language>
    <item>
      <title>Why "Humanize My Writing" Tools Don't Work</title>
      <dc:creator>Ashwin Sathian</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:08:47 +0000</pubDate>
      <link>https://dev.to/ashwinsathian/why-humanize-my-writing-tools-dont-work-3l76</link>
      <guid>https://dev.to/ashwinsathian/why-humanize-my-writing-tools-dont-work-3l76</guid>
      <description>&lt;p&gt;In December 2024, Florida State University linguists Tom Juzek and Zina Ward set out to answer a question that had turned into a running joke among people who read a lot of AI output: why does ChatGPT say "delve" so much?&lt;/p&gt;

&lt;p&gt;Instead of guessing, they built a study designed to eliminate the wrong explanations first, a philosophy-of-science-style method for ruling out competing hypotheses one at a time. First candidate: maybe these words are just common in whatever text the models were trained on. Ruled out. Second candidate: something about the model architecture, or the mechanics of how the model picks its next word. Also ruled out. What was left, after comparing a base version of Llama 2 against the same model fine-tuned with human feedback, was the fine-tuning stage itself. Somewhere in the process of humans rating model outputs as good or bad, "delve" started winning.&lt;/p&gt;

&lt;p&gt;That's not a fringe result. A separate team led by Dmitry Kobak at the University of Tübingen ran the numbers on over 15 million PubMed abstracts published between 2010 and 2024, borrowing a statistical method from epidemiology, "excess mortality," repurposed as "excess vocabulary," to separate ordinary word-frequency drift from something anomalous. "Delve" wasn't the only word that spiked. "Meticulously" rose 137% year over year. "Intricate," 117%. "Commendable," 83%. By their estimate, at least 13.5% of 2024 biomedical abstracts show signs of LLM involvement, a bigger vocabulary shift than the one COVID caused. Kobak's study measures the size of that shift. The fine-tuning explanation for &lt;em&gt;why&lt;/em&gt; it happens is still just the FSU team's own finding, unreplicated so far.&lt;/p&gt;

&lt;p&gt;Here's the part almost nobody mentions when they cite that finding: it's already going stale. A follow-up study from the same FSU team found "delve," "boast," and "meticulous" showing up more often in ordinary spoken language too, in podcasts and YouTube talks. People who read a lot of AI text are picking up its vocabulary and using it themselves. The tell is bleeding into the population it was supposed to detect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word-list trap
&lt;/h2&gt;

&lt;p&gt;This is the problem with almost every "humanize my writing" tool currently on GitHub, even the well-built ones: underneath whatever nuance they add, they're still built around a list of words. I went looking, expecting a handful of thoughtful tools and a pile of junk. What I found instead was a spectrum of care applied to the same underlying mechanism.&lt;/p&gt;

&lt;p&gt;The most-starred tool in the space has over 36,000 stars, a number worth eyeing with some skepticism given it's a single markdown file. It does real work: protecting ordinary formal vocabulary from being flagged, refusing to invent facts during a rewrite, matching a user's own writing sample instead of forcing one house style on everyone. None of that changes what it's actually doing underneath, which is checking a passage against roughly three dozen fixed patterns, a heuristic that goes stale the moment models or detectors shift. A second tool opens by citing real false-positive research on AI detectors before giving a single rule, and states outright that its signals are "worth acting on; not worth ruining someone's day over," arguably the most epistemically honest framing in the entire space. Strip away that framing, though, and the bulk of what's left is the same genre of banned-word list as everything else.&lt;/p&gt;

&lt;p&gt;Then there's the tool at the other end of the spectrum, the one that instructs its model to treat certain words as an instant, unqualified tell: "if even one of these words appears, the text immediately flags as machine-written." Its banned list includes &lt;em&gt;robust&lt;/em&gt;, &lt;em&gt;scalable&lt;/em&gt;, &lt;em&gt;integrated&lt;/em&gt;, &lt;em&gt;proactive&lt;/em&gt;, words that show up constantly in ordinary technical writing because they're often the correct word for the job. Strike "robust" from a paragraph about fault tolerance and the paragraph just gets vaguer. No single word carries that much diagnostic weight on its own.&lt;/p&gt;

&lt;p&gt;Even the more careful tools slip into the same trap. One enforces a rule capping em dashes at "Maximum ONE per 500 words," with no stated methodology behind the number anywhere in its documentation, and separately instructs its model to apply all its rules silently, never mentioning them to the person it's writing for. That's a strange thing to optimize for if the goal is writing that's actually good. Being undetected is a different achievement.&lt;/p&gt;

&lt;p&gt;Word lists share the same fragility no matter how carefully they're built: cheap to build, fast to go stale, and aimed at the weakest part of the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stronger signal actually looks like
&lt;/h2&gt;

&lt;p&gt;Separately from all of this vocabulary-chasing, there's a body of peer-reviewed computational linguistics research quietly measuring sentence shape instead of word choice.&lt;/p&gt;

&lt;p&gt;A 2024 study out of the Universidade da Coruña compared six sets of LLM-generated news text (Mistral, Falcon, and LLaMA at four different sizes) against real human-written news articles, chosen specifically from after the models' training cutoffs so nothing in the comparison could have been memorized. Human writing spread its sentence lengths across a wide range, some short, some long, unevenly. Every model, regardless of which one wrote the piece, clustered tightly into a narrow 10-to-30-token band. That gap between human and machine was larger than the differences between the six models themselves. Whatever "sounds like Mistral" and whatever "sounds like LLaMA" turned out to have more in common with each other, structurally, than either has with an actual human sentence.&lt;/p&gt;

&lt;p&gt;A separate PNAS study found the same convergence in the shape of entire stories instead of sentences: LLM-generated narratives cluster around a much smaller set of recurring plot patterns than human-written ones do, even across different prompts. Same convergence, different scale, still nothing to do with vocabulary.&lt;/p&gt;

&lt;p&gt;This is the inconvenient part for anyone building a "humanizer" out of a word list: structure is a harder problem to fake your way around, and it's also the more durable signal, because it isn't tied to which specific words happen to be fashionable in this quarter's training data. A model can stop saying "delve" tomorrow. Rewriting the shape of its sentences is a different kind of problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The em dash isn't the story people think it is
&lt;/h2&gt;

&lt;p&gt;If there's one tell that's achieved genuine cultural traction outside of NLP circles, it's the em dash. NPR ran a piece on "the unofficial movement to save the em dash" from guilt by association. A Rochester Institute of Technology student paper cited GPT-4.1 using the mark at roughly 3.28 times the rate of typical human essays.&lt;/p&gt;

&lt;p&gt;What's less settled, and this is where the sources genuinely disagree, is whether that overuse even means what people assume it means, let alone why it happens. One independent analysis argues it's a training-data artifact: labs eventually ran out of fresh web text and started digitizing older, pre-1950s books, which use em dashes far more heavily than contemporary writing, and GPT-3.5, trained before this shift, didn't show the same overuse that GPT-4o does. A competing explanation points the other direction: human raters, during the reinforcement-learning stage that shapes a model's final behavior, reward the clarity and pacing an em dash provides, and that preference gets baked in regardless of what the training data looked like.&lt;/p&gt;

&lt;p&gt;Benjamin Dreyer, Random House's longtime copy chief and about as credentialed a human authority on English prose as exists, doesn't think the overuse claim itself holds up. He went looking for the "charts and graphs and proofs" behind the em-dash panic and found none. Examining sample AI prose by hand, he called the whole thing "social media blather" and found nothing anomalous in the counts. What worries him more is the chilling effect: students spooked into avoiding a perfectly good punctuation mark for fear of being falsely flagged, paying a real cost for a signal that was never trustworthy on its own to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the source of all this actually said
&lt;/h2&gt;

&lt;p&gt;At least 6 of the 13 tools I looked at trace their DNA back to one place: Wikipedia's "Signs of AI writing" essay, maintained by volunteer editors who patrol new submissions for undisclosed AI content. It's genuinely excellent applied research. Built from thousands of real cases and revised as models change, it's more rigorous than most of what's built on top of it.&lt;/p&gt;

&lt;p&gt;It also contains a warning that hardly anyone who cites it seems to have read past: its own verdict on the tells it catalogs is that they're surface symptoms of something worse underneath, unreliable sourcing, shallow synthesis, no real editorial judgment. Scrub the vocabulary without touching any of that and you've, in the essay's own words, "obscured the actual concerns." A find-and-replace pass that swaps "delve" for something else and caps the em dashes gets past the pattern-matching. It never produces text anyone actually thought through.&lt;/p&gt;

&lt;p&gt;That's the harder problem, and it's the one worth building for. We ended up doing exactly that: research on 27 cross-referenced tells, ranked by source strength and how often each one actually shows up, plus a teardown of what the tools above get wrong, that became a Claude Code skill built around structure and specificity first, with the vocabulary list demoted to a backup role instead of the primary mechanism. It's not foolproof, and it'll date the same way every tell-based approach eventually does. It's on GitHub if you want to check the sourcing yourself: &lt;a href="https://github.com/AshwinSathian/humanize-writing-skill" rel="noopener noreferrer"&gt;github.com/AshwinSathian/humanize-writing-skill&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>llm</category>
      <category>writing</category>
    </item>
    <item>
      <title>Why Better Auth's MongoDB adapter can't populate() a Mongoose ref</title>
      <dc:creator>Ashwin Sathian</dc:creator>
      <pubDate>Mon, 17 Aug 2026 19:48:36 +0000</pubDate>
      <link>https://dev.to/ashwinsathian/why-better-auths-mongodb-adapter-cant-populate-a-mongoose-ref-e2c</link>
      <guid>https://dev.to/ashwinsathian/why-better-auths-mongodb-adapter-cant-populate-a-mongoose-ref-e2c</guid>
      <description>&lt;p&gt;If your Node app already uses Mongoose and you wire up Better Auth with its official MongoDB adapter, this looks fine at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;betterAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mongodbAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/adapters/mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mongodbAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you try to reference a Better-Auth-created user from your own model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;populate()&lt;/code&gt; silently fails. Not an error — just &lt;code&gt;null&lt;/code&gt;, like the ref never resolved. If you've hit this, you're not doing anything wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mongodbAdapter()&lt;/code&gt; talks to the raw &lt;a href="https://www.npmjs.com/package/mongodb" rel="noopener noreferrer"&gt;&lt;code&gt;mongodb&lt;/code&gt;&lt;/a&gt; driver, not Mongoose. It never registers a Mongoose model for &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;account&lt;/code&gt;, or any of Better Auth's own tables. Two consequences fall out of that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You end up with two disconnected views of the same database.&lt;/strong&gt; Better Auth writes through &lt;code&gt;MongoClient&lt;/code&gt; directly. Your own code reads and writes through Mongoose. No shared schema, no shared validation, no shared hooks between the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;_id&lt;/code&gt; types don't line up.&lt;/strong&gt; Better Auth's default ID generator produces a 32-character base62 string — not a real &lt;code&gt;ObjectId&lt;/code&gt;. Mongoose's &lt;code&gt;populate()&lt;/code&gt; needs a real &lt;code&gt;ObjectId&lt;/code&gt; to resolve a ref against. There's nothing valid on the other end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't a one-off — it's been an open, documented gap on Better Auth's own repo since February 2025:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/better-auth/better-auth/issues/1492" rel="noopener noreferrer"&gt;&lt;code&gt;better-auth#1492&lt;/code&gt;&lt;/a&gt; — forced to install the raw &lt;code&gt;mongodb&lt;/code&gt; package as a duplicate dependency, even in apps that only ever use Mongoose.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/better-auth/better-auth/issues/6289" rel="noopener noreferrer"&gt;&lt;code&gt;better-auth#6289&lt;/code&gt;&lt;/a&gt; — id/session mismatches from mixing Mongoose reads with raw-driver writes on the same collections.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/better-auth/better-auth/discussions/9364" rel="noopener noreferrer"&gt;&lt;code&gt;better-auth&lt;/code&gt; discussion &lt;code&gt;#9364&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/better-auth/better-auth/discussions/1921" rel="noopener noreferrer"&gt;&lt;code&gt;#1921&lt;/code&gt;&lt;/a&gt; — people hitting exactly this &lt;code&gt;populate()&lt;/code&gt; failure and asking for a real Mongoose adapter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workaround everyone lands on — &lt;code&gt;mongoose.connection.getClient()&lt;/code&gt;, hand the raw client to &lt;code&gt;mongodbAdapter()&lt;/code&gt; — only fixes where Better Auth &lt;em&gt;connects&lt;/em&gt;. It doesn't fix what it &lt;em&gt;stores&lt;/em&gt;. Your ref field still has nothing valid to resolve against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/AshwinSathian/better-auth-mongoose" rel="noopener noreferrer"&gt;&lt;code&gt;better-auth-mongoose&lt;/code&gt;&lt;/a&gt; registers Better Auth's tables as real, extendable Mongoose models on a connection you already own, and overrides ID generation to produce real 24-character &lt;code&gt;ObjectId&lt;/code&gt; hex strings instead of Better Auth's default base62 IDs — converting between &lt;code&gt;ObjectId&lt;/code&gt; and &lt;code&gt;string&lt;/code&gt; at the boundary so Better Auth's own core still sees plain strings, but MongoDB stores actual &lt;code&gt;ObjectId&lt;/code&gt;s underneath.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;betterAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mongooseAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth-mongoose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MONGO_URI&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mongooseAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;Post&lt;/code&gt; model, same &lt;code&gt;.populate("author")&lt;/code&gt; call, no workaround:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// resolved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a real, CI-run test in the repo, not a claim in a README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the differentiator: a consumer's own model can .populate() a Better-Auth-created user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resolves Post.author via .populate() after Better Auth creates the user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mongooseAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUpEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;correct-horse-battery-staple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post Author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, populate()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;coerceToObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, populate()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// passes&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also passes the official &lt;a href="https://www.npmjs.com/package/@better-auth/test-utils" rel="noopener noreferrer"&gt;&lt;code&gt;@better-auth/test-utils&lt;/code&gt;&lt;/a&gt; adapter contract suite, so it's not just "populate works" — it's a fully conformant Better Auth adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else comes with it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero direct dependencies.&lt;/strong&gt; &lt;code&gt;mongoose&lt;/code&gt; and &lt;code&gt;better-auth&lt;/code&gt; are peers you already have; the raw &lt;code&gt;mongodb&lt;/code&gt; driver is never pulled in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema extension that doesn't fight Better Auth.&lt;/strong&gt; &lt;code&gt;schemas: { user: new Schema({ role: { type: String, default: "member" } }) }&lt;/code&gt; merges your fields in without letting a required Better Auth field get accidentally loosened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions on by default&lt;/strong&gt;, using real Mongoose sessions, with automatic fallback to non-transactional writes on a standalone &lt;code&gt;mongod&lt;/code&gt; (common in local dev) instead of crashing on boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter-level joins.&lt;/strong&gt; Better Auth 1.4+ can push joins down to the adapter for a documented 2–3x latency improvement on endpoints like &lt;code&gt;get-session&lt;/code&gt;. This adapter turns that straight into &lt;code&gt;.populate()&lt;/code&gt; calls — flip &lt;code&gt;experimental: { joins: true }&lt;/code&gt; and it's already wired up.&lt;/li&gt;
&lt;li&gt;If you're on NestJS: &lt;a href="https://github.com/AshwinSathian/better-auth-mongoose/tree/main/examples/nestjs-mongoose" rel="noopener noreferrer"&gt;&lt;code&gt;examples/nestjs-mongoose&lt;/code&gt;&lt;/a&gt; is a complete app built on &lt;a href="https://github.com/thallesp/nestjs-better-auth" rel="noopener noreferrer"&gt;&lt;code&gt;@thallesp/nestjs-better-auth&lt;/code&gt;&lt;/a&gt;, exercised over real HTTP in CI on every push.&lt;/li&gt;
&lt;li&gt;If you're building multi-tenant SaaS on top of Better Auth's &lt;code&gt;organization&lt;/code&gt; plugin, there's a companion package, &lt;a href="https://www.npmjs.com/package/better-auth-mongoose-tenant" rel="noopener noreferrer"&gt;&lt;code&gt;better-auth-mongoose-tenant&lt;/code&gt;&lt;/a&gt;, for automatic tenant-scoped queries on your own models.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add better-auth-mongoose mongoose better-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://better-auth-mongoose.ashwinsathian.com" rel="noopener noreferrer"&gt;better-auth-mongoose.ashwinsathian.com&lt;/a&gt; has the full writeup, a compatibility matrix (Mongoose 6–9, Better Auth 1.4–1.6), and more recipes. Repo's at &lt;a href="https://github.com/AshwinSathian/better-auth-mongoose" rel="noopener noreferrer"&gt;github.com/AshwinSathian/better-auth-mongoose&lt;/a&gt; — MIT licensed, not affiliated with the Better Auth team, built because this gap has sat open for a year and a half.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>node</category>
      <category>typescript</category>
      <category>mongoose</category>
    </item>
    <item>
      <title>Why ngx-runtime-i18n treats the active language as a signal, not an Observable</title>
      <dc:creator>Ashwin Sathian</dc:creator>
      <pubDate>Mon, 17 Aug 2026 15:49:34 +0000</pubDate>
      <link>https://dev.to/ashwinsathian/why-ngx-runtime-i18n-treats-the-active-language-as-a-signal-not-an-observable-4i3g</link>
      <guid>https://dev.to/ashwinsathian/why-ngx-runtime-i18n-treats-the-active-language-as-a-signal-not-an-observable-4i3g</guid>
      <description>&lt;p&gt;The idea of building &lt;strong&gt;@ngx-runtime-i18n&lt;/strong&gt; came from my personal experience and if I may daresay frustration at the popular options when it came to internationalisation in Angular app. Particularly in applications of scale especially at an enterprise level, it was no picnic. Among many others, one critical decision point was regarding the usage of signals which clearly was the road ahead and yet, even as we speak, not the standard for the community at large.&lt;/p&gt;

&lt;p&gt;Most Angular i18n libraries predate signals. &lt;strong&gt;ngx-translate&lt;/strong&gt; and &lt;strong&gt;transloco&lt;/strong&gt; both started as RxJS-first services, and both have spent the last year retrofitting signal support on top: transloco added &lt;code&gt;translateSignal&lt;/code&gt; by wrapping &lt;code&gt;toObservable()&lt;/code&gt; around its existing pipeline (jsverse/transloco#781 is the tracking issue - still open, with a maintainer confirming a signals-native rewrite is in progress); ngx-translate went further in its v18 rewrite and made signals the primary API. Angular's own built-in i18n never solved this because it isn't runtime at all but a compile step that produces one build per locale, with no way to switch languages&lt;br&gt;
without a page reload, which also means that local development is no party either.&lt;/p&gt;

&lt;p&gt;I wanted the opposite starting point: build the service on signals from day one, and treat RxJS as an optional bridge for people who still need it, not the other way round. That's &lt;strong&gt;@ngx-runtime-i18n/angular&lt;/strong&gt;. &lt;code&gt;I18nService&lt;/code&gt; exposes &lt;code&gt;lang()&lt;/code&gt;, &lt;code&gt;ready()&lt;/code&gt;, and &lt;code&gt;switching()&lt;/code&gt; as signals directly; &lt;code&gt;t$()&lt;/code&gt; is a computed signal that recomputes when the language or the parameters change; there's no &lt;code&gt;toSignal(this.service.stream$)&lt;/code&gt; anywhere in the implementation. If you do need Observables where your codebase uses existing NgRx effects, older RxJS-based code, the &lt;code&gt;I18nCompatService&lt;/code&gt; wraps the same state as &lt;code&gt;lang$&lt;/code&gt;/&lt;code&gt;ready$&lt;/code&gt; instead of the other way round.&lt;/p&gt;

&lt;p&gt;The other piece I didn't want to compromise on was ICU. A lot of runtime i18n libraries handle "1 item" / "2 items" and stop there, which works for English and fails for Arabic (six plural forms), Polish, or Russian. &lt;code&gt;formatIcu()&lt;/code&gt; implements &lt;code&gt;plural&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, and &lt;code&gt;selectordinal&lt;/code&gt; against the actual CLDR plural categories (zero/one/two/few/many/other), so a translator can write correct plural rules for their language instead of the library silently falling back to English-shaped logic.&lt;/p&gt;

&lt;p&gt;SSR was the third constraint. &lt;code&gt;provideRuntimeI18nSsr()&lt;/code&gt; seeds the resolved catalog into Angular's &lt;code&gt;TransferState&lt;/code&gt; on the server, so the client picks up exactly what was rendered instead of re-fetching and re-resolving the language after hydration - no flash of the wrong language, no mismatch warnings.&lt;/p&gt;

&lt;p&gt;None of this is finished in the sense that the library is done growing. It's six packages now (&lt;code&gt;core&lt;/code&gt;, &lt;code&gt;angular&lt;/code&gt;, &lt;code&gt;primeng&lt;/code&gt;, &lt;code&gt;material&lt;/code&gt;, &lt;code&gt;schematics&lt;/code&gt;, &lt;code&gt;cli&lt;/code&gt;), all on npm as of v2.1.0, with real but small usage so far. The parts I'd like feedback on most: route-scoped catalog loading (&lt;code&gt;loadScope()&lt;/code&gt;/&lt;code&gt;unloadScope()&lt;/code&gt;) for large apps with many feature modules, and whether the fallback-chain API (fallbacks: &lt;code&gt;string[]&lt;/code&gt; on top of &lt;code&gt;defaultLang&lt;/code&gt;) covers the cases people actually hit in multi-region products.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/AshwinSathian/ngx-runtime-i18n" rel="noopener noreferrer"&gt;https://github.com/AshwinSathian/ngx-runtime-i18n&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/@ngx-runtime-i18n/angular" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@ngx-runtime-i18n/angular&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://i18n.ashwinsathian.com/" rel="noopener noreferrer"&gt;https://i18n.ashwinsathian.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>angular</category>
    </item>
    <item>
      <title>Announcing @ngx-runtime-i18n v1.2.0 — Runtime i18n Done Right for Angular 16+</title>
      <dc:creator>Ashwin Sathian</dc:creator>
      <pubDate>Fri, 14 Nov 2025 08:03:11 +0000</pubDate>
      <link>https://dev.to/ashwinsathian/announcing-ngx-runtime-i18n-v120-runtime-i18n-done-right-for-angular-16-196i</link>
      <guid>https://dev.to/ashwinsathian/announcing-ngx-runtime-i18n-v120-runtime-i18n-done-right-for-angular-16-196i</guid>
      <description>&lt;p&gt;@ngx-runtime-i18n is a modern, runtime-driven internationalisation system designed for the current Angular ecosystem — signals, SSR, hydration, and streamed rendering.&lt;/p&gt;

&lt;p&gt;The new v1.2.0 release brings three major improvements that close long-standing gaps in the Angular i18n landscape:&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Ordered Fallback Chains
&lt;/h2&gt;

&lt;p&gt;Missing keys now resolve across a full chain:&lt;br&gt;
&lt;code&gt;active → fallbacks[] → defaultLang&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This removes an entire class of runtime failures and enables cleaner catalog organisation.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Catalog Caching Modes
&lt;/h2&gt;

&lt;p&gt;Select the caching strategy for your app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;none:&lt;/strong&gt; minimal memory footprint
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;memory:&lt;/strong&gt; session-level caching
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;storage:&lt;/strong&gt; hydrate translations directly from localStorage (with background verification)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for PWAs, offline-first apps, and SSR setups.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Developer Experience Helpers
&lt;/h2&gt;

&lt;p&gt;New introspection methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getCurrentLang()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getLoadedLangs()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasKey()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are especially useful during SSR, logging, and debugging dynamic or user-driven catalog loads.&lt;/p&gt;




&lt;p&gt;The SSR demo app now reflects these features, and all documentation has been updated.&lt;/p&gt;

&lt;p&gt;If you're using Angular 16 or above and want to modernise your i18n strategy, give it a &lt;a href="https://www.npmjs.com/package/@ngx-runtime-i18n/angular" rel="noopener noreferrer"&gt;try&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>angular</category>
    </item>
    <item>
      <title>Runtime i18n for Angular — Signals-first, SSR-safe, and ~3 kB gzipped</title>
      <dc:creator>Ashwin Sathian</dc:creator>
      <pubDate>Mon, 27 Oct 2025 07:11:58 +0000</pubDate>
      <link>https://dev.to/ashwinsathian/runtime-i18n-for-angular-signals-first-ssr-safe-and-3-kb-gzipped-2h08</link>
      <guid>https://dev.to/ashwinsathian/runtime-i18n-for-angular-signals-first-ssr-safe-and-3-kb-gzipped-2h08</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Angular finally gets a &lt;strong&gt;runtime internationalisation (i18n)&lt;/strong&gt; library that’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signals-first&lt;/strong&gt; with an RxJS fallback&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hydration-safe&lt;/strong&gt; for SSR&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;~3 kB&lt;/strong&gt; gzipped&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Works from &lt;strong&gt;Angular 16 → 20&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And takes &lt;strong&gt;2 lines to set up&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
npm i @ngx-runtime-i18n/angular @ngx-runtime-i18n/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>angular</category>
      <category>opensource</category>
      <category>i18n</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
