<?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: 365Talents</title>
    <description>The latest articles on DEV Community by 365Talents (@365talents).</description>
    <link>https://dev.to/365talents</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9723%2F14b30534-2257-434c-8b82-5bd71ebfaa22.png</url>
      <title>DEV Community: 365Talents</title>
      <link>https://dev.to/365talents</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/365talents"/>
    <language>en</language>
    <item>
      <title>Things I learned picking up a new language as an adult</title>
      <dc:creator>Florian Lackner</dc:creator>
      <pubDate>Tue, 21 Oct 2025 12:33:57 +0000</pubDate>
      <link>https://dev.to/365talents/things-i-learned-picking-up-a-new-language-as-an-adult-52f6</link>
      <guid>https://dev.to/365talents/things-i-learned-picking-up-a-new-language-as-an-adult-52f6</guid>
      <description>&lt;p&gt;Since I came to France six months ago, I have made a lot of progress with the French language. What started as nearly incomprehensible stuttering are now full phrases with rich vocabulary and even one little joke here and there. I've made picking up French my personal project, and it brings a lot of joy in my life. Understanding people in more and more contexts is enriching and helps with the struggles every expat experiences being far from home.&lt;/p&gt;

&lt;p&gt;I recently stumbled across &lt;a href="https://youtu.be/d0yGdNEWdn0" rel="noopener noreferrer"&gt;a talk by Chris Lonsdale&lt;/a&gt; from nearly ten years ago, where he talks about his experience learning languages. I highly recommend watching it if you haven't already. It resonated so well with me that it inspired me to write this blog post. He claims anybody can be fluent in a new language in six months. It sounds crazy, but having tried that myself, I believe he is right.&lt;/p&gt;

&lt;p&gt;In his presentation, Chris claims that there are five basic principles you can apply and seven simple actions you can perform to boost your language skills massively. I will repeat them here and add my own comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five basic principles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Focus on language content that is relevant to you.
&lt;/h3&gt;

&lt;p&gt;I agree with him here. I still have unpleasant memories of high school times were we were forced to study rather obscure books in French, which included weird vocabulary and uninteresting stories. It was hell. It is so much harder to remember irrelevant information than interesting information that it becomes quickly frustrating.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use your new language as a tool to communicate.
&lt;/h3&gt;

&lt;p&gt;I guess the key take-away here is to &lt;strong&gt;use&lt;/strong&gt; a language. If you want to convey some meaning, at least in Europe, you can always fall back to English. This is even possible in France, contrary to popular belief.&lt;/p&gt;

&lt;p&gt;But you shouldn't use this escape hatch. Using a language takes practice you'll never get if you're not training. All beginnings will be hard, but ultimately you will progress, and that's what counts. Every little piece of information you communicate is a major win and should be treated as such. Feeling insecure about your level in a language is only going to hold you back.&lt;/p&gt;

&lt;p&gt;I did a thought experiment one day to deal with this insecurity: I put myself into the other person's position and imagined a situation where I was talking to a person barely able to speak German (my native language). No matter how badly the person speaks, I would never consider their effort anything less than exactly that: a nice effort. In my opinion, it's remarkable if foreigners know your language, be it only so little. I think that the vast majority of people think of it that way. So why was I even embarrassed in the first place?&lt;/p&gt;

&lt;h4&gt;
  
  
  3. When you first understand the message, you will unconsciously acquire the language.
&lt;/h4&gt;

&lt;p&gt;NEVER LEARN GRAMMAR. EVER.&lt;/p&gt;

&lt;p&gt;In serious terms, you should always focus on the message. The rest follows naturally. Learning grammar is the exact opposite. You focus on the language's syntax by trying to learn and apply logical rules. That's how a computer might try to approach learning a language, but for humans, this is a dead-end.&lt;/p&gt;

&lt;p&gt;In fact, have you ever noticed how native speakers of any language do not know grammar rules while applying them correctly 100% of the time? The most extreme example is children. They have never heard of grammar but speak as error-free as their parents. When asking them why they use this or that grammatical construction, they cannot give a better answer than: "Because that's how it is."&lt;/p&gt;

&lt;p&gt;I believe that our brain is naturally "optimized" to find and internalize these rules subconsciously. This way, we can apply them with lightning speed and astonishing correctness without even thinking about them.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Language learning is physiological training.
&lt;/h3&gt;

&lt;p&gt;This might be new to many. Chris tells the story of a Taiwanese woman who excelled in English in school but utterly failed to understand English spoken by native speakers once confronted with it. The idea is that our ears, i.e., our brains, need to learn to hear foreign sounds.&lt;/p&gt;

&lt;p&gt;My favorite example of this is the four types of "tch" sounds in the &lt;a href="https://www.britannica.com/topic/Bosnian-Croatian-Montenegrin-Serbian-language" rel="noopener noreferrer"&gt;Bosnian-Croatian-Montenegrin-Serbian language&lt;/a&gt;. There are two pairs: č / ć and dž / đ. You can find more information (including hearing samples) &lt;a href="https://www.letslearncroatian.co.uk/blog/the-croatian-letters" rel="noopener noreferrer"&gt;here&lt;/a&gt;. It is nearly impossible to hear the differences for me, how about you?&lt;/p&gt;

&lt;p&gt;The same argument also applies to speaking. It takes muscles to do, and different languages use different movements of these muscles. We can train these movements like athletes do in sports. I witnessed a massive improvement in my ability to say certain French words after a couple of months. Initially, it felt like my tongue was somehow tied to a knot when attempting them. This feeling vanishes more and more every day. And if I think about it in retrospect, it really is similar to the clumsy feeling when picking up a new sport. Or when taking first dance lessons.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Psycho-physiological state matters.
&lt;/h3&gt;

&lt;p&gt;This is a no-brainer on the one hand, but on the other hand, it's easy to forget about. If your brain is busy with other things, you won't learn. Especially important is staying relaxed when you don't understand something. It's normal, and being angry about it is as pointless as it is hindering your learning efforts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven actions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Listen a lot.
&lt;/h3&gt;

&lt;p&gt;Obvious and still often overlooked. You can basically soak your mind in a language. I think it's not necessary to listen actively all the time. A radio in the background also helps. Your brain will get used to the language this way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Focus on getting the meaning first.
&lt;/h3&gt;

&lt;p&gt;I wholeheartedly agree. Sometimes, I catch myself obsessing over single words and their meaning. While thinking about it, I lose context and I stop understanding most of what is said.&lt;/p&gt;

&lt;p&gt;Another aspect is body language. It is actually different in most countries and conveys so much meaning. This is especially visible in Italy, the capital of expressive body language, in my opinion, but it applies everywhere.&lt;/p&gt;

&lt;p&gt;And one last thing: We can use things we already know. For example, French shares a lot of its vocabulary with English. I use this a lot to guess the meaning of new words in French, and it works way better than it should.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Start mixing.
&lt;/h3&gt;

&lt;p&gt;The call to action here is to be creative. Try new combinations of words, try to invent new expressions, etc. Babies, my great role models in language learning, talk like this.&lt;/p&gt;

&lt;p&gt;In my experience, the outcome conveys its meaning while also sounding funny to natives. So, you've basically cracked a joke in a foreign language; congratulations. In essence, whatever works is fine and should be used.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Focus on the core.
&lt;/h3&gt;

&lt;p&gt;Chris presents some statistics here. He claims that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1000 words cover 85% of what you say on a daily basis.&lt;/p&gt;

&lt;p&gt;3000 words cover 95% of what you say on a daily basis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If it is true, this is way less than I thought. Three thousand words does not sound impossible; quite the contrary. And if you can say 95% of what you want to say in your daily life one could argue that you speak the language.&lt;/p&gt;

&lt;p&gt;A sub-advice he gives is to focus on your "toolbox" of words. This means phrases like "What is this?" ("Qu'est-ce que c'est ?" in French, believe it or not), "What does this mean?", etc. This gives an excellent foundation and enables you to communicate and learn much quicker because you don't need to leave the language to ask these questions — Big recommendation from me as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Get yourself a language parent.
&lt;/h3&gt;

&lt;p&gt;Chris uses the word parent to hint that babies learn languages this way. One could also say a mentor. A mentor should be a person that is genuinely interested in you as a person and somebody who helps you to communicate. The goal is a safe environment where you can speak comfortably. He claims this concept is successful as it is used by billions of parents all over the world, and all of their kids are able to speak in the end — A convincing argument for me.&lt;/p&gt;

&lt;p&gt;I totally agree with this one. I had several mentors throughout my French adventure, and it is the biggest boost for language learning, in my opinion. One of my mentors was a good French friend with whom I started speaking only in French from the day I moved there.&lt;/p&gt;

&lt;p&gt;It was hard initially, but he did not get tired of helping me out when I was looking for words, and my proficiency in French increased noticeably every week. There are many ways to find these mentors: friends, online dating, sports clubs, work colleagues; you name it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Copy the face.
&lt;/h3&gt;

&lt;p&gt;This supposedly makes it easier to copy the native accent. I tried this once in high school to fake a British accent, and it really worked. I have yet to do more experiments, but it's an interesting idea I will pursue further.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Directly connect to mental images.
&lt;/h3&gt;

&lt;p&gt;Don't translate from one language into another in your head. Connect the idea, the image, the feeling directly to the word in the new language, like it is connected to the word in your native language.&lt;/p&gt;

&lt;p&gt;This is great advice and also one of the many mistakes made by schools over and over. Vocabulary lists are one negative example. They incentivize translating in your head, among other problems they have.&lt;/p&gt;

&lt;p&gt;In my experience, it is a habit to translate in the head. The same is true for actively doing the exact opposite. I'm trying to make it a habit to connect the words directly to the ideas they represent, and I'm seeing two benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The respective words started popping into my mind whenever I saw something or thought about an abstract idea.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I started to have a feeling about when which word is appropriate. Many words have very similar meanings, like walking and marching in English. Still, there is a difference; sometimes, you can't use one where you can use the other. I'm sure there is always an explanation / a rule for these cases, but that doesn't help when you're in the middle of saying a sentence. The feeling is instantaneous and almost always right, so go for it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Chris' talk was eye-opening for me. It put into words better than I ever could many vague feelings I had about language learning in these past couple of months. Of all the tips he gave, I like the one with the mentor best. It has helped me a lot with French while also being very nice from a social perspective. If you take one of the ideas presented here home, make it this one.&lt;/p&gt;

</description>
      <category>french</category>
    </item>
    <item>
      <title>Why do we need async/await in JavaScript?</title>
      <dc:creator>Florian Lackner</dc:creator>
      <pubDate>Tue, 21 Oct 2025 12:32:19 +0000</pubDate>
      <link>https://dev.to/365talents/why-do-we-need-asyncawait-in-javascript-37fe</link>
      <guid>https://dev.to/365talents/why-do-we-need-asyncawait-in-javascript-37fe</guid>
      <description>&lt;p&gt;&lt;em&gt;First posted &lt;a href="//www.lackner.work/posts/2025/03/01/why-do-we-need-async-await-in-js/"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Async/await is a major source of confusion in JavaScript. People tend just to add and remove the &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; keywords until the code works (or seems to work). In this article, I want to explain what async/await actually means by developing the concept from vanilla, &lt;em&gt;synchronous&lt;/em&gt; JavaScript to asynchronous JavaScript with the async/await syntax sugar. It is my personal take on the subject. I hope some will find it interesting.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I use Node.js as an example of a JavaScript runtime in this article. However, what's discussed also applies to other runtimes like web browser JS engines. All these environments follow similar architectures.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem does it solve?
&lt;/h2&gt;

&lt;p&gt;Suppose you have a function that does some I/O.&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;handleRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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="nf"&gt;doDatabaseQuery&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;fileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doElasticsearchQuery&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readMyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&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;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;Here we assume that the query functions and the &lt;code&gt;readMyFile&lt;/code&gt; function will block the JavaScript code while they perform their I/O. Eventually,  it will be done, and we can continue where we left off. They will signal an error by throwing. We handle it with a &lt;code&gt;try/catch&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;Doing database queries or other I/O like network requests or filesystem operations takes a very long time™ compared to running your JavaScript code. This is fine in this example if we look at a single request/response. But web servers usually handle thousands of requests. And then this model breaks. To understand why we have to look at how Node.js runs your JS code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js has a single-threaded event loop
&lt;/h2&gt;

&lt;p&gt;Node.js runs all your JS code in a single thread. &lt;a href="http://book.mixu.net/node/ch2.html" rel="noopener noreferrer"&gt;The exact reasoning&lt;/a&gt; behind this design choice is fascinating but out of scope for this article. In my opinion, the two most compelling arguments in favor of Node's architecture are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The other option would be to use concurrent threads. This comes with many complexity hazards like synchronization, deadlocks, etc. In this sense, Node's model is simpler.&lt;/li&gt;
&lt;li&gt;It turns out that I/O heavy applications like web servers tend to &lt;strong&gt;wait&lt;/strong&gt; a lot. And by that, I mean &lt;strong&gt;a lot&lt;/strong&gt;. They do nothing else, basically. The heavy lifting usually happens outside the web server, in a database, in elastic search, you name it. The web server just requests the data, waits for it, and glues it together to send it back to the client. So having several threads will not make your code more performant because its performance is not CPU-bound.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, while JS code runs sequentially, we can still work on many requests simultaneously. For example, you can start your washing machine, dishwasher, and microwave one after another and then wait for them to finish their respective tasks. You can do the same with requests to your database, elastic search, etc.&lt;/p&gt;

&lt;p&gt;The big question is: &lt;strong&gt;How can we wait without blocking?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Callbacks
&lt;/h2&gt;

&lt;p&gt;So code running in Node is not supposed to stop executing to wait for something external to be done. But we often need to do something &lt;strong&gt;after&lt;/strong&gt; say, a network request was completed. The way this is handled is straightforward: We give the runtime a function to call after it performed our I/O operation with its result. This function is called a callback function. We can see how the example would look like using callbacks.&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;handleRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;doDatabaseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// first callback&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;doElasticsearchQuery&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="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// second callback&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;readMyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// third callback&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;sendToClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// strawman function to send a response to the client&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;The I/O functions take a second argument this time, the callback. Note that we couldn't just return our result in this case. When &lt;code&gt;handleRequest&lt;/code&gt; runs it will call &lt;code&gt;doDatabaseQuery&lt;/code&gt; and then it will return &lt;strong&gt;immediately&lt;/strong&gt;. The Node.js runtime will later call the callback, which will call &lt;code&gt;doElasticsearchQuery&lt;/code&gt;. The callback also returns quickly. It will call only &lt;code&gt;readMyFile&lt;/code&gt; with another callback which will finally call &lt;code&gt;sendToClient&lt;/code&gt; to finish our request once the file was read. We returned &lt;code&gt;result&lt;/code&gt; in the original example. This is impossible with callbacks, so we're using this strawman function &lt;code&gt;sendToClient&lt;/code&gt; that sends the response back to the client.&lt;/p&gt;

&lt;p&gt;Error handling is explicit in this model. By convention, the first argument of the callback function is an error object. If it is not &lt;code&gt;undefined&lt;/code&gt;, something went wrong, and we can handle it in the callback. The second argument is, by convention, the result of the I/O operation. There are other styles, but they all boil down to the same idea: The error is communicated via some error argument to the callback.&lt;/p&gt;

&lt;p&gt;This is the way Node.js handles I/O. Many will rightfully argue that the code looks messier than initially. The code tends to get more and more unreadable if the nesting of the callbacks becomes too deep. And error handling makes things even worse. This problem is referred to as &lt;a href="https://medium.com/techfront/javascript-callback-hell-simply-explained-93c3cf4be884" rel="noopener noreferrer"&gt;callback hell&lt;/a&gt;. Several abstractions over callbacks have been developed to deal with this. The most widely used one is called &lt;code&gt;Promise&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promises
&lt;/h2&gt;

&lt;p&gt;The idea of an asynchronous function not returning its result contributes greatly to the unreadability of callback-spaghetti-code. Promises fix this by letting an asynchronous function return something again. But because asynchronous functions still can't block, it can't be the actual value. Instead, they return a &lt;code&gt;Promise&lt;/code&gt;, which can be interpreted as: "I promise that the value will eventually be here." The jargon is: "The promise resolves to the value". So this time, the I/O functions return promises that resolve to their respective values.&lt;/p&gt;

&lt;p&gt;Furthermore, promises simplify error handling by splitting the error handling logic from the business logic. Instead of getting the error in the callback, you can register a dedicated error callback, which gets called instead of the normal callback if there is an error.&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;handleRequest&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;dataPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doDatabaseQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;dataPromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;fileNamePromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataPromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// first callback&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileNamePromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doElasticsearchQuery&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fileNamePromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;fileNamePromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;resultPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileNamePromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// second callback&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readMyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&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;resultPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;resultPromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;resultPromise&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 cool thing about promises is that they are designed to be &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#chaining" rel="noopener noreferrer"&gt;chained&lt;/a&gt;. You can add callbacks to a promise chain with &lt;code&gt;then(callback)&lt;/code&gt;. The result is again a promise that resolves to the return value of the callback given to &lt;code&gt;then()&lt;/code&gt;. If the callback returns another &lt;code&gt;Promise&lt;/code&gt;, the promise returned by &lt;code&gt;then&lt;/code&gt; will not resolve to this other &lt;code&gt;Promise&lt;/code&gt;, but the value it resolved to. This enables chaining. And chaining avoids the deep nesting of pure callback approaches. Also, we can return something from &lt;code&gt;handleRequest&lt;/code&gt; again. This time it's a &lt;code&gt;Promise&lt;/code&gt; that resolves to &lt;code&gt;result&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Chaining also works for errors! You can add an error callback with &lt;code&gt;catch(callback)&lt;/code&gt;. If an operation fails the &lt;strong&gt;closest&lt;/strong&gt; error callback will be called. This helps a lot in our example because all our error handling code is the same.&lt;/p&gt;

&lt;p&gt;We can write our example more elegantly. Firstly, we can eliminate the repetitive error handling and add the error handler only once. Secondly, we can use our I/O functions and the error handler directly as callbacks. This works because, e.g., the first callback in the code snippet takes data and returns a &lt;code&gt;Promise&lt;/code&gt; resolving to &lt;code&gt;fileName&lt;/code&gt;. This is exactly what &lt;code&gt;doElasticsearchQuery&lt;/code&gt; is doing, which is why we can use it directly as an argument to &lt;code&gt;then&lt;/code&gt;. We can do similarly for &lt;code&gt;readMyFile&lt;/code&gt; and &lt;code&gt;handleError&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&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="nf"&gt;doDatabaseQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doElasticsearchQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readMyFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handleError&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;Promises have already helped tremendously in cutting down the complexity of callbacks. However, the resulting code is still a bit weird and not as clear as the synchronous equivalent. This is why &lt;code&gt;async/await&lt;/code&gt; (and &lt;code&gt;try/catch&lt;/code&gt; in &lt;code&gt;async&lt;/code&gt; contexts) has been added to JavaScript!&lt;/p&gt;

&lt;h2&gt;
  
  
  async/await
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;async/await&lt;/code&gt; is so-called syntax sugar for promises. This means it gets compiled to promises by the JavaScript compiler before executing the code. And remember, promises are callbacks under the hood. Nothing magical about these things. So here is the example written with &lt;code&gt;async/await&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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="nf"&gt;doDatabaseQuery&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;fileName&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;doElasticsearchQuery&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;readMyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;result&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;As you can see, it looks very similar to the initial synchronous example. But actually, it is &lt;strong&gt;identical&lt;/strong&gt; to the &lt;code&gt;Promise&lt;/code&gt; version. This means you could replace the &lt;code&gt;Promise&lt;/code&gt; version with this code and get the exact same function. Let's look at the differences to the initial synchronous example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;handleRequest&lt;/code&gt; became an &lt;code&gt;async function&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use of the &lt;code&gt;await&lt;/code&gt; keyword in front of the I/O functions. Even though they return promises, &lt;code&gt;await&lt;/code&gt; turns those into the values they resolve to.&lt;/li&gt;
&lt;li&gt;We return &lt;code&gt;result&lt;/code&gt; directly. However, &lt;code&gt;handleRequest&lt;/code&gt; will still return a &lt;code&gt;Promise&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;async&lt;/code&gt; will turn a &lt;code&gt;function&lt;/code&gt; into an &lt;code&gt;async function&lt;/code&gt;. This has two effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can use &lt;code&gt;await&lt;/code&gt; inside of &lt;code&gt;async&lt;/code&gt; functions. Outside of them, you can't.&lt;/li&gt;
&lt;li&gt;The return value of an &lt;code&gt;async&lt;/code&gt; function is a &lt;code&gt;Promise&lt;/code&gt; resolving to the value returned by the &lt;code&gt;async function&lt;/code&gt;. Even if you don't explicitly return your &lt;code&gt;async function&lt;/code&gt; will return a &lt;code&gt;Promise&lt;/code&gt; that resolves to &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;await&lt;/code&gt; can only be used inside &lt;code&gt;async&lt;/code&gt; functions (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await" rel="noopener noreferrer"&gt;and JavaScript modules&lt;/a&gt;). It takes a &lt;code&gt;Promise&lt;/code&gt; and turns it into the value it resolves to. It "waits" for value to be ready.&lt;/p&gt;

&lt;p&gt;Furthermore, if there was an error and the &lt;code&gt;Promise&lt;/code&gt; had no error callback attached, &lt;code&gt;await&lt;/code&gt; will &lt;code&gt;throw&lt;/code&gt; the error object that would have been passed as argument to the error callback. This makes error handling with &lt;code&gt;try/catch&lt;/code&gt; easy and convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapup
&lt;/h2&gt;

&lt;p&gt;I hope this showed how &lt;code&gt;async/await&lt;/code&gt; fits into the I/O story for Node.js and JavaScript in general. It's just syntactic sugar for promises, which are themselves an abstraction over callbacks. This article just touches on the basics, though. I deliberately omitted important pieces of the puzzle, like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" rel="noopener noreferrer"&gt;error handling with promises&lt;/a&gt;, to keep it simple.&lt;/p&gt;

&lt;p&gt;As a bonus, I will give some async functions and their identical twins implemented with Promises to show how the code is transformed.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// resolve returns a promise that calls its callback immediately&lt;/span&gt;
  &lt;span class="c1"&gt;// with the value x. i.e. a promise that is immediately "ready"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&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;nothing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// empty body&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nothing&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&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;myFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;myFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch error&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parallelFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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;promises&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// both fetches are being performed simultaneously in the background&lt;/span&gt;
  &lt;span class="c1"&gt;// (outside of the event loop, parallel to this code)&lt;/span&gt;

  &lt;span class="c1"&gt;// new promise that resolves when all given promises have resolved&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bundlePromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// this will wait until both fetches are done&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;resultX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bundlePromise&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="nx"&gt;resultX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultY&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parallelFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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;promises&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&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;bundlePromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promises&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;bundlePromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;resultX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultY&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resultX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultY&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;



</description>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Dates in JS</title>
      <dc:creator>Florian Lackner</dc:creator>
      <pubDate>Tue, 21 Oct 2025 07:23:30 +0000</pubDate>
      <link>https://dev.to/365talents/dates-in-js-300d</link>
      <guid>https://dev.to/365talents/dates-in-js-300d</guid>
      <description>&lt;p&gt;&lt;em&gt;First posted &lt;a href="https://www.lackner.work/posts/2025/10/21/dates-in-js/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Date and time handling is arguably one of the most complicated things in computer science. It seems simple, since everybody knows how to read a clock or how to use a calendar. But the devil is in the details, as always. Since so many people seem to get it wrong &lt;a href="https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time" rel="noopener noreferrer"&gt;over&lt;/a&gt; and &lt;a href="https://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time" rel="noopener noreferrer"&gt;over&lt;/a&gt;, we should talk about it.&lt;/p&gt;

&lt;p&gt;In this blog post I want to offer a mental model to you. It created this famous &lt;em&gt;click&lt;/em&gt; moment for me. Maybe it helps you too. Let me know, I'd be delighted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminology
&lt;/h3&gt;

&lt;p&gt;Let's give clearly defined names to the things we'll talk about. The topic is already complicated enough without confusion induced by the ambiguous nature of human language.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;Date&lt;/code&gt; is the information you can find on a physical calendar. It's a day, month and year.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Time&lt;/code&gt; is the information you can find on a wall clock. It's an hour, minute and second. You can of course go finer by giving milliseconds etc.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;DateTime&lt;/code&gt; is a &lt;code&gt;Date&lt;/code&gt; and a &lt;code&gt;Time&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;Instant&lt;/code&gt; is a point in time on the planet. If two &lt;code&gt;Instant&lt;/code&gt;s are the same, this means that they happened at the same moment, physically speaking.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Timezone&lt;/code&gt; is an algorithm to convert an &lt;code&gt;Instant&lt;/code&gt; to a &lt;code&gt;DateTime&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The key takeaway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;DateTime&lt;/code&gt; is NOT an &lt;code&gt;Instant&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you take one thing home today, it should be this. &lt;strong&gt;A &lt;code&gt;DateTime&lt;/code&gt; is NOT an &lt;code&gt;Instant&lt;/code&gt;.&lt;/strong&gt; Neither can you convert between the two losslessly. They are fundamentally different concepts. In my opinion the confusion between the two is the cause of most bugs in date and time handling.&lt;/p&gt;

&lt;p&gt;The mental model I'm offering is that these are different things that can't be converted without loss of information. You need to ask yourself &lt;em&gt;every time&lt;/em&gt; which of the two you're working with and you can't mix and match them.&lt;/p&gt;

&lt;p&gt;Let's use an example to show the difference. Say you're speaking on the phone with a friend who lives on the other side of the planet. Where you live it's maybe the evening, while your friend's about to start their day. Your &lt;code&gt;DateTime&lt;/code&gt; is very different from your friend's &lt;code&gt;DateTime&lt;/code&gt;. Yet, you're having a conversation at the same &lt;code&gt;Instant&lt;/code&gt; in time. Of course, because you're saying something and your friend responds immediately (ignoring the slight delay imposed by the speed of light).&lt;/p&gt;

&lt;p&gt;So let's repeat this again. &lt;strong&gt;A &lt;code&gt;DateTime&lt;/code&gt; is NOT an &lt;code&gt;Instant&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Timezones
&lt;/h3&gt;

&lt;p&gt;Let's take the example further. Say you didn't have your conversation yet. Since you and your friend are both busy, you schedule it. Easy, you agree on an &lt;code&gt;Instant&lt;/code&gt; in the future and be done with it. While it's true that you must agree on an &lt;code&gt;Instant&lt;/code&gt;, it's unfortunately also true that people don't work on &lt;code&gt;Instant&lt;/code&gt;s. They work on &lt;code&gt;DateTime&lt;/code&gt;s. How impractical. And since your friend lives far away, the &lt;code&gt;DateTime&lt;/code&gt;s for the &lt;code&gt;Instant&lt;/code&gt; of your call won't be the same.&lt;/p&gt;

&lt;p&gt;You need to convert the &lt;code&gt;Instant&lt;/code&gt; to a &lt;code&gt;DateTime&lt;/code&gt; for each one of you. The algorithm to do so is called a &lt;code&gt;Timezone&lt;/code&gt;. I'm saying algorithm, because &lt;code&gt;Timezone&lt;/code&gt;s are more than just a simple offset. There are complications like daylight saving time, leap seconds, and historical changes in time zones. I'd consider them black box algorithms, because they're complex and there's no shortcut around this fact.&lt;/p&gt;

&lt;p&gt;Say you're living in France. Your &lt;code&gt;Timezone&lt;/code&gt; is called "Central European Time" (&lt;code&gt;CET&lt;/code&gt;) or &lt;code&gt;Europe/Paris&lt;/code&gt;. Your friend lives in India. Their &lt;code&gt;Timezone&lt;/code&gt; is called "Indian Standard Time" (&lt;code&gt;IST&lt;/code&gt;) or &lt;code&gt;Asia/Kolkata&lt;/code&gt;. You both agree on an &lt;code&gt;Instant&lt;/code&gt; in the future. Then, you need to apply the respective &lt;code&gt;Timezone&lt;/code&gt;s to the same &lt;code&gt;Instant&lt;/code&gt; to get your &lt;code&gt;DateTime&lt;/code&gt;s. I've used India as an example, because its &lt;code&gt;Timezone&lt;/code&gt; doesn't experience daylight saving time. In France, however, it does. So the way to calculate the &lt;code&gt;DateTime&lt;/code&gt;s in each case are very different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Universal Time Code (UTC)
&lt;/h3&gt;

&lt;p&gt;The keyword "UTC" comes up a lot in discussions around time management. It is the name of a &lt;code&gt;Timezone&lt;/code&gt; called "Universal Time Code". It is very often used to communicate &lt;code&gt;Instant&lt;/code&gt;s between actors in different &lt;code&gt;Timezone&lt;/code&gt;s. It has a special property that makes it very useful for storing &lt;code&gt;Instant&lt;/code&gt;s. It doesn't experience daylight saving time. This means that the conversion between &lt;code&gt;Instant&lt;/code&gt;s and &lt;code&gt;DateTime&lt;/code&gt;s becomes reversible. Or in other words: &lt;strong&gt;A &lt;code&gt;DateTime&lt;/code&gt; in &lt;code&gt;UTC&lt;/code&gt; is equivalent to an &lt;code&gt;Instant&lt;/code&gt;&lt;/strong&gt;. You can convert between the two without loss of information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;p&gt;So how do you store &lt;code&gt;Instant&lt;/code&gt;, &lt;code&gt;DateTime&lt;/code&gt; and &lt;code&gt;Timezone&lt;/code&gt; ?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Instant&lt;/code&gt;s can be stored as offset from a known &lt;code&gt;Instant&lt;/code&gt; (epoch) in seconds. This is also called a timestamp. A common choice of the epoch is the &lt;code&gt;Instant&lt;/code&gt; that corresponds to the &lt;code&gt;DateTime&lt;/code&gt; Jan 1st 1970 at midnight in the &lt;code&gt;Timezone&lt;/code&gt; UTC. The offset can be in seconds, milliseconds, etc. You can choose the granularity. This storage method is space efficient, since you only need to store one number.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;/code&gt;s can be stored as strings or as &lt;code&gt;Instant&lt;/code&gt; in &lt;code&gt;UTC&lt;/code&gt;. There are several formats for the string representation. The most common format is the &lt;a href="https://en.wikipedia.org/wiki/ISO_8601" rel="noopener noreferrer"&gt;ISO 8601 format&lt;/a&gt;. It looks like this: &lt;code&gt;2023-03-15T12:34:56&lt;/code&gt;. This corresponds to March 15th, 2023 at 12:34:56. The &lt;code&gt;Timezone&lt;/code&gt; is not specified in this example.&lt;/p&gt;

&lt;p&gt;The second storage method is to store &lt;code&gt;DateTime&lt;/code&gt;s as &lt;code&gt;Instant&lt;/code&gt;s in &lt;code&gt;UTC&lt;/code&gt;. As already discussed, &lt;code&gt;Instant&lt;/code&gt;s in &lt;code&gt;UTC&lt;/code&gt; map uniquely to &lt;code&gt;DateTime&lt;/code&gt;s and vice-versa. This storage method is space efficient, since you only need to store one number. However, it is important to note that it is easy to make mistakes when using this method. You have to be very careful with your conversions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Timezone&lt;/code&gt; values are usually stored as strings. There are standardized names for timezones. You should use these names. They are supported by libraries implementing &lt;code&gt;Timezone&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;DateTime&lt;/code&gt; or &lt;code&gt;Instant&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Let's do some examples of when to use which for your information.&lt;/p&gt;

&lt;p&gt;The time information of a log entry in a computer system: &lt;code&gt;Instant&lt;/code&gt;, because it happens at a specific point in time.&lt;/p&gt;

&lt;p&gt;Showing an &lt;code&gt;Instant&lt;/code&gt; to a user: &lt;code&gt;DateTime&lt;/code&gt; (obtained by applying the users &lt;code&gt;Timezone&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A users birthdate: &lt;code&gt;Date&lt;/code&gt; / &lt;code&gt;DateTime&lt;/code&gt; , because that's what's written on their official documents.&lt;/p&gt;

&lt;p&gt;The timing information of a one-off calendar event in a shared calendar: &lt;code&gt;Instant&lt;/code&gt;, because it happens at a specific point in time. The &lt;code&gt;Timezone&lt;/code&gt; of one user does not influence the &lt;code&gt;Instant&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The timing information of a recurring calendar event. That's a tricky one. Think of every Monday at 2AM. What happens if the user changes their timezone? What happens on the day the clocks are reset for daylight saving time? Does the event happen twice? In my opinion there's no absolute answer here. It depends on how you want your system to behave. I'd store a &lt;code&gt;DateTime&lt;/code&gt; + &lt;code&gt;Timezone&lt;/code&gt;. I'd add code to handle the edge cases of the &lt;code&gt;Timezone&lt;/code&gt; (using a library of course). And then I'd start praying.&lt;/p&gt;

&lt;h3&gt;
  
  
  The JS &lt;code&gt;Date&lt;/code&gt; class
&lt;/h3&gt;

&lt;p&gt;So what does the JS &lt;code&gt;Date&lt;/code&gt; class represent? It's JS, so let's not be surprised by the answer: &lt;strong&gt;an &lt;code&gt;Instant&lt;/code&gt;.&lt;/strong&gt; Yes, that's the hard truth. And I want to be very clear: There is no &lt;code&gt;Timezone&lt;/code&gt; information stored in the &lt;code&gt;Date&lt;/code&gt; class. No &lt;code&gt;UTC&lt;/code&gt;. It's an &lt;code&gt;Instant&lt;/code&gt;. End of story.&lt;/p&gt;

&lt;p&gt;But why does it have getters and setters for years, months, days, hours, minutes, and seconds? Because it's badly designed. And what do these return? They convert the &lt;code&gt;Instant&lt;/code&gt; to a &lt;code&gt;DateTime&lt;/code&gt; in the local &lt;code&gt;Timezone&lt;/code&gt; of your user (frontend) or your server (backend). And then they extract the year, month, etc. and return them as numbers.&lt;/p&gt;

&lt;p&gt;And here's another hard truth: Your users &lt;code&gt;Timezone&lt;/code&gt; might be &lt;strong&gt;anything&lt;/strong&gt;. You can't assume that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it's correct.&lt;/li&gt;
&lt;li&gt;it does / doesn't experience daylight saving time&lt;/li&gt;
&lt;li&gt;its offset to &lt;code&gt;UTC&lt;/code&gt; is positive / negative&lt;/li&gt;
&lt;li&gt;it won't change&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You need to code with this in mind.&lt;/p&gt;

&lt;p&gt;I think the JS &lt;code&gt;Date&lt;/code&gt; class should be avoided for anything related to &lt;code&gt;DateTime&lt;/code&gt;. Instead, use one of the established libraries: &lt;a href="https://momentjs.com/" rel="noopener noreferrer"&gt;Moment.js&lt;/a&gt; or &lt;a href="https://day.js.org/" rel="noopener noreferrer"&gt;Day.js&lt;/a&gt;. And in the near future, use the successor of &lt;code&gt;Date&lt;/code&gt;, the new &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal" rel="noopener noreferrer"&gt;Temporal API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to write reviewer-friendly pull requests</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Fri, 17 Oct 2025 07:54:37 +0000</pubDate>
      <link>https://dev.to/365talents/how-to-write-reviewer-friendly-pull-requests-16aj</link>
      <guid>https://dev.to/365talents/how-to-write-reviewer-friendly-pull-requests-16aj</guid>
      <description>&lt;p&gt;Reviewing someone else's work is often challenging and time-consuming. It can easily lead to procrastination, frustration, or even tension between developers. Yet, it's an essential part of our daily workflow and deserves careful attention.&lt;/p&gt;

&lt;p&gt;Below are a few unordered tips we've gathered over time to make reviewing pull requests easier (from the author's perspective) to help prevent issues before they arise.&lt;/p&gt;

&lt;p&gt;We use GitHub, so some examples are specific to it, but the same ideas apply to most alternatives.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Write small pull requests
&lt;/h2&gt;

&lt;p&gt;This is the most common (and probably the most important) advice. To reinforce it, we &lt;a href="https://github.com/BedrockStreaming/pr-size-labeler" rel="noopener noreferrer"&gt;automatically add t-shirt-sized labels&lt;/a&gt; to our pull requests. This encourages authors to keep them short and reassures reviewers that the review won't take too long.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Always prioritize readability
&lt;/h2&gt;

&lt;p&gt;This applies not only to the codebase itself but also to the files diff. Don't hesitate to add code comments that provide context (eg. data format examples) even if they seem redundant with TypeScript types or equivalents. Anything that helps someone understand the code outside of an IDE is worth including.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create a simple template
&lt;/h2&gt;

&lt;p&gt;Use &lt;a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository" rel="noopener noreferrer"&gt;GitHub pull request templates&lt;/a&gt; to make authors' work easier and ensure consistency across pull requests. A short template with three or four sections (and maybe a few checkboxes) is often enough to remind authors of key steps in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Explain &lt;em&gt;why&lt;/em&gt; it's done in the description
&lt;/h2&gt;

&lt;p&gt;Include any relevant specifications, documentation, or conversations related to the pull request. This not only helps the reviewer but can also help yourself realize if something has been overlooked (which happens more often than we'd like to admit). Plus, even if the reasoning seems obvious now, it will help future developers understand the decision (especially years down the line). Obviously, make sure to explain how the feature is supposed to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Explain &lt;em&gt;how&lt;/em&gt; it's done in the files diff
&lt;/h2&gt;

&lt;p&gt;Before requesting a review, review your own diff. You might catch accidental commits, hard-to-read code, or even mistakes. It's frustrating for reviewers to find easily avoidable errors. Also, force yourself to add helpful comments in the diff: I personally often find that they actually belong to the code itself and add a commit for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Help the reviewer with tips
&lt;/h2&gt;

&lt;p&gt;I often add "guided tour" comments in the diff, like suggesting to "review with whitespaces ignored" or to "review commit by commit". These small hints can make the process much easier. You can also mark some code lines that are open to discussion or feedback. Encourage the reviewer to be part of the collaboration, not just a gatekeeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make a unique commit for renaming or moving files
&lt;/h2&gt;

&lt;p&gt;Whenever you rename or move files, make it a separate commit from content edits. (Git and GitHub sometimes detect this automatically, but not always.) This allows reviewers to skip the rename commit and focus on the real changes, instead of a massive "deleted and added" diff. This practice made our TypeScript migration much smoother for instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rebase commits
&lt;/h2&gt;

&lt;p&gt;Coding is rarely linear, mistakes are made, sometimes fixed a few days afterwards. Before opening a pull request, consider taking some time to rebase commits to make the history more readable. Don't try to make it perfect, but at least regroup commits that belongs together so it makes more sense when reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Suggest a pair review if necessary
&lt;/h2&gt;

&lt;p&gt;For complex pull requests or ones that require intricate setup, a pair review might be faster and more productive. It's also a great opportunity to explain your code out loud: often, you'll discover parts that are hard to justify or could be simplified.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Kill your ego
&lt;/h2&gt;

&lt;p&gt;Take feedback with an open mind. Make reviewers feel appreciated for the time they spend on your work. This way, they'll be more eager to review for you again. Remember that it's usually more valuable to make the reviewing process smooth and collaborative than to be "right" at all costs. That's easy to forget when we're deep in the work.&lt;/p&gt;




&lt;p&gt;I hope those tips can help making this complex task smoother in your daily workflow!&lt;/p&gt;

</description>
      <category>git</category>
      <category>teamwork</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Automatically take care of outdated browsers thanks to Browserslist and Github Actions</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Mon, 28 Jul 2025 08:46:32 +0000</pubDate>
      <link>https://dev.to/365talents/automatically-take-care-of-outdated-browsers-thanks-to-browserslist-and-github-actions-38p7</link>
      <guid>https://dev.to/365talents/automatically-take-care-of-outdated-browsers-thanks-to-browserslist-and-github-actions-38p7</guid>
      <description>&lt;p&gt;Our policy is to support browsers released within the last two years. Previously, we used Browserslist to transpile our code and automatically add polyfills. At some point, we wondered if we could use the same configuration to inform users when their browser was too old — ideally, without needing to write or maintain that logic ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's all in a regex
&lt;/h2&gt;

&lt;p&gt;Fortunately, there's a package that does exactly that: &lt;a href="https://github.com/browserslist/browserslist-useragent-regexp" rel="noopener noreferrer"&gt;browserslist-useragent-regexp&lt;/a&gt;. It &lt;strong&gt;generates a regular expression based on your Browserslist config&lt;/strong&gt;, which you can run against &lt;code&gt;navigator.userAgent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a simple &lt;code&gt;.browserslist.rc&lt;/code&gt; config targeting our policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;last 2 years
not dead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can generate the regex using a script in your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"supportedBrowsers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx update-browserslist-db@latest &amp;amp;&amp;amp; echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;export default $(browserslist-useragent-regexp --allowHigherVersions);&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; supportedBrowsers.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;npx update-browserslist-db@latest&lt;/code&gt; command ensures your caniuse data is current, preventing the dreaded &lt;code&gt;Browserslist: caniuse-lite is outdated&lt;/code&gt; warning.&lt;/p&gt;

&lt;p&gt;This script outputs something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default /Edge?\/(10[5-9]|1[1-9]\d|[2...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then import and use this regex in your app to notify users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;supportedBrowsers&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./supportedBrowsers.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;supportedBrowsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your browser is not supported, please update it.&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;
  
  
  Automate the update
&lt;/h2&gt;

&lt;p&gt;To keep the regex up to date, we use GitHub Actions to run the script automatically every month. That way, our &lt;code&gt;last 2 years&lt;/code&gt; policy always reflects the current calendar without anyone needing to remember to update it.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of our workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI - Update browsers list&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# At 8:00AM UTC the first day of the month&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update_frontend_browserslist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update browsers list&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;components/frontend&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run supportedBrowsers&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit and push changes if any&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name Internal&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email internal@yourcompany.com&lt;/span&gt;
          &lt;span class="s"&gt;# Check if there are any changes to the browsers list&lt;/span&gt;
          &lt;span class="s"&gt;if [[ -n "$(git status --porcelain .)" ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;git add .&lt;/span&gt;
            &lt;span class="s"&gt;git commit -m "chore: update supported-browsers.ts"&lt;/span&gt;
            &lt;span class="s"&gt;git push&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, we get a fresh, accurate regex every month — keeping users informed only when necessary, without adding manual work for the team.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Live Coding in Kubernetes: How we debug production data remotely with Tilt</title>
      <dc:creator>Kilian Decaderincourt</dc:creator>
      <pubDate>Tue, 01 Jul 2025 09:00:48 +0000</pubDate>
      <link>https://dev.to/365talents/live-coding-in-kubernetes-how-we-debug-production-data-remotely-with-tilt-6kj</link>
      <guid>https://dev.to/365talents/live-coding-in-kubernetes-how-we-debug-production-data-remotely-with-tilt-6kj</guid>
      <description>&lt;p&gt;&lt;em&gt;tldr: We use Tilt to enable developers to live-edit code running directly on our remote Kubernetes cluster, allowing them to debug issues on realistic production data from their local IDE. It plays nicely with our multitenant model, allowing developers to target and replace specific tenant services while keeping everything else running.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tilt does
&lt;/h2&gt;

&lt;p&gt;First, a quick recap on what Tilt does if you're not familiar with it. Tilt is a tool that lets you describe your dev environment as code to run on Kubernetes and start it in a single command with smart rebuild and live updates.&lt;/p&gt;

&lt;p&gt;Let's say you have a Dockerized app you want to test. If you do this manually, you need to first build the Docker image, push it to a repository then create a pod using this image. In its simplest form, Tilt will automate all these steps and do it on every change. It also comes with a nice UI to visualise everything and stream your pod logs.&lt;/p&gt;

&lt;p&gt;You can also go further with it and instead of rebuilding on every file change, you can configure Tilt to sync specific files directly inside the pod. Possibilities are almost endless due to it being easily configurable with some Python scripting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our architecture
&lt;/h2&gt;

&lt;p&gt;As an HR B2B SaaS provider, ensuring data segregation between our customers is very important, you wouldn't want your employee to receive an internal mobility offer.. from a different company. This has a significant impact on how we design our architecture. Each customer gets their own database, S3 storage container, Elasticsearch indexes, Redis namespace, DNS record, etc.&lt;/p&gt;

&lt;p&gt;This is what we call a tenant. Each customer has a production tenant for their employees, optionally one or more sandbox/test tenants, and a corresponding staging tenant. Developers also get their own tenant in the development environment. Overall, we have over 200 tenants spread across 3 environments each with its own purpose.&lt;/p&gt;

&lt;p&gt;To work at that scale, most of our application components are multitenant, meaning a single deployment can handle multiple tenants. That's the case for the data science backoffice components or our frontend application. However, for historical reasons, some components like our API backend have always been developed to serve a single tenant, so it requires a distinct deployment per tenant.&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%2Fedog1hhwjwqwoliklcic.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%2Fedog1hhwjwqwoliklcic.png" alt="Architecture schema" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How we're using it
&lt;/h2&gt;

&lt;p&gt;Instead of using Tilt on a local Kubernetes cluster, like what seems to be the most used scenario, we use it directly on our remote cluster hosted on Azure. The main benefit for us is that we can hijack any tenant pods and make them use a different code that can then be edited in real-time from a local IDE on our laptop.&lt;/p&gt;

&lt;p&gt;When starting Tilt, it builds an image per component, pushes it to our repository and deploys pods using this image in the namespace of the selected tenant. Pods deployed by Tilt reuse all the Secrets and ConfigMap already provisioned and updated by the infrastructure to connect to the right database, S3 bucket, etc.&lt;/p&gt;

&lt;p&gt;For components handling a single tenant, like our API backend, Tilt will first call a script to stop it by scaling it down to zero, which avoids any background running tasks that could edit data. Whereas components shared with other tenants like the frontend, are not stopped, instead Tilt will create a new pod in the tenant namespace and create a new Ingress with a higher priority that will route requests to our new frontend and backend pods.&lt;/p&gt;

&lt;p&gt;To leverage the live update feature and avoid rebuilding on every change, the images are not the same as the prod one. For example, our frontend image uses a Node.js-based image to run the Vite server instead of serving compiled assets with nginx. Every step above is described in the Tiltfile either using built-in functions or a call to custom scripts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tilt up -- --tenant-name tenant-a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ftk7igr2jec11xfghpgms.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%2Ftk7igr2jec11xfghpgms.png" alt="Architecture schema with tilt pods" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For obvious reasons, &lt;strong&gt;we don't allow tilting directly onto the production tenant&lt;/strong&gt;, instead we first make a copy of it. This process would merit its own blog post but in essence, each of our production tenants have a corresponding staging tenant in a lower environment. We can copy on demand all the tenant data and import it into the staging tenant with a layer of anonymisation for personal information like email, names and avatars. This lets our developers investigate issues and debug them in their development environment with real data, making the reproduction of bugs much easier.&lt;/p&gt;

&lt;p&gt;Each developer has their own tenant in the dev cluster so they can use Tilt for day-to-day development work. The code runs on the remote cluster while their IDE stays on their local laptop. To limit the cost of this environment, we run everything on Azure spot instances. Not everyone at 365Talents is using Tilt as their standard development workflow, some prefer the more local solution based on Devcontainers. Either way, Tilt is used by everyone when they need to debug things on a staging tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running code on realistic data
&lt;/h3&gt;

&lt;p&gt;Due to our customers' different use cases, their data is highly heterogeneous depending on the features they use or not. The main advantage for us is the ability for a developer to plug themselves to a specific tenant and edit the code that is run in real time on a realistic dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production similarity
&lt;/h3&gt;

&lt;p&gt;When developing with Tilt, your setup ends up closer to what is happening in production than if you were on your laptop. The code runs in the same place and almost in the same image. The requests go through the same network components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fewer resources used on laptop
&lt;/h3&gt;

&lt;p&gt;Another benefit is to consume less resources on the developer laptops as every service is run remotely in our cloud, this lets more resources available for their IDE and linter. &lt;br&gt;
While not a main concern for our web developers in the stack that was presented in this article, this is more important for our data science team. They also started using Tilt for their own stack running 6 differents data services hungry in memory and computing power.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difficulties
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Wrapper script to switch context
&lt;/h3&gt;

&lt;p&gt;One of the Tilt limitations for security reasons is that once Tilt is started, it cannot switch to another Kubernetes context. For our use case, we had to create a wrapper script to be able to only specify the tenant we want to tilt to without worrying about which cluster it's on. The script simply finds the corresponding cluster based on the requested tenant and starts Tilt with the correct Kubernetes context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow cold start
&lt;/h3&gt;

&lt;p&gt;This one is from our all remote approach, pushing the image to the repository is dependent on the network speed and can take a while. Since the first introduction there have been multiple iterations and attempts to improve this.&lt;/p&gt;

&lt;p&gt;This includes optimizing the Dockerfile order to reuse existing layers, tweaking the docker config to use the repository as a build cache, even trying to remove npm dependencies from the image and downloading them at startup or storing them in a PVC. Each comes with its own drawback, overall this is still a persistent problem. It shouldn't be as long as we don't do a full rebuild very often..&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many uncontrolled full image rebuilds
&lt;/h3&gt;

&lt;p&gt;Despite our best efforts, there are still too many times when a full rebuild is triggered. This mainly occurs when switching branches with lots of changes. This is a persistent pain point that we haven't truly solved yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial friction with Kubernetes
&lt;/h3&gt;

&lt;p&gt;Having control over their development environment is very important for developers. When we introduced Tilt, our Kubernetes migration for all our services was done recently, so they were still unfamiliar with some Kubernetes concepts. This also contributed in a good way to them becoming more comfortable with Kubernetes in the end. They have more control over it now and are the ones editing and improving the Tiltfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Despite the challenges, Tilt has become an integral part of our development workflow. It lets target a single one of our tenant and edit the code running live inside our Kubernetes cluster. Once combined with our process to make a copy of the production data, developers can easily debug issues from the comfort of their local IDE directly on realistic data. We could make it fit our needs thanks to its great customizability.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://tilt.dev" rel="noopener noreferrer"&gt;Tilt&lt;/a&gt; - The official Tilt website with documentation, tutorials, and download links&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://365talents.com" rel="noopener noreferrer"&gt;365Talents&lt;/a&gt; - Adaptive Skills &amp;amp; Talent Intelligence for ambitious HR.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.welcometothejungle.com/fr/companies/365talents/jobs" rel="noopener noreferrer"&gt;365Talents Jobs&lt;/a&gt; - Join us !&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Automating GitBook translations with GitHub Actions: A complete guide</title>
      <dc:creator>Arthur Dufour</dc:creator>
      <pubDate>Mon, 10 Mar 2025 14:07:07 +0000</pubDate>
      <link>https://dev.to/365talents/automating-gitbook-translations-with-github-actions-a-complete-guide-3maf</link>
      <guid>https://dev.to/365talents/automating-gitbook-translations-with-github-actions-a-complete-guide-3maf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.gitbook.com/" rel="noopener noreferrer"&gt;GitBook&lt;/a&gt; is a modern documentation platform that makes it easy to create, edit, and organize documentation. With its clean interface, version control, and collaboration features, it's no wonder it's become so popular among development teams.&lt;/p&gt;

&lt;p&gt;But if you're managing documentation with GitBook and need to support multiple languages, you've probably hit a major roadblock: GitBook doesn't offer native translation capabilities. While it's fantastic for creating and managing documentation, this lack of built-in translation features can be a real headache for international teams.&lt;/p&gt;

&lt;p&gt;In this article, I'll share a complete solution for automating GitBook translations using GitHub Actions and a custom Node.js script. As a fullstack developer who recently implemented this for the startup I work at ; &lt;a href="https://365talents.com" rel="noopener noreferrer"&gt;365Talents&lt;/a&gt;, I discovered that while GitBook does provide &lt;a href="https://docs.gitbook.com/developers/getting-started/guides/use-github-actions-to-translate-gitbook-pages" rel="noopener noreferrer"&gt;some documentation on translation workflows&lt;/a&gt;, it lacks the concrete implementation details you need to actually get it working.&lt;/p&gt;

&lt;p&gt;This guide is for you if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use GitBook for documentation&lt;/li&gt;
&lt;li&gt;Need to maintain content in multiple languages&lt;/li&gt;
&lt;li&gt;Want to automate the translation process&lt;/li&gt;
&lt;li&gt;Have basic familiarity with GitHub Actions &amp;amp; Node.js&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you'll have everything you need to build a fully automated system that detects changes to your primary language content and automatically translates it to your target languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining your requirements
&lt;/h2&gt;

&lt;p&gt;Before diving into implementation, you need to make two important decisions about your workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Translation review process&lt;/strong&gt;: While automated translation has improved dramatically, it's still far from perfect. You'll need to decide how translations will be reviewed and by whom. GitBook's documentation points out something crucial: when a reviewed translation exists and the source document is updated, automatic re-translation will overwrite any manual improvements made during review. Your team needs a clear process to handle these situations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update strategy&lt;/strong&gt;: You must decide how to handle updates to existing content. &lt;br&gt;
For our implementation, we took a conservative approach: we only automatically translate new documents, not updates to existing ones. This preserves any manual edits made to translations but means content authors need to manually update translations when they modify the primary language version. While this approach isn't perfect for multilingual content management, it worked best for our specific needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can enhance your translation process in many ways. For instance, you could just retranslate the specific phrases that changed instead of the entire file, though this would require more development work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Overview: Automated translation workflow
&lt;/h2&gt;

&lt;p&gt;The solution I'm sharing creates an automated pipeline that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects &lt;strong&gt;new files&lt;/strong&gt; added in your primary language&lt;/li&gt;
&lt;li&gt;Translates them using a translation service (Azure Translator)&lt;/li&gt;
&lt;li&gt;Creates the corresponding content in your target language spaces&lt;/li&gt;
&lt;li&gt;Commits the changes back to your repository, automatically updating GitBook&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%2Fc1rgo1vkmgkeh5cx4f11.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%2Fc1rgo1vkmgkeh5cx4f11.png" alt="Diagram of the workflow" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setting up GitBook with GitSync
&lt;/h3&gt;

&lt;p&gt;Before we can automate translations, we need to set up GitBook properly and connect it to GitHub using GitSync.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating a Collection with language Spaces
&lt;/h4&gt;

&lt;p&gt;In GitBook, collections let you group related spaces together. For our translation workflow, we'll create a collection with a separate space for each language:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new collection in GitBook&lt;/li&gt;
&lt;li&gt;Create a space for your primary language (e.g., "English")&lt;/li&gt;
&lt;li&gt;Create an additional space for your target language (e.g., "French")&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Configuring GitSync
&lt;/h4&gt;

&lt;p&gt;Next, we need to connect each space to our GitHub repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your space settings&lt;/li&gt;
&lt;li&gt;Navigate to the "Integrations" tab&lt;/li&gt;
&lt;li&gt;Select "GitHub" and follow the authentication process&lt;/li&gt;
&lt;li&gt;Configure the sync settings:

&lt;ul&gt;
&lt;li&gt;Select your account and repository&lt;/li&gt;
&lt;li&gt;Choose the branch (usually "main" or "master")&lt;/li&gt;
&lt;li&gt;Use the monorepo approach and specify the folder path for your language (e.g., &lt;code&gt;./en&lt;/code&gt; for the "English" space, and &lt;code&gt;./fr&lt;/code&gt; for the French space)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat this process for your target language space, making sure it's connected to the appropriate folder in your repository.&lt;/p&gt;

&lt;h4&gt;
  
  
  Repository structure
&lt;/h4&gt;

&lt;p&gt;After setting up GitSync, your repository structure should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/
├── en/           # Primary language content (English)
├── fr/           # French translations
└── .github/
    └── workflows/
        └── translate.yml  # Our future GitHub Action workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes it easy to manage content for each language separately while keeping the automation in a central location.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Creating the GitHub Action workflow
&lt;/h3&gt;

&lt;p&gt;Now, let's create the GitHub Action workflow that will detect changes and trigger our translation process.&lt;/p&gt;

&lt;p&gt;Create a file at &lt;code&gt;.github/workflows/translate.yml&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Translate GitBook Content&lt;/span&gt;

&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source_language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(e.g.:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;en)"&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;target_language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(e.g.:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fr)"&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;translator_api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The Azure Translator API key&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composite&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get all changed markdown files&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changed-markdown-files&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tj-actions/changed-files@v45&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;${{ inputs.source_language }}/**/*.md&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get all changes in .gitbook/&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changed-gitbook-files&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tj-actions/changed-files@v45&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;${{ inputs.source_language }}/.gitbook/**&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy .gitbook files to target language directory&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.changed-gitbook-files.outputs.any_changed == 'true'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;rm -rf ${{ inputs.target_language }}/.gitbook&lt;/span&gt;
        &lt;span class="s"&gt;mkdir -p ${{ inputs.target_language }}/.gitbook&lt;/span&gt;
        &lt;span class="s"&gt;cp -r ${{ inputs.source_language }}/.gitbook/* ${{ inputs.target_language }}/.gitbook/&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.changed-markdown-files.outputs.any_changed == 'true'&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;23.6.0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up pnpm&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.changed-markdown-files.outputs.any_changed == 'true'&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9.14.2&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.changed-markdown-files.outputs.any_changed == 'true'&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;365talents-actions/actions/gitbook-translator/translator-script&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run translation script&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.changed-markdown-files.outputs.any_changed == 'true'&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;365talents-actions/actions/gitbook-translator/translator-script&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;SOURCE_LANGUAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.source_language }}&lt;/span&gt;
        &lt;span class="na"&gt;TARGET_LANGUAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.target_language }}&lt;/span&gt;
        &lt;span class="na"&gt;NEW_FILES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changed-markdown-files.outputs.all_changed_files }}&lt;/span&gt;
        &lt;span class="na"&gt;TRANSLATOR_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.translator_api_key }}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node index.ts&lt;/span&gt; 

    &lt;span class="c1"&gt;# Make sure that deleting a file in the source_language also deletes it in target_language&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cleanup deleted files&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd ${{ inputs.target_language }}&lt;/span&gt;
        &lt;span class="s"&gt;find . -type f -name "*.md" | while read file; do&lt;/span&gt;
          &lt;span class="s"&gt;if [ ! -f "../${{ inputs.source_language }}/$file" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;rm "$file"&lt;/span&gt;
            &lt;span class="s"&gt;echo "Removed ${{ inputs.target_language }}/$file as it no longer exists in ${{ inputs.source_language }}/"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
        &lt;span class="s"&gt;done&lt;/span&gt;

    &lt;span class="c1"&gt;# Commit translated content&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit changes&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stefanzweifel/git-auto-commit-action@v5&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commit_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Translate files&lt;/span&gt;
        &lt;span class="na"&gt;file_pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.target_language&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/**'&lt;/span&gt;
        &lt;span class="na"&gt;add_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Triggers on file changes&lt;/li&gt;
&lt;li&gt;Uses tj-actions/changed-files to detect which files have changed&lt;/li&gt;
&lt;li&gt;Checks out the repository and sets up Node.js&lt;/li&gt;
&lt;li&gt;Runs our translation script (which we'll create next)&lt;/li&gt;
&lt;li&gt;Commits the translated files back to the repository&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Implementing the translation script
&lt;/h3&gt;

&lt;p&gt;Now we need to develop the Node.js script that will perform the actual translation work. This script is the heart of our automation system, responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Processing the detected file changes&lt;/li&gt;
&lt;li&gt;Communicating with the translation API&lt;/li&gt;
&lt;li&gt;Saving the translated content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've created a complete implementation of this translation script and shared it in a GitHub Gist for you to reference: &lt;a href="https://gist.github.com/adufr/64635aadcb3c94a45d0e9d7fde06bc41" rel="noopener noreferrer"&gt;GitBook Translation Script&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script uses Azure Translator API to convert content between languages, but you can easily modify it to work with other translation services like DeepL, Google Cloud Translation, or OpenAI if those better suit your needs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One critical aspect of the translation script is how it handles Markdown syntax. The Azure Translator API sometimes loses Markdown formatting during translation. To fix this, the script uses regular expressions and string replacements to preserve all Markdown syntax elements after translation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Remember that this implementation reflects my team's specific requirements and workflow. You should review and customize the code to match your team's translation strategy, especially regarding how you want to handle content updates and translation reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Committing translated content back to the repository
&lt;/h3&gt;

&lt;p&gt;The GitHub Action workflow automatically commits the translated files back to your repository. GitSync then updates your GitBook spaces with the new content. You have nothing more to do! 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Customizations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supporting multiple target languages
&lt;/h3&gt;

&lt;p&gt;My script currently only supports one target language, but it can easily be adapted to handle multiple target languages.&lt;/p&gt;

&lt;p&gt;You would need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add more GitBook spaces and configure GitSync correctly for each language&lt;/li&gt;
&lt;li&gt;Update the translation script to loop through all target languages&lt;/li&gt;
&lt;li&gt;Modify the GitHub Action to support multiple target languages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementing manual review workflows
&lt;/h3&gt;

&lt;p&gt;Currently, the described setup doesn't include a manual review process, meaning all new files are automatically translated and instantly added to GitBook.&lt;/p&gt;

&lt;p&gt;To implement a manual review process, you could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a pull request instead of directly committing translations&lt;/li&gt;
&lt;li&gt;Add reviewers to the pull request&lt;/li&gt;
&lt;li&gt;Only merge after approval&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Optimizing for large content bases
&lt;/h3&gt;

&lt;p&gt;Our setup only translates new files, which isn't ideal for frequently updated documentation. If you regularly make changes to your documentation, you'd need to implement incremental translation to process only the content that has changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Translation quality considerations
&lt;/h3&gt;

&lt;p&gt;Azure Translator, like other machine translation services, works well for most general content but may struggle with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical terminology specific to your product or industry&lt;/li&gt;
&lt;li&gt;Complex sentence structures&lt;/li&gt;
&lt;li&gt;Idiomatic expressions&lt;/li&gt;
&lt;li&gt;Cultural nuances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For technical documentation, you might notice certain specialized terms being translated incorrectly or inconsistently across documents.&lt;/p&gt;

&lt;p&gt;Beyond manual review, here are several strategies to enhance your translation quality:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Custom Dictionary/Glossary&lt;/strong&gt;: Create a glossary of technical terms and their approved translations. Some translation APIs (including Azure Translator with Custom Translator) allow you to provide custom dictionaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-processing Content&lt;/strong&gt;: Simplify complex sentences in your source content. Clear, concise writing translates better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-processing Scripts&lt;/strong&gt;: Develop scripts that automatically correct known translation issues, such as consistently replacing certain terms with their correct translations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation Memory&lt;/strong&gt;: Implement a translation memory system that stores previously approved translations, allowing you to reuse them for similar content.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cost considerations
&lt;/h3&gt;

&lt;p&gt;When implementing an automated translation system, it's important to consider the costs involved, especially as your documentation grows.&lt;/p&gt;

&lt;p&gt;A few things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare service costs before committing to one provider&lt;/li&gt;
&lt;li&gt;Implement cost optimization strategies (only translate sections that have changed, store and reuse translations for repeated content)&lt;/li&gt;
&lt;li&gt;Monitor your usage and set up alerts to avoid unexpected costs as your documentation grows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Setting up this GitBook translation system was honestly not that simple to do. GitBook really should just build this in. In my opinion, documentation is global, and teams shouldn't have to hack together custom solutions for something so fundamental.&lt;/p&gt;

&lt;p&gt;Until then, this solution works well enough to save you from manual translation headaches. It's not perfect, but it's definitely better than copying and pasting content between languages!&lt;/p&gt;

</description>
      <category>gitbook</category>
    </item>
    <item>
      <title>What we learned by running an accessibility audit of our app</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Tue, 11 Feb 2025 13:30:06 +0000</pubDate>
      <link>https://dev.to/365talents/what-we-learned-by-running-an-accessibility-audit-of-our-app-37n0</link>
      <guid>https://dev.to/365talents/what-we-learned-by-running-an-accessibility-audit-of-our-app-37n0</guid>
      <description>&lt;p&gt;We recently underwent an accessibility audit (thanks to &lt;a href="https://access42.net/" rel="noopener noreferrer"&gt;Access42&lt;/a&gt;)  for our app due to &lt;a href="https://accessibilite.numerique.gouv.fr/obligations/declaration-accessibilite/" rel="noopener noreferrer"&gt;compliance regulations for large companies in France&lt;/a&gt;. Initially, we believed we were in good shape because we use modern tools, adhere to basic HTML structures, and avoid unusual ergonomics.&lt;/p&gt;

&lt;p&gt;However, we decided to invest effort into this area as we needed to meet a minimum score, and the scoring system is quite stringent: any failure on a specific criterion on any audited page counts as a failure, which can quickly degrade the overall score.&lt;/p&gt;

&lt;p&gt;That's when we realized just how poor our accessibility was, how complex the subject is, and how much work lay ahead of us...&lt;/p&gt;

&lt;h2&gt;
  
  
  Before diving in
&lt;/h2&gt;

&lt;p&gt;As a typical developer, I initially believed that accessibility was merely a technical issue to be resolved. However, I soon realized I was mistaken, and I would like to share our experience before delving into more concrete details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get a proper training
&lt;/h3&gt;

&lt;p&gt;It's easy to assume that accessibility is straightforward, but like everything in web development, there's a rich history, numerous quirks, and backward compatibility issues that complicate matters.&lt;/p&gt;

&lt;p&gt;Fortunately, four developers and I had the opportunity to participate in a three-day training session, including hands-on testing of our own app. This was incredibly instructive. We were able to experiment with tools and ask questions on the spot, which was far more efficient than spending hours on Stack Overflow trying to figure out why VoiceOver wasn't behaving as expected...&lt;/p&gt;

&lt;h3&gt;
  
  
  Involve every stakeholder
&lt;/h3&gt;

&lt;p&gt;A web app should be designed with accessibility in mind from the outset, much like performance and other cross-cutting concerns. While developers can implement fixes, these solutions are often less effective for users and require more effort from the team. Examples include manually adding aria-label attributes to icon-only buttons, creating textual alternatives for complex objects, and managing focus states manually when informations are spread on the interface.&lt;/p&gt;

&lt;p&gt;Certain design-specific aspects, such as text size, color contrasts, and considerations for color blindness, cannot be addressed by developers alone. Currently, here, developers are primarily responsible for ensuring the accessibility of the product through testing. However, we hope that a culture of accessibility will gradually permeate all aspects of our work over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the accessibility tree
&lt;/h3&gt;

&lt;p&gt;You might already be familiar with the DOM (Document Object Model) and CSSOM (Cascading Style Sheets Object Model), but we discovered that there is another, less visible underlying model: &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree" rel="noopener noreferrer"&gt;the accessibility tree&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is primarily derived from HTML elements and results in four properties for each element:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;: a kind of identifier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt;: a secondary string describing the element, similar to a tooltip&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role&lt;/code&gt;: one of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles" rel="noopener noreferrer"&gt;list of roles&lt;/a&gt;. By default, it is &lt;code&gt;generic&lt;/code&gt;; a &lt;code&gt;button&lt;/code&gt; will be &lt;code&gt;button&lt;/code&gt;, an &lt;code&gt;a&lt;/code&gt; will be &lt;code&gt;link&lt;/code&gt;, and it can be overridden by the &lt;code&gt;role=""&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;: indicates if the element is currently active, expanded, pressed, checked, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding these properties is crucial for correctly using attributes like &lt;code&gt;aria-label&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;aria-describedby&lt;/code&gt;, and so on, as well as knowing their priority. For example, &lt;code&gt;aria-labelledby&lt;/code&gt; takes precedence over &lt;code&gt;aria-label&lt;/code&gt;. Modern browsers now offer a view of the accessibility tree in their developer tools, making it easier to debug. More on this later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subjectivity
&lt;/h3&gt;

&lt;p&gt;One final note before diving in, regarding what may be the most challenging aspect of accessibility: &lt;strong&gt;subjectivity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are some scenarios where it can be difficult because no tool can help you make a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should this text be a heading or a paragraph?&lt;/li&gt;
&lt;li&gt;An image has an &lt;code&gt;alt&lt;/code&gt; attribute, but is it truly meaningful?&lt;/li&gt;
&lt;li&gt;A "skip" button has been added, but is it genuinely useful??&lt;/li&gt;
&lt;li&gt;Should this notification be polite or assertive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, even a 100% accessible website could be frustrating to use with a screen reader. The best way to assess this is to use it ourselves under "real" conditions. The only downside is that it's time-consuming, so I suspect few companies actually do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using accessible librairies
&lt;/h2&gt;

&lt;p&gt;Chances are that you rely on a UI library for common components. You first want to make sure it's already accessible enough, or else you'll lose a huge amount of time trying to fix errors that you don't actually control. In our case, we're migrating from &lt;a href="https://element-plus.org/en-US/" rel="noopener noreferrer"&gt;ElementPlus&lt;/a&gt; to &lt;a href="https://www.radix-vue.com/" rel="noopener noreferrer"&gt;Radix Vue&lt;/a&gt; mainly for this reason.&lt;/p&gt;

&lt;p&gt;It seems modern tools finally treat accessibility as a first class citizen, but it was not the case just a few years ago! &lt;a href="https://ark-ui.com/" rel="noopener noreferrer"&gt;Ark UI&lt;/a&gt; looks like a good alternative too. You can check their docs and even use the tool below to check its accessibility compliance.&lt;/p&gt;

&lt;p&gt;We can also refer to &lt;a href="https://www.w3.org/WAI/WCAG21/Techniques/" rel="noopener noreferrer"&gt;Offical WCAG rules&lt;/a&gt; for simple and framework-agnostic examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual testing
&lt;/h2&gt;

&lt;p&gt;Now that we have the basic theory, it's time to get our hands dirty and find issues and test fixes. Here are the main tools we used:&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser accessibility devtools
&lt;/h3&gt;

&lt;p&gt;Browsers have come a long way in terms of tooling, and it's really helpful to understand the state of a specific element and why it's considered that way. For instance, the &lt;code&gt;aria-label&lt;/code&gt; attribute takes precedence over the &lt;code&gt;title&lt;/code&gt; attribute, so Chrome indicates that it will be ignored:&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%2Fnl85z99csg6lk0pe5b58.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%2Fnl85z99csg6lk0pe5b58.png" alt="Screenshot of accessibility tools in Chrome" width="453" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's not as clear in Firefox, but other information is provided:&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%2Fuahdmt8dsv0ych03ozsm.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%2Fuahdmt8dsv0ych03ozsm.png" alt="Screenshot of accessibility tools in Firefox" width="402" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each browser offers different tools with their own advantages and disadvantages, so feel free to try them out to find your preferences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Axe browser extension
&lt;/h3&gt;

&lt;p&gt;Axe is probably the most well-known suite of tools for testing and improving accessibility. It is essentially included in every major testing solution, as we'll see next.&lt;/p&gt;

&lt;p&gt;You can find the &lt;a href="https://www.deque.com/axe/browser-extensions/" rel="noopener noreferrer"&gt;Axe extensions for browsers&lt;/a&gt; on their website (available for Chrome, Firefox, and Edge). It offers a lot of tools to identify accessibility issues:&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%2F9l3i0c9q36ixgfgj77fy.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%2F9l3i0c9q36ixgfgj77fy.png" alt="Axe browser extension screenshot" width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it might not be the most feature-complete, as indicated in &lt;a href="https://alphagov.github.io/accessibility-tool-audit/index.html" rel="noopener noreferrer"&gt;this study by the British Government&lt;/a&gt;, it is probably better to have consistent test results across your tools (unit tests, manual tests, E2E tests, etc.) than ultra-exhaustive error reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows, Firefox &amp;amp; NVDA
&lt;/h3&gt;

&lt;p&gt;This setup is the most widely used worldwide, and NVDA is the most valid and feature-complete free tool available, so it should be preferred when doing tests. If you're on Mac, you can use a virtual machine (we chose Parallels Desktop with Windows 11, which took about 30 minutes to set up and ~130€ one time payment).&lt;/p&gt;

&lt;h3&gt;
  
  
  MacOS, Safari &amp;amp; VoiceOver
&lt;/h3&gt;

&lt;p&gt;VoiceOver is included in every Mac and can be toggled by pressing CMD+F5. It's pretty good by default, but it is known to have some quirks. For instance, it won't read aloud any alert that uses anything other than &lt;code&gt;alert&lt;/code&gt; (&lt;code&gt;log&lt;/code&gt; or &lt;code&gt;status&lt;/code&gt; won't work). If you're on Windows, unfortunately, there's no way to emulate it locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using BrowserStack
&lt;/h3&gt;

&lt;p&gt;Online virtual machines can be useful too. We didn't try them, but &lt;a href="https://www.browserstack.com/docs/live/accessibility-testing/screenreader-desktop" rel="noopener noreferrer"&gt;BrowserStack currently supports screen readers&lt;/a&gt;, and there could be other tools offering similar functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Unit Tests
&lt;/h3&gt;

&lt;p&gt;We believe unit tests are not particularly useful for accessibility, as checking only for HTML attributes is generally insufficient. Tools like Axe can handle this better than manual unit tests, so we didn't use them at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  End-to-End Accessibility Tests with Cypress
&lt;/h3&gt;

&lt;p&gt;End-to-end tests are better suited for accessibility checks because they closely mimic what users will see and interact with. However, there are some quirks with Cypress, such as &lt;a href="https://github.com/cypress-io/cypress/issues/299" rel="noopener noreferrer"&gt;handling tab keys&lt;/a&gt; to check for focus. You might want to look into &lt;a href="https://github.com/dmtrKovalenko/cypress-real-events" rel="noopener noreferrer"&gt;cypress-real-events&lt;/a&gt; for a potential solution. (Edit: It seems like this issue is finally being considered in &lt;a href="https://github.com/orgs/cypress-io/projects/13" rel="noopener noreferrer"&gt;Cypress priorities&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I suppose Playwright might not suffer from those issues as it uses a different, more native way of controlling the browser, but we don't use this tool directly for now.&lt;/p&gt;

&lt;h4&gt;
  
  
  Axe Plugin
&lt;/h4&gt;

&lt;p&gt;We used the &lt;a href="https://github.com/component-driven/cypress-axe" rel="noopener noreferrer"&gt;cypress-axe&lt;/a&gt; plugin to easily integrate Axe into our tests. The upside is that it's straightforward to use; the downside is that reading errors can be challenging (clicking on lines provides better feedback, though):&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%2Ff6118wvyt22t3r9qunbf.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.amazonaws.com%2Fuploads%2Farticles%2Ff6118wvyt22t3r9qunbf.jpg" alt="Example of errors from cypress-axe plugin" width="570" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Cypress Cloud accessibility add-on
&lt;/h4&gt;

&lt;p&gt;Luckily for us, Cypress released a full featured &lt;a href="https://www.cypress.io/accessibility" rel="noopener noreferrer"&gt;accessibility add-on&lt;/a&gt; not so long ago.&lt;/p&gt;

&lt;p&gt;It relies on Axe too, but it's much more integrated in the ecosystem. For instance, the online report is very nice (I can't expose the visual of our app unfortunately):&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%2F8mjjfym61emgi1uzqclr.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.amazonaws.com%2Fuploads%2Farticles%2F8mjjfym61emgi1uzqclr.jpg" alt="Cypress Cloud accessibility add-on results" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, the Slack integration is quite nice too:&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%2Fzgoe84jg1xc2wiyg95aw.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.amazonaws.com%2Fuploads%2Farticles%2Fzgoe84jg1xc2wiyg95aw.jpg" alt="Slack message with a summary of accessibility failures" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The obvious downside is that it's a paid add-on, and it's quite expensive 🥵 so we probably won't be able to keep it in our suite.&lt;/p&gt;

&lt;h4&gt;
  
  
  Specific Tests
&lt;/h4&gt;

&lt;p&gt;For more specific tests that don't rely on rules covered by Axe, you can add some manual checks. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first tab should show and focus a "skip to content" link.&lt;/li&gt;
&lt;li&gt;After opening a dialog, its first input should be focused.&lt;/li&gt;
&lt;li&gt;A notification should be shown once an action is completed (also check its attributes to ensure it will be announced by screen readers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can easily find the focused element in Cypress by running &lt;code&gt;cy.focused()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component Accessibility Tests with Storybook
&lt;/h3&gt;

&lt;p&gt;Storybook proved to be really useful for accessibility testing. It's easier to set up than end-to-end (E2E) tests but still provides visual feedback and debugging tools. It also allows for complex cases where components are used together or under special conditions. You can write specific &lt;code&gt;play&lt;/code&gt; functions that use any code or components you need, not just the one being tested.&lt;/p&gt;

&lt;p&gt;By the way, Storybook uses &lt;a href="https://testing-library.com/" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt;, which you can think of as an element selector tool with accessibility in mind. For instance, it won't let you retrieve an element by its class name but forces you to check its role or name (remember the accessibility tree we talked about above?). This is because we should test what the user faces, not the underlying code. It might seem like a small detail, but it sets you in the right mindset when testing.&lt;/p&gt;

&lt;p&gt;Lastly, Storybook uses Playwright under the hood, so it actually controls the browser instead of creating fake events in JavaScript like Cypress does. This makes it easier to work with native interactions like tabbing, which is huge for accessibility.&lt;/p&gt;

&lt;h4&gt;
  
  
  Axe Plugin
&lt;/h4&gt;

&lt;p&gt;Once again, Axe is part of the solution with an &lt;a href="https://storybook.js.org/docs/writing-tests/accessibility-testing" rel="noopener noreferrer"&gt;official accessibility plugin&lt;/a&gt;. It provides a &lt;code&gt;checkA11y()&lt;/code&gt; function that can be set up to run automatically at the end of every test or manually called in a specific test (using the &lt;code&gt;play()&lt;/code&gt; function in Storybook with the &lt;a href="https://storybook.js.org/addons/@storybook/addon-interactions" rel="noopener noreferrer"&gt;addon interactions&lt;/a&gt;). Here's a truncated example of what can be done:&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;// In the config&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;preVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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="nf"&gt;injectAxe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;postVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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="nf"&gt;checkA11y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&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;detailedReport&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="na"&gt;detailedReportOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;html&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// In the story&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvasElement&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvasElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Show a notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;notification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveTextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is a notification message!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Accessibility tests will be run automatically 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;Again, it's pretty straightforward to set up. Errors in the interface are clear and actionable as shown below, but it's hard to read in the CI (GitHub Actions in our case) and we didn't spend time to try to improve it for now:&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%2F5l5up8e8r80d6m7abik9.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%2F5l5up8e8r80d6m7abik9.png" alt="Accessibility errors in Storybook interface" width="800" height="346"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fszk3iw9wgt4jonawxrx3.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%2Fszk3iw9wgt4jonawxrx3.png" alt="Accessibility errors in Github Actions interface" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Specific Tests
&lt;/h4&gt;

&lt;p&gt;We especially appreciated Storybook for running our own specific tests. For instance, we had a hard time ensuring that tooltips were used correctly (tooltips are much harder to get right than we initially thought). So, we interacted with the trigger element by focusing it or not and checked that everything was working as expected:&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="nx"&gt;play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvasElement&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvasElement&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hover me to show tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Check tabindex to be sure it's focusable&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tabindex&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="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;canvasElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;tooltip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tooltip&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello world!&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Check role button to be sure it's read by screen readers&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&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="s1"&gt;button&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;p&gt;Another case was ensuring our focus trap always worked within an open dialog. We used the same mechanism to check that we looped through interactive elements without tabbing out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linting
&lt;/h3&gt;

&lt;p&gt;ESLint can also help developers avoid simple mistakes early in the development process. We use the &lt;a href="https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility" rel="noopener noreferrer"&gt;ESLint plugin VueJS accessibility&lt;/a&gt;, but there are alternatives for other major libraries. While it is the least capable of all the tools we listed, it offers the quickest feedback loop, making it ideal for simple cases.&lt;/p&gt;

&lt;p&gt;We had to enforce and create custom rules for our specific use cases to ensure components are used correctly. For instance, our &lt;code&gt;&amp;lt;Tooltip&amp;gt;&lt;/code&gt; component should have its interactive element as a direct child, like a &lt;code&gt;&amp;lt;Btn&amp;gt;&lt;/code&gt;. We leveraged the &lt;code&gt;vue/no-restricted-syntax&lt;/code&gt; rule 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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue/no-restricted-syntax&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="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VElement[rawName="Tooltip"] &amp;gt; VElement:not([rawName=/^(a|router-link|button|Btn)$/]) VElement[rawName=/^(a|router-link|button|Btn)$/]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Interactive elements should be the direct child of a tooltip, or its content will not be read aloud by screen readers&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It's &lt;a href="https://github.com/Heydon/principles-of-web-accessibility" rel="noopener noreferrer"&gt;virtually impossible to achieve 100% accessibility&lt;/a&gt; on a complex web app. Companies that claim otherwise are likely mistaken, whether intentionally or not. Some conduct their own internal audits, which are clearly not as reliable as those performed by an external, independent organization.&lt;/p&gt;

&lt;p&gt;We also discovered that there are some difficult and almost undocumented quirks. For instance, a tooltip must be set on an interactive element to be actually announced (e.g., with an implicit or explicit role of &lt;code&gt;button&lt;/code&gt; or &lt;code&gt;link&lt;/code&gt;; a &lt;code&gt;span&lt;/code&gt; would be ignored) and I didn't see documented in common posts and examples. Therefore, the rule is to always test manually and not rely solely on green CI checks for the right attributes.&lt;/p&gt;

&lt;p&gt;It must also be said that an audit is outdated as soon as it's done, as nothing prevents the code from changing from that date. In France, an accessibility declaration is &lt;a href="https://design.numerique.gouv.fr/accessibilite-numerique/declaration-accessibilite/" rel="noopener noreferrer"&gt;only valid for a few years&lt;/a&gt;, and it must be rechecked when the website is "substantially" updated, which is quite subjective.&lt;/p&gt;

&lt;p&gt;So, the "best effort" technique is the only way to go, and accessibility must be treated with sincerity to be achieved at its best. This involves truly treating it as an acceptance criterion, training developers and designers (and every stakeholder, by the way), having automatic tests, and using manual tools.&lt;/p&gt;

&lt;p&gt;Finally, the &lt;a href="https://tink.uk/playing-with-the-accessibility-object-model-aom/" rel="noopener noreferrer"&gt;Accessibility Object Model (AOM)&lt;/a&gt; is an experimental JavaScript API that allows interaction with the accessibility tree directly without using attributes, such as &lt;code&gt;button.accessibleNode.expanded = true&lt;/code&gt;. This could greatly help developers, as it would eliminate the need to rely on easy-to-break IDs everywhere. However, as everything accessibility related, it might be years before it is widely supported...&lt;/p&gt;

</description>
      <category>html</category>
      <category>javascript</category>
      <category>a11y</category>
      <category>vue</category>
    </item>
    <item>
      <title>Customize TypeScript syntax highlighting in VSCode</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Tue, 07 Jan 2025 18:11:18 +0000</pubDate>
      <link>https://dev.to/365talents/customize-typescript-code-in-vscode-gh4</link>
      <guid>https://dev.to/365talents/customize-typescript-code-in-vscode-gh4</guid>
      <description>&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%2Fkpxnecn1x4j1sezc8mlk.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.amazonaws.com%2Fuploads%2Farticles%2Fkpxnecn1x4j1sezc8mlk.jpg" alt="VSCode TypeScript Syntax Highlighting" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TL;DR show me the code&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript can be intimidating&lt;/strong&gt; at first glance, especially when you're used to the relative &lt;strong&gt;simplicity of JavaScript&lt;/strong&gt; (or allergic to the verbosity of other languages you might have studied, like me with Java). When I started learning it a few years ago, I found the code harder to scan, as if &lt;strong&gt;everything was slightly less readable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I then searched for a way to better &lt;strong&gt;distinguish new code from existing code&lt;/strong&gt;, and started exploring how VSCode handles visual themes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editor Tokens &amp;amp; Scopes
&lt;/h2&gt;

&lt;p&gt;Similar to an Abstract Syntax Tree (AST), &lt;strong&gt;VSCode breaks down the code in each file into tokens&lt;/strong&gt; to create an abstract view of it, making it easier to manipulate. This abstraction is then used to build themes and analyze the code.&lt;/p&gt;

&lt;p&gt;To better understand this functionality, there are guides on &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide" rel="noopener noreferrer"&gt;syntax highlighting&lt;/a&gt; and &lt;a href="https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide" rel="noopener noreferrer"&gt;semantic highlighting&lt;/a&gt;. The former focuses on &lt;strong&gt;static&lt;/strong&gt; analysis, while the latter &lt;strong&gt;understands&lt;/strong&gt; the code (for example, recognizing that a variable is actually a parameter and thus ensuring it retains the same color for consistency across occurrences):&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%2Fjavfz0eb9875afvtyndj.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%2Fjavfz0eb9875afvtyndj.png" alt="image" width="774" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simply put, each character in your code is assigned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;token type&lt;/strong&gt;: String, Comment, ... (&lt;a href="https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#standard-token-types-and-modifiers" rel="noopener noreferrer"&gt;complete list here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;One or more &lt;strong&gt;scopes&lt;/strong&gt;, which act like subsets or groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extensions can add new scopes, for example, for Vue files (Single File Components), which contain HTML, JavaScript, and CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customization
&lt;/h2&gt;

&lt;p&gt;To help us understand this system, VSCode offers a command &lt;strong&gt;"Developer: Inspect Editor Tokens and Scopes"&lt;/strong&gt; that displays a tooltip with information about the currently selected code. Here's an example with the &lt;code&gt;compact&lt;/code&gt; function from &lt;a href="https://lodash.com/" rel="noopener noreferrer"&gt;Lodash&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fmap74sf7huj1rd3mc9lo.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%2Fmap74sf7huj1rd3mc9lo.png" alt="VSCode Developer: Inspect Editor Tokens and Scopes example" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s a lot of information, but the only thing we care about for now is the list of scopes; &lt;strong&gt;the most specific scopes are listed first&lt;/strong&gt;. We can use these to target the elements we want to customize.&lt;/p&gt;

&lt;p&gt;Next, we move on to the VSCode settings to adjust the theme to our needs. The &lt;a href="https://code.visualstudio.com/docs/getstarted/themes#_customizing-a-color-theme" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; explains that you need to edit your &lt;code&gt;settings.json&lt;/code&gt; file as follows (use the "Preferences: Open User Settings (JSON)" command):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor.tokenColorCustomizations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"textMateRules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I navigated through the code over the weeks and experimented with rule specificity, I came up with this list of scopes (which is likely not exhaustive):&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor.tokenColorCustomizations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"textMateRules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"support.type.builtin.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.interface.ts punctuation.definition.block.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.annotation.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.declaration.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"entity.name.type.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"support.type.primitive.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"entity.name.type.alias.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"entity.name.type.interface.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"storage.type.interface.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"keyword.operator.type.annotation.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"punctuation.definition.typeparameters.begin.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"punctuation.definition.typeparameters.end.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.object.type.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.parameters.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.object.member.ts meta.type.tuple.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"keyword.control.satisfies.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"keyword.control.as.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.declaration.ts punctuation.definition.block.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.declaration.ts keyword.operator.assignment.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.parameters.ts punctuation.separator.comma.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.parameters.ts punctuation.definition.block.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.parameters.ts meta.brace.square.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.annotation.ts keyword.operator.type.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.annotation.ts punctuation.terminator.statement.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.annotation.ts punctuation.definition.block.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.type.annotation.ts string.quoted.single.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.return.type.arrow.ts meta.type.tuple.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"keyword.operator.definiteassignment.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"keyword.operator.optional.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"storage.type.type.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"meta.interface.ts"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"fontStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"italic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"foreground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#ff0000"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, all scopes end with .ts, and scopes can be further refined to ensure they’re nested within another using a space (similar to a CSS selector): "meta.type.annotation.ts keyword.operator.type.ts".&lt;/p&gt;

&lt;p&gt;The only thing left is to decide which styles to apply. Unfortunately, the options are limited: just text color and a few font styles (bold, italic, underline, strikethrough), or a combination of these. To avoid color conflicts with a theme, I chose to apply italics, which remain fairly discreet in daily use. I had considered using a very light background color, but this still isn’t possible in VSCode (open ticket since 2016).&lt;/p&gt;

&lt;p&gt;Here’s the result on an example from the TypeScript documentation with "fontStyle": "italic":&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%2Ftoqoyqbgrr73h1d6gwgl.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%2Ftoqoyqbgrr73h1d6gwgl.png" alt="image" width="800" height="649"&gt;&lt;/a&gt;{width="495"}&lt;/p&gt;

&lt;p&gt;Feel free to find what works best for you, for example, bold with "fontStyle": "bold":&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%2Fpx0czk5oj3jeubnn5f2v.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%2Fpx0czk5oj3jeubnn5f2v.png" alt="image" width="800" height="647"&gt;&lt;/a&gt;{width="499"}&lt;/p&gt;

&lt;p&gt;You can even experiment with more unusual styles, such as &lt;code&gt;filter: drop-shadow()&lt;/code&gt; (it’s more complicated; here’s how the MoonLight theme does it)](&lt;a href="https://github.com/robb0wen/synthwave-vscode/blob/ec7e97eba96febbcf069256a6513ecedd0b187ae/README.md)):" rel="noopener noreferrer"&gt;https://github.com/robb0wen/synthwave-vscode/blob/ec7e97eba96febbcf069256a6513ecedd0b187ae/README.md)):&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F4txkd1yjtlikjbgo0xdo.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%2F4txkd1yjtlikjbgo0xdo.png" alt="image" width="800" height="635"&gt;&lt;/a&gt;{width="501"}&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For me, this trick helped a lot in the beginning to better digest TypeScript's new concepts. I could remove this customization now, but I’ve grown used to it and kept it. I hope it helps some of you get started with TypeScript!&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Using BroadcastChannel API with Vue to sync a ref across multiple tabs</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Mon, 02 Dec 2024 15:32:48 +0000</pubDate>
      <link>https://dev.to/365talents/using-broadcastchannel-api-with-vue-to-sync-a-ref-across-multiple-tabs-h87</link>
      <guid>https://dev.to/365talents/using-broadcastchannel-api-with-vue-to-sync-a-ref-across-multiple-tabs-h87</guid>
      <description>&lt;p&gt;We try to save time for everyone in our team, not only developers: some people spend a lot of time configuring our app for clients or demos, so we make sure it's as smooth as possible. For instance, we try to make every change real time, so there's no need to reload the app for changes to appear.&lt;/p&gt;

&lt;p&gt;Recently, we figured out those people usually work with &lt;strong&gt;multiple tabs open&lt;/strong&gt;, making sure the configuration works as expected in multiple pages of the app. So we thought about syncing the configuration across tabs.&lt;/p&gt;

&lt;p&gt;We didn't want to store it, neither in session, local storage or anything else, as we will then have to make sure it's always up to date.&lt;/p&gt;

&lt;p&gt;That's when we came across the BroadcastChannel API, I didn't even know it existed. It's not fairly new, but Safari was the last to implement it according to &lt;a href="https://caniuse.com/broadcastchannel" rel="noopener noreferrer"&gt;CanIUse&lt;/a&gt;. Anyway it's largely supported now. You can think of it like the good old &lt;code&gt;window.postMessage()&lt;/code&gt; from &lt;code&gt;iframe&lt;/code&gt;, but across multiple tabs of the same origin.&lt;/p&gt;

&lt;p&gt;Luckily for us, VueUse already made a little composable to ease its usage: &lt;a href="https://vueuse.org/core/useBroadcastChannel/#usebroadcastchannel" rel="noopener noreferrer"&gt;https://vueuse.org/core/useBroadcastChannel/#usebroadcastchannel&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isSupported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isClosed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBroadcastChannel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unique-name&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;So we created a little in house composable based on it to make sure a &lt;code&gt;ref&lt;/code&gt; is always synchronized among all tabs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useBroadcastChannel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchPausable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vueuse/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ref&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&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;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;deep&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="c1"&gt;// Name must be unique&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;post&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useBroadcastChannel&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// When value changes locally, update other tabs&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;pause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resume&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;watchPausable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// When value changes in another tab, update it locally&lt;/span&gt;
  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&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="c1"&gt;// Prevent watch loop when updating config&lt;/span&gt;
      &lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;options&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;So now we can sync a ref with a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
&lt;span class="nf"&gt;useSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&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;deep&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And voilà! That's some hours saved each month across teams 🥳&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Incrementally fixing lots of ESlint errors in a clean way with ESlint Nibble</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Mon, 18 Nov 2024 16:10:46 +0000</pubDate>
      <link>https://dev.to/365talents/incrementally-fixing-lots-of-eslint-errors-in-a-clean-way-30g9</link>
      <guid>https://dev.to/365talents/incrementally-fixing-lots-of-eslint-errors-in-a-clean-way-30g9</guid>
      <description>&lt;p&gt;As our team is growing, we need more &lt;strong&gt;functional&lt;/strong&gt; and &lt;strong&gt;aesthetic rules&lt;/strong&gt; to keep our codebase &lt;strong&gt;less error-prone&lt;/strong&gt; and &lt;strong&gt;more consistent&lt;/strong&gt;. Fortunately, both our backend and frontend use JavaScript (with the same version of TypeScript) so those changes make a big impact on our daily work for the whole team.&lt;/p&gt;

&lt;p&gt;We usually do this kind of improvements on what we call "tech days" (on monday every two weeks) when we can work on tasks we choose ourselves. We try to do them &lt;strong&gt;incrementally to avoid long lasting branches&lt;/strong&gt; that are hard to maintain and review.&lt;/p&gt;

&lt;p&gt;Recently, we chose to add the &lt;a href="https://github.com/sindresorhus/eslint-plugin-unicorn" rel="noopener noreferrer"&gt;Unicorn plugin&lt;/a&gt;, which contain dozens of ESlint rules. It can feel overwhelming at first because it triggers hundreds of errors. Fortunately, we discovered &lt;a href="https://github.com/IanVS/eslint-nibble" rel="noopener noreferrer"&gt;&lt;code&gt;eslint-nibble&lt;/code&gt;&lt;/a&gt;: a command line tool that helps adding rules one by one with a graphical interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;p&gt;The first step is to install the new plugin we want to apply. Then, instead of enabling the rules one by one, we can now enable them all in &lt;code&gt;.eslintrc.js&lt;/code&gt;. Finally, instead of linting everything at once with &lt;code&gt;eslint&lt;/code&gt;, we can simply use &lt;code&gt;eslint-nibble&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx eslint-nibble &lt;span class="nt"&gt;--fixable-only&lt;/span&gt; &lt;span class="nt"&gt;--no-warnings&lt;/span&gt; &lt;span class="nt"&gt;--cache&lt;/span&gt; &lt;span class="s2"&gt;"./src/**/*.{ts,js,vue}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;it speeds up our process at first with &lt;code&gt;--fixable-only&lt;/code&gt; rules&lt;/li&gt;
&lt;li&gt;since we don't make CI fail on warnings, we use &lt;code&gt;--no-warnings&lt;/code&gt; to clear things up&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;--cache&lt;/code&gt; option is similar to the ESlint one and will make the repeated process much faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're then welcomed by a nice graphic that shows every failing rule with the count of errors for each one:&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%2Ftrz3wvpzr61ajdpsqxm0.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%2Ftrz3wvpzr61ajdpsqxm0.png" alt="graphic that shows every failing rule with the count of errors for each one" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When selecting a rule through its interface, we're proposed to autofix it (if possible). Then, we usually review it manually before committing, just in case something odd pops up.&lt;/p&gt;

&lt;p&gt;Another key point is that it's capable of fixing one and only one rule at a time, even if a line a code contains multiple rule errors. It makes the commits atomic, so it's easy to debug and review for instance.&lt;/p&gt;

&lt;p&gt;If you make any additional change before committing — for example we sometimes need to apply Prettier on the changed line — make sure you &lt;strong&gt;don't save &amp;amp; lint the file&lt;/strong&gt;, because it would fix all other errors related to other rules in the file. The correct way is to manually fix the needed problem only by focusing the line and running &lt;code&gt;Quick fix&lt;/code&gt; , then use the command &lt;code&gt;Fix without formatting&lt;/code&gt; of VS Code.&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%2F79nagyxjutw9a6dw2u7s.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%2F79nagyxjutw9a6dw2u7s.png" alt="Example of multiple lint errors on the same line" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;p&gt;The main advantage is to easily &lt;strong&gt;see which rules are the simplest to add next&lt;/strong&gt;. Each day, we can now choose to fix one rule with a lot of errors, or a lot of rules that have only a few occurrences. Previously, we were blindly enabling rules one by one without knowing their impact in advance.&lt;/p&gt;

&lt;p&gt;It's also an opportunity to &lt;strong&gt;understand the rule itself&lt;/strong&gt; by reading its documentation and why it's better (or not) to do it this way, so we &lt;strong&gt;learn new things&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We sometimes decide to &lt;strong&gt;customize or completely disable a rule&lt;/strong&gt; because it doesn't fit our needs or code style. For instance, we decided to disable a rule forcing to use &lt;code&gt;Set&lt;/code&gt; in some cases: as Vue 2 doesn't support reactivity on &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt;, we thought it could introduce bugs or encourage developers to use it in an unexpected way.&lt;/p&gt;

&lt;p&gt;Finally, it's easier for reviewers to read commits about one rule at a time. This tool makes &lt;strong&gt;progressive enhancement&lt;/strong&gt; a breeze by helping us adding a few ESlint rules from time to time.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>eslint</category>
    </item>
    <item>
      <title>How we migrated from Vue 2 to Vue 3</title>
      <dc:creator>Nico Prat</dc:creator>
      <pubDate>Tue, 12 Nov 2024 13:16:11 +0000</pubDate>
      <link>https://dev.to/365talents/how-we-migrated-from-vue-2-to-vue-3-3ld4</link>
      <guid>https://dev.to/365talents/how-we-migrated-from-vue-2-to-vue-3-3ld4</guid>
      <description>&lt;p&gt;About one year ago we finally migrated from Vue 2 to Vue 3. It was 6 months before its official end of life. At the time, our app has around 100 pages and 300 components, and used some of the classic dependencies tied to Vue: Vue-router, Pinia (and Pinia ORM), Vue-i18n, TipTap, ElementUI (ElementPlus for Vue 3)&lt;/p&gt;

&lt;p&gt;Here are a few advices that helped us. As Vue 2 still work pretty well, you better take the time to ease the migration instead of rushing it, breaking your app and destroying your moral!&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Before starting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Warn others
&lt;/h3&gt;

&lt;p&gt;Things will certainly break, even a little. You better make sure it's ok with other teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrade to latest Vue 2.7
&lt;/h3&gt;

&lt;p&gt;Obviously, as most of the new features are available in Vue 2.7, and retrocompatible, it's important to catch up if you don't already have. While migrating to Vue 3 you'll still be able to enjoy its features. The step to migrate will also be smaller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Take the time
&lt;/h3&gt;

&lt;p&gt;This can be a pretty big work so you better anticipate to mitigate the risk over a long period. Also, it helped us (the developers) keep the moral! Plan a few weeks or even months, as you know when you start, but you don't know when you'll finish... Finally, it's better to dedicate some time every week, so any outage won't be disastrous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a team
&lt;/h3&gt;

&lt;p&gt;Don't rely on only one developer if possible, as it's an exhausting task. Also, you need quick feedback &amp;amp; merge loops, because a lot of changes will affect the entire codebase and could create conflicts on a daily basis. It's already hard enough without having to handle this!&lt;/p&gt;

&lt;h3&gt;
  
  
  Test driven migration
&lt;/h3&gt;

&lt;p&gt;Half of the time will be spent testing the whole app, so you better automate it the most possible. In our experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests are not very useful, as they're usually testing things not really tied to Vue (only "pure JS" functions for instance), se we didn't focus on them&lt;/li&gt;
&lt;li&gt;Component testing with &lt;a href=""&gt;vue-test-utils&lt;/a&gt; were a pain to migrate, in the end we even had to disable some of them&lt;/li&gt;
&lt;li&gt;End to end tests were the most valuable as it's not tied to any JS or Vue internals and failed as soon as a error was thrown away in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In conclusion the best is to create so-called "smoke test", where you just navigate within the app with the most basic scenario you can think of. Keep them simple, so they're fast, because the sooner you can make them run, so the more useful they'll be. Bonus point if they run before merging (in a CI for instance).&lt;/p&gt;

&lt;h3&gt;
  
  
  Type everything
&lt;/h3&gt;

&lt;p&gt;TypeScript helped a bit, but support for it in Vue 2 is still poor. Using TypeScript is still a good idea, but it might not help too much there; Once using Vue 3, the &lt;code&gt;setup&lt;/code&gt; syntax will be your best friend though!&lt;/p&gt;

&lt;h3&gt;
  
  
  Lint everything
&lt;/h3&gt;

&lt;p&gt;It's obvious, but a lot of changes will happen in the code, so does a lot of errors. A simple ESLint / Prettier can save a lot of time. If you already do have them, you can check the new rules for Vue 3 meanwhile you migrate: &lt;a href=""&gt;https://eslint.vuejs.org/rules/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Vite
&lt;/h3&gt;

&lt;p&gt;We already had migrated from Webpack to Vite before beginning migrating Vue, so I'm not sure it helps a lot, but as it's the clear standard today, some plugins might not even give instruction for upgrade with Webpack. I guess it's safer to do it first as Vite supports Vue 2, but in the opposite the Vue 3 ecosystem might not support Webpack.&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Small steps first
&lt;/h2&gt;

&lt;p&gt;The goal is to have the less changes possible when actually upgrading Vue itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrade dependencies
&lt;/h3&gt;

&lt;p&gt;Take a look at every Vue-related dependency you use, and check if there's a version that support both Vue 2 and Vue 3, and upgrade to it. &lt;a href=""&gt;Vue-demi&lt;/a&gt; helped a lot plugin maintainers to achieve it, so there's a good chance it's available.&lt;/p&gt;

&lt;p&gt;For instance, you might want to migrate from Vuex to Pinia, which support both versions (Vuex still requires &lt;a href="https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html" rel="noopener noreferrer"&gt;a small migration&lt;/a&gt;) and is the new standard anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace dependencies
&lt;/h3&gt;

&lt;p&gt;For other dependencies, you might want to check if any modern alternative could suit your needs. For instance, we replaced &lt;a href=""&gt;vue-mq&lt;/a&gt; by &lt;a href=""&gt;vue-use&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monkeypatch when needed
&lt;/h3&gt;

&lt;p&gt;We had to be pragmatic, so as some migrations were harder than others, we ended up creating a small layer of abstractions to fix it. For instance, we had a hard time with vue-i18n v9 which brings some (sometimes undocumented) breaking changes. So we finally created a helper that expose a custom version of &lt;code&gt;$t&lt;/code&gt; to prevent having to rewrite every component (because this most used function now doesn't accept "nullish" values for basically no reason). In other terms, don't be too perfectionist!&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Start migrating
&lt;/h2&gt;

&lt;p&gt;Now the hard work begins. Everything we did before will ease the pain. There's no magic here, it will probably be the hardest part of the migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change everything, but nothing
&lt;/h3&gt;

&lt;p&gt;Replace Vue 2 by Vue 3 with the "&lt;a href=""&gt;migration build&lt;/a&gt;" (also called compat mode) by following the &lt;a href=""&gt;instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Turn off every &lt;a href=""&gt;available flag&lt;/a&gt; so the app runs almost as it did with version 2.&lt;/p&gt;

&lt;p&gt;Check that no &lt;a href=""&gt;breaking change&lt;/a&gt; is impacting your app, or fix them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small steps
&lt;/h3&gt;

&lt;p&gt;&lt;a href=""&gt;Incrementally enable each flag &lt;/a&gt;and test the app thoroughly. If some components are too hard to migrate, keep in mind you can &lt;a href=""&gt;override its compatibility option&lt;/a&gt;; so you better merge everything but keep a few components to migrate later instead of waiting for the whole app to work right now. Don't try to fix everything at once, let some days pass so you can catch any bugs (and recharge your mental health).&lt;/p&gt;

&lt;h3&gt;
  
  
  Big steps
&lt;/h3&gt;

&lt;p&gt;Some dependencies don't provide shortcuts though. In our case, migrating our UI library from ElementUI (Vue 2) to ElementPlus (Vue 3) was hard, and couldn't be split into smaller steps. I heard it was even harder for Vuetify. For this, you'll have to be strong, patient, and take time to do it all at once. Maybe allow a full week for it in your team calendar!&lt;/p&gt;

&lt;p&gt;Remember that you can still override the compatibility behavior of each component you import at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ElButton&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;element-plus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;ElButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compatConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;FEATURE_ID_A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// features can also be toggled at component level&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Last step
&lt;/h3&gt;

&lt;p&gt;Once every flag has been turned on, it's time to remove the migration build. Hopefully you won't discover new unexpected issues by now!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't forget to celebrate, you deserved it! 🥳&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
