<?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: Ifeanyi Chima</title>
    <description>The latest articles on DEV Community by Ifeanyi Chima (@ifeanyichima).</description>
    <link>https://dev.to/ifeanyichima</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%2F763463%2F47ea4de3-e833-48d3-8a9b-507ac915483f.jpg</url>
      <title>DEV Community: Ifeanyi Chima</title>
      <link>https://dev.to/ifeanyichima</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ifeanyichima"/>
    <language>en</language>
    <item>
      <title>How to translate MDN web docs to spanish.</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 03 Sep 2026 23:21:22 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/how-to-translate-mdn-web-docs-to-spanish-2p0f</link>
      <guid>https://dev.to/ifeanyichima/how-to-translate-mdn-web-docs-to-spanish-2p0f</guid>
      <description>&lt;p&gt;Good day, for the past 3 weeks I have been busy translating the popular documentation platform known as &lt;a href="https://developer.mozilla.org/en-US/" rel="noopener noreferrer"&gt;MDN web docs&lt;/a&gt; into the Spanish version, to help Spanish-speaking readers as it had been outdated for years with respect to the English version. &lt;/p&gt;

&lt;p&gt;This has been part of my open-source journey. In this article, I want to talk about the process, to enable future contributors to seamlessly contribute one day. &lt;/p&gt;

&lt;p&gt;Here are the &lt;a href="https://github.com/mdn/translated-content/blob/main/docs/es/README.md" rel="noopener noreferrer"&gt;rules to follow&lt;/a&gt; when trying to do the Spanish-translation work. Everything that I am going to write here is already mentioned inside of the &lt;a href="https://github.com/mdn/translated-content/blob/main/docs/es/README.md" rel="noopener noreferrer"&gt;rules&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, what is our job here ? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's a page in English for example: Django's "Admin site" feature.&lt;/li&gt;
&lt;li&gt;There's already a Spanish version of that page, but it's old and out of date.&lt;/li&gt;
&lt;li&gt;Your job is to go through the whole Spanish file, compare it to the whole English file, and rewrite/update it so it matches: same sections, same order, nothing left in English&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is one rule to just bear in mind &lt;/p&gt;

&lt;p&gt;&lt;em&gt;is this text something a human reads (translate it) or something the code/interpreter looks up by exact name (leave it untouched)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  File Paths
&lt;/h2&gt;

&lt;p&gt;This is the most important one; File paths (bolded or inline) are never translated. It stays the same. Below, we will discuss the only thing that changes, which is &lt;code&gt;en-US&lt;/code&gt; to &lt;code&gt;es&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;English: &lt;code&gt;docs/Web/JavaScript/Reference/Global_Objects/Object/assign&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Spanish: &lt;code&gt;docs/Web/JavaScript/Reference/Global_Objects/Object/assign&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Front-matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Final front-matter should only contain: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;l10n.sourceCommit&lt;/code&gt; (plus &lt;code&gt;short-title&lt;/code&gt; when the English file has one).&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;page-type&lt;/code&gt;, &lt;code&gt;sidebar&lt;/code&gt;, &lt;code&gt;original_slug&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; (and &lt;code&gt;short-title&lt;/code&gt;): translate to Spanish.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slug&lt;/code&gt;: These are file paths, not visible text, so they stay exactly as in English do not translate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l10n.sourceCommit&lt;/code&gt;: set to the exact SHA given for that English source commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;External links:&lt;/strong&gt; These are links point to external web pages and they should be left as is. For example:  &lt;a href="https://validator.w3.org/" rel="noopener noreferrer"&gt;https://validator.w3.org/&lt;/a&gt; or &lt;a href="https://developer.flutterwave.com/docs/testing" rel="noopener noreferrer"&gt;https://developer.flutterwave.com/docs/testing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal links:&lt;/strong&gt; All the internal links that point to pages with in the MDN web docs website have to be changed from &lt;code&gt;/en-US/&lt;/code&gt; to &lt;code&gt;/es/&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;For example: &lt;/p&gt;

&lt;p&gt;Original (English source):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has to be changed to point to a spanish file now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/es/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Link Text:&lt;/strong&gt; (ONLY the visible label) gets translated when it's an internal MDN link. &lt;/p&gt;

&lt;p&gt;For example&lt;/p&gt;

&lt;p&gt;Original (English source):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Object basics&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;/en-US/docs/Learn_web_development/Core/Scripting/Object_basics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have to translate The link text "Object basics" to "Fundamentos de objetos"&lt;/p&gt;

&lt;p&gt;Translated (Spanish):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Fundamentos de objetos&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;/es/docs/Learn_web_development/Core/Scripting/Object_basics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Anchors:&lt;/strong&gt; when you have an anchor like this, &lt;code&gt;#heading_about_a_paragraph&lt;/code&gt;. Two things are involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need to check if that section that the anchor is referring to exists. &lt;/li&gt;
&lt;li&gt;If it exists, then you should translate it to Spanish &lt;code&gt;#encabezado_sobre_un_párrafo&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bold / italics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep the bold/italic markers in the same place, only translate the text content.&lt;/li&gt;
&lt;li&gt;Text content that were bold or italics in English stay bold or italics in Spanish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Original (English source):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="ge"&gt;_this is supposed to be italic_&lt;/span&gt; and &lt;span class="gs"&gt;**this is supposed to be bold**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="ge"&gt;_esto se supone que debe estar en cursiva_&lt;/span&gt; and &lt;span class="gs"&gt;**esto se supone que debe estar en negrita**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Image(s)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Never copy images into the files/es/ folder because the build pulls them from the English folder automatically. Just leave the image file path as is&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Only the alt text (the [...] part) gets translated; the filename or file path stays exactly as-is.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example: &lt;/p&gt;

&lt;p&gt;Original (English source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Prototype chain for myObject&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;myobject-prototype-chain.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Cadena de prototipos para myObject&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;myobject-prototype-chain.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Callouts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In GitHub markdown, callouts such as &lt;code&gt;[!NOTE]&lt;/code&gt; and &lt;code&gt;[!WARNING]&lt;/code&gt; stay untranslated because translating them breaks the styled box. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ONLY the user-facing text content contained within the box gets translated. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example,&lt;/p&gt;

&lt;p&gt;Original (English source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt;[!NOTE]&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; See also the [glossary entry on API](/en-US/docs/Glossary/API) for more information.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt;[!NOTE]&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Consulta también la [entrada del glosario sobre API](/es/docs/Glossary/API) para más información.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: &lt;code&gt;[!NOTE]&lt;/code&gt; remains untouched&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Plain Code blocks:&lt;/strong&gt; These are code blocks that contain only literal data: no comments, no prose mixed in so there's nothing to translate 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;https://example.com/articles/
https://example.com/articles/23
https://example.com/articles/?page=2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stays exactly the same in the Spanish file — same URLs, same formatting, no changes whatsoever. There's no comment explaining anything and no prose sentence inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name
age
city
greet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are actual JavaScript property names being listed for the reader to look at, not descriptive text, so translating "city" to "ciudad" or "age" to "edad" would misrepresent what the code actually contains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML table structure:&lt;/strong&gt;&lt;br&gt;
Table tags &lt;code&gt;(&amp;lt;table&amp;gt;, &amp;lt;tr&amp;gt;, &amp;lt;th&amp;gt;, etc.)&lt;/code&gt; stay exactly the same, only the visible text inside is translated.&lt;/p&gt;

&lt;p&gt;For example: &lt;/p&gt;

&lt;p&gt;Original (English source):&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;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Prerequisites&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
        A basic understanding of &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;"/en-US/docs/Learn_web_development/Core/Scripting"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;JavaScript basics&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.
      &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Objective&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;To understand JavaScript's prototype-based inheritance model.&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish):&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;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Prerrequisitos&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
        Un entendimiento básico de &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;"/es/docs/Learn_web_development/Core/Scripting"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;los fundamentos de JavaScript&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.
      &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Objetivo&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Comprender el modelo de herencia basado en prototipos de JavaScript.&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Kumascript macros {{...}}&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Macro names and their arguments (page paths) are never translated. The macro names such as (&lt;code&gt;NextMenu&lt;/code&gt;, &lt;code&gt;PreviousMenuNext&lt;/code&gt;) stay in English because they're internal MDN template functions, not prose.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;{{glossary("term")}}&lt;/code&gt;: if the English term doesn't naturally read well in Spanish, add a second argument with the translated display text — &lt;code&gt;{{glossary("term", "término")}}&lt;/code&gt;. If the term is used as-is in Spanish (e.g. &lt;code&gt;HTTP&lt;/code&gt;, &lt;code&gt;JavaScript&lt;/code&gt;, &lt;code&gt;Python&lt;/code&gt;), leave it with no second argument.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigation macros (&lt;code&gt;{{NextMenu(...)}}&lt;/code&gt;, &lt;code&gt;{{PreviousMenuNext(...)}}&lt;/code&gt;) always left fully untouched whether in English or Spanish.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{NextMenu("Learn_web_development/Core/Scripting/Object_prototypes", "Learn_web_development/Core/Scripting")}}

{{PreviousMenuNext("Learn_web_development/Core/Scripting/Object_prototypes", "Learn_web_development/Core/Scripting/Object_building_practice", "Learn_web_development/Core/Scripting")}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Headings
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Translate headings to match the reference heading list given in the issue, using consistent verb style (gerund/imperative) across the doc. 
When you finish translating, review your work and make sure you have same headings, same order, same structure as the English source and Nothing is left untranslated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;English (Original Source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Summary
## Basics: anatomy of a URL
## Scheme
## Authority
## Path to resource
## Parameters
## Anchor
## How to use URLs
## Absolute URLs vs. relative URLs
## URL usernames and passwords
## Semantic URLs
## See also
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Resumen
## Conceptos básicos: anatomía de una URL
## Esquema
## Autoridad
## Ruta al recurso
## Parámetros
## Ancla
## Cómo utilizar las URL
## URL absolutas frente a URL relativas
## Nombres de usuario y contraseñas en las URL
## URL semánticas
## Véase también
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Watch for accented characters affecting auto-generated anchor targets (e.g. "Validación" becomes anchor &lt;code&gt;validación_html&lt;/code&gt;, not validacion_html) because a missing accent in a link breaks the anchor. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;code&gt;/es/docs/Learn_web_development/Core/Forms/Form_validation#validación_html&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;For example;&lt;/p&gt;

&lt;p&gt;English (Original source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Validation HTML&lt;/span&gt;

Here's a small but easy-to-miss detail about headings and links. When MDN builds a page, it auto-generates an anchor ID for every heading by lowercasing the text and replacing spaces with underscores. The catch is that it keeps accented characters exactly as they are.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated Spanish&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Validación HTML&lt;/span&gt;

Aquí hay un detalle pequeño pero fácil de pasar por alto sobre los encabezados y los enlaces. Cuando MDN construye una página, genera automáticamente un ancla para cada encabezado convirtiendo el texto a minúsculas y reemplazando los espacios con guiones bajos. La particularidad es que mantiene los caracteres acentuados exactamente como están.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comments
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Comments inside of Code blocks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This has two (2) cases: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A comment that is explaining something to the user (reader) should be translated.
&lt;/li&gt;
&lt;li&gt;A comment that does not add any additional useful information should be discarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Useful comment(s)
&lt;/h4&gt;

&lt;p&gt;A comment explaining something (translate it): &lt;br&gt;
This comment is real prose explaining why the line exists or what it does. A Spanish-speaking reader benefits from having this translated&lt;/p&gt;

&lt;p&gt;English (Original Source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Number of visits to this view, as counted in the session
&lt;/span&gt;&lt;span class="n"&gt;num_visits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_visits&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Número de visitas a esta vista, contado en la sesión
&lt;/span&gt;&lt;span class="n"&gt;num_visits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_visits&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Throwaway comment(s)
&lt;/h4&gt;

&lt;p&gt;Example 1&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.contrib.sessions&lt;/span&gt;&lt;span class="sh"&gt;'&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;Here, &lt;code&gt;# ...&lt;/code&gt; isn't explaining anything — it's just a visual placeholder telling the reader "there's more code here we're not showing." There's no real content to translate; translating &lt;code&gt;...&lt;/code&gt; doesn't mean anything different in Spanish. Best left as-is.&lt;/p&gt;

&lt;p&gt;Example 2&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wheels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alloy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or comment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of comment is minor, vague, and doesn't add much context either way, so it's genuinely a coin-flip: leave it in English since it's a throwaway aside, or translate it for full consistency with the rest of the document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keywords, Variables, Functions:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;DO NOT TRANSLATE ANY OF THESE&lt;/p&gt;

&lt;p&gt;Variable names — e.g. &lt;code&gt;numOfVisits&lt;/code&gt;, &lt;code&gt;myCar&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Function/method names — e.g. &lt;code&gt;getPrototypeOf()&lt;/code&gt;, &lt;code&gt;index()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Class names — e.g. &lt;code&gt;Person&lt;/code&gt;, &lt;code&gt;Author&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Property/dictionary keys — e.g. &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;wheels&lt;/code&gt;, &lt;code&gt;my_car['wheels']&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Exception names — e.g. &lt;code&gt;KeyError&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boolean values (true, false):&lt;/strong&gt; remain untranslated in both English and Spanish&lt;/p&gt;

&lt;p&gt;English (Original Source)&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;// Uses 'toString()' by default, returns true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Usa 'toString()' por defecto, devuelve true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Methods&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Method names mentioned inside comments stay untouched.&lt;/p&gt;

&lt;p&gt;English (Original Source)&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;// Calls 'getPrototypeOf()' to find the parent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated (Spanish)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Llama a 'getPrototypeOf()' para encontrar al padre
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Console.log()&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;There are two (2) incidents here: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For literal strings printed to the user, you should translate it. An example is &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;English (Original Source):&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Greetings from Madrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translated Spanish:&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saludos desde Madrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;short/generic literal, here it's a judgment call, and the practical approach is to flag it for the reviewer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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!&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pluralization
&lt;/h2&gt;

&lt;p&gt;If translating a word breaks a language feature (e.g. pluralization), adapt the word so the mechanism still works (e.g. "time" becomes "vece").&lt;/p&gt;

&lt;p&gt;"time" plural form is "times" in English, but the literal spanish translation would be wrong (broken) spanish.&lt;/p&gt;

&lt;p&gt;if we were to follow a literal translation of "time" into spanish "vez" literal plural translation "vezs" (which is wrong spanish)&lt;/p&gt;

&lt;p&gt;"vez" correct plural form in spanish is "vece". &lt;/p&gt;

&lt;p&gt;&lt;code&gt;num_visits&lt;/code&gt; = 3, Here, pluralize looks at &lt;code&gt;num_visits&lt;/code&gt; and, if it's more than 1, appends an "s" to whatever comes right before the template tag — turning "time" into "times".&lt;/p&gt;

&lt;p&gt;English (Original Source)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;You have visited this page &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;num_visits&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt; time&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;num_visits&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;pluralize&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adapted translation (mechanism still works):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;Has visitado esta página &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;num_visits&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt; vece&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;num_visits&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;pluralize&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  More Information
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run Prettier on the finished file, when you are done translating.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prettier &lt;span class="nt"&gt;--write&lt;/span&gt; files/es/learn_web_development/core/scripting/object_prototypes/index.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Check for invisible/zero-width characters (e.g. using a script scanning for &lt;code&gt;U+200B&lt;/code&gt;, &lt;code&gt;U+200C&lt;/code&gt;, &lt;code&gt;U+200D&lt;/code&gt;, &lt;code&gt;U+FEFF&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;INVISIBLE_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x200B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x200C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x200D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xFEFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;INVISIBLE_CHARS&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;check_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Thank you and please follow me.
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to setup WikiEduDashboard for OSS contribution</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Sun, 30 Aug 2026 15:22:12 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/how-to-setup-wikiedudashboard-for-oss-contribution-53e5</link>
      <guid>https://dev.to/ifeanyichima/how-to-setup-wikiedudashboard-for-oss-contribution-53e5</guid>
      <description>&lt;h2&gt;
  
  
  1. Problem Statement
&lt;/h2&gt;

&lt;p&gt;I wanted to contribute to an open source project called &lt;strong&gt;WikiEduDashboard&lt;/strong&gt;, a web application built by Wiki Education. It helps instructors and program leaders run Wikipedia-editing classes and campaigns: students join a course, make edits to Wikipedia, and the dashboard tracks their work.&lt;/p&gt;

&lt;p&gt;To contribute code to this project, I first need a working copy of it running on my own PC. This is called a "local development environment." Without it, I can't test any changes I make before sending them back to the project.&lt;/p&gt;

&lt;p&gt;The problem: this project was built with tools that work best on Mac or Linux, not on plain Windows. So the first challenge wasn't even the project itself, it was figuring out how to run a Linux-friendly project on a Windows PC.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Solution (High Level)
&lt;/h2&gt;

&lt;p&gt;Instead of fighting Windows directly, we used a feature built into Windows called &lt;strong&gt;WSL (Windows Subsystem for Linux)&lt;/strong&gt;. WSL lets a real Linux system (Ubuntu, in our case) run inside Windows, side by side with your normal Windows apps. It's not a separate computer or a virtual machine you have to babysit, it just works like an extra terminal environment on the same PC.&lt;/p&gt;

&lt;p&gt;Once inside Ubuntu, we could follow the project's official setup instructions exactly as written, since those instructions assume a Mac or Linux machine.&lt;/p&gt;

&lt;p&gt;The overall plan looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a Linux environment running on Windows (WSL + Ubuntu)&lt;/li&gt;
&lt;li&gt;Get a personal copy of the project's code (fork it on GitHub, then clone it)&lt;/li&gt;
&lt;li&gt;Install the programming language the project is built with (Ruby)&lt;/li&gt;
&lt;li&gt;Run the project's automated setup script, which installs the rest of the required tools (database, background job system, etc.)&lt;/li&gt;
&lt;li&gt;Start the actual application and view it in a browser&lt;/li&gt;
&lt;li&gt;Build the frontend (the visual, interactive part of the site)&lt;/li&gt;
&lt;li&gt;Set up an editor (VS Code) that can actually see and edit the code living inside Ubuntu&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Step by Step: What We Did and Why
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install WSL and Ubuntu
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; WSL is a Windows feature that runs a real Linux distribution (we used Ubuntu) inside Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we needed it:&lt;/strong&gt; WikiEduDashboard's setup script only officially supports Debian-based Linux (like Ubuntu), Fedora, and macOS. Plain Windows isn't supported well: things like Ruby, Redis, and MySQL are much harder to install and configure correctly on native Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command used (from PowerShell, as Administrator):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;wsl&lt;/span&gt; &lt;span class="na"&gt;--install
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, opening "Ubuntu" from the Start menu gives a terminal prompt that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;adminuser@DESKTOP-OF73031:~$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;~&lt;/code&gt; symbol means "home folder", it's the starting point for everything else we did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Confirm Git Was Available
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Git is a tool used to download ("clone") and manage code from GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; We need it to grab a copy of the project's code onto our PC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returned &lt;code&gt;git version 2.43.0&lt;/code&gt;, confirming git was already installed inside Ubuntu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fork and Clone the Repository
&lt;/h3&gt;

&lt;p&gt;We forked the project on GitHub first (via the browser, clicking the "Fork" button on the project's page), then cloned our fork:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YOUR_USERNAME/WikiEduDashboard.git
&lt;span class="nb"&gt;cd &lt;/span&gt;WikiEduDashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Install Ruby
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is Ruby?&lt;/strong&gt; Ruby is the programming language WikiEduDashboard's backend (the "engine" of the app) is written in, specifically using a framework called &lt;strong&gt;Ruby on Rails&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we needed a specific version:&lt;/strong&gt; The project requires Ruby 3.4.8 exactly. Running the setup script without Ruby installed gave this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ruby: command not found
Ruby-3.4.8 not found. Please install ruby-3.4.8 and run this script again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How we installed it:&lt;/strong&gt; using a tool called &lt;strong&gt;RVM&lt;/strong&gt; (Ruby Version Manager), which makes it easy to install and switch between specific Ruby versions.&lt;/p&gt;

&lt;p&gt;Here is how we install the RVM tool, copy and paste this block of command into your &lt;code&gt;ubuntu&lt;/code&gt; terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gnupg2
gpg &lt;span class="nt"&gt;--keyserver&lt;/span&gt; keyserver.ubuntu.com &lt;span class="nt"&gt;--recv-keys&lt;/span&gt; 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://get.rvm.io | bash &lt;span class="nt"&gt;-s&lt;/span&gt; stable
&lt;span class="nb"&gt;source&lt;/span&gt; /home/adminuser/.rvm/scripts/rvm
rvm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"ruby-3.4.8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirming it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ruby &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;code&gt;ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [x86_64-linux]&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Run the Automated Setup Script
&lt;/h3&gt;

&lt;p&gt;The project ships with a Python script (&lt;code&gt;setup.py&lt;/code&gt;) that automates installing everything else the project needs: gems (Ruby's package system), a database (MySQL/MariaDB), configuration files, and JavaScript packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Doing all of this by hand (the "manual setup" path in the project's docs) is long and error-prone. The script does it consistently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installed Ruby "gems" (Ruby's version of code libraries/packages)&lt;/li&gt;
&lt;li&gt;Created configuration files (&lt;code&gt;config/application.yml&lt;/code&gt; and &lt;code&gt;config/database.yml&lt;/code&gt;) from example templates&lt;/li&gt;
&lt;li&gt;Created the development and test databases&lt;/li&gt;
&lt;li&gt;Created a MySQL user for the app to use&lt;/li&gt;
&lt;li&gt;Ran database migrations (setting up the actual tables the app needs)&lt;/li&gt;
&lt;li&gt;Installed JavaScript packages using &lt;strong&gt;Yarn&lt;/strong&gt; (a package manager for frontend/JavaScript code, similar in purpose to &lt;code&gt;gem&lt;/code&gt; for Ruby)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end, it printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your development environment setup is complete.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Start Redis
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is Redis?&lt;/strong&gt; Redis is a fast, lightweight in-memory data store. In this project, it's used by a background job system called &lt;strong&gt;Sidekiq&lt;/strong&gt;, which handles tasks like scheduled updates and syncing data, things that shouldn't slow down the main website while they run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; without Redis running, features that rely on background jobs (some Wikipedia-editing features especially) won't work properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We got an error saying the port was already in use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not create server TCP listening socket *:6379: bind: Address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This actually meant Redis was &lt;strong&gt;already running&lt;/strong&gt; in the background (likely started automatically during setup). We confirmed this instead of restarting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;code&gt;PONG&lt;/code&gt;, meaning Redis was alive and responding correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Start the Rails Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; This starts the actual backend server, the part that listens for requests and serves the website.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once running, it showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;* Listening on http://127.0.0.1:3000
Use Ctrl-C to stop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the app was now reachable at &lt;code&gt;http://localhost:3000&lt;/code&gt; in a browser. This terminal window needs to stay open and running the whole time the app is in use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Open the Frontend Build
&lt;/h3&gt;

&lt;p&gt;open a &lt;strong&gt;second&lt;/strong&gt; Ubuntu terminal window (leaving the first one running &lt;code&gt;rails s&lt;/code&gt; untouched), move into the project folder, and run &lt;code&gt;yarn start&lt;/code&gt;, see example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;WikiEduDashboard
yarn start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles the frontend and also watches for future changes, automatically rebuilding when files are edited.&lt;/p&gt;

&lt;p&gt;After this finished, refreshing &lt;code&gt;http://localhost:3000&lt;/code&gt; showed the actual dashboard homepage, fully styled and working, logged in as &lt;code&gt;Chimaifeanyi29&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Set Up VS Code to Edit the Code
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; the project's code physically lives inside the Ubuntu (WSL) filesystem, not in a normal Windows folder. Opening VS Code the usual way on Windows wouldn't show the right files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; VS Code has a "Remote - WSL" extension that lets it connect directly into the Ubuntu environment.&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install VS Code normally on Windows (if not already installed)&lt;/li&gt;
&lt;li&gt;Inside VS Code, go to the Extensions tab and install the extension called &lt;strong&gt;"WSL"&lt;/strong&gt; (by Microsoft)&lt;/li&gt;
&lt;li&gt;From inside the Ubuntu terminal, inside the project folder, run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens VS Code on the Windows desktop, but connected directly to the Ubuntu filesystem, shown by a "WSL: Ubuntu" indicator in the bottom-left corner of the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Summary: What's Currently Running
&lt;/h2&gt;

&lt;p&gt;At the end of this session, the local development setup consists of three things running at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rails s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The backend server (handles requests, talks to the database)&lt;/td&gt;
&lt;td&gt;Terminal 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds and watches the frontend (JavaScript/React code)&lt;/td&gt;
&lt;td&gt;Terminal 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis (background)&lt;/td&gt;
&lt;td&gt;Powers background jobs via Sidekiq&lt;/td&gt;
&lt;td&gt;Runs automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The app is reachable in a browser at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Key Terms Explained (Quick Reference)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WSL (Windows Subsystem for Linux):&lt;/strong&gt; A way to run real Linux (Ubuntu, here) inside Windows without a separate computer or full virtual machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork:&lt;/strong&gt; Your own personal copy of someone else's GitHub project, needed before you can propose changes back to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone:&lt;/strong&gt; Downloading a copy of a GitHub project's code onto your own machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby / Ruby on Rails:&lt;/strong&gt; The programming language and web framework the backend of this project is built with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RVM:&lt;/strong&gt; A tool for installing and managing specific versions of Ruby.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gems:&lt;/strong&gt; Ruby's term for external code packages/libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yarn:&lt;/strong&gt; A package manager for JavaScript, used here to install and build the frontend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis:&lt;/strong&gt; A fast in-memory data store, used to support background job processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidekiq:&lt;/strong&gt; A background job system (built on Ruby) that uses Redis, used for scheduled tasks like data syncing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rails server (&lt;code&gt;rails s&lt;/code&gt;):&lt;/strong&gt; The command that starts the actual running web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;manifest.json:&lt;/strong&gt; A file generated by the frontend build process that tells the backend where compiled frontend assets live.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I solved a 5 year old bug</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 30 Jul 2026 22:48:44 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/i-solved-a-5-year-old-bug-356h</link>
      <guid>https://dev.to/ifeanyichima/i-solved-a-5-year-old-bug-356h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Let me start by saying I am an Open-source contributor at Mozilla, I am trying to get better as a software engineer. I will be using the words &lt;strong&gt;Site&lt;/strong&gt; or &lt;strong&gt;Tile&lt;/strong&gt; interchangeably, they mean the same thing. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Report
&lt;/h2&gt;

&lt;p&gt;The title of the &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1628669" rel="noopener noreferrer"&gt;bug&lt;/a&gt; report was "&lt;strong&gt;Cannot remove amazon.com from top sites list&lt;/strong&gt;" on Bugzilla and I believe it really captures the pain-point that users experienced because of the bug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmt25jbxqyaolmqn8lg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmt25jbxqyaolmqn8lg7.png" alt="Bug report" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this bug report the user is complaining that he cannot remove &lt;u&gt;amazon.com&lt;/u&gt; from &lt;code&gt;Top Sites&lt;/code&gt; in the &lt;strong&gt;New tab page&lt;/strong&gt; of his Mozilla Firefox browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;A little back-story of what &lt;code&gt;Top Sites&lt;/code&gt;, &lt;code&gt;New tab page&lt;/code&gt; and &lt;code&gt;Places&lt;/code&gt; is: &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top Sites&lt;/strong&gt; is that grid of website shortcuts you see on Firefox's New Tab Page. It shows the sites you visit most often, plus some sponsored tiles (called "spocs," short for sponsored content) that Mozilla partners provide. The image below shows a row of Top Sites on the New Tab page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2e764iiwkxsp94k97rr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2e764iiwkxsp94k97rr.png" alt="Top Sites in Firefox" width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New tab page&lt;/strong&gt; is the page that opens when you open a new tab or click the home button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Places&lt;/strong&gt; is a SQLite database that Firefox uses to store browsing history.  &lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine you open a new tab in Firefox and see a grid of your most-visited websites (called "Top Sites"). &lt;u&gt;amazon.com&lt;/u&gt; keeps showing up there, even though you never visit it and you keep trying to delete it.&lt;/p&gt;

&lt;p&gt;You right-click on the Amazon icon, click "Delete from History," but... nothing happens. It just sits there, refusing to leave. You try again. Still there. You try "Dismiss." Still there. It's like a stubborn guest who won't leave your house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;Firefox's Top Sites can come from three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your actual browsing history (sites you really visited)&lt;/li&gt;
&lt;li&gt;Pinned sites (sites you manually pinned to stay there)&lt;/li&gt;
&lt;li&gt;Default/curated sites (sites Firefox puts there automatically, like Amazon, because they think you might want them)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay with me and we would find out which one of these is the culprit. &lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bug (Part A)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Investigation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By reading through the thread on the bug report, I discovered a trick other users on the thread had tried&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one user on the thread had the idea of changing the URL and then the problem went away, but after a new firefox release Firefox 82.0 (64-bit) the trick could no longer work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reproducing:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To reproduce the bug locally, I had to visit &lt;code&gt;https://amazon.com&lt;/code&gt; multiple times (up to 20 times) on my local PC so that I can get &lt;code&gt;amazon.com&lt;/code&gt; to show up as a &lt;strong&gt;Top Site&lt;/strong&gt; on my New Tab page. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviewer Feedback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first attempt was to block the &lt;code&gt;url&lt;/code&gt; whenever a user clicked "Delete History" so that it will never show up again in &lt;strong&gt;Top Sites&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mak&lt;/strong&gt; (a mozilla engineer) pushed back on this, pointing out that if a user just wants to delete history, silently also blocking the site (removing it entirely from Top Sites without saying so) gives them unexpected behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The problem with the fix is that the dialog asks to the user if they want to remove from history, but then the code proceed also blocking the site, that is not told to the user and may be unexpected.&lt;br&gt;
If the user wanted to effectively just remove history, not block the site, they will get the wrong behavior.&lt;/p&gt;

&lt;p&gt;I'm not expert of this code, but it looks like the proposed solution is broad and may cause unexpected behavior, actually removing control from the user rather than increasing it.&lt;/p&gt;

&lt;p&gt;It's touching the right problem though, someone should eval a UX solution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I got a second feedback from another reviewer &lt;strong&gt;thecount&lt;/strong&gt; (a mozilla engineer) and he was the one who led me to the right fix for the bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you log &lt;code&gt;link.url&lt;/code&gt; and &lt;code&gt;searchProvider.url&lt;/code&gt;, like this &lt;code&gt;console.log(link.url, searchProvider.url);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Right before this line: &lt;a href="https://searchfox.org/firefox-main/source/toolkit/modules/NewTabUtils.sys.mjs#1093" rel="noopener noreferrer"&gt;https://searchfox.org/firefox-main/source/toolkit/modules/NewTabUtils.sys.mjs#1093&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then on startup, I see this in the logs: "&lt;a href="https://www.amazon.com/" rel="noopener noreferrer"&gt;https://www.amazon.com/&lt;/a&gt; &lt;a href="https://amazon.com" rel="noopener noreferrer"&gt;https://amazon.com&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;So this is the line that causes the url to be changed, and now the &lt;code&gt;url&lt;/code&gt; is no longer removable because it no longer matches the original places &lt;code&gt;url&lt;/code&gt;.&lt;br&gt;
one solution might be to store "originalUrl" &lt;code&gt;link.originalUrl = link.url&lt;/code&gt; in the block I linked above, and then when we delete the url, check if we have "originalUrl" and use that instead"? So something like &lt;code&gt;link.originalUrl || link.url&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;With this feedback, I was able to finally understand the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;&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;// toolkit/modules/NewTabUtils.sys.mjs&lt;/span&gt;

&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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;let&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shortURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;= swap is done here&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;The issue was from here: you see Firefox does a check to see if &lt;u&gt;amazon.com&lt;/u&gt; is already stored as a search-shortcut provider and then it quietly drops the &lt;code&gt;url&lt;/code&gt; and replaces it with the internal &lt;code&gt;searchProvider.url&lt;/code&gt;. This is why when we click "Delete History" nothing happens because we have lost the &lt;code&gt;url&lt;/code&gt; that was supposed to match what we have in &lt;code&gt;Places&lt;/code&gt; (a database in Firefox).&lt;/p&gt;

&lt;p&gt;What is a search-shortcut provider, It means "a site Firefox knows how to search directly", and Amazon qualifies because Amazon has its own product search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before a tile ever reaches the New Tab page, Firefox checks each link against its built-in list of search-shortcut sites (Google, Amazon, Bing, DuckDuckGo, eBay, Twitter, Wikipedia). If the link matches one of these, Firefox swaps that tile's url to a version meant to trigger a search on that site, not to visit the homepage you actually browsed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bug (Part B)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mental model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mental model to have in Firefox is&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; the New Tab Page (React), shows the tiles, builds the request&lt;br&gt;
&lt;strong&gt;The request itself:&lt;/strong&gt; the payload sent from frontend to backend&lt;br&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; &lt;code&gt;PlacesFeed.sys.mjs&lt;/code&gt;, receives the request, touches the actual database (your browsing history)&lt;/p&gt;

&lt;p&gt;Like when building a To-do App, when you click on an item to delete from the database, you send the &lt;code&gt;id&lt;/code&gt; of the item as a payload to the backend (either in the URL or Request Body). But here, the real &lt;code&gt;id&lt;/code&gt; (&lt;code&gt;original_url&lt;/code&gt;) was quietly being generated and never forwarded, the frontend only ever knew about &lt;code&gt;url&lt;/code&gt;, which by that point pointed to the wrong thing. &lt;/p&gt;

&lt;p&gt;So, for the fix we are going to be borrowing heavily from the full-stack MVC architecture &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking down the fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We make sure that we do not lose reference to the &lt;code&gt;url&lt;/code&gt; before it gets silently swapped out by Firefox, so we do something like this &lt;code&gt;link.original_url = link.url;&lt;/code&gt; here, we are storing away the &lt;code&gt;url&lt;/code&gt; in a variable called &lt;code&gt;original_url&lt;/code&gt; and it will be referenced later when we want to delete a &lt;strong&gt;site&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Allow me to break down the fix using REST API from Nodejs.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt; This is like a backend endpoint that serves your frontend a list which we use to create the &lt;strong&gt;Tiles&lt;/strong&gt;. Here is where we create the &lt;strong&gt;Top Sites&lt;/strong&gt; and send to the frontend, but we quietly rename a field along the way.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/topsites&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLinksFromDatabase&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;=backup the real id/url&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// &amp;lt;=overwrite what the frontend will see&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;&lt;strong&gt;B&lt;/strong&gt; A user clicks a button which fires off a delete request and then we send both the &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;original_url&lt;/code&gt; to the backend server.&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/delete-history&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C&lt;/strong&gt; The backend receives the request and decides what to actually delete&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/delete-history&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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 checks: "do I have an original_url? If yes, use that to delete. If not, fall back to url." &lt;/p&gt;

&lt;p&gt;This is important, because if it only used url, it would try to delete the search provider's link instead of the real saved site, and the deletion would silently fail or delete the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Finally !!!!, now that we have a solid understanding of the issue and the fix, let us go ahead and implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; Equivalent to Step 1 from the Rest API&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;// toolkit/modules/NewTabUtils.sys.mjs&lt;/span&gt;
&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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;let&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shortURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B.&lt;/strong&gt; Equivalent to Step 2 from the Rest API&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;// browser/extensions/newtab/content-src/lib/link-menu-options.mjs&lt;/span&gt;
&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onConfirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AlsoToMain&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;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DELETE_HISTORY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;forceBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmarkGuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&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="p"&gt;],&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;&lt;strong&gt;C.&lt;/strong&gt; Equivalent to Step 3 from the Rest API&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;// browser/extensions/newtab/lib/PlacesFeed.sys.mjs&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DELETE_HISTORY_URL&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forceBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activityStreamLinks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteHistoryEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;break&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I waited for my patch to be approved and landed. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2p2jja42oync33gyao3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2p2jja42oync33gyao3.gif" alt="Bug Fix" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The issue was that Amazon was in the user's actual history, but the displayed &lt;code&gt;url&lt;/code&gt; got silently swapped to the &lt;code&gt;search-shortcut&lt;/code&gt; version, so the delete call looked for a &lt;code&gt;url&lt;/code&gt; that no longer existed in &lt;code&gt;Places&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🕷️ BUG: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1628669" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=1628669&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💗 PATCH: &lt;a href="https://phabricator.services.mozilla.com/D292910" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D292910&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 MY PROFILE ON BUGZILLA: &lt;a href="https://bugzilla.mozilla.org/user_profile?user_id=786766" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/user_profile?user_id=786766&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, please follow me
&lt;/h2&gt;

</description>
      <category>mozilla</category>
      <category>browser</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Ask me anything (RAG)</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:15:19 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/ask-me-anything-rag-32jl</link>
      <guid>https://dev.to/ifeanyichima/ask-me-anything-rag-32jl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;At LASU, students constantly ask each other things like "is this lecturer strict," "is this hostel safe," "what's SIWES actually like at this company," but that knowledge lives scattered across WhatsApp groups, Twitter threads, and word of mouth. It gets lost the moment a graduating class leaves. AMA was built to solve that: a shared memory for the school, where real student experiences get collected once and stay searchable by anyone, forever, instead of vanishing into a thousand disconnected chats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/XlswFvh2mfU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/MasterIfeanyi/hackathon-today" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/hackathon-today&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used Gemma 4
&lt;/h2&gt;

&lt;p&gt;We used Gemma 4 (via OpenRouter) as the answer-generation model, and built the project around Retrieval-Augmented Generation (RAG), not fine-tuning, since we needed the answers to update instantly as new stories get submitted, without retraining anything.&lt;/p&gt;

&lt;p&gt;The pipeline works like this: when a student submits a story or review, we generate an embedding for it using Google's Gemini embedding model and store both the text and the embedding in MongoDB Atlas, using MongoDB Vector Search as our vector database. When someone asks a question, we embed the question the same way, run a vector similarity search to pull the most relevant stories, and pass only those into Gemma 4 as context, so every answer is grounded in real submissions instead of the model guessing.&lt;/p&gt;

&lt;p&gt;On the frontend, we used Next.js (App Router), Tailwind CSS v4 for styling, Zustand for lightweight UI state, and TanStack React Query for handling data fetching and caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon
&lt;/h2&gt;

&lt;p&gt;This was done as part of a university hackathon hosted by Google. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbq82re8jb7n9hrstwqx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbq82re8jb7n9hrstwqx.jpg" alt="LASU Google Gemma Hackathon" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, please follow me
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>ai</category>
    </item>
    <item>
      <title>Fullscreen icon RTL language support in Firefox browser</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Tue, 14 Jul 2026 11:34:31 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/fullscreen-icon-rtl-language-support-in-firefox-browser-247</link>
      <guid>https://dev.to/ifeanyichima/fullscreen-icon-rtl-language-support-in-firefox-browser-247</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy593f7jw14pebfx828e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy593f7jw14pebfx828e0.png" alt="Zoom Icon in LTR mode" width="389" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our target is to make sure that when the firefox browser is set to RTL mode the fullscreen icon flips as well to follow suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When Firefox is set to an RTL (right-to-left) language like Arabic, the fullscreen icon inside the zoom sidebar still looks like it's designed for LTR (left-to-right) languages. It should be mirrored/flipped to match RTL direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's RTL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Languages like Arabic and Hebrew read from right to left. Browsers have a concept called &lt;code&gt;dir="rtl"&lt;/code&gt; that flips the layout. Icons that have a "direction" to them (like arrows, expand icons) should also flip to feel natural.&lt;/p&gt;

&lt;h3&gt;
  
  
  The plan:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;First, let's find where the zoom sidebar and its fullscreen icon live in the Firefox codebase&lt;/li&gt;
&lt;li&gt;See how the icon is currently being used&lt;/li&gt;
&lt;li&gt;Figure out how to make it flip for RTL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can set your firefox browser to read from right-to-left using this in the console &lt;code&gt;CTRL+SHIFT+ALT+I&lt;/code&gt;&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dir&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rtl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Find the files:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Zoom sidebar HTML file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can start by searching Searchfox (Mozilla's online code search) for terms like "zoom sidebar fullscreen" and "appmenu zoom controls." That search led me to &lt;code&gt;appmenu-viewcache.inc.xhtml&lt;/code&gt;, and inside it I saw this markup:&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;toolbaritem&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-zoom-controls"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton toolbaritem-combined-buttons"&lt;/span&gt; &lt;span class="na"&gt;closemenu=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-text"&lt;/span&gt; &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-zoom"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  ...
  &lt;span class="nt"&gt;&amp;lt;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/toolbaritem&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;FLUENT:&lt;/strong&gt; Firefox uses the &lt;code&gt;fluent&lt;/code&gt; localization system (more in later articles) &lt;/p&gt;

&lt;p&gt;&lt;code&gt;appmenuitem-zoom&lt;/code&gt; is the localization label for the word "Zoom" that sits above the row of buttons (&lt;code&gt;-&lt;/code&gt;, &lt;code&gt;100%&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, fullscreen). It's a distinctive, low-noise string and searching for it locally in VS code is a good way to land on the exact zoom row markup without pulling in unrelated matches.&lt;/p&gt;

&lt;p&gt;Let's start by searching for the zoom sidebar files using this &lt;code&gt;grep&lt;/code&gt; syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"appmenuitem-zoom"&lt;/span&gt; browser/base/content/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we found: &lt;/p&gt;

&lt;p&gt;The fullscreen icon lives inside the zoom row of the hamburger menu (&lt;code&gt;☰&lt;/code&gt;). In the HTML file at &lt;code&gt;browser/base/content/appmenu-viewcache.inc.xhtml&lt;/code&gt;, the button is:&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;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID of the fullscreen button in the zoom row of the hamburger menu is &lt;code&gt;appMenu-fullscreen-button2&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This is very important to keep in mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoom sidebar CSS file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let us find the CSS file where its icon is defined using this &lt;code&gt;grep&lt;/code&gt; syntax&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"appMenu-fullscreen-button2"&lt;/span&gt; browser/themes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we found: &lt;/p&gt;

&lt;p&gt;This finds &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;, the file where the fullscreen icon's image is actually set, which is where we will make our fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fullscreen icon&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"fullscreen.svg"&lt;/span&gt; browser/themes/shared/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that there is an image &lt;code&gt;fullscreen.svg&lt;/code&gt; that serves as the icon, located at &lt;code&gt;browser/themes/shared/icons/fullscreen.svg&lt;/code&gt; and gets applied via &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here, we see the fullscreen icon, which looks like pointing outward from a center. &lt;/p&gt;

&lt;p&gt;Left-to-right &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15pgzm4czgdduwx28nd3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15pgzm4czgdduwx28nd3.png" alt="LTR" width="43" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right-to-left&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvazo4hobdx0izwdtwyuu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvazo4hobdx0izwdtwyuu.png" alt="RTL" width="51" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Figuring out how to make it flip
&lt;/h3&gt;

&lt;p&gt;We need to take note of two things that will guide us to the actual fix.&lt;/p&gt;

&lt;p&gt;From what we saw here, we know that the zoom icon could live inside of &lt;code&gt;toolbarbutton&lt;/code&gt;&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;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to Mozilla &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/toolbarbutton" rel="noopener noreferrer"&gt;guidelines&lt;/a&gt;, &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;toolbarbutton&amp;gt;&lt;/code&gt; is a Firefox custom element (defined in toolkit code, not in this XHTML file), and it generates its own internal "anonymous content" at runtime; meaning the &lt;code&gt;&amp;lt;image class="toolbarbutton-icon"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;label class="toolbarbutton-text"&amp;gt;&lt;/code&gt; children are injected by the custom element's own implementation at render time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can confirm the actual DOM structure using&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;outerHTML&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns&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;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt; &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-text"&lt;/span&gt; &lt;span class="na"&gt;crop=&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt; &lt;span class="na"&gt;flex=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we know &lt;code&gt;&amp;lt;toolbarbutton&amp;gt;&lt;/code&gt; elements in Firefox's UI don't render the icon directly on themselves; they render it inside a child element with the class &lt;code&gt;.toolbarbutton-icon&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;We can confirm this once again by inspecting the element in the Browser Toolbox, when we run&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.toolbarbutton-icon&lt;/span&gt;&lt;span class="dl"&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 returned:&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;image&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that's where the icon or image actually lives and we can target it using this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#appMenu-fullscreen-button2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.toolbarbutton-icon&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads us to the next step in finding the fix.&lt;/p&gt;

&lt;p&gt;According to Mozilla &lt;a href="https://firefox-source-docs.mozilla.org/code-quality/coding-style/rtl_guidelines.html" rel="noopener noreferrer"&gt;guidelines&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;icons implying direction should mirror in RTL, using this exact pattern as the recommended technique&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.element-with-icon&lt;/span&gt;&lt;span class="nd"&gt;:dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;rtl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/** scale: -1 1; does the same horizontal flip **/&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we know what our fix should be&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix, then, is to add a CSS rule that uses &lt;code&gt;:dir(rtl)&lt;/code&gt; to horizontally flip the fullscreen icon whenever Firefox is in RTL mode. Since the icon itself lives inside the &lt;code&gt;.toolbarbutton-icon&lt;/code&gt; child element, that's the element we need to target, using &lt;code&gt;scale: -1 1&lt;/code&gt; to perform the flip.&lt;/p&gt;

&lt;p&gt;The file to edit is: &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* To flip an icon for RTL using scale: */&lt;/span&gt;
&lt;span class="nf"&gt;#appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="nd"&gt;:dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;rtl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.toolbarbutton-icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt; &lt;span class="m"&gt;1&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;This means "when this specific button is inside an RTL context, flip its icon child horizontally."&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;When you open the hamburger menu (&lt;code&gt;☰&lt;/code&gt;) in Firefox, there's a zoom row that looks like: &lt;code&gt;Zoom — [-] 100% [+] [⤢]&lt;/code&gt;. That last icon &lt;code&gt;[⤢]&lt;/code&gt; is the fullscreen button (appMenu-fullscreen-button2).&lt;/p&gt;

&lt;p&gt;In Arabic and other RTL languages, this icon doesn't flip to match the reading direction, it stays the same as in English (LTR). It should be mirrored.&lt;/p&gt;

&lt;p&gt;💗 PATCH: &lt;a href="https://phabricator.services.mozilla.com/D309258" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D309258&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🕷️ BUG: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2029514" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=2029514&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 MY PROFILE ON BUGZILLA: &lt;a href="https://bugzilla.mozilla.org/user_profile?user_id=786766" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/user_profile?user_id=786766&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firefox</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Writing unit test for Firefox private browsing window</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Sun, 05 Jul 2026 17:35:35 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/writing-unit-test-for-firefox-private-browsing-window-25oj</link>
      <guid>https://dev.to/ifeanyichima/writing-unit-test-for-firefox-private-browsing-window-25oj</guid>
      <description>&lt;p&gt;Our indicator looks like this: an icon + a label. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gd1anw6rzz19qhkawbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gd1anw6rzz19qhkawbb.png" alt="private window indicator" width="235" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser has an indicator for when a user is in private mode. The indicator lives in two places under different instances: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The first instance is when the user is in "Horizontal Tabs" when the browser tabs are in the &lt;code&gt;TabsToolbar&lt;/code&gt; (tab-bar or toolbar). So, the indicator shows both the icon and it's label. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The second instance is when the user has "Vertical Tabs" turned on. So, the indicator moves or shows up in the &lt;code&gt;Navbar&lt;/code&gt; (which contains extensions, address bar) and this time only the icon shows up, no label. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our task is to write a unit test to confirm that the icon shows up in both instance. &lt;/p&gt;

&lt;p&gt;We start off by running this search &lt;code&gt;grep -r "private-browsing-indicator-with-label" --include="*.xhtml" --include="*.js" --include="*.css" .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We found two things. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "Shared" CSS file that controls the visibility of this icon &lt;code&gt;browser/themes/shared/browser-shared.css&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;The &lt;code&gt;browser/base/content/private-browsing-indicator.inc.xhtml&lt;/code&gt; — this is where the indicator's actual markup (HTML-like structure) lives.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, we can check the CSS file for where it controls the visibility of the indicator by using this syntax &lt;code&gt;grep -n -A 10 "private-browsing-indicator-with-label" browser/themes/shared/browser-shared.css&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;What we find out in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.private-browsing-indicator-with-label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="py"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;privatebrowsingmode&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"temporary"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;navigator-toolbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;tabs-hidden&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nav-bar&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;#navigator-toolbox&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;tabs-hidden&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nf"&gt;#nav-bar&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;margin-inline-end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-medium&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.private-browsing-indicator-label&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this means: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When the window isn't a private window (we are in normal mode) - it will hide the indicator completely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When we have horizontal tabs in private window, hide the indicator from the navbar (which contains extensions, address bar) and then the indicator will move to the &lt;code&gt;TabsToolbar&lt;/code&gt; (which actually contains browser tabs).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf2v4een8lrkp5lwrry5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf2v4een8lrkp5lwrry5.png" alt="Horizontal Tabs enabled" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The image above shows that we are still in horizontal tabs&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When we have vertical tabs while still in private window, the &lt;code&gt;private-browsing-indicator&lt;/code&gt; will move to the navbar and then hide the label part of the indicator and keep only the icon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv3izr2pj29ar99jzunu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv3izr2pj29ar99jzunu.png" alt="Vertical Tab is enabled" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The above image shows vertical tabs enabled&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the tests
&lt;/h2&gt;

&lt;p&gt;Before we begin writing the tests, we would need to find other existing tests in the codebase to take inspiration from them, by using the search query: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -n "is_visible\|isVisible\|is_hidden\|isHidden" testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs&lt;/code&gt; and then we read the file &lt;code&gt;sed -n '325,350p' testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;This gives us a lot of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What to check: both copies of &lt;code&gt;.private-browsing-indicator-with-label&lt;/code&gt; (one inside &lt;code&gt;TabsToolbar&lt;/code&gt;, one inside &lt;code&gt;nav-bar&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to toggle vertical tabs: by using the pref &lt;code&gt;sidebar.verticalTabs&lt;/code&gt; as seen here&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Task structure: &lt;code&gt;add_task(async function () {...})&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check visibility: &lt;code&gt;BrowserTestUtils.isVisible(element)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to open a private window (modern style): &lt;code&gt;BrowserTestUtils.openNewBrowserWindow({ private: true })&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we can write the tests as follows:&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="cm"&gt;/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */&lt;/span&gt;

&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The private browsing indicator lives in two possible spots:&lt;/span&gt;
&lt;span class="c1"&gt;// - Inside #TabsToolbar (shown with icon + label, for horizontal tabs)&lt;/span&gt;
&lt;span class="c1"&gt;// - Inside #nav-bar (shown with icon only, for vertical tabs)&lt;/span&gt;
&lt;span class="c1"&gt;// This test makes sure at least one copy is always visible in a&lt;/span&gt;
&lt;span class="c1"&gt;// private window, no matter which tabs layout is active.&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_horizontal_tabs&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;win&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;private&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#TabsToolbar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;navBarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#nav-bar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should be visible in TabsToolbar with horizontal tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navBarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator in nav-bar should be hidden with horizontal tabs&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_vertical_tabs&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;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;set&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&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;win&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;private&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#TabsToolbar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;navBarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#nav-bar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator in TabsToolbar should be hidden with vertical tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navBarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should be visible in nav-bar with vertical tabs&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&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;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popPrefEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_not_shown_in_normal_window&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;win&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;indicators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;indicator&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should never be visible in a normal (non-private) window&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;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&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;h2&gt;
  
  
  Explaining the test:
&lt;/h2&gt;

&lt;p&gt;Now, here is what this test is doing. Remember at the beginning, I said our private browsing lives in two possible spots: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Inside #TabsToolbar (shown with icon + label, for horizontal tabs): This is when we have private browsing enabled, but we are still using horizontal browser tabs (the default)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside #nav-bar (shown with icon only, for vertical tabs): This is when the user has enabled / turned on Vertical browser tabs. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  Test 1: Horizontal Browser Tabs
&lt;/h5&gt;

&lt;p&gt;The first test is saying open a private browser window using &lt;code&gt;await BrowserTestUtils.openNewBrowserWindow({ private: true });&lt;/code&gt;  and then we are making sure that when we are in Horizontal Mode - the navbar indicator IS NOT showing. The only indicator that should be showing in Horizontal Mode is the &lt;code&gt;tabsToolbarIndicator&lt;/code&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Test 2: Vertical Browser Tabs
&lt;/h5&gt;

&lt;p&gt;We open a private browser window using &lt;code&gt;await BrowserTestUtils.openNewBrowserWindow({ private: true });&lt;/code&gt; and now, we assume that the user has turned on vertical tabs, how do we do that in our test ?.... by using this &lt;code&gt;pref&lt;/code&gt; to turn on vertical tabs during the test run&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;set&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in Vertical Mode the only indicator that should show is the &lt;code&gt;navBarIndicator&lt;/code&gt; and the other one should not show. Also, do not forget to close the browser window and cleanup the &lt;code&gt;Pref&lt;/code&gt; using&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&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;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popPrefEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Test 3: Normal Browser Window
&lt;/h5&gt;

&lt;p&gt;In this test, we open a normal browsing window and we make sure that none of those labels either &lt;code&gt;navBarIndicator&lt;/code&gt; or &lt;code&gt;tabsToolbarIndicator&lt;/code&gt; show up when the user is NOT using private browsing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TLDR:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What each test does, briefly&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_horizontal_tabs&lt;/code&gt;: opens a private window in normal (default) tabs mode, checks the TabsToolbar indicator is visible and the nav-bar indicator is hidden.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_vertical_tabs&lt;/code&gt;: opens a private window, turns on vertical tabs mode, check that the nav-bar indicator is visible, TabsToolbar indicator hidden.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_not_shown_in_normal_window&lt;/code&gt;: opens a completely normal (non-private window) and makes sure neither indicator shows up at all since this isn't a private window in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Registering the test
&lt;/h2&gt;

&lt;p&gt;In Firefox, whenever you write a test, you must register the test in &lt;code&gt;browser.toml&lt;/code&gt; which is meant for that particular component - We are writing a test for the private browsing component; hence the file is located at &lt;code&gt;browser/components/privatebrowsing/test/browser/browser.toml&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;Components are registered in an alphabetical order in this file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;["browser_privatebrowsing_favicon.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_history_shift_click.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_indicator.js"]&lt;/span&gt; &lt;span class="c"&gt;# our unit test &lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_last_private_browsing_context_exited.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_lastpbcontextexited.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_localStorage.js"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SUMMARY:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user switches Firefox to private browsing mode, the indicator shows up, and the bug report was that there was never a unit test to ensure that the indicator is present in various browsing modes and so we wrote unit tests for those instances of the browser mode&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal tabs mode (while private browsing is turned on)&lt;/li&gt;
&lt;li&gt;Vertical tabs mode (while private browsing is turned on)&lt;/li&gt;
&lt;li&gt;Normal browsing mode. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find the Patch Here: &lt;a href="https://phabricator.services.mozilla.com/D310523" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D310523&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Find the Bug Here: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1927615" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=1927615&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THANK YOU&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>browser</category>
      <category>firefox</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 29 Jun 2026 08:29:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/-3pbj</link>
      <guid>https://dev.to/ifeanyichima/-3pbj</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-story__hidden-navigation-link"&gt;Dev Opportunity Radar #5: A Fully Funded Trip to AWS re:Invent, Google Cloud Career Launchpad, and a $1,000 Award&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Weekly community-sourced tech resources&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/devengers"&gt;
            &lt;img alt="The DEVengers logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12483%2Fb3a03d2d-fbb6-4355-9593-dff0f92638e9.webp" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/hemapriya_kanagala" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3307586%2F2dffaf97-946d-44a6-8a39-07d94a72e07d.png" alt="hemapriya_kanagala profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/hemapriya_kanagala" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hemapriya Kanagala
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hemapriya Kanagala
                
                
              
              &lt;div id="story-author-preview-content-3999153" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/hemapriya_kanagala" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3307586%2F2dffaf97-946d-44a6-8a39-07d94a72e07d.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hemapriya Kanagala&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/devengers" class="crayons-story__secondary fw-medium"&gt;The DEVengers&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" id="article-link-3999153"&gt;
          Dev Opportunity Radar #5: A Fully Funded Trip to AWS re:Invent, Google Cloud Career Launchpad, and a $1,000 Award
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/community"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;community&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opportunities"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opportunities&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/resources"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;resources&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;71&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              26&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            9 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:37:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/-54id</link>
      <guid>https://dev.to/ifeanyichima/-54id</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-story__hidden-navigation-link"&gt;From React Developer to AI Engineer: What Actually Changes&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kristinz" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3973719%2F2ff45f78-40d5-4877-bb58-746f577abeee.png" alt="kristinz profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kristinz" class="crayons-story__secondary fw-medium m:hidden"&gt;
              KristinZ
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                KristinZ
                
                
              
              &lt;div id="story-author-preview-content-3846873" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kristinz" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3973719%2F2ff45f78-40d5-4877-bb58-746f577abeee.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;KristinZ&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" id="article-link-3846873"&gt;
          From React Developer to AI Engineer: What Actually Changes
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/career"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;career&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;4&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Solstice cipher: Turing complete</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:49:21 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/solstice-cipher-turing-complete-2k1b</link>
      <guid>https://dev.to/ifeanyichima/solstice-cipher-turing-complete-2k1b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Solstice Cipher is a browser-based game built around the tension between light and darkness — the core theme of the June Solstice.&lt;/p&gt;

&lt;p&gt;The screen is split into two halves: pitch black on the left, golden yellow on the right. A Caesar cipher is presented to the player in a floating modal. Decode it correctly and the light grows. Guess wrong and the darkness creeps forward.&lt;/p&gt;

&lt;p&gt;The game ends when either side fully consumes the screen. Darkness wins by default if you keep getting answers wrong.&lt;/p&gt;

&lt;p&gt;The cipher words are selected randomly from a pool of over 6,500 common English words and encrypted using Caesar cipher with a shift of 3. Google's Gemma 4 model handles the encryption of each word on the server, with a verified fallback to ensure the player always gets a mathematically correct puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/tFLnM4Jbg5I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MasterIfeanyi" rel="noopener noreferrer"&gt;
        MasterIfeanyi
      &lt;/a&gt; / &lt;a href="https://github.com/MasterIfeanyi/solstice-game" rel="noopener noreferrer"&gt;
        solstice-game
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;This is a &lt;a href="https://nextjs.org" rel="nofollow noopener noreferrer"&gt;Next.js&lt;/a&gt; project bootstrapped with &lt;a href="https://github.com/vercel/next.js/tree/canary/packages/create-next-app" rel="noopener noreferrer"&gt;&lt;code&gt;create-next-app&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting Started&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;First, run the development server:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
yarn dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
pnpm dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
bun dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="nofollow noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; with your browser to see the result.&lt;/p&gt;
&lt;p&gt;You can start editing the page by modifying &lt;code&gt;app/page.js&lt;/code&gt;. The page auto-updates as you edit the file.&lt;/p&gt;
&lt;p&gt;This project uses &lt;a href="https://nextjs.org/docs/app/building-your-application/optimizing/fonts" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;next/font&lt;/code&gt;&lt;/a&gt; to automatically optimize and load &lt;a href="https://vercel.com/font" rel="nofollow noopener noreferrer"&gt;Geist&lt;/a&gt;, a new font family for Vercel.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Learn More&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;To learn more about Next.js, take a look at the following resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs" rel="nofollow noopener noreferrer"&gt;Next.js Documentation&lt;/a&gt; - learn about Next.js features and API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/learn" rel="nofollow noopener noreferrer"&gt;Learn Next.js&lt;/a&gt; - an interactive Next.js tutorial.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check out &lt;a href="https://github.com/vercel/next.js" rel="noopener noreferrer"&gt;the Next.js GitHub repository&lt;/a&gt; - your feedback and contributions are welcome!&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Deploy on Vercel&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The easiest way to deploy your Next.js app is to use the &lt;a href="https://vercel.com/new?utm_medium=default-template&amp;amp;filter=next.js&amp;amp;utm_source=create-next-app&amp;amp;utm_campaign=create-next-app-readme" rel="nofollow noopener noreferrer"&gt;Vercel Platform&lt;/a&gt; from the creators of Next.js.&lt;/p&gt;
&lt;p&gt;Check out our &lt;a href="https://nextjs.org/docs/app/building-your-application/deploying" rel="nofollow noopener noreferrer"&gt;Next.js deployment documentation&lt;/a&gt; for more…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MasterIfeanyi/solstice-game" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The stack is Next.js 16 with JavaScript, plain CSS modules, and Tailwind v4.&lt;/p&gt;

&lt;p&gt;The architecture is built around a single GameContext using React's Context API, which broadcasts game state to every component without prop drilling. localStorage persists the light and dark widths across page refreshes so your progress survives an accidental reload.&lt;/p&gt;

&lt;p&gt;The word selection pipeline works like this: a random word is picked on the server from a curated list of 6,500+ words sourced from Google's open source 10,000 most common English words list. That word is sent to Gemma 4 via OpenRouter to encrypt using Caesar cipher shift 3. Our server then verifies Gemma's encryption independently and corrects it silently if there is a mismatch, so the player always gets a valid puzzle.&lt;/p&gt;

&lt;p&gt;For true randomness, a unique seed and offset generated with Math.floor(Math.random() * 1000000) is passed with every request. A rate limit fallback handles OpenRouter 429 responses gracefully by falling back to local encryption, and a network failure fallback handles complete internet loss.&lt;/p&gt;

&lt;p&gt;Key architecture decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DRY principle applied throughout with reusable Button, Modal, Header, and Navbar components.&lt;/li&gt;
&lt;li&gt;A storage.js utility abstracts all localStorage operations with SSR safety guards.&lt;/li&gt;
&lt;li&gt;Caesar cipher logic lives in one encryptCaesar function used everywhere on the server.&lt;/li&gt;
&lt;li&gt;useRef tracks recently used words to prevent Gemma from repeating itself without triggering re-renders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Ode to Alan Turing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a computer science grad myself, I look up to Turing. His work in cryptography inspired this mini-game. I am in awe of his ability to turn a problem of decrypting German messages into a mathematical problem and then how he built a physical machine that took his instructions and was able to decrypt German signals.&lt;/p&gt;

&lt;p&gt;That same spirit lives in Solstice Cipher. The Caesar cipher is one of the oldest encryption algorithms in history, and this game puts the player in the position Turing's team faced at Bletchley Park: staring at encrypted text and racing to decode it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Google AI Usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google's Gemma 4 model powers the encryption engine of every round via OpenRouter. Each word selected for the player is sent to Gemma 4 with a structured prompt instructing it to apply Caesar cipher shift 3 and return a verified JSON response. The server independently verifies Gemma's output and corrects any mismatch, ensuring the AI is genuinely doing cryptographic work on every single round rather than acting as a simple random number generator.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Copilot with an Eagle eye</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 25 May 2026 19:31:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/copilot-with-an-eagle-eye-1j0d</link>
      <guid>https://dev.to/ifeanyichima/copilot-with-an-eagle-eye-1j0d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;In Nigeria today, insecurity and corruption is a very big issue with no hope in sight of solving it as they both go in tandem. I have been in multiple situations where I wished I could contact emergency services as soon as possible but to no avail. Also, to be honest when I started this project in 2025, the goal was to provide a very simple way to combat corruption by turning even the layman into a watchful vigilante. So, the goal of this project was to prototype a simple system called "Eagle Eye" that will allow you to make reports to the appropriate authorities about corruption and any insecurity in an area. &lt;/p&gt;

&lt;p&gt;I really hope to make this platform or website actually grow into a nationwide tool complete with an admin dashboard and a response team. I took massive inspiration from the London underground railway system: "see something, say something".&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/VFb07sPWJzU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Live link: &lt;a href="https://ifeanyi-eagle-eye.netlify.app/home" rel="noopener noreferrer"&gt;https://ifeanyi-eagle-eye.netlify.app/home&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/MasterIfeanyi/Eagle-Eye-App" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/Eagle-Eye-App&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;The project was previously using Firebase before and I racked up a bill of $180 which hurt massively, so I decided to re-write it using Nextjs and MongoDB, and my assistant of choice was GitHub Co-pilot. I chose Nextjs and MongoDB because Nextjs to me feels like a full-stack option with a built-in /api folder and MongoDB Atlas is a free database service that I could use to store reports and perform authentication of users. Along the way I decided to add a page where you could get phone number of emergency services. If not for this hack-a-thon, I may have never come back to this project because I have been putting it off for some time as I am was busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Building Eagle Eye was not a smooth process. There were moments where the project felt completely stuck, and honestly, GitHub Copilot was a massive part of that process, as I primarily write Nextjs, but Co-pilot guided me and took away my fears. GitHub Copilot helped me think through the architecture from the ground up, guiding decisions like how to structure the Next.js app directory, how to handle authentication with JWT and cookies, and how to connect the frontend to MongoDB. When I hit bugs, like a broken scroll on the hotlines page, a sticky header that refused to stick, or a location detector pulling the wrong city, GitHub Copilot did not just hand me answers. It explained why things were breaking so I actually understood the fix. What I appreciate most is that GitHub Copilot treated me like a developer who was learning, not one who needed things done for them. This project genuinely would not exist in its current state without that support. Although, I was limited to the Claude Haiku 4.5 model (free plan), I feel like it was sufficient enought to help me.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>How to use Google Gemma 4 model</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 14 May 2026 08:58:30 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/how-to-use-google-gemma-4-model-2o3l</link>
      <guid>https://dev.to/ifeanyichima/how-to-use-google-gemma-4-model-2o3l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Write About Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I Built a Celebrity AI Chat App with Google Gemma 4 26B-A4B, Here's Everything You Need to Know&lt;/p&gt;

&lt;p&gt;I've always been fascinated by the idea of talking to your Elon Musk, but for now we would have to settle down for a &lt;strong&gt;chatbot&lt;/strong&gt; that actually &lt;em&gt;feels&lt;/em&gt; like them. So when Google dropped Gemma 4 in April 2026, I saw my chance. I built a web app with a chat UI where you pick a celebrity, and Gemma 4 slips into their persona and holds a full conversation with you. The results were genuinely impressive and sometimes hilarious.&lt;/p&gt;

&lt;p&gt;But before I walk you through how I built it, let me give you a proper introduction to the models that made it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understanding the Google Gemma 4 Model Family
&lt;/h2&gt;

&lt;p&gt;Gemma 4 is Google DeepMind's latest generation of open models, released on April 2, 2026. It comes in &lt;strong&gt;four distinct sizes&lt;/strong&gt;, each targeting a different use case, from running on your phone to powering a server-grade workstation. All four models are multimodal (they can process text and images), licensed under Apache 2.0, and support over 140 languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Comparison Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Audio&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;Efficient Dense&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Mobile / Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;Efficient Dense&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;On-device assistants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B A4B&lt;/td&gt;
&lt;td&gt;Mixture of Experts&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;High-throughput APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;Dense&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Maximum quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. How to Get an API Key
&lt;/h2&gt;

&lt;p&gt;Before you can start building anything with Gemma 4, you need API access. The good news? You have a couple of options and one of them is completely free to start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Google AI Studio (Direct from Google)
&lt;/h3&gt;

&lt;p&gt;Google provides direct API access to Gemma 4 through &lt;strong&gt;Google AI Studio&lt;/strong&gt;. Here's how to get your key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://aistudio.google.com" rel="noopener noreferrer"&gt;aistudio.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign in with your Google account&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Get API key"&lt;/strong&gt; in the left sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create API key"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy your key and store it somewhere safe (you won't be able to see it again)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmip5p7ed8ne4dzfj82v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmip5p7ed8ne4dzfj82v1.png" alt="Google AI Studio" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it. Google AI Studio gives you a generous free tier to experiment with before you need to think about billing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never hardcode your API key in your frontend code. Always store it in an environment variable (&lt;code&gt;.env&lt;/code&gt; file) and access it from your backend.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; before your first commit, not after.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Option B: OpenRouter
&lt;/h3&gt;

&lt;p&gt;OpenRouter is a unified API gateway that gives you access to hundreds of models including all Gemma 4 variants through a single, OpenAI-compatible endpoint. That means you can switch models with one line of code change, get automatic fallbacks if a provider goes down, and even access the &lt;strong&gt;free tier&lt;/strong&gt; of Gemma 4 31B without spending a cent to start.&lt;/p&gt;

&lt;p&gt;To get your OpenRouter API key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;openrouter.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Sign In"&lt;/strong&gt; and create an account&lt;/li&gt;
&lt;li&gt;Once logged in, click &lt;strong&gt;"Get API Key"&lt;/strong&gt; button from your dashboard&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"New Key"&lt;/strong&gt;, give it a name (e.g. &lt;code&gt;celeb-chat-dev&lt;/code&gt;), and hit create&lt;/li&gt;
&lt;li&gt;Copy and save your key it starts with &lt;code&gt;sk-or-...&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can optionally add credits to your account for paid usage, but to start experimenting, the free-tier models (including &lt;code&gt;google/gemma-4-26b-a4b-it:free&lt;/code&gt;) are more than enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How to Use Google Gemma 4 Through OpenRouter
&lt;/h2&gt;

&lt;p&gt;Alright, this is where the fun begins. Let me walk you through exactly how I wired up Gemma 4 via OpenRouter to power my celebrity chat app, from the first API call all the way to a working chat UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Base URL and Model IDs
&lt;/h3&gt;

&lt;p&gt;OpenRouter exposes Gemma 4 through a single endpoint that's fully compatible with the OpenAI Chat API format. If you've ever called GPT-4 or any other model via OpenAI's SDK, this will feel immediately familiar, you're literally changing one URL and one model string.&lt;/p&gt;

&lt;p&gt;The base URL is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://openrouter.ai/api/v1/chat/completions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Gemma 4 model IDs on OpenRouter are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Model ID&lt;/th&gt;
&lt;th&gt;Free Tier?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-e2b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-e4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B A4B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-26b-a4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;:free&lt;/code&gt; suffix)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-31b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;:free&lt;/code&gt; suffix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To use the free tier of the 26B model for example, use &lt;code&gt;google/gemma-4-26b-a4b-it:free&lt;/code&gt; as the model string.&lt;/p&gt;




&lt;h3&gt;
  
  
  Switching Models Without Rewriting Your Code
&lt;/h3&gt;

&lt;p&gt;This is the quiet superpower of going through OpenRouter. Because the API schema is identical across all models, switching from the free 31B to the 26B MoE (which is faster and cheaper in production) is literally one line:&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;// During development&lt;/span&gt;
&lt;span class="nx"&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;google/gemma-4-31b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// In production (faster, cheaper MoE)&lt;/span&gt;
&lt;span class="nx"&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;google/gemma-4-26b-a4b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No refactoring. No new SDK. Just change the string. This is why I recommend OpenRouter for any serious project, you can prototype on free-tier models and graduate to the right production model without touching your integration code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Your First API Call
&lt;/h3&gt;

&lt;p&gt;Let's start simple with a raw &lt;code&gt;fetch&lt;/code&gt; call in JavaScript, no SDKs needed:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://openrouter.ai/api/v1/chat/completions&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="na"&gt;method&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&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;google/gemma-4-26b-a4b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&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="na"&gt;role&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What's the capital of Nigeria?&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Three things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your API key goes in the &lt;code&gt;Authorization&lt;/code&gt; header as a Bearer token&lt;/li&gt;
&lt;li&gt;The response shape mirrors OpenAI, your answer lives at &lt;code&gt;data.choices[0].message.content&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You're billed (or rate-limited on free tier) per token, so &lt;code&gt;max_tokens&lt;/code&gt; is worth setting explicitly&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I was getting this error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google/gemma-4-26b-a4b-it:free is temporarily rate-limited upstream.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free tier of Gemma 4 on OpenRouter is being hammered by too many users right now and Google AI Studio is throttling it. When you use the free tier on OpenRouter &lt;code&gt;(google/gemma-4-26b-a4b-it:free)&lt;/code&gt;, you are sharing a pool of rate limits with every other developer in the world using that same free model. So when too many people hit it at once, Google throttles it and you get that &lt;code&gt;429&lt;/code&gt; error. But you already have a Google AI Studio API key sitting in your &lt;code&gt;.env.local&lt;/code&gt; doing nothing right now. OpenRouter lets you connect your own Google AI Studio key so that your requests go through your personal quota instead of the shared free pool.&lt;/p&gt;

&lt;p&gt;You have two options:&lt;/p&gt;

&lt;p&gt;Option 1 — Add your Google AI Studio key to OpenRouter&lt;/p&gt;

&lt;p&gt;Option 2 — Add retry logic to your API route&lt;/p&gt;

&lt;p&gt;I went with Option 1 which is adding my Google AI Studio key to OpenRouter integrations&lt;/p&gt;

&lt;p&gt;Go to: &lt;code&gt;openrouter.ai/settings/integrations&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find Google AI Studio&lt;/li&gt;
&lt;li&gt;Paste your Google AI Studio API key there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you your own dedicated rate limit instead of sharing the free pool with everyone&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm8ghwn94mnwxihpsmxkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm8ghwn94mnwxihpsmxkw.png" alt="OpenRouter integration" width="799" height="326"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The System Prompt; The Secret Weapon for My Project
&lt;/h3&gt;

&lt;p&gt;Here's where Gemma 4 genuinely impressed me. Gemma 4 officially supports the &lt;code&gt;system&lt;/code&gt; role natively; a big deal for an open model, since earlier generations required hacky workarounds. However, in my actual project I used a slightly different pattern that I found worked even better for persona-heavy apps: the &lt;strong&gt;user/assistant injection pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of passing the persona as a &lt;code&gt;system&lt;/code&gt; message, I injected it as the first &lt;code&gt;user&lt;/code&gt; message and followed it with a primed &lt;code&gt;assistant&lt;/code&gt; response where the model explicitly "agrees" to become the character before the real conversation even starts:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// persona instructions injected here&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// the model "confirms" the persona before the conversation begins&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Understood. I am &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;personName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. I will only speak about my own life, work, and experiences. Ask me anything about who I am.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// then the real conversation history follows&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;messages&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;The subtle advantage of this approach over a plain &lt;code&gt;system&lt;/code&gt; message: the model has explicitly &lt;em&gt;agreed&lt;/em&gt; to the role as part of the conversation context. That in-context confirmation produces noticeably stronger character lock-in across long conversations. The model is far less likely to slip out of persona when it has already "committed" to it in an early assistant turn.&lt;/p&gt;

&lt;p&gt;The results were honestly uncanny. The model held the persona across multiple turns, matched the celebrity's tone and vocabulary, and even pushed back on out-of-character questions in a way that felt natural. The 256K context window meant I could carry a long conversation without the model ever "forgetting" who it was supposed to be.&lt;/p&gt;




&lt;h3&gt;
  
  
  Enabling Reasoning Mode
&lt;/h3&gt;

&lt;p&gt;One of Gemma 4's standout features is its configurable thinking mode, you can ask the model to reason through a problem before giving its final answer. OpenRouter exposes this through a &lt;code&gt;reasoning&lt;/code&gt; parameter:&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&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;google/gemma-4-26b-a4b-it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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="c1"&gt;// turn on chain-of-thought&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When enabled, the response includes a &lt;code&gt;reasoning_details&lt;/code&gt; array showing the model's internal thought process before the final answer. I didn't use this for the celebrity chat (you don't want Morgan Freeman visibly deliberating), but it's incredibly useful for any app where you want to show &lt;em&gt;why&lt;/em&gt; the model said something for example; tutoring apps, code explainers, or decision-support tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One tip from the OpenRouter docs: if you're building an agentic tool or integrating with something like Claude Code or Cline, &lt;strong&gt;turn reasoning off&lt;/strong&gt;. It adds latency and those tools are optimized for fast back-and-forth, not deep deliberation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Thanks for Reading !
&lt;/h3&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>My Buddy and I</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Wed, 13 May 2026 16:33:04 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/my-buddy-and-i-5i5</link>
      <guid>https://dev.to/ifeanyichima/my-buddy-and-i-5i5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I chose this project because as a kid (since age: 17) I have always had this vivid dreams of speaking with Elon Musk, he is kind of like my Idol and I would cherish the opportunity to have a conversation with him, until that day comes, this is what we would manage. When I came across this hackathon to test the lates model built by Google (Gemma 4) it was a no-brainer for me. I want the opportunity to understand the memory and context management of this model. I decided not to limit it to Elon Musk and included as much celebrities as possible. I want to give my friends the opportunity to test and use it. &lt;/p&gt;

&lt;p&gt;Buddy is a web application that lets you have real, in-character conversations with iconic public figures such as athletes, billionaires, historical leaders, artists, and innovators. You pick a person, and the AI steps into their shoes completely. It speaks the way they speak, stays grounded in their real life and career, and refuses to break character.&lt;/p&gt;

&lt;p&gt;Although, with this project I am not solving a problem like climate change or malaria cure, The problem Buddy solves is simple. Most AI chatbots are general-purpose. You can ask them anything, and they will answer as a neutral assistant. That is useful, but it is not immersive. Buddy flips that entirely. The AI is not an assistant anymore, it is a person. Ask Lionel Messi about his childhood in Rosario. Ask Steve Jobs why he believed design was everything. You get answers that feel like a real conversation, not a Wikipedia summary read back to you.&lt;/p&gt;

&lt;p&gt;The experience Buddy creates is curiosity without friction. It is the feeling of sitting across from someone remarkable and just... asking them things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Link: &lt;a href="https://ifeanyi-celeb-chat.netlify.app" rel="noopener noreferrer"&gt;https://ifeanyi-celeb-chat.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/MasterIfeanyi/google-gemma4-hackathon" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/google-gemma4-hackathon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used Gemma 4
&lt;/h2&gt;

&lt;p&gt;Buddy runs on Gemma 4 26B, accessed via OpenRouter using the &lt;code&gt;google/gemma-4-26b-a4b-it:free model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The choice of Gemma 4 over the smaller variants came down to one core requirement; persona consistency. Keeping an AI locked inside a specific person's personality across a full multi-turn conversation is genuinely hard. The model needs to remember who it is, match the person's tone and vocabulary, deflect off-topic questions naturally without sounding robotic, and stay grounded in real publicly known facts all at the same time. The smaller E2B and E4B variants are impressive for their size, but they tend to drift out of character more easily under pressure, especially when a user deliberately tries to break the persona with trick questions or unrelated topics.&lt;/p&gt;

&lt;p&gt;The 26B model holds the character much more firmly. When you ask Messi a question in Spanish, it does not just translate, it responds with the rhythm and warmth of how Messi actually speaks. When you push Steve Jobs on a topic he would find irrelevant, it redirects with the kind of sharp confidence Jobs was known for. That level of nuance needs a bigger model.&lt;/p&gt;

&lt;p&gt;One specific technical challenge worth mentioning is Gemma 4 does not support a native system role in its message format. To get around this, Buddy injects the personality prompt as a user / assistant turn pair at the very start of every request, effectively tricking the model into treating the persona as established context before the real conversation begins. The 26B model responds to this pattern reliably and stays locked in. Smaller models were noticeably less consistent with the same approach.&lt;/p&gt;

&lt;p&gt;What &lt;strong&gt;Gemma 4&lt;/strong&gt; ultimately unlocked for Buddy was believability. Because the model is large enough to hold complex persona context across many turns, we never had to simplify the experience to make it work. We did not have to limit conversation length, restrict the types of questions users could ask, or hardcode fallback responses. The model handled edge cases we never explicitly planned for. When a user asked Messi to speak Spanish, &lt;strong&gt;Gemma 4&lt;/strong&gt; switched languages naturally and stayed in character at the same time, responding with the warmth and rhythm of how Messi actually communicates, without any instruction from us to do so. When users tried to break the persona with off-topic questions, the model deflected in the voice of the person, not with a generic AI refusal. That level of emergent, in-character behaviour is what a smaller model cannot reliably give you and it is what makes Buddy feel like a conversation rather than a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you.
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
