<?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: Lumora</title>
    <description>The latest articles on DEV Community by Lumora (@lumorakids).</description>
    <link>https://dev.to/lumorakids</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%2F4067707%2Fae8e2b61-d861-489e-a9e2-58324f331f7e.png</url>
      <title>DEV Community: Lumora</title>
      <link>https://dev.to/lumorakids</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lumorakids"/>
    <language>en</language>
    <item>
      <title>The value was in nine places and one enum, and the enum was the only one that ran</title>
      <dc:creator>Lumora</dc:creator>
      <pubDate>Sat, 15 Aug 2026 05:09:42 +0000</pubDate>
      <link>https://dev.to/lumorakids/the-value-was-in-nine-places-and-one-enum-and-the-enum-was-the-only-one-that-ran-5ck9</link>
      <guid>https://dev.to/lumorakids/the-value-was-in-nine-places-and-one-enum-and-the-enum-was-the-only-one-that-ran-5ck9</guid>
      <description>&lt;p&gt;You add a value to a feature. It goes into the type union, into the metadata map the create form renders from, into the access table, into the shelf that decides what appears on the landing pages, into the sitemap. You write sample content for it. You run the suite. Green. Then someone opens the site, picks the new option, and the request comes back &lt;code&gt;400 Invalid input&lt;/code&gt; — every time, for everybody, all day.&lt;/p&gt;

&lt;p&gt;The value was &lt;code&gt;birthday&lt;/code&gt;, a seventh theme for a picture-book generator. The thing that rejected it was one hand-written schema inside the route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/generate&lt;/span&gt;
&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedtime&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;adventure&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;fantasy&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;friendship&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;wisdom&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;learning&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;Six literals, typed out by hand months earlier. The selector offered a seventh. The gate had never heard of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a full day of work never touched it
&lt;/h2&gt;

&lt;p&gt;Every book generated that day came out of a script that called &lt;code&gt;insertStory&lt;/code&gt; directly and pushed the job onto the queue. Six books, six good renders, six sets of illustrations we sat and reviewed. None of them went through &lt;code&gt;POST /api/generate&lt;/code&gt;, because the script did not need to. The unit suite did not go through it either — by design it runs with no database and no network, so it never loads that route.&lt;/p&gt;

&lt;p&gt;So the pipeline was fine. Generation was fine. The prompts, the wardrobe rules, the visual notes, all fine. The one edge nobody exercised was the door the product actually opens.&lt;/p&gt;

&lt;p&gt;The gap has a name worth keeping: &lt;em&gt;the pipeline works&lt;/em&gt; and &lt;em&gt;the product works&lt;/em&gt; are two different claims, and testing the back half proves only the first. It surfaced from the site, not from the suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grep found five more copies of the same six strings
&lt;/h2&gt;

&lt;p&gt;The literal list had been copy-pasted around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api/internal/stories/seed&lt;/code&gt;: same six, same omission&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin/stories&lt;/code&gt;: the back-office filter could not find a birthday story at all&lt;/li&gt;
&lt;li&gt;a dev-only loading preview: harmless, drifting anyway&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/seo/landingCatalog&lt;/code&gt;: the comment above it read "themes that ship today", and it listed six. Every generated landing page had been silently skipping the new theme&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tests/landingCombos.test.ts&lt;/code&gt;: the worst one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last file asserts that every landing combination uses a valid theme, and it carries its own definition of "valid": the stale six. So while the theme was broken, the test was green. Fix the omission and add birthday combos, and the test goes red. A test that passes because reality has not reached it yet, and fails the moment it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern was already in the same file, two lines down
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;six&lt;/span&gt; &lt;span class="nx"&gt;literals&lt;/span&gt;&lt;span class="p"&gt;...]),&lt;/span&gt;
&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORY_INPUT_LANGUAGES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// derived, so the API gate cannot drift from the create form&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Somebody had already hit this, written the fix, written the comment explaining it, and applied it to exactly one field. The repair was to derive &lt;code&gt;STORY_THEMES&lt;/code&gt; from the metadata map the create form already renders, import it everywhere, and add a test that scans &lt;code&gt;src/&lt;/code&gt; and fails any file quoting three or more theme literals in a row.&lt;/p&gt;

&lt;p&gt;Curated subsets are the awkward part. The age-band recommendations and the random default pool on the create page genuinely should not grow every time a theme ships. Those now carry a &lt;code&gt;THEME-SUBSET:&lt;/code&gt; marker saying why they are a subset — because without it, &lt;em&gt;deliberate&lt;/em&gt; and &lt;em&gt;forgot&lt;/em&gt; are byte-for-byte identical in a diff, which is the whole reason this shipped broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that would have caught it takes three requests
&lt;/h2&gt;

&lt;p&gt;Not one. Three, sent through the front door with a control group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;theme=birthday  -&amp;gt; 403 premium_theme    the thing being fixed
theme=wisdom    -&amp;gt; 403 premium_theme    positive control: a theme known to work
theme=nonsense  -&amp;gt; 400 Invalid input    negative control: the gate still rejects junk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read alone, the first line cannot tell you whether the theme got fixed or the validation got deleted. The second says the response is the normal one for a working theme. The third says the schema is still doing its job. Any new user-facing value gets these three before it counts as shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the theme was for
&lt;/h2&gt;

&lt;p&gt;There is a moment about an hour into a child's birthday party. The wrapping paper is on the floor, the guests have moved outside, and two or three presents are still on the table that nobody has gone back to. At the parties we have been to, almost everything left there had the child's name printed on it: the mug, the T-shirt, the wooden puzzle cut into the letters. The child read the name once, confirmed it was theirs, and left. A name is a check, and checks finish.&lt;/p&gt;

&lt;p&gt;That is the hour the theme was written for, and we should be exact about what it is: something we have watched, not something we counted. No sample, no control group, none of the discipline of the section above. It stays an observation, and it costs nothing to run at the next party you go to: wait an hour, then read what is still on the table.&lt;/p&gt;

&lt;p&gt;Disclosure, because it decides what counts as a bug here: we build Lumora, which takes one photo of a child and writes and illustrates a story with that child inside it rather than printed on the cover. What we are aiming at is &lt;a href="https://lumora.kids/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=r7" rel="noopener noreferrer"&gt;a birthday gift a child is still reading after the guests have gone home&lt;/a&gt;. The theme built for that hour was, for one day, the only one the API refused to accept — and the six sample books we spent the day reviewing all rendered beautifully, which is exactly why nobody noticed.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>«es» no es un mercado: el bug de i18n que nos costó reescribir una campaña entera</title>
      <dc:creator>Lumora</dc:creator>
      <pubDate>Fri, 07 Aug 2026 15:18:24 +0000</pubDate>
      <link>https://dev.to/lumorakids/-no-es-un-mercado-el-bug-de-i18n-que-nos-costo-reescribir-una-campana-entera-3oep</link>
      <guid>https://dev.to/lumorakids/-no-es-un-mercado-el-bug-de-i18n-que-nos-costo-reescribir-una-campana-entera-3oep</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Nota: en Lumora construimos libros infantiles personalizados con IA, y escribimos en diez idiomas.&lt;br&gt;
Este artículo cuenta un problema de i18n con el que chocamos de frente y cómo lo resolvimos. Lo contamos&lt;br&gt;
desde dentro, con nuestro nombre, porque el error nos costó reescribir una campaña entera.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Casi todos los equipos tratamos el idioma como si fuera el país. Ponemos &lt;code&gt;es&lt;/code&gt; en el selector, sacamos las cadenas a un JSON y damos el problema por cerrado. Funciona hasta el día en que tu producto tiene algo que ver con una fecha.&lt;/p&gt;

&lt;p&gt;Y entonces descubres que &lt;strong&gt;&lt;code&gt;es&lt;/code&gt; no es un mercado, son varios calendarios distintos que comparten vocabulario.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  El día en que el regalo no llega el 25
&lt;/h2&gt;

&lt;p&gt;Teníamos una campaña de regalos escrita en español. Correcta gramaticalmente, revisada, sin errores de traducción. Y aun así estaba mal para una parte grande de quien la leía:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En &lt;strong&gt;España&lt;/strong&gt;, el gran momento de regalo infantil no es el 25 de diciembre: son los &lt;strong&gt;Reyes Magos, el 6 de enero&lt;/strong&gt;. Un mensaje que dice "pídelo antes del 24" le está dando a una familia española una fecha límite equivocada por casi dos semanas.&lt;/li&gt;
&lt;li&gt;En &lt;strong&gt;México&lt;/strong&gt; conviven las dos cosas: Navidad y Reyes, con la rosca del 6 de enero.&lt;/li&gt;
&lt;li&gt;En &lt;strong&gt;Argentina, Chile o Uruguay&lt;/strong&gt;, la Navidad es el 25 de diciembre… &lt;strong&gt;en pleno verano&lt;/strong&gt;. A 30 grados. Con las vacaciones escolares largas empezando, no terminando.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fíjate en lo incómodo del asunto: los tres casos hablan español. Los tres pasan el mismo test de traducción. Y los tres necesitan un mensaje distinto, una imagen distinta y una fecha límite distinta.&lt;/p&gt;

&lt;p&gt;El mismo golpe existe en portugués. En &lt;strong&gt;Brasil&lt;/strong&gt; la Navidad también cae en pleno verano, enero es mes de vacaciones, y el día en que de verdad se reinicia la rutina familiar no es el 1 de enero: es la &lt;strong&gt;volta às aulas&lt;/strong&gt;, en febrero. Si tu calendario de contenidos asume "año nuevo, hábitos nuevos" en enero, en Brasil llegas un mes antes de que a nadie le importe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué el código de locale no te salva
&lt;/h2&gt;

&lt;p&gt;La respuesta obvia es "usa &lt;code&gt;es-ES&lt;/code&gt; y &lt;code&gt;es-AR&lt;/code&gt;". Es correcta y casi nunca es suficiente, por dos razones prácticas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. La cadena se colapsa.&lt;/strong&gt; En cuanto el locale viaja por un &lt;code&gt;Accept-Language&lt;/code&gt;, un &lt;code&gt;navigator.language&lt;/code&gt;, una URL &lt;code&gt;/es&lt;/code&gt; o un CDN, casi todo el mundo acaba normalizando a los dos primeros caracteres. Nuestra propia web sirve &lt;code&gt;/es&lt;/code&gt; y &lt;code&gt;/pt&lt;/code&gt;. El subtag de región existe en teoría y ha desaparecido en la práctica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Estás mezclando dos ejes que no son el mismo.&lt;/strong&gt; El idioma responde a &lt;em&gt;en qué palabras te hablo&lt;/em&gt;. El calendario y el hemisferio responden a &lt;em&gt;en qué momento vives&lt;/em&gt;. Un argentino que navega en español desde Madrid y una familia española son el mismo &lt;code&gt;es-ES&lt;/code&gt; por cabecera y dos realidades distintas. Y al revés: alguien puede querer el libro en español para un sobrino en Chile.&lt;/p&gt;

&lt;p&gt;El bug de fondo es que estábamos derivando una &lt;strong&gt;fecha cultural&lt;/strong&gt; a partir de una &lt;strong&gt;etiqueta de idioma&lt;/strong&gt;. Son dos ejes independientes y los teníamos acoplados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que cambiamos
&lt;/h2&gt;

&lt;p&gt;Separamos las dos cosas. El idioma decide las palabras. Un concepto aparte, que llamamos "momento de regalo", decide la fecha, la estación y las imágenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Hemisphere&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;north&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GiftMoment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navidad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// la fecha que de verdad importa&lt;/span&gt;
  &lt;span class="nl"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// el deadline que mostramos&lt;/span&gt;
  &lt;span class="nl"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hemisphere&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// El idioma NO entra en esta tabla. Solo el territorio.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GIFT_MOMENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GiftMoment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;01-06&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-28&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;north&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;MX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;north&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navidad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;CL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navidad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;BR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navidad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;giftDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12-12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hemisphere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&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="c1"&gt;// Dos entradas, no una. Y el idioma nunca se usa para inferir el territorio.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveCampaign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;moment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GIFT_MOMENTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;GIFT_MOMENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ES&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="na"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`campaign.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="na"&gt;season&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hemisphere&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&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;summer&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;winter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderBy&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;Tres consecuencias que no habíamos anticipado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La nieve es un bug de contenido.&lt;/strong&gt; Nuestras ilustraciones navideñas por defecto llevaban nieve y bufandas. Para diciembre en el hemisferio sur eso no es "genérico", es sencillamente otra estación. La estación pasó a ser un parámetro del prompt, no una constante.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El deadline es parte del producto.&lt;/strong&gt; Si lo que entregas se imprime, la fecha límite real depende de la fecha de regalo local. Un único "pide antes del X" global es incorrecto para la mitad de tu audiencia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando no sabes el país, no adivines por idioma.&lt;/strong&gt; Preferimos preguntar, o mostrar un texto neutro respecto a la fecha, antes que asumir que &lt;code&gt;es&lt;/code&gt; significa España.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  La parte que más nos sorprendió: traducir arrastra la cultura de origen
&lt;/h2&gt;

&lt;p&gt;Relacionado con esto, y quizá lo más útil que hemos aprendido: &lt;strong&gt;escribimos cada idioma de forma nativa en lugar de traducir desde el inglés.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No fue una decisión estética. Un cuento escrito en inglés y traducido al español llega con las asunciones del original metidas dentro de la estructura: la cena de Nochebuena en el sitio equivocado, el invierno como telón de fondo, los nombres, el ritmo de las frases, incluso qué se considera una travesura. La traducción arregla las palabras y deja intacto el marco cultural. Es exactamente el mismo error que el de las fechas, una capa más arriba.&lt;/p&gt;

&lt;p&gt;Si estás construyendo algo multiidioma, esta es la pregunta que nos habría ahorrado más trabajo: &lt;strong&gt;¿esta cadena depende del idioma, o depende del lugar?&lt;/strong&gt; Si depende del lugar, sácala del sistema de traducción hoy. En cuanto vive en el &lt;code&gt;i18n.json&lt;/code&gt; ya está acoplada al idioma y desacoplarla después es mucho más caro.&lt;/p&gt;




&lt;p&gt;Por transparencia: esto salió de trabajar en Lumora, donde hacemos &lt;a href="https://lumora.kids/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=es" rel="noopener noreferrer"&gt;libros ilustrados personalizados a partir de una foto&lt;/a&gt;. Subes una foto de tu peque, la IA escribe e ilustra el cuento, y el niño o la niña es el protagonista con el mismo aspecto en todas las páginas. Escribimos en diez idiomas de forma nativa, no traducida, y el primer libro es gratis y sin tarjeta.&lt;/p&gt;

&lt;p&gt;Si has peleado con esto en otro idioma, nos interesa mucho saber cómo lo resolviste. El caso del árabe nos dio otro problema completamente distinto —de capa de texto en el PDF— y esa es otra historia.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>programming</category>
      <category>spanish</category>
    </item>
  </channel>
</rss>
