<?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: Stefan Wienert</title>
    <description>The latest articles on DEV Community by Stefan Wienert (@zealot128).</description>
    <link>https://dev.to/zealot128</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%2Fuser%2Fprofile_image%2F283712%2F67215164-5917-4f6c-a844-6576debec27d.jpeg</url>
      <title>DEV Community: Stefan Wienert</title>
      <link>https://dev.to/zealot128</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zealot128"/>
    <language>en</language>
    <item>
      <title>Migrating from (Rails) Webpack(er) to Vite</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Mon, 11 Jul 2022 09:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/migrating-from-rails-webpacker-to-vite-1f5c</link>
      <guid>https://dev.to/zealot128/migrating-from-rails-webpacker-to-vite-1f5c</guid>
      <description>&lt;p&gt;After introducing the Rails wrapper &lt;code&gt;webpacker&lt;/code&gt; in 2018/2019 into our projects, it has been a great addition and helped to propel our Javascript frontend development. But since a couple of months, there are other alternatives to consider. Rails in general likes to move to more simpler Import-Maps by default, or using esbuild directly. But in our case, having a separate build server that enables productivity enhancing features such as Hot Module Reload, or playing around with SSR, is a much more useful alternative for us. That’s why, after evaluating a couple of other options, we are going to Vite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why move?
&lt;/h2&gt;

&lt;p&gt;Compared to Webpack, it’s much &lt;strong&gt;faster&lt;/strong&gt; , as it does less and relies more on the Module loading (and thus, delayed Runtime errors) in the browser. Especially the initial start took like 1-2 minute with Webpack and after a while the server took several (~1-4) gigabyte of RAM (multiplied by Number of Projects Times Number of Developers can grow to unhealthy limits) and needed regular restarts.&lt;/p&gt;

&lt;p&gt;Configuration fatigue: Starting with Webpack 2, and going up to 4, migrating between the Rails wrapper Webpacker 3 - 5, it found it is a major PITA to manage all the details, such as Babel-config, loaders, plugins and many other configuration snippets. Vite is a more opinionated approach and works OOTB without many plugins - We only use Vue plugin, the rest, like Sass, Typescript, even Pug, just work if you have the related packages installed). For our migration, the &lt;code&gt;vite.config.js&lt;/code&gt; is much smaller than the whole of Webpack config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git diff --shortstat origin/master -- package.json vite.config.js config/webpack config/webpacker.yml
 11 files changed, 91 insertions(+), 255 deletions(-)

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

&lt;/div&gt;



&lt;p&gt;Comparing only the dep-size of the yarn.lock is even more shocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git diff --shortstat origin/master -- yarn.lock
 1 file changed, 1358 insertions(+), 7642 deletions(-)

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

&lt;/div&gt;



&lt;p&gt;Vite (as does Webpack) supports configuration options for using a public development proxy that is handled by a Reverse Proxy. In our case, we mostly work on a central server, so everybody got a couple of ports assigned to map to frontend https. Vite can be configured via &lt;code&gt;server.hmr.clientPort&lt;/code&gt; + &lt;code&gt;server.hmr.host&lt;/code&gt; and still support HMR live updates via websockets, which was not the case for some other bundlers.&lt;/p&gt;

&lt;p&gt;The biggest drawback against Vite would be, that we could not use &lt;code&gt;wkhtmltopdf&lt;/code&gt; with styled documents anymore. For this particular project, that is not necessary, but in the future other projects, that generate styled PDFs must be migrated to puppeteer or similar (&lt;a href="https://github.com/ElMassimo/vite_ruby/discussions/201"&gt;See the discussion on Github&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration
&lt;/h2&gt;

&lt;p&gt;At the beginning, I’ve tried 2 automatic migration tool, that did almost nothing. I throw them away and started with a fresh vite-config generated by &lt;code&gt;vite-ruby&lt;/code&gt;. Then I replaced all &lt;code&gt;javascript_pack_tag&lt;/code&gt; with &lt;code&gt;vite_javascript_tag&lt;/code&gt; and &lt;strong&gt;removed the &lt;code&gt;stylesheet_pack_tag&lt;/code&gt;&lt;/strong&gt; (Vite only needs the entrypoint javascript, and then it will include all dependencies and generated stylesheets). Then, I removed all of webpack, webpacker and babel: &lt;code&gt;babelrc&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;Gemfile&lt;/code&gt;, &lt;code&gt;config/webpack*&lt;/code&gt;, &lt;code&gt;bin/webpack*&lt;/code&gt;. You may keep postcss.yml.&lt;/p&gt;

&lt;p&gt;The various problems that occurred afterwards were solved as followed below:&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix import aliases
&lt;/h3&gt;

&lt;p&gt;To reference our own Javascript modules, with used plain path without &lt;code&gt;@/&lt;/code&gt; or &lt;code&gt;~/&lt;/code&gt; at the beginning.&lt;/p&gt;

&lt;p&gt;Like:&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;import&lt;/span&gt; &lt;span class="nx"&gt;Stuff&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;admin/Stuff.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;while &lt;code&gt;admin/Stuff.vue&lt;/code&gt; would be located in &lt;code&gt;app/javascript/admin/Stuff.vue&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With Vite, the simplest approach seemed to be to define an &lt;strong&gt;alias&lt;/strong&gt; for each folder in app/javascript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&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="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;candi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/candi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/channels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;hrfilter_check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/hrfilter_check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;job-booking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/job-booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/registration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/assets/images&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Or automatic:&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;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lstatSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;isDirectory&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;aliasesFromJavascriptRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="nx"&gt;directories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&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;aliasesFromJavascriptRoot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&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="nx"&gt;aliasesFromJavascriptRoot&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;In any case, you can also rename all the imports and prefix them with “@/”, but it’s harder to automate, and the method above worked great!&lt;/p&gt;

&lt;h3&gt;
  
  
  require.context - load a whole folder into Javascript
&lt;/h3&gt;

&lt;p&gt;In some cases, we were using &lt;code&gt;require.context&lt;/code&gt; to load a whole folder of javascript into a module.&lt;/p&gt;

&lt;p&gt;In one case, it was simplest to &lt;strong&gt;just inline&lt;/strong&gt; all the necessary imports under each other. That makes it also easier for Typescript to analyze imports.&lt;/p&gt;

&lt;p&gt;If you want to keep the behavior of loading a whole folder, Vite has a similiar thing, called import.meta.globEager.&lt;/p&gt;

&lt;p&gt;So, if you want to load your Stimulus controllers like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webpack:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;controllers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/_controller&lt;/span&gt;&lt;span class="se"&gt;\.[&lt;/span&gt;&lt;span class="sr"&gt;jt&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;s$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;definitionsFromContext&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…then, have a look at ElMassimo’s repo: &lt;a href="https://github.com/ElMassimo/stimulus-vite-helpers"&gt;https://github.com/ElMassimo/stimulus-vite-helpers&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Import needs file suffix for non-js/ts files
&lt;/h3&gt;

&lt;p&gt;Vue-files need to be fully specified with suffix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import FooComponent from './FooComponent'
&lt;/span&gt;&lt;span class="gi"&gt;+ import FooComponent from './FooComponent.vue'
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same with lazy loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   components: {
&lt;span class="gd"&gt;- "backlink-modal": () =&amp;gt; import("../modals/BacklinkModal"),
&lt;/span&gt;&lt;span class="gi"&gt;+ "backlink-modal": () =&amp;gt; import("../modals/BacklinkModal.vue"),
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same was true when loading &lt;strong&gt;scss&lt;/strong&gt; files from Javascript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Import GraphQL queries
&lt;/h3&gt;

&lt;p&gt;In some cases, we are importing GraphQL-queries, like:&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;import&lt;/span&gt; &lt;span class="nx"&gt;FETCH_USER&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;../queries/FetchUser.graphql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;There are a myriad of not-functioning plugins out there. In the end &lt;strong&gt;@rollup/plugin-graphql&lt;/strong&gt; worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// yarn add @rollup/plugin-graphql&lt;/span&gt;
&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;graphql&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;@rollup/plugin-graphql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nx"&gt;plugins&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="nx"&gt;graphql&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;h3&gt;
  
  
  require not found / not a function
&lt;/h3&gt;

&lt;p&gt;Vite does not support commonjs import anymore! Most dependencies already support the ESM (EcmaScript Modules import/export syntax), some older not just needed a upgrade. So usually it was enough to change it 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;# find all usages of require("foo")
rg -F 'require('


- const qs = require("qs")
+ import qs from 'qs'

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

&lt;/div&gt;



&lt;p&gt;Sentry error tracking also changed the import, but &lt;code&gt;* as&lt;/code&gt; was needed to added here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-const Sentry = require('@sentry/browser')
&lt;/span&gt;&lt;span class="gi"&gt;+import * as Sentry from '@sentry/browser';
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We dropped &lt;code&gt;moment.js&lt;/code&gt; and replaced it with &lt;code&gt;dayjs&lt;/code&gt; or straight &lt;code&gt;Intl&lt;/code&gt;. Dayjs is for the most part compatible with momentjs and in many cases a simple search &amp;amp; replace is all you need (After configuring the necessary plugins for Dayjs)&lt;/p&gt;

&lt;h3&gt;
  
  
  Scss Postcss import
&lt;/h3&gt;

&lt;p&gt;When removing &lt;code&gt;@rails/webpacker&lt;/code&gt; from the package.json, the deps that it brought, like sass, postcss, were missing now, so we added it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add postcss-import postcss-preset-env postcss sass

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

&lt;/div&gt;



&lt;p&gt;When importing deps, reference it without the &lt;code&gt;~&lt;/code&gt; at the beginning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- @import "~bootstrap-vue/src/index";
&lt;/span&gt;&lt;span class="gi"&gt;+ @import "bootstrap-vue/src/index";
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had one Stylesheet theme (Bootswatch) which did not work, as it always wanted to include a Google Font. We copied the Scss file from the module into our repo and deleted the font import.&lt;/p&gt;

&lt;p&gt;Also, we are using an older SASS version, so we received a lot of Scss warnings every time. The best way seems to be to downgrade sass for now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add sass@1.32.0

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  vuelidate in test/production
&lt;/h3&gt;

&lt;p&gt;One page failed ONLY in test mode when running the full suite, using development server worked always! It was a frustrating and time consuming thing to debug, First I thought vue-router is the culprit, but after forcing sourcemaps and using Chrome to get onto the test-server, I found that &lt;code&gt;vuelidate&lt;/code&gt; was the thing responsible.&lt;/p&gt;

&lt;p&gt;The error I received, was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; TypeError: can't access property "components", e is undefined

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

&lt;/div&gt;



&lt;p&gt;Which, according to &lt;a href="https://stackoverflow.com/questions/55672109/typeerror-cannot-read-property-components-of-undefined-in-vue2"&gt;Stackoverflow&lt;/a&gt; was related to mixins, which are using with Vuelidate’s ValidationMixin. It seemed, that in production builds, Vite (Rollup) dropped the content, and just imported “undefined”.&lt;/p&gt;

&lt;p&gt;So I tried to just change the import syntax slightly:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validationMixin&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;vuelidate/src/index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validationMixin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// works!&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;maxLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vuelidate/lib/validators&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And also disabling optimization for this particular dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vuelidate&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;Then it worked! If this helped you, make sure to &lt;a href="https://stackoverflow.com/a/72924881/220292"&gt;upvote my Stackoverflow answer&lt;/a&gt; ;)&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy / IE11
&lt;/h3&gt;

&lt;p&gt;I checked our access logs, and it seems that the customers using that particular service are finally free from using Internet explorer 11.&lt;/p&gt;

&lt;p&gt;But some customers are using older Firefox versions (ESR), so adding a &lt;code&gt;legacy&lt;/code&gt; polyfill etc. was necessary.&lt;/p&gt;

&lt;p&gt;Have a look at the final &lt;code&gt;vite.config.js&lt;/code&gt; (Without Sentry):&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;legacy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vitejs/plugin-legacy&lt;/span&gt;&lt;span class="dl"&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;createVuePlugin&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;vite-plugin-vue2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RubyPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite-plugin-ruby&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FullReload&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite-plugin-full-reload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;graphql&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;@rollup/plugin-graphql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAILS_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lstatSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;isDirectory&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;aliasesFromJavascriptRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="nx"&gt;directories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&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;aliasesFromJavascriptRoot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;hmr&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 our case the environment variable VITE_WSS_HOST is set by our system&lt;/span&gt;
      &lt;span class="c1"&gt;// so VITE knows were it should direct the wss://HOST requests&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_WSS_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss&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;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&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="nx"&gt;aliasesFromJavascriptRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/assets/images&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;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;RubyPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;createVuePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsx&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="nx"&gt;FullReload&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config/routes.rb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/views/ **/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/controllers/** /*&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;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;isProd&lt;/span&gt;
    &lt;span class="c1"&gt;// if you want to support older browsers.&lt;/span&gt;
    &lt;span class="c1"&gt;// make sure, you have core-js 3 installed&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;defaults&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;polyfills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es.promise.finally&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es/map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es/set&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;additionalLegacyPolyfills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;regenerator-runtime/runtime&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="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="na"&gt;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vuelidate&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;vuelidate/lib&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;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// keep files between deployments - we are using capistrano-like deployment,&lt;/span&gt;
    &lt;span class="c1"&gt;// if you are using Docker or Heroku, then you can leave it&lt;/span&gt;
    &lt;span class="na"&gt;emptyOutDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isProd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// force sourcemaps in test, too!&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

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

&lt;/div&gt;



</description>
      <category>rails</category>
      <category>javascript</category>
      <category>webpacker</category>
      <category>vite</category>
    </item>
    <item>
      <title>Improving Typescript experience in Rails by generating a Typescript schema from Rails models (with enums and associations)</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Wed, 15 Sep 2021 14:01:17 +0000</pubDate>
      <link>https://dev.to/zealot128/improving-typescript-experience-in-rails-by-generating-a-typescript-schema-from-rails-models-with-enums-and-associations-5a13</link>
      <guid>https://dev.to/zealot128/improving-typescript-experience-in-rails-by-generating-a-typescript-schema-from-rails-models-with-enums-and-associations-5a13</guid>
      <description>&lt;p&gt;For a side project (which I won’t reveal at the moment), I develop most of the frontend in Vue/TSX+Typescript. At the moment, I don’t have a specific “API”, but just use Inertia.js with a couple of main models that are simply serialized by &lt;code&gt;model.as_json&lt;/code&gt;. Having then to type out a deep model schema with dozens of connected models (using delegated types in my case) by hand is a PITA. Fortunately, I came across &lt;a href="https://github.com/kawamataryo/schema2type"&gt;schema2type&lt;/a&gt;, which is a Rails plugin that generates a Typescript schema for all of your models by only using &lt;code&gt;schema.rb&lt;/code&gt;, &lt;strong&gt;BUT&lt;/strong&gt; because of this constraint, it cannot know about Rails enums and associations.&lt;/p&gt;

&lt;p&gt;This is why I quickly cobbled together a script that improves on that idea:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/zealot128/419949f1c426330493c84bb8eadc4533"&gt;&lt;strong&gt;Gist&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To “install” and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://gist.github.com/zealot128/419949f1c426330493c84bb8eadc4533/raw/1c4629c05506630d3fe8543320cd6d2026405d8e/rails-models-to-typescript-schema.rb

rails runner rails-models-to-typescript-schema.rb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; app/javascript/types/schema.d.ts

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

&lt;/div&gt;



&lt;p&gt;Afterwards you can easily use &lt;code&gt;schema.MyModel.&amp;lt;TAB&amp;gt;&lt;/code&gt; to autocomplete all the things! In my usecase, I then built upon that schema to define more specific types that I serialize and pass to my frontend (via Inertia.js). It also works for the built-in ActiveStorage models and spit out this:&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="kr"&gt;declare&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageBlob&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;filename&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="nl"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;service_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="nl"&gt;byte_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;checksum&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="nl"&gt;created_at&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="nl"&gt;variant_records&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageVariantRecord&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageAttachment&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;preview_image_attachment&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageAttachment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;preview_image_blob&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageBlob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageVariantRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;blob_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;variation_digest&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="nl"&gt;image_attachment&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageAttachment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;image_blob&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageBlob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageBlob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageAttachment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&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="nl"&gt;record_type&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="nl"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;blob_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;created_at&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="nl"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ActiveStorageBlob&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 80 line script already supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;belongs_to, has_one and has_many associations (un-polymorphic)&lt;/li&gt;
&lt;li&gt;enums&lt;/li&gt;
&lt;li&gt;null and not null&lt;/li&gt;
&lt;li&gt;type mapping, borrowed from the original schema2type script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security implications: The script generates a type with all attributes, that includes password-digests, tokens etc. You probably shouldn’t send those informations to frontend clients in general. For the typings that should be fine though, because, typings are only used in development and discarded in a production build.&lt;/p&gt;

&lt;p&gt;To further improve your Typescript experience, have a look at &lt;code&gt;https://github.com/bitjourney/ts_routes-rails&lt;/code&gt; - IMO here you really should make sure to not leak Routes to clients that you don’t want them to access. Depending on your production build unused route helpers may or may not be removed due to treeshaking.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Previewing Rails 7 upcoming changes</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Tue, 24 Aug 2021 12:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/previewing-rails-7-upcoming-changes-j0p</link>
      <guid>https://dev.to/zealot128/previewing-rails-7-upcoming-changes-j0p</guid>
      <description>&lt;p&gt;Rails 7 is taking up speed. There is no beta out yet, but a lot of features, especially in ActiveRecord are available, if one want’s to wade through the Changelogs.&lt;/p&gt;

&lt;p&gt;To view all the changes per framework gem, just access the &lt;code&gt;main&lt;/code&gt; branch’s &lt;code&gt;CHANGELOG.md&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/activerecord/CHANGELOG.md"&gt;ActiveRecord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actionmailbox/CHANGELOG.md"&gt;ActionMailbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actionmailer/CHANGELOG.md"&gt;ActionMailer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actionpack/CHANGELOG.md"&gt;ActionPack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actiontext/CHANGELOG.md"&gt;ActionText&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actionview/CHANGELOG.md"&gt;ActionView&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/activejob/CHANGELOG.md"&gt;ActiveJob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/activemodel/CHANGELOG.md"&gt;ActiveModel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/actioncable/CHANGELOG.md"&gt;ActionCable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/activestorage/CHANGELOG.md"&gt;ActiveStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/activesupport/CHANGELOG.md"&gt;ActiveSupport&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/rails/blob/main/railties/CHANGELOG.md"&gt;Railties&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Too many? … Most of those changes are very specific and maybe not interesting for all audiences, and ActionText, ActionMailbox is not used by us. I collected the most interesting changes for me below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Active Record
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attribute level database encryption&lt;/strong&gt; The most relevant feature in the bunch, this will make it so much easier to be more secure “by default”. See the &lt;a href="https://edgeguides.rubyonrails.org/active_record_encryption.html"&gt;new guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Post.where(...).in_order_of(1, 2, 3)&lt;/code&gt; why would you need that? For example if you get the ordering from another source, like Redis inline calculations, Elasticsearch Results. You want to load the same models from the DB and keep the order from Elasticsearch.&lt;/li&gt;
&lt;li&gt;Tagging of SQL queries to make it easier to find the culprit in the slowlog: &lt;code&gt;config.active_record.query_log_tags_enabled = true&lt;/code&gt;. Controller &amp;amp; action name are already used as the default tags.&lt;/li&gt;
&lt;li&gt;Nulls First/Nulls Last can be specified, unfortunately only in the internal Arel, not in Relation AFAICS: &lt;code&gt;User.arel_table[:first_name].desc.nulls_last&lt;/code&gt; and &lt;code&gt;nulls_first&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MyModel.enumerate_columns_in_select_statements = true&lt;/code&gt; per Model 

&lt;ul&gt;
&lt;li&gt;Normally if you make &lt;code&gt;Post.first&lt;/code&gt; you will generate a &lt;code&gt;select * from&lt;/code&gt;. With this flag, AR will use the column names: &lt;code&gt;select title, description from posts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Enum: removing the underscore from prefix, suffix, default: &lt;code&gt;enum state: [], _prefix: :state, _suffix: :foo&lt;/code&gt; -&amp;gt; &lt;code&gt;prefix: suffix&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Load Async: Another maybe major feature: You are now able to parallelize the SQL loading, best on controller level, by appending &lt;code&gt;.load_aync&lt;/code&gt; the query:&lt;code&gt;@posts = Post.load_async&lt;/code&gt;I need to fiddle around with this feature and try to find limitations/thread limits etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;relation.excluding(post, another_post)&lt;/code&gt; replaces the common pattern of finding sibling objects without one self:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  class Post
    def simliar_posts
      user.posts.excluding(self)
    end

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timstamptz&lt;/code&gt; now supported as a column type - I have one app, where I only use structure.sql because of this. Happy to be able to go back to schema.rb&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;relation.where().invert_where&lt;/code&gt; nice convenience&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;relation.where.associated(:user)&lt;/code&gt; all records that have an association, shorthand for &lt;code&gt;where.not(user_id: nil)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;relation.where.missing(:user)&lt;/code&gt; opposite of above&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Predefined variants
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize: "100x100"
    attachable.variant :medium, resize: "300x300", monochrome: true
  end
  ...
  user.avatar.variant(:thumb)

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;nice! I used model methods that made the commonly used variants, e.g. company logo. Can remove that now.

&lt;ul&gt;
&lt;li&gt;vips is the new default, but mini_magick can still be used. To opt-in with old apps: &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.active_storage.variant_processor = :vips&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Analyzer improved: will determine if the file has audio and/or video data and prefills: &lt;code&gt;metadata[:audio]&lt;/code&gt; and &lt;code&gt;metadata[:video]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Previewing video files: FFMpeg args configurable under &lt;code&gt;config.active_storage.video_preview_arguments&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Audio analyzer added: duration + bitrate extracted&lt;/li&gt;
&lt;li&gt;blob expiration can be set individually per call:: &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rails_blob_path(user.avatar, disposition: "attachment", expires_in: 30.minutes)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Active Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Added a faster and more compact ActiveSupport::Cache serialization format. 

&lt;ul&gt;
&lt;li&gt;Opt-in: &lt;code&gt;config.active_support.cache_format_version = 7.0&lt;/code&gt;, it is backwards compatible&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Improved test parallelization - only in effect if testing more than one file etc. 

&lt;ul&gt;
&lt;li&gt;as RSpec user I look with envy to the easier parallelization features over at Test::Unit…&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rails app:update&lt;/code&gt; doesn’t overwrite &lt;code&gt;en.yml, routes.rb, puma.rb, storage.yml&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Nice! Always a nuisance when using that very very helpful task&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Quick benchmark: &lt;code&gt;Rails.benchmark("some info") { ... }&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;I guess that’s just an alias for &lt;code&gt;Benchmark.measure { ... }&lt;/code&gt;, but good to have it available&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How long until Rails 7 is released/RC is published?
&lt;/h3&gt;

&lt;p&gt;Of course, no one except the core team knows this. To make an educated guess, these are the days between each releases (from Rubygems):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version jump&lt;/th&gt;
&lt;th&gt;days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4.0.0 -&amp;gt; 4.1.0.beta1&lt;/td&gt;
&lt;td&gt;176&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.1.0 -&amp;gt; 4.2.0.beta1&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.2.0 -&amp;gt; 5.0.0.beta1&lt;/td&gt;
&lt;td&gt;363&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.0.0 -&amp;gt; 5.1.0.beta1&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.1.0 -&amp;gt; 5.2.0.beta1&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.2.0 -&amp;gt; 6.0.0.beta1&lt;/td&gt;
&lt;td&gt;284&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.0.0 -&amp;gt; 6.1.0.rc1&lt;/td&gt;
&lt;td&gt;444&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The “minor” jumps between 4.0 -&amp;gt; 4.1 -&amp;gt; 4.2 etc. took an &lt;strong&gt;average of 273 days&lt;/strong&gt; from release of the previous minor upgrade to the first beta/rc or release.&lt;/p&gt;

&lt;p&gt;6.1.0 was released on 9th December 2020, adding 258 days would put the release on &lt;strong&gt;September 2021&lt;/strong&gt;! Hopefully not taking 444 days as Rails 6.1 took… that would put the release on &lt;strong&gt;February 2021&lt;/strong&gt;. 😉&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Endless Scroll / Infinite Loading with Turbo Streams &amp; Stimulus</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Mon, 03 May 2021 19:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/endless-scroll-infinite-loading-with-turbo-streams-stimulus-5d89</link>
      <guid>https://dev.to/zealot128/endless-scroll-infinite-loading-with-turbo-streams-stimulus-5d89</guid>
      <description>&lt;p&gt;&lt;a href="https://turbo.hotwire.dev/"&gt;Hotwire Turbo&lt;/a&gt; by the Ruby on Rails developers is the new solution to enhance server side rendered apps with interactive behavior without much Javascript at all.&lt;/p&gt;

&lt;p&gt;In this post, I want to show you, how I built an &lt;strong&gt;infinite scrolling&lt;/strong&gt; feature, meaning: When reaching the bottom of a list, load the next page and add it to the DOM. There are many many ways in handling this kind of solution, like, using a 3rd party library like “lazyload” and more. To get more familiar with the Hotwire Stack of Stimulus + Turbo, I decided to use &lt;strong&gt;Turbo Streams&lt;/strong&gt; for handling the DOM-part, and Stimulus to connect with an &lt;strong&gt;Interaction Observer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For this post, I will make the following assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We hava a controller &lt;code&gt;PostsController&lt;/code&gt; with an &lt;code&gt;index&lt;/code&gt; action&lt;/li&gt;
&lt;li&gt;We already handling pagination via the great &lt;code&gt;pagy&lt;/code&gt; Gem&lt;/li&gt;
&lt;li&gt;Turbo + Stimulus are all set up&lt;/li&gt;
&lt;li&gt;I use &lt;strong&gt;SLIM&lt;/strong&gt; as the template language, because I like it’s brevity and clearness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclaimer: When &lt;a href="https://github.com/hotwired/turbo/pull/146"&gt;this PR&lt;/a&gt; get’s released, this solution might be simplified much more, but just using &lt;code&gt;&amp;lt;turbo-frame action="append"&amp;gt;&lt;/code&gt; with a little glue code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add stimulus to our posts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;// index.html.slim&lt;/span&gt;

&lt;span class="nc"&gt;.list-group&lt;/span&gt;(&lt;span class="na"&gt;data-controller&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"infinite-scroll"&lt;/span&gt;)
  &lt;span class="c"&gt;// If you need to enable Live Updates, you could connect to a&lt;/span&gt;
  &lt;span class="c"&gt;// = turbo_stream_from current_user, :posts&lt;/span&gt;
  &lt;span class="nf"&gt;#posts&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;(&lt;span class="na"&gt;data-infinite-scroll-target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'scrollArea'&lt;/span&gt;)

  &lt;span class="nf"&gt;#pagination&lt;/span&gt;&lt;span class="nc"&gt;.list-group-item.pt-3&lt;/span&gt;(&lt;span class="na"&gt;data-infinite-scroll-target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pagination"&lt;/span&gt;)
    &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pagy_bootstrap_nav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;In this index we,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wrap our posts with a Stimulus Controller and&lt;/li&gt;
&lt;li&gt;mark the posts into a div with id=posts (to later append to)&lt;/li&gt;
&lt;li&gt;add a &lt;code&gt;scrollArea&lt;/code&gt; empty element div just below our posts list - This area will be used for our Intersection Observer later on&lt;/li&gt;
&lt;li&gt;add the &lt;code&gt;pagy_nav&lt;/code&gt; or &lt;code&gt;pagy_bootstrap_nav&lt;/code&gt; pagination tags on the bottom, also wrapped in a Stimulus Target to later on pick the next page’s link from it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, before we modify the controller to respond to Turbo events, we implement the Stimulus Controller&lt;/p&gt;

&lt;h3&gt;
  
  
  Stimulus controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/infinite_scoll_controller.js&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;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scrollArea&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pagination&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createObserver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;createObserver&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;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleIntersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// https://github.com/w3c/IntersectionObserver/issues/124#issuecomment-476026505&lt;/span&gt;
        &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&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="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollAreaTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;handleIntersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loadMore&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="nx"&gt;loadMore&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paginationTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[rel=next]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/vnd.turbo-stream.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Turbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderStreamMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;href&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;ul&gt;
&lt;li&gt;We define the areas ScrollArea + Pagination&lt;/li&gt;
&lt;li&gt;When loaded, the controller puts an Interaction Observer onto the scroll area&lt;/li&gt;
&lt;li&gt;When the scroll area get’s into the viewport, we &lt;code&gt;loadMore&lt;/code&gt; posts, by picking the next page’s url from the pagination&lt;/li&gt;
&lt;li&gt;Import: We can’t use &lt;code&gt;Turbo.visit&lt;/code&gt; but we are using &lt;code&gt;fetch&lt;/code&gt; instead, because the Turbo-Stream Magic &lt;a href="https://github.com/hotwired/turbo/pull/52"&gt;only works on POST/PATCH/&lt;/a&gt;… requests. But we want our controller to respond to this GET request with a specific Turbo Stream that handles the DOM manipulation. That’s why we use &lt;code&gt;fetch&lt;/code&gt; manually with the special &lt;code&gt;Accept: text/vnd.turbo-stream.html&lt;/code&gt; header here.&lt;/li&gt;
&lt;li&gt;When the fetch returns, &lt;a href="https://github.com/hotwired/turbo/issues/34"&gt;we pipe the result manually&lt;/a&gt; to &lt;code&gt;Turbo.renderStreamMessage&lt;/code&gt; which evaluates the html content for Turbo Stream actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PostsController
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Pagy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Backend&lt;/span&gt;
  &lt;span class="n"&gt;helper&lt;/span&gt; &lt;span class="no"&gt;Pagy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Frontend&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
      &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Straight forward Turbo: we respond with “turbo_stream” format, or with html (template at the top in this post). Let’s show the turbo_stream template:&lt;/p&gt;

&lt;h3&gt;
  
  
  index.turbo_stream.slim - Turbo Stream response
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="nt"&gt;turbo&lt;/span&gt;-stream&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"append"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"posts"&lt;/span&gt;
  &lt;span class="nt"&gt;template&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'posts/post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;collection: &lt;/span&gt;&lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nt"&gt;turbo&lt;/span&gt;-stream&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pagination"&lt;/span&gt;
  &lt;span class="nt"&gt;template&lt;/span&gt;
    &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pagy_bootstrap_nav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We &lt;strong&gt;append&lt;/strong&gt; this page’s posts to the div with id=posts&lt;/li&gt;
&lt;li&gt;We &lt;strong&gt;replace&lt;/strong&gt; the pagination completely to reflect the new page&lt;/li&gt;
&lt;li&gt;Important to switch the format to html to get our ‘post’ partial, don’t know &lt;a href="https://github.com/hotwired/turbo-rails/issues/65"&gt;if intended or bug&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it! Because the scroll area will be always on the bottom, it will trigger over and over, Stimulus will pick out the current next page’s url from the pagination part, and Rails will respond with a Turbo stream that updates both the new posts and replace the pagination.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>Growing Rails - Utilizing Form Models for complex validations or side effects</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Sat, 01 May 2021 22:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/growing-rails-utilizing-form-models-for-complex-validations-or-side-effects-5909</link>
      <guid>https://dev.to/zealot128/growing-rails-utilizing-form-models-for-complex-validations-or-side-effects-5909</guid>
      <description>&lt;p&gt;Rails by default has a couple of “buckets” to put your code into. Some people claim that this is not sufficient when building larger apps, other people hold against, that you could upgrade your “model” directory and put all kinds of Ruby POROs (Plain Ruby Objects) into it. I am not totally convinced to mix database-backed models and all kinds of different objects, but rather like to identify common patterns and create subdirectories directly under &lt;code&gt;app/&lt;/code&gt;, like: &lt;code&gt;app/queries&lt;/code&gt;, &lt;code&gt;app/api&lt;/code&gt;, &lt;code&gt;app/api_clients&lt;/code&gt; or the bespoken Form Models under &lt;code&gt;app/forms&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages of Form Models
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Validations
&lt;/h4&gt;

&lt;p&gt;The longer I work with Rails, and the longer or larger the project horizon is planned, the less I like to use complex &lt;strong&gt;validations&lt;/strong&gt; on the model. Why? Because in my experience, the validations only make sense, when the model is created by a user through a UI. But not all objects are created by a UI. Think of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ad-hoc creation of small objects in development, test&lt;/li&gt;
&lt;li&gt;Cronjobs that create or update a model and fail because one item has gone invalid because a validation / column has been introduced after the creation of the item itself - this happens quite often IME&lt;/li&gt;
&lt;li&gt;different validations depending on the state of the user (Form wizards, registration vs. updating profile, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Side Effects
&lt;/h4&gt;

&lt;p&gt;The second big win of form models are &lt;strong&gt;side effects&lt;/strong&gt; , like sending a email/notification, enqueuing jobs, creating log/audit records, update elasticsearch, etc.pp. Doing those in the controller is maybe feasible but it can go out of hand very fast. Doing those in callbacks is IMO a very bad practice: Thinking about backfilling some attributes, but accidentally sending a notification to all. You always have to know, which side effects are present, even when updating in the background. So, I think the &lt;code&gt;save&lt;/code&gt; method of a Form Object is a perfect place to kick off various actions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Database-less actions
&lt;/h4&gt;

&lt;p&gt;Also, you sometimes have actions that not necessarily have a database table attached, think of: CSV export (with config), Providing a test action for an integration (Webhook test, IMAP integration, SAML integration, …). Those are perfect candidates for Form Models!&lt;/p&gt;

&lt;h4&gt;
  
  
  Controller does not need to know the model’s attributes
&lt;/h4&gt;

&lt;p&gt;Another advantage, which I later found out about, is that I can get rid of the &lt;code&gt;permitted_params&lt;/code&gt; / &lt;code&gt;params.require&lt;/code&gt; stuff from the controller (which is there rightly so to prevent Mass Assignment Injections). Because our form model can only reveal the attributes which the user can update anyways, we can build a very simple wrapper, that automatically permits all attributes of the form model. I really like that, because now the controller does not have to know about the model’s fields – How often did you forgot to add a missing attribute to the &lt;code&gt;permit(..)&lt;/code&gt; method?&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Form base class
&lt;/h2&gt;

&lt;p&gt;Over the years, our base class changed. One thing I want of a Form Model, is Parameter Coercion (e.g. casting “1” to true for a boolean). In the past, we used the &lt;code&gt;virtus&lt;/code&gt; Gem to handle the definition of the attributes and coercion. But recently, after Rails released the &lt;a href="https://api.rubyonrails.org/classes/ActiveModel/Attributes/ClassMethods.html"&gt;Attributes API&lt;/a&gt;, we can just use &lt;code&gt;ActiveModel::Attributes&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/forms/application_form&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationForm&lt;/span&gt;
  &lt;span class="c1"&gt;# ActiveModel: We get validations, model_name stuff, etc.&lt;/span&gt;
  &lt;span class="c1"&gt;# now our object quaks almost like an ActiveRecord::Base model&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;

  &lt;span class="c1"&gt;# Gives us the `attribute `` method to define attributes with data types:&lt;/span&gt;
  &lt;span class="c1"&gt;# attribute :email, :string,&lt;/span&gt;
  &lt;span class="c1"&gt;# attribute :active, :boolean, default: true&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attributes&lt;/span&gt;

  &lt;span class="c1"&gt;# Helper Method to call from the controller:&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# MyForm.new_with_permitted_params(params)&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# It fetches the correct key, e.g. params.require(:my_form).permit(:a, :b, c: [], d: {})&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_with_permitted_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;permitted_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
      &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;param_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
      &lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;permitted_params_arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permitted_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Maps the defined `attributes` to a argument list for params.permit()&lt;/span&gt;
  &lt;span class="c1"&gt;# Array and Hash attribues must be written in hash form.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permitted_params_arguments&lt;/span&gt;
    &lt;span class="n"&gt;structures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primitives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
      &lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:array&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;elsif&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:hash&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;else&lt;/span&gt;
          &lt;span class="nb"&gt;name&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# rubocop:disable Style/MultilineBlockChain&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;structures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:merge&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:blank?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;params&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# placeholder to implement by the inherited form instances&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NotImplementedError&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example Usage
&lt;/h3&gt;

&lt;p&gt;Imagine you are for once not using Devise and implementing Password Reset yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/forms/password_reset_form.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PasswordResetForm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationForm&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MailTo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EMAIL_REGEXP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;

    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;
      &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;regenerate_reset_password_token_if_not_active&lt;/span&gt;
    &lt;span class="no"&gt;Mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;password_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_now&lt;/span&gt;
    &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;As the model is 100% compatible with a ActiveRecord Model, we can use it the same in the controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PasswordResetController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PasswordResetForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PasswordResetForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_with_permitted_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"E-Mail instructions have been sent"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now, if we get the requirement to log account activities (audit trail), the save method is a perfect place to continue. For this purpose, I usually define “normal” attribute accessors that the controllers fill. Those fields will not be available through the permitted params sieve and are safe for this purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PasswordResetForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_with_permitted_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;###&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PasswordResetForm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationForm&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:request&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="c1"&gt;#...&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;ip: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;user_agent: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;#.&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Hope that helps you in your organisation of form models! For us, those are a frequently used pattern.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>PostgreSQL (Rails) - Reverse search (tagging, subscription)</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Tue, 03 Nov 2020 09:11:03 +0000</pubDate>
      <link>https://dev.to/zealot128/postgresql-rails-reverse-search-tagging-subscription-30hl</link>
      <guid>https://dev.to/zealot128/postgresql-rails-reverse-search-tagging-subscription-30hl</guid>
      <description>&lt;p&gt;Using PostgresQL’s search capabilities (tsvector etc.) is a well-known low-key alternative to using a full blown search engine, like Elasticsearch/Solr/Algolia. But it turns out, it is also quite easily usable as a “reverse search engine”, what Elasticsearch calls “Percolate”. One use case could be user supplied “Subscriptions”, think of &lt;strong&gt;“search subscriptions” of classified&lt;/strong&gt; that you want to trigger whenever a new item reaches the database.&lt;/p&gt;

&lt;p&gt;After fiddling around one day with Elasticsearch percolators I was not so satisfied with the results, that I decided to trying PG’s tsvector/tsquery.&lt;/p&gt;

&lt;p&gt;Recently, I’ve wanted to build a &lt;strong&gt;keyword tagger&lt;/strong&gt; , that given a blob of text uses a predefined vocabulary database to give you the “most relevant” tags for that tags. The process should be absolutely the same as with search subscriptions by reversing Query (queries are stored in the DB) and Documents (only one document: the document to match with the queries). For this example I will use &lt;code&gt;keyword&lt;/code&gt; matching as we implemented it in &lt;strong&gt;Empfehlungsbund&lt;/strong&gt; in various places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Having a table &lt;code&gt;keywords&lt;/code&gt; with a string column &lt;code&gt;keyword&lt;/code&gt;, e.g. a managed table of most important keywords for your domain. In our cases that includes all the technolgy terms of developers/administrators (like “Javascript” or “Ruby on Rails”)&lt;/li&gt;
&lt;li&gt;there is also a little hierarchie of keywords in “competence groups” and we also add the “search relevance” which expresses how often this term is used by Job Seekers on our job platforms of Empfehlungsbund.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Adding TS-columns
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;We add a tsquery column &lt;code&gt;keyword_query&lt;/code&gt; and a normalized token &lt;code&gt;keyword_search_token&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keyword_search_token&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keyword_query&lt;/span&gt; &lt;span class="n"&gt;tsquery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keyword_query&lt;/code&gt; if you follow several guides about PG’s fulltext search, I never came across one that proposed to just use a &lt;code&gt;tsquery&lt;/code&gt; instead of a &lt;code&gt;tsvector&lt;/code&gt;. But in our case (percalote) this makes total sense to add the string as &lt;code&gt;tsquery&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;keyword_search_token&lt;/code&gt; is a normalized version of our query, a processing pipeline that we ran before saving a record AND before tagging a document on that document later on. In our case the Pipeline consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove All HTML,&lt;/li&gt;
&lt;li&gt;normalize collocations (“java-developer” and “java developer”)&lt;/li&gt;
&lt;li&gt;Remove common (German) stop words,&lt;/li&gt;
&lt;li&gt;REPLACE important special chars that our search engine will need, like “.” (‘.NET’, “Vue.js”) or prefix/suffix ‘#’, ‘+’ (C++, C#). Many search engines does get this not right and strip out all special chars by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here a Ruby snippet from our codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;STOPWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;YAML&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'config/stopwords.yml'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                 
&lt;span class="no"&gt;WORD_BOUNDARY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(^|\s|[,\.\-\!\?])'&lt;/span&gt;
&lt;span class="no"&gt;REPLACE_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;WORD_BOUNDARY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;STOPWORDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;)&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;WORD_BOUNDARY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;

&lt;span class="no"&gt;REMOVE_STRING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="no"&gt;REPLACE_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;%r{&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;[mwd/ ]+&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="sr"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
]

d&lt;/span&gt;&lt;span class="sr"&gt;e&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;preprocess_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
      &lt;span class="nf"&gt;yield_self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;replace_special_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;
      &lt;span class="nf"&gt;yield_self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;REMOVE_STRING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&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="o"&gt;.&lt;/span&gt;
      &lt;span class="c1"&gt;# "Foo/Bar"&lt;/span&gt;
      &lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' / '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
      &lt;span class="c1"&gt;# EcmaScript5 -&amp;gt; Ecmascript 5&lt;/span&gt;
      &lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/(\w+)(\d+)&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;WORD_BOUNDARY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;, '\\1 \\2 \\3').
      tr('-', ' ').
      g&lt;/span&gt;&lt;span class="sr"&gt;su&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s+/&lt;/span&gt;&lt;span class="err"&gt;, ' ').
      &lt;/span&gt;&lt;span class="sr"&gt;s&lt;/span&gt;&lt;span class="n"&gt;trip&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace_special_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# C# C++, F#, ...&lt;/span&gt;
  &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/[#\+]/&lt;/span&gt;&lt;span class="err"&gt;, '#' =&amp;gt; "RAUTE", '+' =&amp;gt; 'PLUS').
    # v&lt;/span&gt;&lt;span class="sr"&gt;ue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;js&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;net&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="n"&gt;dot&lt;/span&gt;
    &lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/(\w+)?\.(\w+)/&lt;/span&gt;&lt;span class="err"&gt;, '\\1DOT\\2')
  &lt;/span&gt;&lt;span class="sr"&gt;en&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Adding a tsquery column and trigger
&lt;/h3&gt;

&lt;p&gt;Now, we want to build a trigger that manages the keyword_query conversion automatically. We can to this inline by splitting the words by whitespace and joining them back together with the infix &lt;strong&gt;“&amp;lt;-&amp;gt;”&lt;/strong&gt; operator, which means “the words on either side of the operator should be near together”. (Introduced in PG 9.6). This is extremely useful for searching for collocations like “Ruby on Rails”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_results_before_insert_update_row_tr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
  &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_query&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_search_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;' &amp;lt;-&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;keyword_results_before_insert_update_row_tr&lt;/span&gt; 
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_results&lt;/span&gt; 
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt;  &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_results_before_insert_update_row_tr&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  2a. Rails tsquery migration
&lt;/h3&gt;

&lt;p&gt;If you are using Rails you can use &lt;code&gt;hairtrigger&lt;/code&gt; Gem to more easily create the trigger in a migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:keyword_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:keyword_search_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:keyword_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:keyword_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsquery&lt;/span&gt;
&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:keyword_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:keyword_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="s1"&gt;'gist'&lt;/span&gt;

&lt;span class="n"&gt;create_trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;compatibility: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:keyword_results&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="s2"&gt;"new.keyword_query := to_tsquery(array_to_string(string_to_array(new.keyword_search_token, ' '), ' &amp;lt;-&amp;gt; '))"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;BUT: ActiveRecord’s PG-Adapter at this moment does not know at the moment to handle &lt;code&gt;tsquery&lt;/code&gt; columns and will fail with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unknown OID 3615: failed to recognize type of 'keyword_query'. It will be treated as String.

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

&lt;/div&gt;



&lt;p&gt;And the table will be missing from &lt;code&gt;db/schema.rb&lt;/code&gt;, and thus not loaded in test, with this comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Could not dump table "keywords" because of following StandardError
# Unknown type 'tsquery' for column 'keyword_query'

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

&lt;/div&gt;



&lt;p&gt;The normal solution which you find for this class of problem is generally to switch to “SQL”-mode of Schema dumper. I don’t like that, because it makes the diffs gigantic and merges/rebases fail more often. I found, I could just tell Rails via patching how to handle that problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_record_pg_types.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"active_record/connection_adapters/postgresql_adapter"&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PGAddTypes&lt;/span&gt;
  &lt;span class="c1"&gt;# 1st place: how to handle this column = it's a string!&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize_type_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_type&lt;/span&gt; &lt;span class="s2"&gt;"tsquery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ConnectionAdapters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQLAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OID&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SpecializedString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:tsquery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3615&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ConnectionAdapters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQLAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OID&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SpecializedString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:tsquery&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# 2nd place:&lt;/span&gt;
&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ConnectionAdapters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQLAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NATIVE_DATABASE_TYPES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:tsquery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"tsquery"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ConnectionAdapters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQLAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt; &lt;span class="no"&gt;PGAddTypes&lt;/span&gt;

&lt;span class="c1"&gt;# 3rd place: mapping a schema / migration line to a database mapping&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnMethods&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsquery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;With this code in place, schema-dumping and loading will work again!&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reverse searching against table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;preprocessed_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preprocess_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# create intermediate from table with our keywords and the preprocessed full_text:&lt;/span&gt;
&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;KeywordResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize_sql_array&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'keywords, to_tsvector(?) full_text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preprocessed_query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;KeywordResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
  &lt;span class="c1"&gt;# where: having a match&lt;/span&gt;
  &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%{full_text @@ tsv_body}&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
  &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
  &lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'score desc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
  &lt;span class="c1"&gt;# select: all normal columns + a "score"&lt;/span&gt;
  &lt;span class="nb"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"keywords.*, ts_rank(full_text, tsv_body) as score"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;In SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tsv_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;keyword_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Tag this string with keywords like Ruby on Rails'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;full_text&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;tsv_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;(Of course, you should NOT pass the string in raw like this but use prepared statements with a placeholder variable)&lt;/p&gt;

&lt;h3&gt;
  
  
  Tips
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Improving the accuracy by scoping
&lt;/h4&gt;

&lt;p&gt;If you have a large spectrum of keywords there is the problem that some words can mean different things for different audiences. For example, when parsing a job ad or a applicant CV, imagine you find the word “Amazon” - For an IT person that probably is related to “AWS”, for a Retail staff maybe the Amazon Ad or Shops. Or think of generic greek/latin names for all kind of technologies (“Atlas”, “Prometheus” etc.) which could have all kind of meanings in different contexts, or roles like “Architect” or “Designer”. In our case we add the general sector to a keyword, like “IT” (programmer, architects, admins, tech designers), “Engineering”, “medicine”, “white-collar office” (Marketing, Sales, HR, Accounting) and similar. In a first step we first find, which of the Tags would have the most direct keyword matches and then only search for those keywords in this sector.&lt;/p&gt;

&lt;h4&gt;
  
  
  Suggest new keywords
&lt;/h4&gt;

&lt;p&gt;As mentioned in the beginning, we have a 2 tier hierarchy of our keywords. Each keyword belongs to a “competence” (E.g. “Ruby on Rails”, “RSpec” belongs to “Ruby”). To suggest keywords we just take the keywords found, go up to the competence and then take the most relevant N keywords ordered by the “search popularity”.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Internationalize a medium Rails App with Vue frontend effectively with tooling</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Sat, 10 Oct 2020 12:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/internationalize-a-medium-rails-app-with-vue-frontend-effectively-with-tooling-4eo6</link>
      <guid>https://dev.to/zealot128/internationalize-a-medium-rails-app-with-vue-frontend-effectively-with-tooling-4eo6</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; I released a &lt;a href="https://github.com/pludoni/extract_i18n/" rel="noopener noreferrer"&gt;Ruby CLI&lt;/a&gt; tool that can semi-automatically extract raw Strings from Ruby-files, SLIM, ERB-Views and Vue-Pug templates. Read on about the background.&lt;/p&gt;

&lt;p&gt;Recently, we started to internationalize our ATS (Applicant tracking system) recruiter backend, to also show support English speaking recruiters, a step that most growing Rails apps sooner or later will face. That’s why I want to share some tooling that I use (or build) to help you make this very first step, extracting ALL the strings in the app into the I18n space.&lt;/p&gt;



&lt;p&gt;Additionally, we also use bunch of Vue-components (about ~175) to add interactivity in many parts of the app. Having started lean with YAGNI in mind, Internationalization for the internal users (the Recruiters) was not a major concern in the first 2 years of running our ATS, so most Strings where plain German around the app. In our case, I used some Regex searches and found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;160 SLIM-Views with at least one string&lt;/li&gt;
&lt;li&gt;175 Vue-Components&lt;/li&gt;
&lt;li&gt;bunch of Controller flash messages, page titles, breadcrumbs&lt;/li&gt;
&lt;li&gt;Models/Forms (custom error messages, enum-translations etc.)&lt;/li&gt;
&lt;li&gt;…and more business related classes with custom descriptions/titles (in our case, E-mail-template definitions with placeholders, states, events, actions etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Slim - SlimKeyfy
&lt;/h3&gt;

&lt;p&gt;Manually extract keys from templates is tedious as well as error prone, as one will mix up keys quiet easily, forgot to save files/overwrite changes and so forth. This is why I tried searching for some tooling that helps with &lt;strong&gt;Extraction&lt;/strong&gt;. Luckily I found &lt;a href="https://github.com/phrase/slimkeyfy" rel="noopener noreferrer"&gt;Slimkeyfy&lt;/a&gt; first, which did an OK job extraction most SLIM-keys. But first, there where some problems with the tool, namely it could not parse parts of our Slim-syntax, and some of the extraction regexp could be improved. This is why I started forking the Gem to add missing functionality. Anyhow, this still leaves us with a bunch of Vue-Components and Ruby-classes… But the slimkeyfy gave me the idea.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-slim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-slim.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ruby Classes - Using Ruby-parser
&lt;/h3&gt;

&lt;p&gt;I was reading a couple of posts about using the awesome &lt;strong&gt;Parser((-Gem, which allows to rewrite Ruby code on the fly using hooks. This is what tools like **Rubocop&lt;/strong&gt; using under the hood. Using another great Gem, &lt;strong&gt;tty-prompt&lt;/strong&gt; , I cobbled together a small CLI that interactively asks the user for every “interesting” String in given Ruby files and automatically extract those. The result was extremely satisfying, as all the code was still valid and the transformations very precise. Even extraction of Heredocs and interpolating arguments worked perfectly!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-models.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-models.png"&gt;&lt;/a&gt; &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-controller.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-controller.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Vue Extraction
&lt;/h3&gt;

&lt;p&gt;This one is a little trickier… Most of the components are written with the &lt;strong&gt;Pug&lt;/strong&gt; -templating language, which is very similar to Slim. So I hacked the Slimkeyfy Gem to also handle Vue-Pug files, which worked good, too. That means I could extract those in the same manner.&lt;/p&gt;

&lt;p&gt;What about the Vue-components that are still using the default HTML-like templating? After fiddling (without success) with HTML-parsers like Nokogiri, that had major showstoppers (Nokogiri will gulp the “&lt;a class="mentioned-user" href="https://dev.to/click"&gt;@click&lt;/a&gt;” and similiar attributes of Vue), I just decided to &lt;strong&gt;convert the remaining html templates to Pug&lt;/strong&gt;. Luckily, there are several online API’s that can convert that easily and are about 98% correct, so I just build a &lt;a href="https://gist.github.com/zealot128/6c41df1d33a810856a557971a04989f6" rel="noopener noreferrer"&gt;small Ruby script to walk all the unconverted files, send them to the “API” and write it into a file&lt;/a&gt;. Afterwards, extraction worked the same as above&lt;/p&gt;

&lt;p&gt;The missing 2%: Just watch out if you are using a bunch of self-closing custom tags next to each other, those were nested after converting with the tool in 1 case instead of next to each other which a test picked up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-vue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-vue.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Manual effort required for the finishing
&lt;/h3&gt;

&lt;p&gt;In the end, most apps have some kind of exceptional usage, that will not be covered by the tooling above, but the Tooling helped to reach the Goal to 90%. To find obvious omissions, I’ve created a ToDo-List by using Rip-grep/Ack/Ag regex search, e.g. Find all Strings that begin with a uppercase letter or Heredocs is a very good heuristic to find untranslated Strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rg '"[A-Z][a-z]' app/ &amp;gt;&amp;gt; todos.txt
rg "'[A-Z][a-z]" app/ &amp;gt;&amp;gt; todos.txt

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Putting it together: Introducing ExtractI18n Gem
&lt;/h3&gt;

&lt;p&gt;Afterwards, after having used different tools, I decided to put it all together, optimize and streamline the interaction. This is why I want to show you a Gem that does all the work above, but also can convert ERB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pludoni/extract_i18n/" rel="noopener noreferrer"&gt;&lt;strong&gt;ExtractI18n&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ExtractI18n is Ruby Command-Line-Tool, that given a path to Ruby/Slim/Vue-Pug/ERB-Files will walk those and ask you for every string, if you want to extract those in a given yml file (e.g. &lt;code&gt;config/locales/unsorted.de.yml&lt;/code&gt;). Before every file system change you will be shown a Diff at the end, so you can decide if the tool messed up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extract-i18n --locale de --yaml config/locales/unsorted.de.yml app/views/user

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-extracti18n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2020%2Fi18n-extracti18n.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It uses SlimKeyfy code as a basis for the SLIM/Vue-extraction, also HTML-Extractor for ERB and the mentioned Ruby-Parser into a unified interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sorting it out: I18n-tasks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/glebm/i18n-tasks" rel="noopener noreferrer"&gt;&lt;strong&gt;I18n-tasksn&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EVERY Rails app, that has a non-trivial amount of I18n-strings, should make use of the awesome &lt;code&gt;i18n-tasks&lt;/code&gt;. I ran &lt;code&gt;i18n-tasks normalize -p&lt;/code&gt; repeatedly during the internationalization process to sort the just extracted keys into the right files. In the end, every major view/part of the app has it’s own I18n-file, like: &lt;code&gt;config/locales/email_templates.LOCALE.yml&lt;/code&gt;, &lt;code&gt;config/locales/admin.LOCALE.yml&lt;/code&gt; etc.&lt;/p&gt;

&lt;p&gt;We also use the Rails I18n backend for storing the Vue’s frontend translations under the &lt;code&gt;js.&lt;/code&gt; namespace. I’ve &lt;a href="https://www.stefanwienert.de/blog/2019/11/02/integrating-javascript-vue-i18n-into-rails-missing-auto-translate-pipeline/" rel="noopener noreferrer"&gt;wrote an article about that integration&lt;/a&gt; some time ago. To make I18n-tasks find usages of those Vue-strings, you can also &lt;a href="https://gist.github.com/zealot128/e6ec1767a40a6c3d85d7f171f4d88293" rel="noopener noreferrer"&gt;download and require my Vue-Scanner for I18n-tasks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also checking the “health” with &lt;code&gt;i18n-tasks health -l de&lt;/code&gt; (just focus on the extracted language, as the other language is still missing) and see, if some keys are missed, overwritten etc. Granted, that will produce a huge Git-diff once, but afterwards the extraction and I18n is totally managed by I18n-tasks.&lt;/p&gt;

&lt;p&gt;Then, I exported all missing keys for the target language, used the Google-Translate feature to make a basic translation and generated a CSV with the suggested translation by filtering for the missing keys. After getting those fixed by a non-technical person, I can reimport it, too! Great tool!&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats
&lt;/h3&gt;

&lt;p&gt;In the end, I extracted about 2000 keys of about 120.000 characters, which is not too much. Google Translate API via I18n-tasks took about 3 seconds to translate those. During this process, I touched almost every file of the app and produced a giant diff, here only the app/ folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git diff origin/master --shortstat -- app
512 files changed, 4641 insertions(+), 5096 deletions(-)

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

&lt;/div&gt;



&lt;p&gt;But … with the tooling and producing a Gem along the way, learning about the Ruby-Parser Transformer, it was much more fun than anticipated! :) In the future, when building new views, I will still write those out in German first, and run this tool in the end, as the result is more correct than me copy pasting stuff and messing up interpolations.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>vue</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Self-hosting Sentry Error Tracking starting at 5 EUR/m on Hetzner Cloud with Docker + SAML/Mattermost integration</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Tue, 14 Jan 2020 10:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/self-hosting-sentry-error-tracking-starting-at-5-eur-m-on-hetzner-cloud-with-docker-saml-mattermost-integration-1n9f</link>
      <guid>https://dev.to/zealot128/self-hosting-sentry-error-tracking-starting-at-5-eur-m-on-hetzner-cloud-with-docker-saml-mattermost-integration-1n9f</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Tracking application errors should be a check list item for all production apps. In simple cases, a automatic "email the stacktrace to admin" is suffice to start but reaches the limit very fast. Items like&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Javascript Error Tracking, (in 2020 with source maps),&lt;/li&gt;
&lt;li&gt;Environment information, like logged in user,&lt;/li&gt;
&lt;li&gt;combining, ignoring, merging errors,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are requirements most growing projects will have sooner or later. Having an error tracking system in place is the best solution. &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsentry_breadcrumbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsentry_breadcrumbs.png" title="Sentry JavaScript Breadcrumbs" alt="Sentry Breadcrumbs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For our case, we prefer to &lt;strong&gt;self-hosted&lt;/strong&gt; solutions like this, which has significant advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no additional &lt;strong&gt;data protection&lt;/strong&gt; issues, or signing additional data processing agreements, as all the data remains in the company's sphere of influence.&lt;/li&gt;
&lt;li&gt;integration with company systems, like &lt;strong&gt;VPN/SAML&lt;/strong&gt; usually more easy or is even only possible by self hosting,&lt;/li&gt;
&lt;li&gt;sometimes being the only user of your systems provides &lt;strong&gt;performance benefits&lt;/strong&gt; , e.g. comparing with using Sentry's hosted version, our current installation seems to be orders of magnitude faster (experienced response times like 5-10 seconds when using Sentry free tier), also because the error tracking and app servers are not only in the same country but maybe even the same DC like your app which drastically reduces latency (Sentry) or transport cost when using AWS.&lt;/li&gt;
&lt;li&gt;saving some money per month instead of paying "per app"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the downside, running things themselves can be more risky, especially when a Error tracking system is hard to setup.&lt;/p&gt;

&lt;p&gt;From the very beginning, we used Errbit error tracking, which provides a Airbrake compatible API. But having tried out Sentry.io for private projects before, I was blown away by the features, like tons of plugins, beautiful clean UI, top notch Javascript integration with breadcrumbs and source maps, so I've decided to set it up in a self-hosted manner for our &lt;a href="https://www.pludoni.de" rel="noopener noreferrer"&gt;company&lt;/a&gt;. Fortunately, Sentry provides a Docker installation script, which I will use during this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation of self hosted Sentry
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsentry_logo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsentry_logo.png" alt="Sentry Logo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we set up a Cloud VPC. Size of the cloud instance depends on your prospected error volume. We started with a Hetzner CX21 which costs about 5 EUR per month and can be upgraded easily via button click and rescaled later on, if necessary.&lt;/p&gt;

&lt;p&gt;After starting the instance, install Docker and Docker compose on the host, or do it like us, and use a cloud-config yaml during installing (Pasting into field user config on Hetzner Cloud), e.g.:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="c1"&gt;# cloud-config&lt;/span&gt;
&lt;span class="na"&gt;apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker.list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deb&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[arch=amd64]&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://download.docker.com/linux/ubuntu&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$RELEASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stable'&lt;/span&gt;
      &lt;span class="na"&gt;keyid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0EBFCD88&lt;/span&gt; &lt;span class="c1"&gt;# GPG key ID published on a key server&lt;/span&gt;
&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-transport-https&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bash-completion&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ca-certificates&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command-not-found&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;debian-archive-keyring&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dnsutils&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;fail2ban&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-ce&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;golang-go&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git-core&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;htop&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lshw&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lsof&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ltrace&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;software-properties-common&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sysstat&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tar&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unattended-upgrades&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vim&lt;/span&gt;


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

&lt;/div&gt;
&lt;p&gt;After powering up, clone this repository to /opt/sentry&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/getsentry/onpremise /opt/sentry

&lt;span class="c"&gt;# Or, use this fork that has Caddy/Letsencrypt support&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/merantix/sentry /opt/sentry


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

&lt;/div&gt;
&lt;p&gt;This setup will include everything you need &lt;strong&gt;BUT&lt;/strong&gt; a HTTPS proxy. To have Caddy run as an HTTPS Proxy with Auto-Letsencrypt, check out this &lt;a href="https://github.com/merantix/sentry" rel="noopener noreferrer"&gt;fork merantix/sentry&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adjustments and configuration
&lt;/h3&gt;

&lt;p&gt;If you are wanting to run Sentry in a organisation setting, you might want to install some plugins. In our case, these are a Mattermost plugin and a SAML2 plugin for user authentication.&lt;/p&gt;

&lt;p&gt;Add those requirements to the &lt;code&gt;Dockerfile&lt;/code&gt; in the getsentry folder:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;

&lt;span class="c"&gt;# /opt/sentry/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SENTRY_IMAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ${SENTRY_IMAGE}-onbuild&lt;/span&gt;

+ RUN pip install https://github.com/getsentry/sentry-auth-saml2/archive/master.zip
+ RUN pip install -e git+https://github.com/NDrive/sentry-mattermost@master&lt;span class="c"&gt;#egg=sentry-mattermost&lt;/span&gt;
+ # Or instead, a forked version with Mattermost multi channel support
+ RUN pip install -e git+https://github.com/zealot128/sentry-mattermost.git@merged&lt;span class="c"&gt;#egg=sentry-mattermost&lt;/span&gt;


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

&lt;/div&gt;
&lt;p&gt;(If you are using Mattermost, and like to reuse webhooks between multiple channels/projects, I've recommend my fork until this &lt;a href="https://github.com/NDrive/sentry-mattermost/pull/12" rel="noopener noreferrer"&gt;PR&lt;/a&gt; is merged)&lt;/p&gt;

&lt;p&gt;Now, check out configuration in &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt;. In general, we had very little configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SENTRY_SECRET_KEY in &lt;code&gt;env&lt;/code&gt; (or will generate during installation)&lt;/li&gt;
&lt;li&gt;changed Caddy hostname in &lt;code&gt;Caddy/Caddyfile&lt;/code&gt;, make sure, that hostname resolves to the server's ip for letsencrypt&lt;/li&gt;
&lt;li&gt;set, or remove email settings in docker-compose.yml or env&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are finished, run:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;# will ask for a start password for the admin user&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;./install.sh

&lt;span class="c"&gt;# start all the services&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose up


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

&lt;/div&gt;
&lt;p&gt;Some quick docker-compose commands you might need later on:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;# show stdout of a host&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose logs web

&lt;span class="c"&gt;# enter a host&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose &lt;span class="nb"&gt;exec &lt;/span&gt;web bash

&lt;span class="c"&gt;# start/stop a host&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose restart web
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose up web
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose down web


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  SAML
&lt;/h3&gt;

&lt;p&gt;If you are using a SAML2 provider for your organisation, you can try to add the SAML authentication next. The specific settings are not standardized, and almost all supplier use different kind of names for the person's attributes (Claims). For example, we are using a (of course self-hosted) custom SAML2 server based on &lt;a href="https://github.com/onelogin/ruby-saml#signing" rel="noopener noreferrer"&gt;Ruby-SAML by Onelogin&lt;/a&gt; and also added several extra fields via a OID extra.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsaml_example.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsaml_example.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those settings are based on these attributes (claims) by Ruby SAML, but like I said, that depends on your provider:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsaml_claims.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fsaml_claims.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Debugging attribute mapping
&lt;/h3&gt;

&lt;p&gt;To find out the specific names of the mapping, that you can use, you can add a print statement into the right Django file. I've gone this way:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose &lt;span class="nb"&gt;exec &lt;/span&gt;web bash
&lt;span class="nv"&gt;$ &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;vim
&lt;span class="nv"&gt;$ &lt;/span&gt;vim /usr/local/lib/python2.7/site-packages/sentry/auth/helper.py


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

&lt;/div&gt;
&lt;p&gt;Add &lt;code&gt;import pprint&lt;/code&gt; somewhere in the top of the file, and add a printf statement in the &lt;code&gt;finish_pipeline&lt;/code&gt; function:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;finish_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# The state data may have expried, in which case the state data will
&lt;/span&gt;        &lt;span class="c1"&gt;# simply be None.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ERR_INVALID_IDENTITY&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="o"&gt;+&lt;/span&gt;           &lt;span class="n"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_identity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;
&lt;p&gt;Exit the docker container and restart web &lt;code&gt;docker-compose restart web&lt;/code&gt;. Now try set up of auth again and watch &lt;code&gt;docker-compose logs web&lt;/code&gt; for debugging output.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mattermost
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fmattermost_logo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fwww.stefanwienert.de%2Fimages%2Fblog%2F2019%2Fmattermost_logo.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mattermost integration was straight forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a webhook as a Mattermost admin, copy the API url&lt;/li&gt;
&lt;li&gt;Mattermost must be enabled as a &lt;strong&gt;Legacy Integration&lt;/strong&gt; per project, after enabling add Webhook url.&lt;/li&gt;
&lt;li&gt;My fork of the plugin also supports changing the channel per project and reusing the Webhook.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;Open Source used:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/getsentry" rel="noopener noreferrer"&gt;
        getsentry
      &lt;/a&gt; / &lt;a href="https://github.com/getsentry/self-hosted" rel="noopener noreferrer"&gt;
        self-hosted
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sentry, feature-complete and packaged up for low-volume deployments and proofs-of-concept
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Self-Hosted Sentry&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://sentry.io/" rel="nofollow noopener noreferrer"&gt;Sentry&lt;/a&gt;, feature-complete and packaged up for low-volume deployments and proofs-of-concept.&lt;/p&gt;

&lt;p&gt;Documentation &lt;a href="https://develop.sentry.dev/self-hosted/" rel="nofollow noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/getsentry/self-hosted" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/vara-ai" rel="noopener noreferrer"&gt;
        vara-ai
      &lt;/a&gt; / &lt;a href="https://github.com/vara-ai/sentry" rel="noopener noreferrer"&gt;
        sentry
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sentry On-Premise setup
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Self-Hosted Sentry Nightly &lt;a href="https://git.io/JUYkh" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://github.com/getsentry/onpremise/workflows/test/badge.svg" alt="Build Status"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Official bootstrap for running your own &lt;a href="https://sentry.io/" rel="nofollow noopener noreferrer"&gt;Sentry&lt;/a&gt; with &lt;a href="https://www.docker.com/" rel="nofollow noopener noreferrer"&gt;Docker&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Docker 19.03.8+&lt;/li&gt;
&lt;li&gt;Compose 1.24.1+&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Minimum Hardware Requirements:&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;You need at least 2400MB RAM&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;To get started with all the defaults, simply clone the repo and run &lt;code&gt;./install.sh&lt;/code&gt; in your local check-out.&lt;/p&gt;
&lt;p&gt;During the install, a prompt will ask if you want to create a user account. If you require that the install not be blocked by the prompt, run &lt;code&gt;./install.sh --no-user-prompt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There may need to be modifications to the included example config files (&lt;code&gt;sentry/config.example.yml&lt;/code&gt; and &lt;code&gt;sentry/sentry.conf.example.py&lt;/code&gt;) to accommodate your needs or your environment (such as adding GitHub credentials). If you want to perform these, do them before you run the install script and copy them without the &lt;code&gt;.example&lt;/code&gt; extensions in the name (such as &lt;code&gt;sentry/sentry.conf.py&lt;/code&gt;) before running the &lt;code&gt;install.sh&lt;/code&gt; script.&lt;/p&gt;
&lt;p&gt;The recommended way to customize your configuration is using the files…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/vara-ai/sentry" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/getsentry" rel="noopener noreferrer"&gt;
        getsentry
      &lt;/a&gt; / &lt;a href="https://github.com/getsentry/sentry-auth-saml2" rel="noopener noreferrer"&gt;
        sentry-auth-saml2
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      SAML2 SSO provider for Sentry
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;SAML2 Auth for Sentry&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;DEPRECATED: This project now lives in &lt;a href="https://github.com/getsentry/sentry/tree/master/src/sentry/auth/providers/saml2" rel="noopener noreferrer"&gt;sentry&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; SAML2 Authenttication is still currently an experimental feature.&lt;/p&gt;
&lt;p&gt;An SSO provider for Sentry which enables SAML SSO and SLO support, including
various identity provider support.&lt;/p&gt;
&lt;p&gt;The following identity providers are supported&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.onelogin.com/" rel="nofollow noopener noreferrer"&gt;OneLogin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.okta.com/" rel="nofollow noopener noreferrer"&gt;Okta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/" rel="nofollow noopener noreferrer"&gt;Auth0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rippling.com/" rel="nofollow noopener noreferrer"&gt;Rippling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A generic SAML2 module is also provided, which may be configured with any
Identity Provider that conforms to the SAML2 specification.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;$ pip install https://github.com/getsentry/sentry-auth-saml2/archive/master.zip
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Configuration&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Refer to the Sentry &lt;a href="https://docs.sentry.io/learn/sso/" rel="nofollow noopener noreferrer"&gt;Single Sign-On
documentation&lt;/a&gt; for individual SAML Identity
Provider configurations.&lt;/p&gt;
&lt;p&gt;Refer to the &lt;a href="https://docs.sentry.io/server/sso/#enabling-sso" rel="nofollow noopener noreferrer"&gt;Enabling SSO
documentation&lt;/a&gt;
for what feature flags to enable for this plugin.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/getsentry/sentry-auth-saml2" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/NDrive" rel="noopener noreferrer"&gt;
        NDrive
      &lt;/a&gt; / &lt;a href="https://github.com/NDrive/sentry-mattermost" rel="noopener noreferrer"&gt;
        sentry-mattermost
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sends Sentry notifications to Mattermost Open Source Chat
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sentry Mattermost&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;A plugin for Sentry to enable notifications to Mattermost Open Source Chat.
This is based in the sentry-slack plugin: &lt;a href="https://github.com/getsentry/sentry-slack" rel="noopener noreferrer"&gt;https://github.com/getsentry/sentry-slack&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/NDrive/sentry-mattermostexample.png"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FNDrive%2Fsentry-mattermostexample.png" alt="Example"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Usage&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;Install with pip and enable the plugin in a Sentry Project:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;pip install sentry_mattermost
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Configure Mattermost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an Incoming Webhook&lt;/li&gt;
&lt;li&gt;Enable override usernames and profile picture icons in System Console Integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Contributing&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;We use Docker to setup a development stack. Make sure you have the latest
Docker Toolbox installed first.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;First time setup&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Setups Docker containers and Sentry admin:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;make bootstrap restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Development&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Each time you update the code, restart the containers:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;make restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And access the sentry admin at&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;http://&amp;lt;DOCKER IP&amp;gt;:8081
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/NDrive/sentry-mattermost" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>sentry</category>
      <category>docker</category>
      <category>hetzner</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Upgrading Rails from 4.2 to 5.2, 6.0 - collected notes</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Tue, 14 Jan 2020 08:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/upgrading-rails-from-4-2-to-5-2-6-0-collected-notes-a0o</link>
      <guid>https://dev.to/zealot128/upgrading-rails-from-4-2-to-5-2-6-0-collected-notes-a0o</guid>
      <description>&lt;p&gt;Recently, I've upgraded a bunch of apps to Rails 5.2 and 6.0, I want to share some common issues and the process I follow:&lt;/p&gt;

&lt;h2&gt;
  
  
  General upgrade process
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Have a quick overview of the Rails upgrade guide, to get a feeling what kind of things changed between versions:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html"&gt;https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I change the Rails requirement in Gemfile to the next minor version, like &lt;code&gt;gem "rails", "~&amp;gt; 5.0.0"&lt;/code&gt;, Maybe also bump the Ruby version to the required one, if your current one is too old. But I try to usually do it separately, because newer Ruby versions can also break your app.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now comes the &lt;code&gt;bundle update rails&lt;/code&gt;, This will probably fail because some other Gems collide. If those Gems have version constraints in your Gemfile, relax that. Try adding more and more Gems to &lt;code&gt;bundle update rails rack sass-rails&lt;/code&gt; etc., Use a &lt;code&gt;bundle update&lt;/code&gt; without Gems only as last resort, as this will update ALL Gems at once and usually break a lot of things. &lt;br&gt;
In my experience, common Gems that have to be upgraded together with Rails,  are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paginators (will_paginate)&lt;/li&gt;
&lt;li&gt;form gems (simple-form)&lt;/li&gt;
&lt;li&gt;ActiveRecord Gems (paper_trail, acts-as-taggable-on)&lt;/li&gt;
&lt;li&gt;Admin-interfaces (Active Admin)&lt;/li&gt;
&lt;li&gt;Asset-Pipeline Gems (sass, sass-rails, font-awesome-rails)&lt;/li&gt;
&lt;li&gt;bootsnap, rspec-rails&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try to boot app, like &lt;code&gt;bundle exec rails c&lt;/code&gt;, Fix any errors that happen, then&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;rails app:update&lt;/code&gt;. The will launch a command line wizard, that will interactively compare all the config files of your app to that of a newly generated Rails app. My tip: Try to adjust your files in a editor to match the Rails standard as closest as possible. E.g. our &lt;code&gt;config/environments/development.rb&lt;/code&gt; looks almost the same like the generated. All custom config by us is then on the bottom of the file and can be easily moved between upgrades later. Alternatively, use &lt;a href="http://railsdiff.org/5.2.4/6.0.2.1"&gt;Rails-Diff&lt;/a&gt; to compare config changes between versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try to run tests, fix deprecations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run development server and click around&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Push to CI. Hint: Try to make deprecations more visible, make them an error, and eager load on test (to remove bugs that happen by invalid files that the development system didn't catch):&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/test.rb&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eager_load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CI'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_support&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deprecation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CI'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="ss"&gt;:raise&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:stderr&lt;/span&gt;
    &lt;span class="c1"&gt;# Show the full stacktrace of deprecations, &lt;/span&gt;
    &lt;span class="c1"&gt;#   e.g. middleware. Maybe put that line in application.rb after loading rails&lt;/span&gt;
    &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Deprecation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rails 5.0
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Protected Attributes is gone!
&lt;/h3&gt;

&lt;p&gt;If you previously have used &lt;code&gt;attr_accessible&lt;/code&gt; and similar, that stuff is gone!&lt;/p&gt;

&lt;p&gt;Finding possible occurences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ag new.&lt;span class="se"&gt;\*&lt;/span&gt;params app | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; permit
ag update.&lt;span class="se"&gt;\*&lt;/span&gt;params app | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; permit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes, e.g. for query models on a get request, this kind of pattern is useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# passing whole params&lt;/span&gt;
&lt;span class="no"&gt;MyForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# passing only params, that might not be there on the first page load&lt;/span&gt;
&lt;span class="no"&gt;MyForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:my_form&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit!&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Important: Belongs_to required by default!
&lt;/h3&gt;

&lt;p&gt;This will probably brake old apps. To reduce friction for future upgrades, adjust &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MyApp&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="c1"&gt;# Initialize configuration defaults for originally generated Rails version.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_defaults&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to_required_by_default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some later Rails generator brings this line into a config/initializers, but that seems to be having no effect, only adding to application.rb.&lt;/p&gt;

&lt;h3&gt;
  
  
  before_filter -&amp;gt; before_action
&lt;/h3&gt;

&lt;p&gt;(Same with after_action)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/before_filter/before_action/g'&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;find app &lt;span class="nt"&gt;-type&lt;/span&gt; f&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/after_filter/after_action/g'&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;find app &lt;span class="nt"&gt;-type&lt;/span&gt; f&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Controller tests/specs - get/post must be keyword arguments
&lt;/h3&gt;

&lt;p&gt;In case you are using RSpec, just use this awesome Gems to convert everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;rails5-spec-converter
rails5-spec-converter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts 95% of the scenarios, only very custom session/cookies stuff must be checked manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  redirect_to :back deprecated
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- redirect_to :back, alert: "whatever"
&lt;/span&gt;&lt;span class="gi"&gt;+ redirect_back fallback_location: '/', alert: "whatever"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rails 5.1
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;image_tag does not allow nil! &lt;code&gt;nil is not a valid asset source&lt;/code&gt;, wrap all image_tag in an &lt;code&gt;if my_model.attachment.present?&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;database_cleaner is not required for browser tests anymore (Rspec: System Tests)&lt;/li&gt;
&lt;li&gt;add 'listen' gem to development/test group &lt;code&gt;gem "listen"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  response.success? -&amp;gt; response.sucessful?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/be_success$/be_successful/g'&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;ag be_success&lt;span class="nv"&gt;$ &lt;/span&gt;spec &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Foreign Key mismatch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ActiveRecord::MismatchedForeignKey: Column `cooperation_id` on table `cooperation_data_points` does not match column `id` on `cooperations`, which has type `bigint(20)`. To resolve this issue, change the type of
the `cooperation_id` column on `cooperation_data_points` to be :bigint. (For example `t.bigint :cooperation_id`).
Original message: Mysql2::Error: Cannot add foreign key constraint: ALTER TABLE `cooperation_data_points` ADD CONSTRAINT `fk_rails_3979ee89c8`
FOREIGN KEY (`cooperation_id`)
  REFERENCES `cooperations` (`id`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Problem: db/schema.rb does not specify correct primary key types (integer vs. bigint)&lt;/li&gt;
&lt;li&gt;Solution: Run &lt;code&gt;rails db:schema:dump&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rails 5.2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Arel.sql
&lt;/h3&gt;

&lt;p&gt;All &lt;code&gt;order&lt;/code&gt; and &lt;code&gt;pluck&lt;/code&gt; columns should be checked and any non-trivial statement must be wrapped in Arel.sql:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- .order('length(name) asc').first
+ .order(Arel.sql('length(name) asc')).first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find occurences of order/pluck with a string, check if there is function call or even an SQL injection possibility :) (like &lt;code&gt;order(params[:sort])&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ag "order\('"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ag 'order\("'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ag "pluck\('"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ag 'pluck\("'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rails 6.0
&lt;/h2&gt;

&lt;p&gt;One very obvious error was the introduction of host checking (against DNS rebinding attacks). We don't need that, as all our apps are proxied in production. In addition, we have dynamic hostnames in development for every developer, so we disable that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config/application.rb
config.hosts.clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other things we noticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scope&lt;/code&gt; - cannot contain the class name itself, just plain calls to where/order etc. (no &lt;code&gt;scope :active { User.where(active: true) }&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_attributes&lt;/code&gt; -&amp;gt; &lt;code&gt;update&lt;/code&gt; simple sed&lt;/li&gt;
&lt;li&gt;Tests/specs: &lt;code&gt;content_type&lt;/code&gt; -&amp;gt; &lt;code&gt;media_type&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gems:

&lt;ul&gt;
&lt;li&gt;in one project, that update line succeeded first: &lt;code&gt;bundle update rails draper devise sass-rails font-awesome-rails annotate premailer-rails&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;other gems that produced errors while booting: &lt;code&gt;bundle update coffee-rails slim-rails bullet pry-rails pry-rescue bootsnap airbrake rspec-rails&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gem 'rspec-rails', '~&amp;gt; 4.0.0.beta2'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gem "cancancan", "~&amp;gt; 3.0"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;will-paginate must be upgraded&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;If you are needing helpers in non-controller/view files, there was an older snippet on Stackoverflow, to use &lt;code&gt;ActionView::Base.new&lt;/code&gt;, replace with: &lt;code&gt;ActionView::Base.new(ActionView::LookupContext.new('.'), {})&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ruby 2.4+
&lt;/h2&gt;

&lt;p&gt;Meanwhile, if you also upgrading Ruby, one error we ran into:&lt;/p&gt;

&lt;h3&gt;
  
  
  webmock &amp;gt; 2, vcr &amp;gt; 3
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;undefined method `&amp;lt;3, :continue_timeout=&amp;gt;nil, :debug_output=&amp;gt;nil}:Hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you had running VCR version less than 3 and used HTTP Basic auth, you need to convert your cassettes: &lt;a href="https://gist.github.com/glaszig/9170b1cf2186674faeead74a68606c5d"&gt;https://gist.github.com/glaszig/9170b1cf2186674faeead74a68606c5d&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deprecations Fixnum/Bignum -&amp;gt; Integer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Substitute your own usages of "Fixnum" with Integer.&lt;/li&gt;
&lt;li&gt;Upgrade all Gems with the error&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;So far, I will update that post if I find more common stuff.&lt;/p&gt;

</description>
      <category>rails</category>
    </item>
    <item>
      <title>Integrating Javascript (Vue) I18n into Rails pipeline with missing/auto translate features</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Sat, 02 Nov 2019 13:48:00 +0000</pubDate>
      <link>https://dev.to/zealot128/integrating-javascript-vue-i18n-into-rails-pipeline-with-missing-auto-translate-features-2ak7</link>
      <guid>https://dev.to/zealot128/integrating-javascript-vue-i18n-into-rails-pipeline-with-missing-auto-translate-features-2ak7</guid>
      <description>&lt;p&gt;Adding I18n features to a backend / "vanilla" Rails app is a common and well documented use case. Awesome tools, like &lt;code&gt;i18n-tasks&lt;/code&gt; helping manage to grow the translations by providing tasks like: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto translate (via Google Translate), &lt;/li&gt;
&lt;li&gt;check for unused/missing translations, &lt;/li&gt;
&lt;li&gt;auto sorting translations,&lt;/li&gt;
&lt;li&gt;keeping all locales in sync and same structure/order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of our apps nowadays are &lt;strong&gt;hybrid apps&lt;/strong&gt;, though, that are using some amount of Javascript (Vue.js/Angular/React) on the frontside. A full I18n will also have to take that into account. When starting an app, I tend to keep the translations together with the components (like Vue i18n which adds a new block into the single file components). But using this approach, one loses the consistent tooling and translations are starting to sprinkle around the application, which makes adding another locale harder and harder.&lt;/p&gt;

&lt;p&gt;For this purpose, I've build the &lt;strong&gt;following pipeline&lt;/strong&gt;, which fulfills the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;i. fast development cycle - reloading a page reloads the translations also for I18n&lt;/li&gt;
&lt;li&gt;ii. separate shipping of locale texts for each locale - e.g. a German user only has to download German locales and not download all&lt;/li&gt;
&lt;li&gt;iii. fingerprinting of said separate locales - so that those files can be cached efficiently and at the same time don't get stale&lt;/li&gt;
&lt;li&gt;iv. unified storage for all translations and support for the Rails tooling (i18n-tasks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The high-level overview is the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using &lt;code&gt;i18n-js&lt;/code&gt; gem, we can generate a Javascript file for each locale easily. That gem also helps us during development by providing a middleware that reloads those files&lt;/li&gt;
&lt;li&gt;We use a namespace in our I18n locale tree, e.g. &lt;code&gt;js.&lt;/code&gt; to ship only that tree to the client and not all of that keys&lt;/li&gt;
&lt;li&gt;In every layout that requires Javascript translations, we put a little partial, that loads those files and put them into a global Javascript field onto &lt;code&gt;window&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The Javascript initializer, in our case Vue-i18n can take that data from the window and load it as locale data&lt;/li&gt;
&lt;li&gt;During production asset building, we need to add the generation of those files to our build pipeline&lt;/li&gt;
&lt;li&gt;We can add a custom &lt;code&gt;i18n-tasks scanner&lt;/code&gt; that helps collecting ALL usages of our Javascript keys to sync which the actual translations (e.g. integrating that into "i18n-tasks unused", "i18n-tasks missing")&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process is described in the following flow chart:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hCA01InT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.stefanwienert.de/images/blog/2019/i18n-pipeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hCA01InT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.stefanwienert.de/images/blog/2019/i18n-pipeline.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  i. Adding dependencies and configure I18n-js Gem
&lt;/h2&gt;

&lt;p&gt;Add Gem, bundle, add configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"i18n-js"&lt;/span&gt;

&lt;span class="c1"&gt;# config/i18n-js.yml&lt;/span&gt;
&lt;span class="ss"&gt;translations:
  &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="ss"&gt;file: &lt;/span&gt;&lt;span class="s1"&gt;'vendor/assets/javascripts/locales/%{locale}.js'&lt;/span&gt;
    &lt;span class="ss"&gt;namespace: &lt;/span&gt;&lt;span class="s2"&gt;"LocaleData"&lt;/span&gt;
    &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt;

&lt;span class="ss"&gt;export_i18n_js: &lt;/span&gt;&lt;span class="s2"&gt;"vendor/assets/javascripts/locales/"&lt;/span&gt;
&lt;span class="ss"&gt;js_extend: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# this will disable Javascript I18n.extend globally&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Yes, that's right! We are using the &lt;strong&gt;old&lt;/strong&gt; asset pipeline to handle the auto generated translations files, here under &lt;code&gt;vendor/assets/javascript/locales&lt;/code&gt;, because that provides a extremely simple fingerprinting and no-fuzz integration into our existing precompile step. To make that work, we need to add all those files to the asset-precompile list, e.g. when using sprockets 4+ in &lt;code&gt;app/assets/config/manifest.js&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="c1"&gt;//= link locales/en.js&lt;/span&gt;
&lt;span class="c1"&gt;//= link locales/de.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Also, manually add the I18n-JS Middleware, to enable a seamless development reloading experience.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="c1"&gt;# config/environment/development.rb&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Also, add the export path into gitignore, because that files change often and are auto generated anyways:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"vendor/assets/javascripts/locales"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Don't forget, to run the i18n-js Rake export task before deployment, otherwise those files might be missing in your deployment server/ci whatever:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"task 'assets:precompile' =&amp;gt; 'i18n:js:export'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; Rakefile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  ii. Loading the correct locale data into your Javascript
&lt;/h2&gt;

&lt;p&gt;First, add some example locale. As stated in the introduction, we are using a &lt;strong&gt;namespace &lt;code&gt;js.*&lt;/code&gt;&lt;/strong&gt; where all Javascript keys are kept inside. This has the advantage that we can very simply export only the necessary locale data to the client. There is an option in i18n-js, which can hide that "js." namespace in the client, e.g. a Rails i18n key of &lt;code&gt;js.component.button&lt;/code&gt; would be accessible as &lt;code&gt;component.button&lt;/code&gt; in the frontend. But I've decided against, because that namespace interferes when using I18n-tasks to check for missing/unused in several cases&lt;sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/sup&gt;&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="c1"&gt;# config/locales/js.en.yml'&lt;/span&gt;
&lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;button_text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Save"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, make sure, every relevant layout, that has translated Javascript on it, loads the locale before everything else, e.g. using a layout partial which you require &lt;strong&gt;before&lt;/strong&gt; loading any other Javascript.&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="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;layouts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;_i18n_loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;erb&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Locale&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;raw&lt;/span&gt; &lt;span class="nx"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to_json&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocaleData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="nx"&gt;javascript_include_tag&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locales/#{I18n.locale}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then, our pack / framework initializer can pick that up. In our case, we are using Vue-i18n:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascripts/utils/i18n.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Vue&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VueI18n&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-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VueI18n&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;VueI18n&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fallbackLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocaleData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setLocaleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now we can include that i18n key when initializing every Vue root "app" on our site:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascripts/packs/app.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;i18n&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;utils/i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;i18n&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;That's it! Every component can now access locale data, like that:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- passing as efficient v-t handler --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;v-t=&lt;/span&gt;&lt;span class="s"&gt;"'js.button_text'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- using i18n arguments --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;{{ $t('js.text_with_arguments', { name: name }) }}
&lt;span class="nt"&gt;&amp;lt;/template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Make sure to reference the documentation&lt;sup&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/sup&gt; of Vue-i18n, as there are some differences to Rails built-in message style:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passing arguments&lt;/strong&gt; : Instead of &lt;code&gt;"somekey %{argument}"&lt;/code&gt; use &lt;code&gt;somekey {argument}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pluralization&lt;/strong&gt; : instead of:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;somekey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&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;car"&lt;/span&gt;
  &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cars"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;use: &lt;code&gt;somekey: 1 car | {n} cars&lt;/code&gt; or with "nothing": &lt;code&gt;somekey: no car | {n} car | {n} cars&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  iii. Adding I18n-Tasks Scanner Adapter
&lt;/h2&gt;

&lt;p&gt;Translations should work find now. To make I18n-Tasks aware of our locales, use this scanner:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/vue_i18n_scanner.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'i18n/tasks/scanners/file_scanner'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'pry'&lt;/span&gt;

&lt;span class="c1"&gt;# finds v-t="" and $t(c) usages&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VueI18nScanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Scanners&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FileScanner&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Scanners&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OccurrenceFromPosition&lt;/span&gt;

  &lt;span class="no"&gt;KEY_IN_QUOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/(?["'])(?[\w\.]+)(?["'])/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
  &lt;span class="no"&gt;WRAPPED_KEY_IN_QUOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/(?["'])&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;KEY_IN_QUOTES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;(?["'])/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="c1"&gt;# @return [Array]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# single file component translation used&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;i18n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="c1"&gt;# v-t="'key'" v-t='"key"'&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/v-t=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;WRAPPED_KEY_IN_QUOTES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;occurrence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;occurrence_from_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurrence&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/\$tc?\(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;KEY_IN_QUOTES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;occurrence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;occurrence_from_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurrence&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_scanner&lt;/span&gt; &lt;span class="s1"&gt;'VueI18nScanner'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%w(*.vue)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And load that scanner in your &lt;code&gt;config/i18n-tasks.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;% require './lib/vue_i18n_scanner' %&amp;gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; config/i18n-tasks.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, i18n-tasks health/unused/missing will scan our javascripts too.&lt;/p&gt;



&lt;p&gt;Used Open-Source:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vJ70wriM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glebm"&gt;
        glebm
      &lt;/a&gt; / &lt;a href="https://github.com/glebm/i18n-tasks"&gt;
        i18n-tasks
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Manage translation and localization with static analysis, for Ruby i18n
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
i18n-tasks &lt;a href="https://travis-ci.org/glebm/i18n-tasks" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/313f19a70806ab4972958066770314be7bd30b522e0d044a0d98502962dcc3a2/68747470733a2f2f696d672e736869656c64732e696f2f7472617669732f676c65626d2f6931386e2d7461736b732e737667" alt="Build Status"&gt;&lt;/a&gt; &lt;a href="https://codeclimate.com/github/glebm/i18n-tasks" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/68bda20dc5708165b12892ecf1e5dadc814b4314562d6eabb04a71b2bedf4d87/68747470733a2f2f6170692e636f6465636c696d6174652e636f6d2f76312f6261646765732f35643137336539306164613864663037636564632f746573745f636f766572616765" alt="Coverage Status"&gt;&lt;/a&gt; &lt;a href="https://gitter.im/glebm/i18n-tasks?utm_source=badge&amp;amp;utm_medium=badge&amp;amp;utm_campaign=pr-badge&amp;amp;utm_content=badge" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/5dbac0213da25c445bd11f168587c11a200ba153ef3014e8408e462e410169b3/68747470733a2f2f6261646765732e6769747465722e696d2f4a6f696e253230436861742e737667" alt="Gitter"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;i18n-tasks helps you find and manage missing and unused translations.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://camo.githubusercontent.com/d311841840407c4f4ecc4b27f6b87e0e6d1f5f84477171e029f31a3d75061110/68747470733a2f2f692e696d6775722e636f6d2f585a4264386c372e706e67"&gt;&lt;img width="539" height="331" src="https://camo.githubusercontent.com/d311841840407c4f4ecc4b27f6b87e0e6d1f5f84477171e029f31a3d75061110/68747470733a2f2f692e696d6775722e636f6d2f585a4264386c372e706e67"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This gem analyses code statically for key usages, such as &lt;code&gt;I18n.t('some.key')&lt;/code&gt;, in order to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Report keys that are missing or unused.&lt;/li&gt;
&lt;li&gt;Pre-fill missing keys, optionally from Google Translate or DeepL Pro.&lt;/li&gt;
&lt;li&gt;Remove unused keys.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thus addressing the two main problems of &lt;a href="https://github.com/svenfuchs/i18n" title="svenfuchs/i18n on Github"&gt;i18n gem&lt;/a&gt; design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing keys only blow up at runtime.&lt;/li&gt;
&lt;li&gt;Keys no longer in use may accumulate and introduce overhead, without you knowing it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
Installation&lt;/h2&gt;
&lt;p&gt;i18n-tasks can be used with any project using the ruby &lt;a href="https://github.com/svenfuchs/i18n" title="svenfuchs/i18n on Github"&gt;i18n gem&lt;/a&gt; (default in Rails).&lt;/p&gt;
&lt;p&gt;Add i18n-tasks to the Gemfile:&lt;/p&gt;
&lt;div class="highlight highlight-source-ruby js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;gem&lt;/span&gt; &lt;span class="pl-s"&gt;'i18n-tasks'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'~&amp;gt; 0.9.33'&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Copy the default &lt;a href="https://raw.githubusercontent.com/glebm/i18n-tasks/main/#configuration"&gt;configuration file&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-text-shell-session js-code-highlight"&gt;
&lt;pre&gt;$ &lt;span class="pl-s1"&gt;cp &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;i18n-tasks gem-path&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;/templates/config/i18n-tasks.yml config/&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Copy rspec test to test for missing and unused translations as part of the suite (optional):&lt;/p&gt;
&lt;div class="highlight highlight-text-shell-session js-code-highlight"&gt;
&lt;pre&gt;$ &lt;span class="pl-s1"&gt;cp &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;i18n-tasks gem-path&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;/templates/rspec/i18n_spec.rb spec/&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Or for minitest:&lt;/p&gt;
&lt;div class="highlight highlight-text-shell-session js-code-highlight"&gt;
&lt;pre&gt;$ &lt;span class="pl-s1"&gt;cp &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;i18n-tasks gem-path&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glebm/i18n-tasks"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vJ70wriM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/fnando"&gt;
        fnando
      &lt;/a&gt; / &lt;a href="https://github.com/fnando/i18n-js"&gt;
        i18n-js
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      It's a small library to provide the I18n translations on the Javascript. It comes with Rails support.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/fnando/i18n-js/raw/main/i18njs.png"&gt;&lt;img width="250" height="58" src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ec5YoSZc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/fnando/i18n-js/raw/main/i18njs.png" alt="i18n.js"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  It's a small library to provide the Rails I18n translations on the JavaScript
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/fnando/i18n-js/actions?query=workflow%3ATests"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zyMNtwPY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/fnando/i18n-js/workflows/Tests/badge.svg" alt="Tests"&gt;&lt;/a&gt;
  &lt;a href="http://badge.fury.io/rb/i18n-js" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/f8f48c519a6ff70c6c7872e981d9712220876b4b1992a923aee04dcb1b9997da/687474703a2f2f696d672e736869656c64732e696f2f67656d2f762f6931386e2d6a732e737667" alt="Gem Version"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/i18n-js" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/671931224a0e3e28fb67046c2e994bdd72b07ef03fb4aef04e6965b49703b662/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f6931386e2d6a732e737667" alt="npm"&gt;&lt;/a&gt;
  &lt;a href="https://opensource.org/licenses/MIT" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/78f47a09877ba9d28da1887a93e5c3bc2efb309c1e910eb21135becd2998238a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
  &lt;a href="https://travis-ci.org/fnando/i18n-js" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/b39b9336abd725850ea1f838d2228328c23be77731014afd80fcb5e396e2ffa0/687474703a2f2f696d672e736869656c64732e696f2f7472617669732f666e616e646f2f6931386e2d6a732e737667" alt="Build Status"&gt;&lt;/a&gt;
  &lt;a href="https://coveralls.io/r/fnando/i18n-js" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/f51d7b850c1d643dca2aa6eb7ecb6c285c34ff351e87c1e50ea80da5d7462e3e/687474703a2f2f696d672e736869656c64732e696f2f636f766572616c6c732f666e616e646f2f6931386e2d6a732e737667" alt="Coverage Status"&gt;&lt;/a&gt;
  &lt;a href="https://gitter.im/fnando/i18n-js" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/2bbe3525bf796a203a2ec3a8534cddbe0b7f5b578db134ba78cdcc9ecb7e8fe0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6769747465722d6a6f696e253230636861742d3164636537332e737667" alt="Gitter"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pluralization&lt;/li&gt;
&lt;li&gt;Date/Time localization&lt;/li&gt;
&lt;li&gt;Number localization&lt;/li&gt;
&lt;li&gt;Locale fallback&lt;/li&gt;
&lt;li&gt;Asset pipeline support&lt;/li&gt;
&lt;li&gt;Lots more! :)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
Version Notice&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;main&lt;/code&gt; branch (including this README) is for latest &lt;code&gt;3.0.0&lt;/code&gt; instead of
&lt;code&gt;2.x&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
Usage&lt;/h2&gt;
&lt;h3&gt;
Installation&lt;/h3&gt;
&lt;h4&gt;
Rails app&lt;/h4&gt;
&lt;p&gt;Add the gem to your Gemfile.&lt;/p&gt;
&lt;div class="highlight highlight-source-ruby js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;gem&lt;/span&gt; &lt;span class="pl-s"&gt;"i18n-js"&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
Rails with &lt;a href="https://github.com/rails/webpacker"&gt;webpacker&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;If you're using &lt;code&gt;webpacker&lt;/code&gt;, you may need to add the dependencies to your client
with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yarn add i18n-js
# or, if you're using npm
npm install i18n-js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more details, see
&lt;a href="https://gist.github.com/bazzel/ecdff4718962e57c2d5569cf01d332fe"&gt;this gist&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
Rails app with &lt;a href="http://guides.rubyonrails.org/asset_pipeline.html" rel="nofollow"&gt;Asset Pipeline&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;If you're using the
&lt;a href="http://guides.rubyonrails.org/asset_pipeline.html" rel="nofollow"&gt;asset pipeline&lt;/a&gt;, then you
must add the following line to your &lt;code&gt;app/assets/javascripts/application.js&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-js js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;//&lt;/span&gt;
&lt;span class="pl-c"&gt;// This is optional (in case you have `I18n is not defined` error)&lt;/span&gt;
&lt;span class="pl-c"&gt;// If you want to put this line, you must put it BEFORE `i18n/translations`&lt;/span&gt;
&lt;span class="pl-c"&gt;//= require i18n&lt;/span&gt;
&lt;span class="pl-c"&gt;// Some people&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/fnando/i18n-js"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
 


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vJ70wriM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kazupon"&gt;
        kazupon
      &lt;/a&gt; / &lt;a href="https://github.com/kazupon/vue-i18n"&gt;
        vue-i18n
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🌐 Internationalization plugin for Vue.js
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/./assets/vue-i18n-logo.png"&gt;&lt;img width="128px" height="112px" src="https://res.cloudinary.com/practicaldev/image/fetch/s--By2SplAL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/./assets/vue-i18n-logo.png" alt="Vue I18n logo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
vue-i18n&lt;/h1&gt;
&lt;p&gt;
  &lt;a href="https://circleci.com/gh/kazupon/vue-i18n/tree/dev" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/8cf5cbb60327c9f4c35cfced95254646b4464c57ce503d0cd3cc7ea00aba4b3c/68747470733a2f2f636972636c6563692e636f6d2f67682f6b617a75706f6e2f7675652d6931386e2f747265652f6465762e7376673f7374796c653d736869656c64" alt="Build Status"&gt;&lt;/a&gt;
  &lt;a href="https://codecov.io/gh/kazupon/vue-i18n" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/19c8506aec4681f1082534aac450048ab1836f4236e5b079049bbc6085f4f82c/68747470733a2f2f636f6465636f762e696f2f67682f6b617a75706f6e2f7675652d6931386e2f6272616e63682f6465762f67726170682f62616467652e737667" alt="Coverage Status"&gt;&lt;/a&gt;
  &lt;a href="http://badge.fury.io/js/vue-i18n" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/da3e198f3f97ddbe6079fe02b690ecc364448537d1873432a3ca12dcb005c8f4/68747470733a2f2f62616467652e667572792e696f2f6a732f7675652d6931386e2e737667" alt="NPM version"&gt;&lt;/a&gt;
  &lt;a href="https://discord.gg/4yCnk2m" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/b5ca2bca07036801cce637f9a0202116bf6045ffef7862bf39d394eab33a2142/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446973636f72642d6a6f696e253230636861742d3733386264372e737667" alt="vue-i18n channel on Discord"&gt;&lt;/a&gt;
  &lt;a href="https://devtoken.rocks/package/vue-i18n" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/73b13fc981e752969d316ef87514db5662716cba53e8bc0d5b4dd0652751ed3e/68747470733a2f2f62616467652e646576746f6b656e2e726f636b732f7675652d6931386e" alt="vue-i18n Dev Token"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Internationalization plugin for Vue.js&lt;/p&gt;



&lt;h3&gt;
🥇 Gold Sponsors&lt;/h3&gt;

&lt;p&gt;
  &lt;a href="https://nuxtjs.org/" rel="nofollow"&gt;
    &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2MlvsMZ1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/vuepress/.vuepress/public/patrons/nuxt.png"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;h3&gt;
🥈 Silver Sponsors&lt;/h3&gt;

&lt;p&gt;
  &lt;a href="https://www.codeandweb.com/babeledit?utm_campaign=vue-i18n-2019-01" rel="nofollow"&gt;
    &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t9nmoiS_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/vuepress/.vuepress/public/patrons/babeledit.png" width="320px"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;h3&gt;
🥉 Bronze Sponsors&lt;/h3&gt;

&lt;p&gt;
  &lt;a href="https://zenarchitects.co.jp/" rel="nofollow"&gt;
    &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L21wkvo7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/vuepress/.vuepress/public/patrons/zenarchitects.png" width="200px"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.sendcloud.com/" rel="nofollow"&gt;
    &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--96xx2Myf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/vuepress/.vuepress/public/patrons/sendcloud.png" width="200px"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.vuemastery.com/" rel="nofollow"&gt;
    &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yJrwHx0h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/kazupon/vue-i18n/v8.x/vuepress/.vuepress/public/patrons/vuemastery.png" width="200px"&gt;
  &lt;/a&gt;
&lt;/p&gt;



&lt;h2&gt;
📢 Notice&lt;/h2&gt;

&lt;p&gt;vue-i18n will soon be transferred to &lt;a href="https://github.com/intlify"&gt;intlify organization&lt;/a&gt;. After that, it will be developed and maintained on intlify.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;vue-i18n&lt;/code&gt; that has been released on npm will be released as &lt;code&gt;@intlify/vue-i18n&lt;/code&gt; in near future.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vue-i18n&lt;/code&gt; next major version repo is &lt;a href="https://github.com/intlify/vue-i18n-next"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Intlify is a new i18n project kickoff by &lt;a class="comment-mentioned-user" href="https://dev.to/kazupon"&gt;@kazupon&lt;/a&gt;
. 😉&lt;/p&gt;

&lt;h2&gt;
📖 Documentation&lt;/h2&gt;

&lt;p&gt;See &lt;a href="http://kazupon.github.io/vue-i18n/" rel="nofollow"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
📜 Changelog&lt;/h2&gt;

&lt;p&gt;Detailed changes for each release are documented in the &lt;a href="https://github.com/kazupon/vue-i18n/blob/dev/CHANGELOG.md"&gt;CHANGELOG.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
❗ Issues&lt;/h2&gt;

&lt;p&gt;Please make sure to read the &lt;a href="https://github.com/kazupon/vue-i18n/blob/dev/CONTRIBUTING.md#issue-reporting-guidelines"&gt;Issue Reporting Checklist&lt;/a&gt; before opening an issue. Issues not conforming to the guidelines may be closed immediately.&lt;/p&gt;

&lt;h2&gt;
💪 Contribution&lt;/h2&gt;

&lt;p&gt;Please make sure to read the &lt;a href="https://github.com/kazupon/vue-i18n/blob/dev/CONTRIBUTING.md"&gt;Contributing Guide&lt;/a&gt; before making a pull request.&lt;/p&gt;

&lt;h2&gt;
©️ License&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://opensource.org/licenses/MIT" rel="nofollow"&gt;MIT&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kazupon/vue-i18n"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
 

&lt;p&gt;&lt;a&gt;2.&lt;/a&gt; Adding the prefix in our custom scanner while scanning the files was a way that worked, BUT the default I18n-scanner already picked up some of the keys by it's own regex scanner (&lt;code&gt;$t(..)&lt;/code&gt;-usages) which then would then be marked as missed. There seemed to be no way to ignore the app/javascript/ for only the default scanner but only for all. &lt;/p&gt;

&lt;p&gt;&lt;a&gt;3.&lt;/a&gt; &lt;a href="https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting"&gt;https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting&lt;/a&gt; &lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>vue</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Simple but realistic Elasticsearch load test before deploying</title>
      <dc:creator>Stefan Wienert</dc:creator>
      <pubDate>Mon, 14 Jan 2019 09:00:00 +0000</pubDate>
      <link>https://dev.to/zealot128/simple-but-realistic-elasticsearch-load-test-before-deploying-jd8</link>
      <guid>https://dev.to/zealot128/simple-but-realistic-elasticsearch-load-test-before-deploying-jd8</guid>
      <description>&lt;p&gt;Load testing an Elasticsearch cluster before a migration, upgrade or similar is always a good strategy to reduce bad surprises afterwards. With &lt;a href="https://www.pludoni.de"&gt;pludoni GmbH&lt;/a&gt;, we use Elasticsearch since 2014 for our Job search backend for all of our (German) community websites (Empfehlungsbund), as well as blog article search and for keyword optimizations.&lt;/p&gt;

&lt;p&gt;Recently, I've prepared such a migration and needed a way to verify that the cluster holds after switching, of better yet, improving the performance in regards to the costs. After checking Github et. al. for other specific code, I was not satisfied enough and build some small script around the awesome &lt;code&gt;siege&lt;/code&gt; tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation - Gather good queries for test
&lt;/h2&gt;

&lt;p&gt;To get a realistic performance test, I suggest to grab original payloads of queries that your production ES runs. Even more, I only took a couple of queries that are the slowest to boost my confidence in the end.&lt;/p&gt;

&lt;p&gt;To to that, first enable &lt;code&gt;Slow Log&lt;/code&gt; in ES settings in your UI (cerebro/kopf whatever frontend) or via curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'http://localhost:9200/index_settings/update'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json;charset=utf-8'&lt;/span&gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"index":"CLUSTERNAME","settings":{"index.search.slowlog.threshold.query.warn":"1s","index.search.slowlog.threshold.query.info":"0.5s"},"host":"http://localhost:9200"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, wait a while or produce slow logs via querying. Afterwards the file &lt;code&gt;/var/log/elasticsearch/*_index_search_slowlog.log&lt;/code&gt; will fill up.&lt;/p&gt;

&lt;p&gt;Extract the query payloads by copying all json between brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;2019-12-11 04:30:29,725][INFO][index.search.slowlog.query] &lt;span class="o"&gt;[&lt;/span&gt;es01.localhost] &lt;span class="o"&gt;[&lt;/span&gt;ebsearch_production][2] took[1.7s],
  took_millis[1774], types[], stats[], search_type[QUERY_THEN_FETCH], total_shards[5], &lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;&amp;gt;]&lt;/span&gt;, extra_source[],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Put each payload each in individual file in a common folder, e.g. &lt;code&gt;payloads/1&lt;/code&gt;, &lt;code&gt;payloads/2&lt;/code&gt; etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build new test cluster
&lt;/h2&gt;

&lt;p&gt;One hint if not yet used: Use Repository + Snapshots (S3) to quickly seed a new cluster with production grade data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the (old/new) cluster
&lt;/h2&gt;

&lt;p&gt;The test is run by the battle-tested tool &lt;strong&gt;Siege&lt;/strong&gt; , which should be easy to install from all your OS repo (Apt, Brew, etc.). Siege supports a input parameter with a file with urls to test. Later, we will utilize Siege like that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Concurrency: 3, for 1 minute&lt;/span&gt;
siege &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./siege.log &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
  &lt;span class="nt"&gt;--internet&lt;/span&gt; &lt;span class="nt"&gt;--delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15 &lt;span class="nt"&gt;-c&lt;/span&gt; 3 &lt;span class="nt"&gt;-t&lt;/span&gt; 1M &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;urls.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The urls.txt has the format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://server/index/_search POST {".....search payload"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To generate &lt;code&gt;urls.txt&lt;/code&gt; easily with all of the payloads, I've created a &lt;code&gt;Rakefile&lt;/code&gt;, because Ruby is awesome. Also, we are living in 2019(+), so the Ruby that shipped with your distro should be just fine, no rvm/rbenv needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;SERVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10.10.10.100:9200'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="no"&gt;DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1M'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt; &lt;span class="c1"&gt;# 1 minute each test&lt;/span&gt;
&lt;span class="no"&gt;CONCURRENCY_TESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# or [1, 5, 10, 20, 100] etc.&lt;/span&gt;
&lt;span class="no"&gt;INDEX_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ebsearch_production'&lt;/span&gt;

&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'create urls.text file with all payloads in payloads/*'&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:urls&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'payloads/*'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="s2"&gt;"http://&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;INDEX_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/_search POST &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"recreating urls.txt for &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; with &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; requests"&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'urls.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'run series!'&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:run&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'siege.log'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="no"&gt;MAX_CONCURRENCY&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"==== &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Concurrent ==== "&lt;/span&gt;

    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="sx"&gt;%[siege -b -m "#{SERVER}-C#{c}" --log=./siege.log -H 'Content-Type: application/json' --internet --delay=15 -c #{c} -t #{DURATION} --file=urls.txt]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'show csv as tsv for copy paste into google spreadsheets'&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:csv&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'siege.log'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lines&lt;/span&gt;
  &lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" ****"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:run&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Modify the params in the header of the file&lt;/li&gt;
&lt;li&gt;Run it! &lt;code&gt;rake&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After it is finished (CONCURRENCY_TESTS * DURATION), you can output the data: &lt;code&gt;rake csv&lt;/code&gt; and copy the output in e.g. Google Spreadsheets to easily generate charts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonuspoints: quick chart with ascii-charts
&lt;/h2&gt;

&lt;p&gt;Install bundler, if not done yet &lt;code&gt;gem install bundler&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Append to Rakefile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'bundler/inline'&lt;/span&gt;
&lt;span class="n"&gt;gemfile&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://rubygems.org'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'ascii_charts'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'chart'&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:chart&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'ascii_charts'&lt;/span&gt;
  &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'siege.log'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lines&lt;/span&gt;
  &lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" ****"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Elap Time'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:strip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sx"&gt;%w[date transactions duration transfer response_time requests_s mbs conc success failed]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'pry'&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"======= Response Time / Concurrency ========"&lt;/span&gt;
  &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'conc'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'response_time'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;AsciiCharts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cartesian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;

  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"======= Requests/s / Concurrency ========"&lt;/span&gt;
  &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'conc'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'requests_s'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;AsciiCharts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cartesian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Anecdote: results for our smallish cluster
&lt;/h2&gt;

&lt;p&gt;Our search used custom search plugins that are quite CPU intensive, especially with long queries. Overall our concurrent users are not that many, so a 3-4 node cluster is generally enough.&lt;/p&gt;

&lt;p&gt;Deployment target is the very cost efficient Hetzner Cloud (HCloud). Here a quick overview over the different cloud instance types Hetzner offers at this point (2019):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CX11 (1 Core, 2GB, 3 EUR)&lt;/li&gt;
&lt;li&gt;CX21 (2 Core, 4GB, 6 EUR)&lt;/li&gt;
&lt;li&gt;CX31 (2 Core, 8GB, 11 EUR) (not included, because no CPU improvement and RAM is not utilized)&lt;/li&gt;
&lt;li&gt;CX41 (4 Core, 16GB, 19 EUR)&lt;/li&gt;
&lt;li&gt;CX51 (8 Core, 32GB, 36 EUR)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've tried the following combinations in the Hetzner cloud. Please note, that if the rq/s looks ridiculous low, but please keep in mind that those 10 concurrent users are only searching with the worst queries that I could found.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;nodes&lt;/th&gt;
&lt;th&gt;EUR/month&lt;/th&gt;
&lt;th&gt;rq/s @ 10ccu&lt;/th&gt;
&lt;th&gt;response time @ 10 ccu&lt;/th&gt;
&lt;th&gt;requests/s/EUR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1x Coordinator (CX11) + 2x Data CX21&lt;/td&gt;
&lt;td&gt;15 EUR&lt;/td&gt;
&lt;td&gt;5.90&lt;/td&gt;
&lt;td&gt;1.67&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1x Coordinator (CX11) + 3x Data CX21&lt;/td&gt;
&lt;td&gt;21 EUR&lt;/td&gt;
&lt;td&gt;4.84&lt;/td&gt;
&lt;td&gt;2.03&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1x Coordinator (CX11) + 2x Data CX41&lt;/td&gt;
&lt;td&gt;41 EUR&lt;/td&gt;
&lt;td&gt;11.24&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1x Coordinator (CX11) + 3x Data CX41&lt;/td&gt;
&lt;td&gt;60 EUR&lt;/td&gt;
&lt;td&gt;17.19&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1x Coordinator (CX11) + 4x Data CX41&lt;/td&gt;
&lt;td&gt;79 EUR&lt;/td&gt;
&lt;td&gt;16.56&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even the smallest instance seemed to be fine for the Coordinator node, the CPU usage never reached any kind of utilization number&lt;/li&gt;
&lt;li&gt;Going from 2 CX21 to 3 CX21 did not improve the core metrics (reqs/s, response time), but worsening it. My conclusion is, that the CX21 has too low CPU power to&lt;/li&gt;
&lt;li&gt;Same, going to 4 CX41 seems to be worse than 3 CX41&lt;/li&gt;
&lt;li&gt;2 or 3 CX41 are best performance for the price&lt;/li&gt;
&lt;li&gt;CX51 untested&lt;/li&gt;
&lt;li&gt;PLEASE NOTE: That findings could be totally related to our type of querying which includes custom search algorithm written in Groovy, Also: I am not a Elasticsearch expert, so there might be tuning params, sharding settings that could be adjusted.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>elasticsearch</category>
      <category>siege</category>
      <category>hetzner</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
