<?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: BottleneckPC</title>
    <description>The latest articles on DEV Community by BottleneckPC (@bottleneckpc-team).</description>
    <link>https://dev.to/bottleneckpc-team</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%2F4030944%2Fff979279-83c2-4b05-975a-b04cadacd1f6.png</url>
      <title>DEV Community: BottleneckPC</title>
      <link>https://dev.to/bottleneckpc-team</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bottleneckpc-team"/>
    <language>en</language>
    <item>
      <title>Eleven languages for zero new translation work</title>
      <dc:creator>BottleneckPC</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:10:32 +0000</pubDate>
      <link>https://dev.to/bottleneckpc/eleven-languages-for-zero-new-translation-work-4ep3</link>
      <guid>https://dev.to/bottleneckpc/eleven-languages-for-zero-new-translation-work-4ep3</guid>
      <description>&lt;p&gt;We shipped an embeddable version of our calculator in eleven languages last week. The translation&lt;br&gt;
cost was zero, and not because we used machine translation. It was zero because the strings already&lt;br&gt;
existed and we had never noticed.&lt;/p&gt;

&lt;p&gt;This is a short post about a boring realisation: if you localise an app properly, the embeddable&lt;br&gt;
version of that app is usually already localised, and the only thing standing between you and&lt;br&gt;
eleven markets is where you read the locale from.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The product is a CPU and GPU bottleneck calculator. A while back we translated it into ten&lt;br&gt;
languages beyond English, which meant building the usual things: a dictionary per locale, a route&lt;br&gt;
per locale, and a rule that the URL slug is a keyword in that language rather than a transliteration&lt;br&gt;
of the English one. Somebody searching &lt;code&gt;calculadora cuello de botella&lt;/code&gt; should see that phrase in&lt;br&gt;
the path, because on a head term the URL is part of the signal.&lt;/p&gt;

&lt;p&gt;Separately, we had an embeddable widget. An iframe, deliberately chrome-less, so other sites could&lt;br&gt;
drop the calculator into an article. It was English only.&lt;/p&gt;

&lt;p&gt;The instinct was to treat "localise the widget" as a project. Estimate the strings, get them&lt;br&gt;
translated, wire up a language switcher.&lt;/p&gt;
&lt;h2&gt;
  
  
  The realisation
&lt;/h2&gt;

&lt;p&gt;Every string the widget renders was already in the dictionaries.&lt;/p&gt;

&lt;p&gt;Of course it was. The widget is the same calculator. It shows a CPU label, a GPU label, two search&lt;br&gt;
placeholders, a resolution control, and a verdict line that says balanced, CPU limited or GPU&lt;br&gt;
limited. All of that had been translated months earlier for the full-page version. The widget was&lt;br&gt;
English purely because nothing ever told it otherwise.&lt;/p&gt;

&lt;p&gt;The actual work was one decision: &lt;strong&gt;where does an iframe read its locale from?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the main site, locale comes from the path - &lt;code&gt;/es/...&lt;/code&gt;, &lt;code&gt;/de/...&lt;/code&gt;. An iframe embedded on somebody&lt;br&gt;
else's page has no path of ours to read. It has a query string, and that is it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/embed?cpu=&amp;lt;id&amp;gt;&amp;amp;gpu=&amp;lt;id&amp;gt;&amp;amp;res=1440p&amp;amp;lang=de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the locale is read client-side from the query string, and the host page chooses the language by&lt;br&gt;
choosing the URL. That is the entire feature.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that was hiding underneath
&lt;/h2&gt;

&lt;p&gt;One line spoiled it, and it is the kind of thing that only shows up when you localise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// renders "CPU LIMITED" in every language&lt;/span&gt;
&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verdict was being built by upper-casing an internal enum value. In English that reads as a&lt;br&gt;
sentence and nobody questions it. In German it reads as an English word sitting in the middle of a&lt;br&gt;
German widget.&lt;/p&gt;

&lt;p&gt;The fix is obvious once seen - map the enum to a dictionary key rather than rendering the enum -&lt;br&gt;
but the lesson generalises: &lt;strong&gt;any string you construct from a variable rather than look up is a&lt;br&gt;
string you have not translated.&lt;/strong&gt; Grep for &lt;code&gt;toUpperCase&lt;/code&gt;, &lt;code&gt;capitalize&lt;/code&gt;, and template literals that&lt;br&gt;
interpolate an internal value into user-visible text. That is where untranslated strings hide, not&lt;br&gt;
in the JSX where they are easy to see.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that actually mattered commercially
&lt;/h2&gt;

&lt;p&gt;An embeddable widget is a distribution mechanism. Somebody puts it on their page, and the&lt;br&gt;
attribution link under it points back at you.&lt;/p&gt;

&lt;p&gt;That link has to sit &lt;strong&gt;outside&lt;/strong&gt; the iframe. Search engines do not pass equity through an iframe,&lt;br&gt;
so a widget with the credit inside its own frame gives you traffic and nothing else. Ours is a&lt;br&gt;
paragraph in the host page's own HTML, generated alongside the embed code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://bottleneckpc.com/embed?lang=de"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://bottleneckpc.com/de/flaschenhals-rechner"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Flaschenhals-Rechner&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
   by &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://bottleneckpc.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;BottleneckPC&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the anchor text and target. For a German host we link the German page with its German name,&lt;br&gt;
not the English one. A reader clicking through lands somewhere that makes sense to them, which is&lt;br&gt;
the difference between a link that gets kept and a link that gets removed six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing, and it cost nothing
&lt;/h2&gt;

&lt;p&gt;While testing on other people's pages it became obvious that the widget was loading our analytics&lt;br&gt;
and our ad partner's script onto their site. That is a bad thing to ask of anyone, and an&lt;br&gt;
impossible thing to ask of a European publisher with a privacy policy.&lt;/p&gt;

&lt;p&gt;The root layout now path-guards every third-party script, so the embed route loads none of them.&lt;br&gt;
The verification is simple enough to be worth doing rather than assuming: open the widget in a real&lt;br&gt;
browser and count the requests that leave your own domain. Ours is zero.&lt;/p&gt;

&lt;p&gt;If you ship an embeddable anything, do that check. The answer is rarely what you assume.&lt;/p&gt;




&lt;p&gt;We build &lt;a href="https://bottleneckpc.com" rel="noopener noreferrer"&gt;BottleneckPC&lt;/a&gt;, a free CPU and GPU bottleneck calculator. The&lt;br&gt;
widget is &lt;a href="https://bottleneckpc.com/bottleneck-calculator-widget" rel="noopener noreferrer"&gt;free to embed&lt;/a&gt; in any of the&lt;br&gt;
eleven languages, and the scoring engine is &lt;a href="https://github.com/BottleneckPC/bottleneck-checker" rel="noopener noreferrer"&gt;open source&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>i18n</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Stop matching product listings by name</title>
      <dc:creator>BottleneckPC</dc:creator>
      <pubDate>Mon, 03 Aug 2026 01:47:09 +0000</pubDate>
      <link>https://dev.to/bottleneckpc-team/stop-matching-product-listings-by-name-22b9</link>
      <guid>https://dev.to/bottleneckpc-team/stop-matching-product-listings-by-name-22b9</guid>
      <description>&lt;p&gt;Every price-tracking system starts the same way. You have a catalogue of products, a feed of retailer listings, and a function that decides which listing belongs to which product. Almost everyone writes that function as string matching, because for a while it works.&lt;/p&gt;

&lt;p&gt;Then you try it on PC hardware, and it stops working in a way that is very hard to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is invisible by construction
&lt;/h2&gt;

&lt;p&gt;Here is the whole problem in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;catalogue:  AMD RX 9060
listing:    PowerColor Reaper Radeon RX 9060 XT 16GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every token of the catalogue name appears in the listing. Any matcher built on "do our words appear in their title" scores this a perfect hit. It is a different card, it costs more, and nothing in the string tells you so.&lt;/p&gt;

&lt;p&gt;Now flip it. The RX 9060 XT sells in 8GB and 16GB versions with names that differ by two characters. If your price rule is "cheapest live listing wins" - which is the sane rule - the smaller card wins every single day, and the bigger card gets advertised at a price nobody charges for it. Your system is not broken. It is confidently, consistently wrong.&lt;/p&gt;

&lt;p&gt;Hardware is dense with these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trap&lt;/th&gt;
&lt;th&gt;Looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance suffix&lt;/td&gt;
&lt;td&gt;RX 9060 / RX 9060 &lt;strong&gt;XT&lt;/strong&gt;, RTX 5070 / 5070 &lt;strong&gt;Ti&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity variant&lt;/td&gt;
&lt;td&gt;RTX 5060 Ti &lt;strong&gt;8GB&lt;/strong&gt; / &lt;strong&gt;16GB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Board revision&lt;/td&gt;
&lt;td&gt;MAG Z890 Tomahawk WIFI / WIFI &lt;strong&gt;II&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colourway SKU&lt;/td&gt;
&lt;td&gt;X870E Aorus Pro / Aorus Pro &lt;strong&gt;ICE&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel SKU&lt;/td&gt;
&lt;td&gt;PRIME B860M-A WIFI / WIFI-&lt;strong&gt;CSM&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form factor, hidden inside the model code&lt;/td&gt;
&lt;td&gt;X870 / X870*&lt;em&gt;I&lt;/em&gt;&lt;em&gt;, Z890 / Z890&lt;/em&gt;&lt;em&gt;M&lt;/em&gt;*&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the nastiest. &lt;code&gt;X870I&lt;/code&gt; is a mini-ITX board and &lt;code&gt;X870&lt;/code&gt; is ATX. One letter, buried mid-token, and a human skims straight past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory kits hide the spec entirely
&lt;/h2&gt;

&lt;p&gt;RAM deserves its own paragraph, because the identifying attribute is usually not in the title at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Corsair Vengeance DDR5-6400 CL32 32GB (2x16GB)
CORSAIR Vengeance 32GB (2 x 16GB) DDR5 6400 ... Model CMK32GX5M2B6400C36
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same brand, same line, same speed, same capacity, same module count. Different kit. The CAS latency is CL32 for ours and CL36 for theirs, and the only place that appears is the trailing &lt;code&gt;C36&lt;/code&gt; of the manufacturer part number. Match on the words and you take the wrong kit, at the wrong price, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is to stop matching in the hot path
&lt;/h2&gt;

&lt;p&gt;The rule we settled on is one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Identity is resolved once, behind a gate, and never re-derived from names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Concretely, that means splitting one job into two that had been tangled together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution&lt;/strong&gt; happens rarely, is allowed to be slow and expensive, and produces a durable binding of &lt;code&gt;product -&amp;gt; exact retailer ID&lt;/code&gt;. Name matching lives here and only here, wrapped in whatever gates you need. A proposal that fails any gate goes to a human instead of into the catalogue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; happens every morning and does no matching whatsoever. It asks each retailer "what is item B0F8PSH3Q9 selling for today", because we already decided months ago what that item is. There is no string comparison left in the daily path to get wrong.&lt;/p&gt;

&lt;p&gt;The reason this works is not cleverness, it is that the expensive, error-prone step now runs a thousand times less often, and its output is reviewable. You can look at a registry of bindings and audit it. You cannot audit a matcher that re-decides on every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bindings rot, so check them
&lt;/h2&gt;

&lt;p&gt;A durable binding introduces its own failure: a retailer can change what sits behind an ID. A listing gets repurposed, a product page becomes its successor, an ASIN quietly turns into a different model.&lt;/p&gt;

&lt;p&gt;So the daily fetch does one comparison after all, but it is a much weaker one. It re-reads the listing title and asks whether our model's tokens still appear in it. Not "is this the right product" - that decision is already made - but "has this listing become something else". When it drifts, the listing stops feeding prices and goes to a queue. It is a smoke alarm, not a matcher.&lt;/p&gt;

&lt;p&gt;Then add whatever domain check actually separates your siblings. For graphics cards ours is capacity: whatever VRAM the title states must be the VRAM we list, or the binding is wrong regardless of how well the names line up. One rule, and the entire 8GB-priced-as-16GB class stops being possible.&lt;/p&gt;

&lt;p&gt;A wrinkle worth knowing if you copy this: card titles do not write capacity consistently. &lt;code&gt;16GB&lt;/code&gt;, &lt;code&gt;16G&lt;/code&gt;, and a trailing &lt;code&gt;-6GD&lt;/code&gt; inside a part number all mean the same thing, and a regex that only knows &lt;code&gt;\d+GB&lt;/code&gt; will sail straight past two of the three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell my past self
&lt;/h2&gt;

&lt;p&gt;Do not try to make the matcher smarter. That is the instinct, and it is a trap - every new rule you add is another rule that fires on the wrong edge case at 5am while you are asleep.&lt;/p&gt;

&lt;p&gt;Make the matcher rarer instead. Resolve identity once, store the exact ID, gate the promotion to "verified" behind a human, and let the daily job be boring. Boring is the goal. The daily job should be incapable of being creative.&lt;/p&gt;

&lt;p&gt;The honest admission: this costs you coverage. Parts nobody has resolved yet sit unpriced, and there is a real temptation to auto-approve the backlog just to fill the catalogue. We have that backlog. It is better than the alternative, which is a catalogue that looks complete and lies in places you cannot predict.&lt;/p&gt;




&lt;p&gt;We build &lt;a href="https://bottleneckpc.com" rel="noopener noreferrer"&gt;BottleneckPC&lt;/a&gt;, a free CPU and GPU bottleneck calculator, and the engine behind it is &lt;a href="https://github.com/BottleneckPC/bottleneck-checker" rel="noopener noreferrer"&gt;open source under MIT&lt;/a&gt; if you want to read the honest-range scoring rather than the pricing side.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>data</category>
      <category>software</category>
    </item>
  </channel>
</rss>
